diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 31392c7d38..33cdb33d0b 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -4,12 +4,88 @@ ## Major changes -Dropped support for version 2 of Tor protocol. That means +### Add support for channel aliases and zeroconf channels + +:information_source: Those features are only supported for channels of type `AnchorOutputsZeroFeeHtlcTx`, which is the +newest channel type and the one enabled by default. If you are opening a channel with a node that doesn't run Eclair, +make sure they support `option_anchors_zero_fee_htlc_tx`. + +#### Channel aliases + +Channel aliases offer a way to use arbitrary channel identifiers for routing. This feature improves privacy by not +leaking the funding transaction of the channel during payments. + +This feature is enabled by default, but your peer has to support it too, and it is not compatible with public channels. + +#### Zeroconf channels + +Zeroconf channels make it possible to use a newly created channel before the funding tx is confirmed on the blockchain. + +:warning: Zeroconf requires the fundee to trust the funder. For this reason it is disabled by default, and you should +only enable it on a peer-by-peer basis. + +##### Enabling through features + +Below is how to enable zeroconf with a given peer in `eclair.conf`. With this config, your node will _accept_ zeroconf +channels from node `03864e...`. + +```eclair.conf +override-init-features = [ + { + nodeid = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + features = { + // these features need to be enabled + var_onion_optin = mandatory + payment_secret = mandatory + option_channel_type = optional + // dependencies of zeroconf + option_static_remotekey = optional + option_anchors_zero_fee_htlc_tx = optional + option_scid_alias = optional + // enable zeroconf + option_zeroconf = optional + } + } +] +``` + +Note that, as funder, Eclair will happily use an unconfirmed channel if the peer sends an early `channel_ready`, even if +the `option_zeroconf` feature isn't enabled, as long as the peer provides a channel alias. + +##### Enabling through channel type + +You can enable `option_scid_alias` and `option_zeroconf` features by requesting them in the channel type, even if those +options aren't enabled in your features. + +The new channel types variations are: + +- `anchor_outputs_zero_fee_htlc_tx+scid_alias` +- `anchor_outputs_zero_fee_htlc_tx+zeroconf` +- `anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf` + +Examples using the command-line interface: + +- open a public zeroconf channel: + +```shell +$ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+zeroconf --announceChannel=true +``` + +- open a private zeroconf channel with aliases: + +```shell +$ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf --announceChannel=false +``` + +### Remove support for Tor v2 + +Dropped support for version 2 of Tor protocol. That means: - Eclair can't open control connection to Tor daemon version 0.3.3.5 and earlier anymore - Eclair can't create hidden services for Tor protocol v2 with newer versions of Tor daemon -IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still use Tor v0.3.3.5 or earlier before upgrading to this release. +IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still use Tor v0.3.3.5 or earlier before +upgrading to this release. ### API changes @@ -20,25 +96,33 @@ IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still u #### Delay enforcement of new channel fees -When updating the relay fees for a channel, eclair can now continue accepting to relay payments using the old fee even if they would be rejected with the new fee. -By default, eclair will still accept the old fee for 10 minutes, you can change it by setting `eclair.relay.fees.enforcement-delay` to a different value. +When updating the relay fees for a channel, eclair can now continue accepting to relay payments using the old fee even +if they would be rejected with the new fee. +By default, eclair will still accept the old fee for 10 minutes, you can change it by +setting `eclair.relay.fees.enforcement-delay` to a different value. -If you want a specific fee update to ignore this delay, you can update the fee twice to make eclair forget about the previous fee. +If you want a specific fee update to ignore this delay, you can update the fee twice to make eclair forget about the +previous fee. #### New minimum funding setting for private channels -New settings have been added to independently control the minimum funding required to open public and private channels to your node. +New settings have been added to independently control the minimum funding required to open public and private channels +to your node. -The `eclair.channel.min-funding-satoshis` setting has been deprecated and replaced with the following two new settings and defaults: +The `eclair.channel.min-funding-satoshis` setting has been deprecated and replaced with the following two new settings +and defaults: * `eclair.channel.min-public-funding-satoshis = 100000` * `eclair.channel.min-private-funding-satoshis = 100000` -If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these new settings. +If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these +new settings. #### Expired incoming invoices now purged if unpaid -Expired incoming invoices that are unpaid will be searched for and purged from the database when Eclair starts up. Thereafter searches for expired unpaid invoices to purge will run once every 24 hours. You can disable this feature, or change the search interval with two new settings: +Expired incoming invoices that are unpaid will be searched for and purged from the database when Eclair starts up. +Thereafter searches for expired unpaid invoices to purge will run once every 24 hours. You can disable this feature, or +change the search interval with two new settings: * `eclair.purge-expired-invoices.enabled = true * `eclair.purge-expired-invoices.interval = 24 hours` @@ -77,13 +161,16 @@ Use the following command to generate the eclair-node package: mvn clean install -DskipTests ``` -That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc` +That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one +we provide and sign in `SHA256SUMS.asc` -(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not tried everything. +(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not +tried everything. ## Upgrading -This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart. +This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, +upgrade and restart. ## Changelog diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8b6739d551..66ec236654 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -57,9 +57,13 @@ eclair { option_dual_fund = disabled option_onion_messages = optional option_channel_type = optional + option_scid_alias = optional option_payment_metadata = optional - trampoline_payment_prototype = disabled + // By enabling option_zeroconf, you will be trusting your peers as fundee. You will lose funds if they double spend + // their funding tx. + option_zeroconf = disabled keysend = disabled + trampoline_payment_prototype = disabled } override-init-features = [ // optional per-node features # { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala b/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala index f00c70f40b..0a22160df0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala @@ -36,6 +36,8 @@ case class BlockHeight(private val underlying: Long) extends Ordered[BlockHeight def toInt: Int = underlying.toInt def toLong: Long = underlying def toDouble: Double = underlying.toDouble + + override def toString() = underlying.toString // @formatter:on } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 0d6be5469e..d162fe7890 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -182,14 +182,18 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { // we want the open timeout to expire *before* the default ask timeout, otherwise user will get a generic response val openTimeout = openTimeout_opt.getOrElse(Timeout(20 seconds)) - (appKit.switchboard ? Peer.OpenChannel( - remoteNodeId = nodeId, - fundingSatoshis = fundingAmount, - pushMsat = pushAmount_opt.getOrElse(0 msat), - channelType_opt = channelType_opt, - fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), - channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), - timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse] + for { + _ <- Future.successful(0) + open = Peer.OpenChannel( + remoteNodeId = nodeId, + fundingSatoshis = fundingAmount, + pushMsat = pushAmount_opt.getOrElse(0 msat), + channelType_opt = channelType_opt, + fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), + channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), + timeout_opt = Some(openTimeout)) + res <- (appKit.switchboard ? open).mapTo[ChannelOpenResponse] + } yield res } override def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index bb5e5575c9..f6816f4cbe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -240,11 +240,21 @@ object Features { val mandatory = 44 } + case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_scid_alias" + val mandatory = 46 + } + case object PaymentMetadata extends Feature with InvoiceFeature { val rfcName = "option_payment_metadata" val mandatory = 48 } + case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_zeroconf" + val mandatory = 50 + } + case object KeySend extends Feature with NodeFeature { val rfcName = "keysend" val mandatory = 54 @@ -278,9 +288,11 @@ object Features { DualFunding, OnionMessages, ChannelType, + ScidAlias, PaymentMetadata, - TrampolinePaymentPrototype, - KeySend + ZeroConf, + KeySend, + TrampolinePaymentPrototype ) // Features may depend on other features, as specified in Bolt 9. 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 44fc67ff6c..2265f220b7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala @@ -16,46 +16,71 @@ package fr.acinq.eclair -/** - * A short channel id uniquely identifies a channel by the coordinates of its funding tx output in the blockchain. - * See BOLT 7: https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements - */ -case class ShortChannelId(private val id: Long) extends Ordered[ShortChannelId] { - - def toLong: Long = id - - def blockHeight = ShortChannelId.blockHeight(this) +import fr.acinq.eclair.ShortChannelId.toShortId - override def toString: String = { +// @formatter:off +sealed trait ShortChannelId extends Ordered[ShortChannelId] { + def toLong: Long + // we use an unsigned long comparison here + override def compare(that: ShortChannelId): Int = (this.toLong + Long.MinValue).compareTo(that.toLong + Long.MinValue) + override def hashCode(): Int = toLong.hashCode() + override def equals(obj: Any): Boolean = obj match { + case scid: ShortChannelId => this.toLong.equals(scid.toLong) + case _ => false + } + def toCoordinatesString: String = { val TxCoordinates(blockHeight, txIndex, outputIndex) = ShortChannelId.coordinates(this) s"${blockHeight.toLong}x${txIndex}x$outputIndex" } - - // we use an unsigned long comparison here - override def compare(that: ShortChannelId): Int = (this.id + Long.MinValue).compareTo(that.id + Long.MinValue) + def toHex: String = s"0x${toLong.toHexString}" +} +/** Sometimes we don't know what a scid really is */ +case class UnspecifiedShortChannelId(private val id: Long) extends ShortChannelId { + 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 { + override def toLong: Long = id + override def toString: String = toCoordinatesString + def blockHeight: BlockHeight = ShortChannelId.blockHeight(this) + def outputIndex: Int = ShortChannelId.outputIndex(this) +} +case class Alias(private val id: Long) extends ShortChannelId { + override def toLong: Long = id + override def toString: String = toHex +} +// @formatter:on object ShortChannelId { - def apply(s: String): ShortChannelId = s.split("x").toList match { - case blockHeight :: txIndex :: outputIndex :: Nil => ShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt)) + case blockHeight :: txIndex :: outputIndex :: Nil => UnspecifiedShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt)) case _ => throw new IllegalArgumentException(s"Invalid short channel id: $s") } - def apply(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int): ShortChannelId = ShortChannelId(toShortId(blockHeight.toInt, txIndex, outputIndex)) + def apply(l: Long): ShortChannelId = UnspecifiedShortChannelId(l) def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long = ((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL) + def generateLocalAlias(): Alias = Alias(System.nanoTime()) // TODO: fixme (duplicate, etc.) + @inline - def blockHeight(shortChannelId: ShortChannelId): BlockHeight = BlockHeight((shortChannelId.id >> 40) & 0xFFFFFF) + def blockHeight(shortChannelId: ShortChannelId): BlockHeight = BlockHeight((shortChannelId.toLong >> 40) & 0xFFFFFF) @inline - def txIndex(shortChannelId: ShortChannelId): Int = ((shortChannelId.id >> 16) & 0xFFFFFF).toInt + def txIndex(shortChannelId: ShortChannelId): Int = ((shortChannelId.toLong >> 16) & 0xFFFFFF).toInt @inline - def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.id & 0xFFFF).toInt + def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.toLong & 0xFFFF).toInt def coordinates(shortChannelId: ShortChannelId): TxCoordinates = TxCoordinates(blockHeight(shortChannelId), txIndex(shortChannelId), outputIndex(shortChannelId)) } +/** + * A real short channel id uniquely identifies a channel by the coordinates of its funding tx output in the blockchain. + * See BOLT 7: https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements + */ +object RealShortChannelId { + def apply(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int): RealShortChannelId = RealShortChannelId(toShortId(blockHeight.toInt, txIndex, outputIndex)) +} + case class TxCoordinates(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int) \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala index 966ae3024d..3924d70f34 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala @@ -105,7 +105,7 @@ private class BalanceActor(context: ActorContext[Command], Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainConfirmed).update(result.onChain.confirmed.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainUnconfirmed).update(result.onChain.unconfirmed.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForFundingConfirmed).update(result.offChain.waitForFundingConfirmed.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForFundingLocked).update(result.offChain.waitForFundingLocked.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForChannelReady).update(result.offChain.waitForChannelReady.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.normal).update(result.offChain.normal.total.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.shutdown).update(result.offChain.shutdown.total.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.closingLocal).update(result.offChain.closing.localCloseBalance.total.toMilliBtc.toDouble) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala index 8dd65b359c..7d08a54d09 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala @@ -77,13 +77,13 @@ object CheckBalance { * The overall balance among all channels in all states. */ case class OffChainBalance(waitForFundingConfirmed: Btc = 0.sat, - waitForFundingLocked: Btc = 0.sat, + waitForChannelReady: Btc = 0.sat, normal: MainAndHtlcBalance = MainAndHtlcBalance(), shutdown: MainAndHtlcBalance = MainAndHtlcBalance(), negotiating: Btc = 0.sat, closing: ClosingBalance = ClosingBalance(), waitForPublishFutureCommitment: Btc = 0.sat) { - val total: Btc = waitForFundingConfirmed + waitForFundingLocked + normal.total + shutdown.total + negotiating + closing.total + waitForPublishFutureCommitment + val total: Btc = waitForFundingConfirmed + waitForChannelReady + normal.total + shutdown.total + negotiating + closing.total + waitForPublishFutureCommitment } def updateMainBalance(localCommit: LocalCommit): Btc => Btc = { v: Btc => @@ -201,7 +201,7 @@ object CheckBalance { channels .foldLeft(OffChainBalance()) { case (r, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => r.modify(_.waitForFundingConfirmed).using(updateMainBalance(d.commitments.localCommit)) - case (r, d: DATA_WAIT_FOR_FUNDING_LOCKED) => r.modify(_.waitForFundingLocked).using(updateMainBalance(d.commitments.localCommit)) + case (r, d: DATA_WAIT_FOR_CHANNEL_READY) => r.modify(_.waitForChannelReady).using(updateMainBalance(d.commitments.localCommit)) case (r, d: DATA_NORMAL) => r.modify(_.normal).using(updateMainAndHtlcBalance(d.commitments, knownPreimages)) case (r, d: DATA_SHUTDOWN) => r.modify(_.shutdown).using(updateMainAndHtlcBalance(d.commitments, knownPreimages)) case (r, d: DATA_NEGOTIATING) => r.modify(_.negotiating).using(updateMainBalance(d.commitments.localCommit)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala index cd672f27a4..0fb652c107 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala @@ -44,7 +44,7 @@ object Monitoring { object OffchainStates { val waitForFundingConfirmed = "waitForFundingConfirmed" - val waitForFundingLocked = "waitForFundingLocked" + val waitForChannelReady = "waitForChannelReady" val normal = "normal" val shutdown = "shutdown" val negotiating = "negotiating" 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 536f205539..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 @@ -25,7 +25,7 @@ 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._ @@ -136,8 +136,8 @@ object ZmqWatcher { /** This event is sent when a [[WatchSpentBasic]] condition is met. */ sealed trait WatchSpentBasicTriggered extends WatchTriggered - case class WatchExternalChannelSpent(replyTo: ActorRef[WatchExternalChannelSpentTriggered], txId: ByteVector32, outputIndex: Int, shortChannelId: ShortChannelId) extends WatchSpentBasic[WatchExternalChannelSpentTriggered] - case class WatchExternalChannelSpentTriggered(shortChannelId: ShortChannelId) extends WatchSpentBasicTriggered + case class WatchExternalChannelSpent(replyTo: ActorRef[WatchExternalChannelSpentTriggered], txId: ByteVector32, outputIndex: Int, shortChannelId: RealShortChannelId) extends WatchSpentBasic[WatchExternalChannelSpentTriggered] + case class WatchExternalChannelSpentTriggered(shortChannelId: RealShortChannelId) extends WatchSpentBasicTriggered case class WatchFundingSpent(replyTo: ActorRef[WatchFundingSpentTriggered], txId: ByteVector32, outputIndex: Int, hints: Set[ByteVector32]) extends WatchSpent[WatchFundingSpentTriggered] case class WatchFundingSpentTriggered(spendingTx: Transaction) extends WatchSpentTriggered diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala index 07d58d7c12..a01b2fcf75 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -49,7 +49,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax channelType match { case ChannelTypes.Standard | ChannelTypes.StaticRemoteKey => proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate - case ChannelTypes.AnchorOutputs | ChannelTypes.AnchorOutputsZeroFeeHtlcTx => + case ChannelTypes.AnchorOutputs | _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => // when using anchor outputs, we allow any feerate: fees will be set with CPFP and RBF at broadcast time false } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala index 3309037414..0b7825aef4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala @@ -104,10 +104,10 @@ object BlockchainWatchdog { case CheckLatestHeaders(blockHeight) => val id = UUID.randomUUID() if (headersOverDnsEnabled) { - context.spawn(HeadersOverDns(nodeParams.chainHash, blockHeight), s"${HeadersOverDns.Source}-${blockHeight.toLong}-$id") ! HeadersOverDns.CheckLatestHeaders(context.self) + context.spawn(HeadersOverDns(nodeParams.chainHash, blockHeight), s"${HeadersOverDns.Source}-${blockHeight}-$id") ! HeadersOverDns.CheckLatestHeaders(context.self) } explorers.foreach { explorer => - context.spawn(ExplorerApi(nodeParams.chainHash, blockHeight, explorer), s"${explorer.name}-${blockHeight.toLong}-$id") ! ExplorerApi.CheckLatestHeaders(context.self) + context.spawn(ExplorerApi(nodeParams.chainHash, blockHeight, explorer), s"${explorer.name}-${blockHeight}-$id") ! ExplorerApi.CheckLatestHeaders(context.self) } Behaviors.same case headers@LatestHeaders(blockHeight, blockHeaders, source) => 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 b77a4c1797..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 @@ -18,13 +18,13 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, PossiblyHarmful} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, Alias, MilliSatoshi, RealShortChannelId, ShortChannelId, UInt64} import scodec.bits.ByteVector import java.util.UUID @@ -53,7 +53,7 @@ case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState case object WAIT_FOR_FUNDING_CREATED extends ChannelState case object WAIT_FOR_FUNDING_SIGNED extends ChannelState case object WAIT_FOR_FUNDING_CONFIRMED extends ChannelState -case object WAIT_FOR_FUNDING_LOCKED extends ChannelState +case object WAIT_FOR_CHANNEL_READY extends ChannelState case object NORMAL extends ChannelState case object SHUTDOWN extends ChannelState case object NEGOTIATING extends ChannelState @@ -85,7 +85,9 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, remoteInit: Init, channelFlags: ChannelFlags, channelConfig: ChannelConfig, - channelType: SupportedChannelType) + channelType: SupportedChannelType) { + require(!(channelType.features.contains(Features.ScidAlias) && channelFlags.announceChannel), "option_scid_alias is not compatible with public channels") +} case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, @@ -422,12 +424,37 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, fundingTx: Option[Transaction], waitingSince: BlockHeight, // how long have we been waiting for the funding tx to confirm - deferred: Option[FundingLocked], + deferred: Option[ChannelReady], lastSent: Either[FundingCreated, FundingSigned]) extends PersistentChannelData -final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked) extends PersistentChannelData +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 } +} + +/** + * Short identifiers for the channel + * + * @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]] 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]] + */ +case class ShortIds(real: RealScidStatus, + localAlias: Alias, + remoteAlias_opt: Option[Alias]) final case class DATA_NORMAL(commitments: Commitments, - shortChannelId: ShortChannelId, - buried: Boolean, + shortIds: ShortIds, channelAnnouncement: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, localShutdown: Option[Shutdown], 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 236acffead..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, 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. @@ -44,13 +44,32 @@ case class ChannelRestored(channel: ActorRef, channelId: ByteVector32, peer: Act case class ChannelIdAssigned(channel: ActorRef, remoteNodeId: PublicKey, temporaryChannelId: ByteVector32, channelId: ByteVector32) extends ChannelEvent -case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, previousShortChannelId: Option[ShortChannelId]) extends ChannelEvent - -case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: AbstractCommitments) extends ChannelEvent +/** + * This event will be sent whenever a new scid is assigned to the channel, be it a real, local alias or remote alias. + */ +case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey) extends ChannelEvent + +case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: AbstractCommitments) extends ChannelEvent { + /** + * 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] = { + 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) + } + } +} -case class ChannelUpdateParametersChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, channelUpdate: ChannelUpdate) extends ChannelEvent +case class ChannelUpdateParametersChanged(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, channelUpdate: ChannelUpdate) extends ChannelEvent -case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey) extends ChannelEvent +case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey) extends ChannelEvent case class ChannelStateChanged(channel: ActorRef, channelId: ByteVector32, peer: ActorRef, remoteNodeId: PublicKey, previousState: ChannelState, currentState: ChannelState, commitments_opt: Option[AbstractCommitments]) extends ChannelEvent @@ -66,7 +85,7 @@ case class TransactionPublished(channelId: ByteVector32, remoteNodeId: PublicKey case class TransactionConfirmed(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction) extends ChannelEvent // NB: this event is only sent when the channel is available. -case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, commitments: AbstractCommitments) extends ChannelEvent +case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, commitments: AbstractCommitments) extends ChannelEvent case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: PersistentChannelData) extends ChannelEvent 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 5ef17a4a11..7bd79bb98e 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 @@ -17,6 +17,7 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.{Feature, FeatureSupport, Features, InitFeature} import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature} /** @@ -32,7 +33,7 @@ case class ChannelFeatures(features: Set[PermanentChannelFeature]) { val channelType: SupportedChannelType = { if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - ChannelTypes.AnchorOutputsZeroFeeHtlcTx + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = features.contains(Features.ScidAlias), zeroConf = features.contains(Features.ZeroConf)) } else if (hasFeature(Features.AnchorOutputs)) { ChannelTypes.AnchorOutputs } else if (hasFeature(Features.StaticRemoteKey)) { @@ -105,11 +106,16 @@ object ChannelTypes { override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs" } - case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { - override def features: Set[ChannelTypeFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) + case class AnchorOutputsZeroFeeHtlcTx(scidAlias: Boolean, zeroConf: Boolean) extends SupportedChannelType { + override def features: Set[ChannelTypeFeature] = Set( + if (scidAlias) Some(Features.ScidAlias) else None, + if (zeroConf) Some(Features.ZeroConf) else None, + Some(Features.StaticRemoteKey), + Some(Features.AnchorOutputsZeroFeeHtlcTx) + ).flatten override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat - override def toString: String = "anchor_outputs_zero_fee_htlc_tx" + override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" } case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { override def features: Set[InitFeature] = featureBits.activated.keySet @@ -117,7 +123,14 @@ object ChannelTypes { } // @formatter:on - private val features2ChannelType: Map[Features[_ <: InitFeature], SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx) + private val features2ChannelType: Map[Features[_ <: InitFeature], SupportedChannelType] = Set( + Standard, + StaticRemoteKey, + AnchorOutputs, + AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), + AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true), + AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false), + AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)) .map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) .toMap @@ -125,12 +138,14 @@ object ChannelTypes { def fromFeatures(features: Features[InitFeature]): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) /** Pick the channel type based on local and remote feature bits, as defined by the spec. */ - def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): SupportedChannelType = { - if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) { - AnchorOutputsZeroFeeHtlcTx - } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) { + def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): SupportedChannelType = { + def canUse(feature: InitFeature) = Features.canUseFeature(localFeatures, remoteFeatures, feature) + + if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) { + AnchorOutputsZeroFeeHtlcTx(scidAlias = canUse(Features.ScidAlias) && !announceChannel, zeroConf = canUse(Features.ZeroConf)) // alias feature is incompatible with public channel + } else if (canUse(Features.AnchorOutputs)) { AnchorOutputs - } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) { + } else if (canUse(Features.StaticRemoteKey)) { StaticRemoteKey } else { Standard diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 00d0b26989..2294a09a6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -915,7 +915,7 @@ object Commitments { case _: CommitSig => s"sig" case _: RevokeAndAck => s"rev" case _: Error => s"err" - case _: FundingLocked => s"funding_locked" + case _: ChannelReady => s"channel_ready" case _ => "???" } 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 bde048e81b..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 @@ -56,7 +56,7 @@ object Helpers { remoteParams = data.commitments.remoteParams.copy(initFeatures = remoteInit.features)) data match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = commitments1) - case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = commitments1) + case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1) case d: DATA_NORMAL => d.copy(commitments = commitments1) case d: DATA_SHUTDOWN => d.copy(commitments = commitments1) case d: DATA_NEGOTIATING => d.copy(commitments = commitments1) @@ -65,22 +65,6 @@ object Helpers { } } - /** - * Returns the number of confirmations needed to safely handle the funding transaction, - * we make sure the cumulative block reward largely exceeds the channel size. - * - * @param fundingSatoshis funding amount of the channel - * @return number of confirmations needed - */ - def minDepthForFunding(channelConf: ChannelConf, fundingSatoshis: Satoshi): Long = fundingSatoshis match { - case funding if funding <= Channel.MAX_FUNDING => 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) - } - def extractShutdownScript(channelId: ByteVector32, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { val canUseUpfrontShutdownScript = Features.canUseFeature(localFeatures, remoteFeatures, Features.UpfrontShutdownScript) val canUseAnySegwit = Features.canUseFeature(localFeatures, remoteFeatures, Features.ShutdownAnySegwit) @@ -164,10 +148,10 @@ object Helpers { case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) => // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` return Left(MissingChannelType(open.temporaryChannelId)) - case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures) => + case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, open.channelFlags.announceChannel) => // If we have overridden the default channel type, but they didn't support explicit channel type negotiation, // we need to abort because they expect a different channel type than what we offered. - return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures))) + return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, open.channelFlags.announceChannel))) case _ => // we agree on channel type } @@ -201,6 +185,29 @@ object Helpers { extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } + /** + * The general rule is that we use remote_alias for our channel_update until the channel is publicly announced, and + * then we use the real scid. + * + * Private channels are handled like public channels that have not yet been announced, there is no special case. + * + * Decision tree: + * - received remote_alias from peer + * - 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): spec violation, our peer MUST send an alias when using zero-conf + */ + 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): 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 * other nodes. @@ -226,7 +233,7 @@ object Helpers { remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw } - def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = { + def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: RealShortChannelId): AnnouncementSignatures = { val features = Features.empty[Feature] // empty features for now val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) val witness = Announcements.generateChannelAnnouncementWitness( @@ -278,6 +285,37 @@ object Helpers { object Funding { + /** + * As funder we trust ourselves to not double spend funding txs: we could always use a zero-confirmation watch, + * but we need a scid to send the initial channel_update and remote may not provide an alias. That's why we always + * 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): Option[Long] = { + if (channelFeatures.hasFeature(Features.ZeroConf)) { + None + } else { + Some(1) + } + } + + /** + * Returns the number of confirmations needed to safely handle the funding transaction, + * we make sure the cumulative block reward largely exceeds the channel size. + * + * @param fundingSatoshis funding amount of the channel + * @return number of confirmations needed, if any + */ + 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 + Some(channelConf.minDepthBlocks.max(blocksToReachFunding)) + } + def makeFundingInputInfo(fundingTxId: ByteVector32, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = { val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript)) 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 8e949012dc..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 @@ -47,13 +47,16 @@ class Register extends Actor with ActorLogging { case ChannelIdAssigned(channel, remoteNodeId, temporaryChannelId, channelId) => context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds, channelsTo + (channelId -> remoteNodeId) - temporaryChannelId) - case ShortChannelIdAssigned(_, channelId, shortChannelId, _) => - context become main(channels, shortIds + (shortChannelId -> channelId), channelsTo) + 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_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) case Terminated(actor) if channels.values.toSet.contains(actor) => val channelId = channels.find(_._2 == actor).get._1 - val shortChannelId = shortIds.find(_._2 == channelId).map(_._1).getOrElse(ShortChannelId(0L)) - context become main(channels - channelId, shortIds - shortChannelId, channelsTo - channelId) + val shortChannelIds = shortIds.collect { case (key, value) if value == channelId => key } + context become main(channels - channelId, shortIds -- shortChannelIds, channelsTo - channelId) case Symbol("channels") => sender() ! channels 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 adc691063d..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 @@ -31,7 +31,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.Commitments.PostRevocationAction import fr.acinq.eclair.channel.Helpers.Syncing.SyncResult -import fr.acinq.eclair.channel.Helpers.{Closing, Syncing, getRelayFees} +import fr.acinq.eclair.channel.Helpers.{Closing, Syncing, getRelayFees, scidForChannelUpdate} import fr.acinq.eclair.channel.Monitoring.Metrics.ProcessMessage import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} import fr.acinq.eclair.channel._ @@ -299,7 +299,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case normal: DATA_NORMAL => watchFundingTx(data.commitments) - context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.channelUpdate.shortChannelId, None)) + context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.shortIds, remoteNodeId)) // we check the configuration because the values for channel_update may have changed while eclair was down val fees = getRelayFees(nodeParams, remoteNodeId, data.commitments) @@ -355,7 +355,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendAdd(d.commitments, c, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) match { case Right((commitments1, add)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending add case Left(cause) => handleAddHtlcCommandError(c, cause, Some(d.channelUpdate)) } @@ -370,7 +370,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendFulfill(d.commitments, c) match { case Right((commitments1, fulfill)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fulfill case Left(cause) => // we acknowledge the command right away in case of failure @@ -390,7 +390,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendFail(d.commitments, c, nodeParams.privateKey) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail case Left(cause) => // we acknowledge the command right away in case of failure @@ -401,7 +401,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendFailMalformed(d.commitments, c) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail case Left(cause) => // we acknowledge the command right away in case of failure @@ -424,7 +424,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendFee(d.commitments, c, nodeParams.onChainFeeConf) match { case Right((commitments1, fee)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fee case Left(cause) => handleCommandError(cause, c) } @@ -481,7 +481,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } if (d.commitments.availableBalanceForSend != commitments1.availableBalanceForSend) { // we send this event only when our balance changes - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) } context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) stay() using d.copy(commitments = commitments1) storing() sending revocation @@ -613,62 +613,76 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(c: CurrentFeerates, d: DATA_NORMAL) => handleCurrentFeerate(c, d) - case Event(WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, _), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty => - val shortChannelId = ShortChannelId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt) - log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex shortChannelId=$shortChannelId") - // if final shortChannelId is different from the one we had before, we need to re-announce it - val channelUpdate = if (shortChannelId != d.shortChannelId) { - log.info(s"short channel id changed, probably due to a chain reorg: old=${d.shortChannelId} new=$shortChannelId") - // we need to re-announce this shortChannelId - context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId, Some(d.shortChannelId))) + 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)) + 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)) + } + 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, shortChannelId, 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) { + 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 + } + 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, shortChannelId)) - } else None - // we use goto() instead of stay() because we want to fire transitions - goto(NORMAL) using d.copy(shortChannelId = shortChannelId, buried = true, channelUpdate = channelUpdate) 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) - if (d.buried) { - // we are aware that the channel has reached enough confirmations - // we already had sent our announcement_signatures but we don't store them so we need to recompute it - val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, d.shortChannelId) - d.channelAnnouncement match { - case None => - require(d.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${d.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") - log.info(s"announcing channelId=${d.channelId} on the network with shortId=${d.shortChannelId}") - import d.commitments.{localParams, remoteParams} - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, fundingPubKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) - if (!Announcements.checkSigs(channelAnn)) { - handleLocalError(InvalidAnnouncementSignatures(d.channelId, remoteAnnSigs), d, Some(remoteAnnSigs)) - } else { - // we use goto() instead of stay() because we want to fire transitions - goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn)) storing() - } - case Some(_) => - // they have sent their announcement sigs, but we already have a valid channel announcement - // 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 - // TODO: we should only be nice once, current behaviour opens way to DOS, but this should be handled higher in the stack anyway - log.info("re-sending our announcement sigs") - stay() sending localAnnSigs - } - } else { - // 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 - log.debug("received remote announcement signatures, delaying") - context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) - stay() + d.shortIds.real match { + case RealScidStatus.Final(realScid) => + // we are aware that the channel has reached enough confirmations + // we already had sent our announcement_signatures but we don't store them so we need to recompute it + val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, realScid) + d.channelAnnouncement match { + case None => + 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 d.commitments.{localParams, remoteParams} + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, fundingPubKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) + if (!Announcements.checkSigs(channelAnn)) { + handleLocalError(InvalidAnnouncementSignatures(d.channelId, remoteAnnSigs), d, Some(remoteAnnSigs)) + } else { + // we generate a new channel_update because the scid used may change if we were previously using an alias + val scidForChannelUpdate = Helpers.scidForChannelUpdate(Some(channelAnn), d.shortIds) + val channelUpdate = 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)) + // we use goto() instead of stay() because we want to fire transitions + goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate) storing() + } + case Some(_) => + // they have sent their announcement sigs, but we already have a valid channel announcement + // 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 + // TODO: we should only be nice once, current behaviour opens way to DOS, but this should be handled higher in the stack anyway + log.info("re-sending our announcement sigs") + stay() sending localAnnSigs + } + case _ => + // 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 + log.debug("received remote announcement signatures, delaying") + context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) + stay() } case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) log.info(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) @@ -677,7 +691,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(BroadcastChannelUpdate(reason), d: DATA_NORMAL) => val age = TimestampSecond.now() - d.channelUpdate.timestamp - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) reason match { case Reconnected if d.commitments.announceChannel && Announcements.areSame(channelUpdate1, d.channelUpdate) && age < REFRESH_CHANNEL_UPDATE_INTERVAL => // we already sent an identical channel_update not long ago (flapping protection in case we keep being disconnected/reconnected) @@ -701,7 +715,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // if we have pending unsigned htlcs, then we cancel them and generate an update with the disabled flag set, that will be returned to the sender in a temporary channel failure val d1 = if (d.commitments.localChanges.proposed.collectFirst { case add: UpdateAddHtlc => add }.isDefined) { log.debug("updating channel_update announcement (reason=disabled)") - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) // NB: the htlcs stay() in the commitments.localChange, they will be cleaned up after reconnection d.commitments.localChanges.proposed.collect { case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), add, HtlcResult.DisconnectedBeforeSigned(channelUpdate1)) @@ -714,7 +728,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d) - case Event(_: FundingLocked, _: DATA_NORMAL) => stay() // will happen after a reconnection if no updates were ever committed to the channel + case Event(_: ChannelReady, _: DATA_NORMAL) => stay() // will happen after a reconnection if no updates were ever committed to the channel }) @@ -1307,21 +1321,22 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val when(SYNCING)(handleExceptions { case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => val minDepth = if (d.commitments.localParams.isInitiator) { - nodeParams.channelConf.minDepthBlocks + Helpers.Funding.minDepthFunder(d.commitments.channelFeatures) } else { // when we're not the channel initiator we scale the min_depth confirmations depending on the funding amount - Helpers.minDepthForFunding(nodeParams.channelConf, d.commitments.commitInput.txOut.amount) + 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_FUNDING_LOCKED) => - log.debug("re-sending fundingLocked") + case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => + log.debug("re-sending channelReady") val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) - goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked + val channelReady = ChannelReady(d.commitments.channelId, nextPerCommitmentPoint) + goto(WAIT_FOR_CHANNEL_READY) sending channelReady case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => Syncing.checkSync(keyManager, d, channelReestablish) match { @@ -1332,12 +1347,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // normal case, our data is up-to-date if (channelReestablish.nextLocalCommitmentNumber == 1 && d.commitments.localCommit.index == 0) { - // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit funding_locked, otherwise it MUST NOT - log.debug("re-sending fundingLocked") + // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT + log.debug("re-sending channelReady") val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) - sendQueue = sendQueue :+ fundingLocked + val channelReady = ChannelReady(d.commitments.channelId, nextPerCommitmentPoint) + sendQueue = sendQueue :+ channelReady } // we may need to retransmit updates and/or commit_sig and/or revocation @@ -1365,24 +1380,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val sendQueue = sendQueue :+ localShutdown } - if (!d.buried) { - // 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) - blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) - } else { - // channel has been buried enough, should we (re)send our announcement sigs? - d.channelAnnouncement match { - case None if !d.commitments.announceChannel => - // that's a private channel, nothing to do - () - case None => + d.shortIds.real match { + case RealScidStatus.Final(realShortChannelId) => + // should we (re)send our announcement sigs? + if (d.commitments.announceChannel && d.channelAnnouncement.isEmpty) { // BOLT 7: a node SHOULD retransmit the announcement_signatures message if it has not received an announcement_signatures message - val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, d.shortChannelId) + val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, realShortChannelId) sendQueue = sendQueue :+ localAnnSigs - case Some(_) => - // channel was already announced, nothing to do - () - } + } + case _ => + // 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) + blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) } if (d.commitments.announceChannel) { @@ -1441,14 +1450,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } // This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send - // a channel_reestablish when reconnecting a channel that recently got confirmed, and instead send a funding_locked + // a channel_reestablish when reconnecting a channel that recently got confirmed, and instead send a channel_ready // first and then go silent. This is due to a race condition on their side, so we trigger a reconnection, hoping that // we will eventually receive their channel_reestablish. - case Event(_: FundingLocked, d) => - log.warning("received funding_locked before channel_reestablish (known lnd bug): disconnecting...") + case Event(_: ChannelReady, d) => + log.warning("received channel_ready before channel_reestablish (known lnd bug): disconnecting...") // NB: we use a small delay to ensure we've sent our warning before disconnecting. context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId)) - stay() sending Warning(d.channelId, "spec violation: you sent funding_locked before channel_reestablish") + stay() sending Warning(d.channelId, "spec violation: you sent channel_ready before channel_reestablish") // This handler is a workaround for an issue in lnd similar to the one above: they sometimes send announcement_signatures // before channel_reestablish, which is a minor spec violation. It doesn't halt the channel, we can simply postpone @@ -1593,52 +1602,54 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val cancelTimer(RevocationTimeout.toString) } - // if channel is private, we send the channel_update directly to remote - // they need it "to learn the other end's forwarding parameters" (BOLT 7) - (state, nextState, stateData, nextStateData) match { - case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && !d1.buried && d2.buried => - // for a private channel, when the tx was just buried we need to send the channel_update to our peer (even if it didn't change) - send(d2.channelUpdate) - case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && d2.buried => - // otherwise if we're coming back online, we rebroadcast the latest channel_update - // this makes sure that if the channel_update was missed, we have a chance to re-send it - send(d2.channelUpdate) - case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && d1.channelUpdate != d2.channelUpdate && d2.buried => - // otherwise, we only send it when it is different, and tx is already buried - send(d2.channelUpdate) - case _ => () - } - sealed trait EmitLocalChannelEvent - case class EmitLocalChannelUpdate(d: DATA_NORMAL) extends EmitLocalChannelEvent + /* + * This event is for: + * - the router: so it knows about this channel to find routes + * - the relayer: so it learns about the channel real scid/alias and can route to it + * - the peer: so they can "learn the other end's forwarding parameters" (BOLT 7) + */ + case class EmitLocalChannelUpdate(reason: String, d: DATA_NORMAL, sendToPeer: Boolean) extends EmitLocalChannelEvent + /* + * When a channel that could previously be used to relay payments starts closing, we advertise the fact that this + * channel can't be used for payments anymore. If the channel is private we don't really need to tell the + * counterparty because it is already aware that the channel is being closed + */ case class EmitLocalChannelDown(d: DATA_NORMAL) extends EmitLocalChannelEvent + // 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(d)) - case (WAIT_FOR_FUNDING_LOCKED, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate(d)) - case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) - case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) - case (NORMAL, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) - case (OFFLINE, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) - // When a channel that could previously be used to relay payments starts closing, we advertise the fact that this channel can't be used for payments anymore - // If the channel is private we don't really need to tell the counterparty because it is already aware that the channel is being closed + 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)) + case (NORMAL, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("normal->offline", d2, sendToPeer = false)) + case (OFFLINE, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("offline->offline", d2, sendToPeer = false)) case (NORMAL | SYNCING | OFFLINE, SHUTDOWN | NEGOTIATING | CLOSING | CLOSED | ERR_INFORMATION_LEAK | WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, d: DATA_NORMAL, _) => Some(EmitLocalChannelDown(d)) case _ => None } emitEvent_opt.foreach { - case EmitLocalChannelUpdate(d) => - log.info("emitting channel_update={} enabled={} ", d.channelUpdate, d.channelUpdate.channelFlags.isEnabled) - context.system.eventStream.publish(LocalChannelUpdate(self, d.channelId, d.shortChannelId, d.commitments.remoteParams.nodeId, d.channelAnnouncement, d.channelUpdate, d.commitments)) + 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")) + 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) { + send(d.channelUpdate) + } case EmitLocalChannelDown(d) => - context.system.eventStream.publish(LocalChannelDown(self, d.channelId, d.shortChannelId, d.commitments.remoteParams.nodeId)) + log.debug(s"emitting channel down event") + val lcd = LocalChannelDown(self, d.channelId, d.shortIds, d.commitments.remoteParams.nodeId) + context.system.eventStream.publish(lcd) } // When we change our channel update parameters (e.g. relay fees), we want to advertise it to other actors. (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_FUNDING_LOCKED->NORMAL - case (_: DATA_WAIT_FOR_FUNDING_LOCKED, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) + // 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 _ => () } } @@ -1784,7 +1795,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val if (d.channelUpdate.channelFlags.isEnabled) { // if the channel isn't disabled we generate a new channel_update log.info("updating channel_update announcement (reason=disabled)") - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) // then we update the state and replay the request self forward c // we use goto() to fire transitions @@ -1797,7 +1808,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } private def handleUpdateRelayFeeDisconnected(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) = { - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) log.info(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) @@ -1834,7 +1845,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val private def maybeEmitChannelUpdateChangedEvent(newUpdate: ChannelUpdate, oldUpdate_opt: Option[ChannelUpdate], d: DATA_NORMAL): Unit = { if (oldUpdate_opt.isEmpty || !Announcements.areSameIgnoreFlags(newUpdate, oldUpdate_opt.get)) { - context.system.eventStream.publish(ChannelUpdateParametersChanged(self, d.channelId, newUpdate.shortChannelId, d.commitments.remoteNodeId, newUpdate)) + context.system.eventStream.publish(ChannelUpdateParametersChanged(self, d.channelId, d.commitments.remoteNodeId, newUpdate)) } } 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 97f71b18fc..8966b25364 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, ChannelTlv, Error, FundingCreated, FundingLocked, FundingSigned, OpenChannel, TlvStream} -import fr.acinq.eclair.{Features, 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 | |--------------- ---------------| | \/ | | /\ | @@ -80,7 +80,8 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = false, open.temporaryChannelId, open.feeratePerKw, None)) val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val minimumDepth = Helpers.minDepthForFunding(nodeParams.channelConf, open.fundingSatoshis) + val minimumDepth = Funding.minDepthFundee(nodeParams.channelConf, channelFeatures, open.fundingSatoshis) + log.info("will use fundingMinDepth={}", minimumDepth) // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. // See https://github.com/lightningnetwork/lightning-rfc/pull/714. val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty @@ -88,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, @@ -151,6 +152,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { initFeatures = remoteInit.features, shutdownScript = remoteShutdownScript) log.debug("remote params: {}", remoteParams) + log.info("remote will use fundingMinDepth={}", accept.minimumDepth) val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeerate).pipeTo(self) @@ -254,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 = Helpers.minDepthForFunding(nodeParams.channelConf, 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) + } } } @@ -294,8 +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) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - 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 @@ -313,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) => @@ -342,26 +354,30 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions { - case Event(msg: FundingLocked, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - log.info(s"received their FundingLocked, deferring message") - stay() using d.copy(deferred = Some(msg)) // no need to store, they will re-send if we get disconnected + 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") + // 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 + } 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(_) => - log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") - blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - if (!d.commitments.localParams.isInitiator) context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) - context.system.eventStream.publish(TransactionConfirmed(commitments.channelId, remoteNodeId, fundingTx)) - val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelConfig) - val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) + 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")) + context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx)) + + val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt)) + val (shortIds, channelReady) = acceptFundingTx(commitments, realScidStatus = realScidStatus) deferred.foreach(self ! _) - // this is the temporary channel id that we 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 - // (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime) - val shortChannelId = ShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt) - goto(WAIT_FOR_FUNDING_LOCKED) using DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, fundingLocked) storing() sending fundingLocked + 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) @@ -396,30 +412,37 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleRemoteError(e, d) }) - when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions { - case Event(FundingLocked(_, nextPerCommitmentPoint, _), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _)) => - // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) - blockchain ! WatchFundingDeeplyBuried(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) - context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) + when(WAIT_FOR_CHANNEL_READY)(handleExceptions { + case Event(channelReady: ChannelReady, d: DATA_WAIT_FOR_CHANNEL_READY) => + val shortIds1 = d.shortIds.copy(remoteAlias_opt = channelReady.alias_opt) + shortIds1.remoteAlias_opt.foreach { remoteAlias => + log.info("received remoteAlias={}", remoteAlias) + context.system.eventStream.publish(ShortChannelIdAssigned(self, d.commitments.channelId, shortIds = shortIds1, remoteNodeId = remoteNodeId)) + } + log.info("shortIds: real={} localAlias={} remoteAlias={}", shortIds1.real.toOption.getOrElse("none"), shortIds1.localAlias, shortIds1.remoteAlias_opt.getOrElse("none")) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced - val fees = getRelayFees(nodeParams, remoteNodeId, commitments) - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, shortIds1) + log.info("using shortChannelId={} for initial channel_update", scidForChannelUpdate) + val relayFees = getRelayFees(nodeParams, remoteNodeId, d.commitments) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, relayFees.feeBase, relayFees.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) - goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None, None) storing() + // 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)), shortIds1, None, initialChannelUpdate, None, None, None) storing() - case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel => + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_CHANNEL_READY) if d.commitments.announceChannel => log.debug("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(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_CHANNEL_READY) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleInformationLeak(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_CHANNEL_READY) => handleInformationLeak(tx, d) - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleRemoteError(e, d) + case Event(e: Error, d: DATA_WAIT_FOR_CHANNEL_READY) => handleRemoteError(e, d) }) } 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/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index fe836fd088..ac6003d0aa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -410,6 +410,7 @@ object TransportHandler { case class Encryptor(state: CipherState) { /** * see BOLT #8 + * {{{ * +------------------------------- * |2-byte encrypted message length| * +------------------------------- @@ -425,6 +426,7 @@ object TransportHandler { * | 16-byte MAC of the | * | lightning message | * +------------------------------- + * }}} * * @param plaintext plaintext * @return a (cipherstate, ciphertext) tuple where ciphertext is encrypted according to BOLT #8 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index e112fea7b6..a1f63f57ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -108,7 +108,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case e: ChannelStateChanged => // NB: order matters! e match { - case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_FUNDING_LOCKED, NORMAL, Some(commitments: Commitments)) => + case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_CHANNEL_READY, NORMAL, Some(commitments: Commitments)) => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Created).increment() val event = ChannelEvent.EventType.Created auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.capacity, commitments.localParams.isInitiator, !commitments.announceChannel, event)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index 9b9d5056e2..1fa137dcf8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -2,6 +2,7 @@ package fr.acinq.eclair.db import com.google.common.util.concurrent.ThreadFactoryBuilder import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.Databases.{FileBackup, PostgresDatabases, SqliteDatabases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent @@ -113,17 +114,17 @@ case class DualNetworkDb(primary: NetworkDb, secondary: NetworkDb) extends Netwo primary.removeChannels(shortChannelIds) } - override def listChannels(): SortedMap[ShortChannelId, Router.PublicChannel] = { + override def listChannels(): SortedMap[RealShortChannelId, Router.PublicChannel] = { runAsync(secondary.listChannels()) primary.listChannels() } - override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = { + override def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit = { runAsync(secondary.addToPruned(shortChannelIds)) primary.addToPruned(shortChannelIds) } - override def removeFromPruned(shortChannelId: ShortChannelId): Unit = { + override def removeFromPruned(shortChannelId: RealShortChannelId): Unit = { runAsync(secondary.removeFromPruned(shortChannelId)) primary.removeFromPruned(shortChannelId) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala index 80973d1c2c..ec3031fd0a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.router.Router.PublicChannel import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} @@ -44,11 +45,11 @@ trait NetworkDb { def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit - def listChannels(): SortedMap[ShortChannelId, PublicChannel] + def listChannels(): SortedMap[RealShortChannelId, PublicChannel] - def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit + def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit - def removeFromPruned(shortChannelId: ShortChannelId): Unit + def removeFromPruned(shortChannelId: RealShortChannelId): Unit def isPruned(shortChannelId: ShortChannelId): Boolean diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala index afe716350e..c44ca7c892 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.db.pg import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.NetworkDb @@ -199,11 +200,11 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } - override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Postgres) { + override def listChannels(): SortedMap[RealShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Postgres) { inTransaction { pg => using(pg.createStatement()) { statement => statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM network.public_channels") - .foldLeft(SortedMap.empty[ShortChannelId, PublicChannel]) { (m, rs) => + .foldLeft(SortedMap.empty[RealShortChannelId, PublicChannel]) { (m, rs) => val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val txId = ByteVector32.fromValidHex(rs.getString("txid")) val capacity = rs.getLong("capacity_sat") @@ -223,12 +224,13 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { })")) { statement => shortChannelIds + .map(_.toLong) .grouped(batchSize) .foreach { group => - val padded = group.toArray.padTo(batchSize, ShortChannelId(0L)) + val padded = group.toArray.padTo(batchSize, 0L) for (i <- 0 until batchSize) { - statement.setLong(1 + i, padded(i).toLong) // index for jdbc parameters starts at 1 + statement.setLong(1 + i, padded(i)) // index for jdbc parameters starts at 1 } statement.executeUpdate() } @@ -236,7 +238,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } - override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Postgres) { + override def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Postgres) { inTransaction { pg => using(pg.prepareStatement("INSERT INTO network.pruned_channels VALUES (?) ON CONFLICT DO NOTHING")) { statement => @@ -249,7 +251,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } - override def removeFromPruned(shortChannelId: ShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Postgres) { + override def removeFromPruned(shortChannelId: RealShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Postgres) { inTransaction { pg => using(pg.prepareStatement(s"DELETE FROM network.pruned_channels WHERE short_channel_id=?")) { statement => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index 844e886aae..b05e743ffb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.db.sqlite import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.NetworkDb @@ -124,10 +125,10 @@ class SqliteNetworkDb(val sqlite: Connection) extends NetworkDb with Logging { } } - override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Sqlite) { + override def listChannels(): SortedMap[RealShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Sqlite) { using(sqlite.createStatement()) { statement => statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM channels") - .foldLeft(SortedMap.empty[ShortChannelId, PublicChannel]) { (m, rs) => + .foldLeft(SortedMap.empty[RealShortChannelId, PublicChannel]) { (m, rs) => val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val txId = ByteVector32.fromValidHex(rs.getString("txid")) val capacity = rs.getLong("capacity_sat") @@ -142,18 +143,19 @@ class SqliteNetworkDb(val sqlite: Connection) extends NetworkDb with Logging { val batchSize = 100 using(sqlite.prepareStatement(s"DELETE FROM channels WHERE short_channel_id IN (${List.fill(batchSize)("?").mkString(",")})")) { statement => shortChannelIds + .map(_.toLong) .grouped(batchSize) .foreach { group => - val padded = group.toArray.padTo(batchSize, ShortChannelId(0L)) + val padded = group.toArray.padTo(batchSize, 0L) for (i <- 0 until batchSize) { - statement.setLong(1 + i, padded(i).toLong) // index for jdbc parameters starts at 1 + statement.setLong(1 + i, padded(i)) // index for jdbc parameters starts at 1 } statement.executeUpdate() } } } - override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Sqlite) { + override def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Sqlite) { using(sqlite.prepareStatement("INSERT OR IGNORE INTO pruned VALUES (?)"), inTransaction = true) { statement => shortChannelIds.foreach(shortChannelId => { statement.setLong(1, shortChannelId.toLong) @@ -163,7 +165,7 @@ class SqliteNetworkDb(val sqlite: Connection) extends NetworkDb with Logging { } } - override def removeFromPruned(shortChannelId: ShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Sqlite) { + override def removeFromPruned(shortChannelId: RealShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Sqlite) { using(sqlite.prepareStatement(s"DELETE FROM pruned WHERE short_channel_id=?")) { statement => statement.setLong(1, shortChannelId.toLong) statement.executeUpdate() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 0fa63ef102..a37270e758 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -146,14 +146,15 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA } else { val channelConfig = ChannelConfig.standard // If a channel type was provided, we directly use it instead of computing it based on local and remote features. - val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) + val channelFlags = c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags) + val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, channelFlags.announceChannel)) val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingSatoshis, origin_opt = Some(sender())) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) val temporaryChannelId = randomBytes32() val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with type=$channelType fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, channelFlags, channelConfig, channelType) stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) } @@ -165,12 +166,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA // remote explicitly specifies a channel type: we check whether we want to allow it case Some(remoteChannelType) => ChannelTypes.areCompatible(d.localFeatures, remoteChannelType) match { case Some(acceptedChannelType) => Right(acceptedChannelType) - case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures), remoteChannelType)) + case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, msg.channelFlags.announceChannel), remoteChannelType)) } // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.ChannelType) => Left(MissingChannelType(msg.temporaryChannelId)) // remote doesn't specify a channel type: we use spec-defined defaults - case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) + case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, msg.channelFlags.announceChannel)) } chosenChannelType match { case Right(channelType) => @@ -465,9 +466,10 @@ object Peer { case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[ChannelFlags], timeout_opt: Option[Timeout]) extends PossiblyHarmful { - require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis") - require(fundingSatoshis >= 0.sat, s"fundingSatoshis must be positive") - require(pushMsat >= 0.msat, s"pushMsat must be positive") + require(!(channelType_opt.exists(_.features.contains(Features.ScidAlias)) && channelFlags.exists(_.announceChannel)), "option_scid_alias is not compatible with public channels") + require(pushMsat <= fundingSatoshis, "pushMsat must be less or equal to fundingSatoshis") + require(fundingSatoshis >= 0.sat, "fundingSatoshis must be positive") + require(pushMsat >= 0.msat, "pushMsat must be positive") fundingTxFeeratePerKw_opt.foreach(feeratePerKw => require(feeratePerKw >= FeeratePerKw.MinimumFeeratePerKw, s"fee rate $feeratePerKw is below minimum ${FeeratePerKw.MinimumFeeratePerKw} rate/kw")) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 5c44a25968..a03abfb410 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -36,7 +36,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, Alias, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature, channel} import org.json4s import org.json4s.JsonAST._ import org.json4s.jackson.Serialization @@ -443,10 +443,14 @@ private[json] case class MessageReceivedJson(pathId: Option[ByteVector], encoded object OnionMessageReceivedSerializer extends ConvertClassSerializer[OnionMessages.ReceiveMessage](m => MessageReceivedJson(m.pathId, m.finalPayload.replyPath.map(route => blindedRouteCodec.encode(route.blindedRoute).require.bytes.toHex), m.finalPayload.replyPath.map(_.blindedRoute), m.finalPayload.records.unknown.map(tlv => tlv.tag.toString -> tlv.value).toMap)) // @formatter:on -case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints { - val reverse: Map[String, Class[_]] = custom.map(_.swap) +// @formatter:off +/** this is cosmetic, just to not have a '_opt' field in json, which will only appear if the option is defined anyway */ +private case class ShortIdsJson(real: RealScidStatus, localAlias: Alias, remoteAlias: Option[ShortChannelId]) +object ShortIdsSerializer extends ConvertClassSerializer[ShortIds](s => ShortIdsJson(s.real, s.localAlias, s.remoteAlias_opt)) +// @formatter:on - override def typeHintFieldName: String = "type" +case class CustomTypeHints(custom: Map[Class[_], String], override val typeHintFieldName: String = "type") extends TypeHints { + val reverse: Map[String, Class[_]] = custom.map(_.swap) override val hints: List[Class[_]] = custom.keys.toList @@ -495,7 +499,7 @@ object CustomTypeHints { classOf[DATA_WAIT_FOR_FUNDING_INTERNAL], classOf[DATA_WAIT_FOR_FUNDING_CREATED], classOf[DATA_WAIT_FOR_FUNDING_SIGNED], - classOf[DATA_WAIT_FOR_FUNDING_LOCKED], + classOf[DATA_WAIT_FOR_CHANNEL_READY], classOf[DATA_WAIT_FOR_FUNDING_CONFIRMED], classOf[DATA_NORMAL], classOf[DATA_SHUTDOWN], @@ -503,6 +507,12 @@ object CustomTypeHints { classOf[DATA_CLOSING], classOf[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] ), typeHintFieldName = "type") + + val realScidStatuses: CustomTypeHints = CustomTypeHints(Map( + classOf[RealScidStatus.Unknown.type] -> "unknown", + classOf[RealScidStatus.Temporary] -> "temporary", + classOf[RealScidStatus.Final] -> "final", + ), typeHintFieldName = "status") } object JsonSerializers { @@ -516,6 +526,7 @@ object JsonSerializers { CustomTypeHints.onionMessageEvent + CustomTypeHints.channelSources + CustomTypeHints.channelStates + + CustomTypeHints.realScidStatuses + ByteVectorSerializer + ByteVector32Serializer + ByteVector64Serializer + @@ -562,6 +573,7 @@ object JsonSerializers { GlobalBalanceSerializer + PeerInfoSerializer + PaymentFailedSummarySerializer + - OnionMessageReceivedSerializer + OnionMessageReceivedSerializer + + ShortIdsSerializer } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala index cf87799532..8f5852e22b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -16,12 +16,11 @@ package fr.acinq -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} import fr.acinq.eclair.crypto.StrongRandom import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.wire.protocol.ChannelUpdate import scodec.Attempt import scodec.bits.{BitVector, ByteVector} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index bcec91b1d0..3475b97f74 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -52,7 +52,8 @@ object ChannelRelay { Behaviors.withMdc(Logs.mdc( category_opt = Some(Logs.LogCategory.PAYMENT), parentPaymentId_opt = Some(relayId), // for a channel relay, parent payment id = relay id - paymentHash_opt = Some(r.add.paymentHash))) { + paymentHash_opt = Some(r.add.paymentHash), + nodeAlias_opt = Some(nodeParams.alias))) { context.self ! DoRelay new ChannelRelay(nodeParams, register, channels, r, context).relay(Seq.empty) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index f874479681..741f508970 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -46,11 +46,13 @@ object ChannelRelayer { private[payment] case class WrappedLocalChannelUpdate(localChannelUpdate: LocalChannelUpdate) extends Command private[payment] case class WrappedLocalChannelDown(localChannelDown: LocalChannelDown) extends Command private[payment] case class WrappedAvailableBalanceChanged(availableBalanceChanged: AvailableBalanceChanged) extends Command - private[payment] case class WrappedShortChannelIdAssigned(shortChannelIdAssigned: ShortChannelIdAssigned) extends Command // @formatter:on def mdc: Command => Map[String, String] = { case c: Relay => Logs.mdc(paymentHash_opt = Some(c.channelRelayPacket.add.paymentHash)) + case c: WrappedLocalChannelUpdate => Logs.mdc(channelId_opt = Some(c.localChannelUpdate.channelId)) + case c: WrappedLocalChannelDown => Logs.mdc(channelId_opt = Some(c.localChannelDown.channelId)) + case c: WrappedAvailableBalanceChanged => Logs.mdc(channelId_opt = Some(c.availableBalanceChanged.channelId)) case _ => Map.empty } @@ -63,9 +65,8 @@ object ChannelRelayer { context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelUpdate](WrappedLocalChannelUpdate)) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelDown](WrappedLocalChannelDown)) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[AvailableBalanceChanged](WrappedAvailableBalanceChanged)) - context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ShortChannelIdAssigned](WrappedShortChannelIdAssigned)) context.messageAdapter[IncomingPaymentPacket.ChannelRelayPacket](Relay) - Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT)), mdc) { + Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), nodeAlias_opt = Some(nodeParams.alias)), mdc) { Behaviors.receiveMessage { case Relay(channelRelayPacket) => val relayId = UUID.randomUUID() @@ -90,35 +91,33 @@ object ChannelRelayer { replyTo ! Relayer.OutgoingChannels(selected.toSeq) Behaviors.same - case WrappedLocalChannelUpdate(LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments)) => - context.log.debug(s"updating local channel info for channelId=$channelId shortChannelId=$shortChannelId remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) + case WrappedLocalChannelUpdate(lcu@LocalChannelUpdate(_, channelId, shortIds, remoteNodeId, _, channelUpdate, commitments)) => + context.log.debug(s"updating local channel info for channelId=$channelId realScid=${shortIds.real} localAlias=${shortIds.localAlias} remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) val prevChannelUpdate = channels.get(channelId).map(_.channelUpdate) val channel = Relayer.OutgoingChannel(remoteNodeId, channelUpdate, prevChannelUpdate, commitments) val channels1 = channels + (channelId -> channel) - val scid2channels1 = scid2channels + (channelUpdate.shortChannelId -> channelId) + val mappings = lcu.scidsForRouting.map(_ -> channelId).toMap + context.log.debug("adding mappings={} to channelId={}", mappings.keys.mkString(","), channelId) + val scid2channels1 = scid2channels ++ mappings val node2channels1 = node2channels.addOne(remoteNodeId, channelId) apply(nodeParams, register, channels1, scid2channels1, node2channels1) - case WrappedLocalChannelDown(LocalChannelDown(_, channelId, shortChannelId, remoteNodeId)) => - context.log.debug(s"removed local channel info for channelId=$channelId shortChannelId=$shortChannelId") + case WrappedLocalChannelDown(LocalChannelDown(_, channelId, shortIds, remoteNodeId)) => + context.log.debug(s"removed local channel info for channelId=$channelId localAlias=${shortIds.localAlias}") val channels1 = channels - channelId - val scid2Channels1 = scid2channels - shortChannelId + val scid2Channels1 = scid2channels - shortIds.localAlias -- shortIds.real.toOption val node2channels1 = node2channels.subtractOne(remoteNodeId, channelId) apply(nodeParams, register, channels1, scid2Channels1, node2channels1) - case WrappedAvailableBalanceChanged(AvailableBalanceChanged(_, channelId, shortChannelId, commitments)) => + case WrappedAvailableBalanceChanged(AvailableBalanceChanged(_, channelId, shortIds, commitments)) => val channels1 = channels.get(channelId) match { case Some(c: Relayer.OutgoingChannel) => - context.log.debug(s"available balance changed for channelId=$channelId shortChannelId=$shortChannelId availableForSend={} availableForReceive={}", commitments.availableBalanceForSend, commitments.availableBalanceForReceive) + context.log.debug(s"available balance changed for channelId=$channelId localAlias=${shortIds.localAlias} availableForSend={} availableForReceive={}", commitments.availableBalanceForSend, commitments.availableBalanceForReceive) channels + (channelId -> c.copy(commitments = commitments)) case None => channels // we only consider the balance if we have the channel_update } apply(nodeParams, register, channels1, scid2channels, node2channels) - case WrappedShortChannelIdAssigned(ShortChannelIdAssigned(_, channelId, shortChannelId, previousShortChannelId_opt)) => - context.log.debug(s"added new mapping shortChannelId=$shortChannelId for channelId=$channelId") - val scid2channels1 = scid2channels + (shortChannelId -> channelId) - apply(nodeParams, register, channels, scid2channels1, node2channels) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index d0917fdf4b..d35bb6c37f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -194,7 +194,7 @@ object EclairInternalsSerializer { .typecase(31, lengthPrefixedNodeAnnouncementCodec.as[NodeUpdated]) .typecase(32, publicKey.as[NodeLost]) .typecase(33, iterable(singleChannelDiscoveredCodec).as[ChannelsDiscovered]) - .typecase(34, shortchannelid.as[ChannelLost]) + .typecase(34, realshortchannelid.as[ChannelLost]) .typecase(35, iterable(lengthPrefixedChannelUpdateCodec).as[ChannelUpdatesReceived]) .typecase(36, double.as[SyncProgress]) .typecase(40, lengthPrefixedAnnouncementCodec.as[GossipDecision.Accepted]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 83b8930183..da8381840c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector @@ -28,7 +29,7 @@ import shapeless.HNil */ object Announcements { - def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[Feature], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = + def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: RealShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[Feature], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: tlvStream :: HNil)))) def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features[Feature], addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = @@ -37,7 +38,7 @@ object Announcements { def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: TimestampSecond, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], tlvStream: TlvStream[ChannelUpdateTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: tlvStream :: HNil)))) - def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[Feature]): ByteVector = + def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: RealShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[Feature]): ByteVector = if (isNode1(localNodeId, remoteNodeId)) { channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, features, TlvStream.empty) } else { @@ -46,7 +47,7 @@ object Announcements { def signChannelAnnouncement(witness: ByteVector, key: PrivateKey): ByteVector64 = Crypto.sign(witness, key) - def makeChannelAnnouncement(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, localNodeSignature: ByteVector64, remoteNodeSignature: ByteVector64, localBitcoinSignature: ByteVector64, remoteBitcoinSignature: ByteVector64): ChannelAnnouncement = { + def makeChannelAnnouncement(chainHash: ByteVector32, shortChannelId: RealShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, localNodeSignature: ByteVector64, remoteNodeSignature: ByteVector64, localBitcoinSignature: ByteVector64, remoteBitcoinSignature: ByteVector64): ChannelAnnouncement = { val (nodeId1, nodeId2, bitcoinKey1, bitcoinKey2, nodeSignature1, nodeSignature2, bitcoinSignature1, bitcoinSignature2) = if (isNode1(localNodeId, remoteNodeId)) { (localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, localNodeSignature, remoteNodeSignature, localBitcoinSignature, remoteBitcoinSignature) 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 2cf45c35e2..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 @@ -18,11 +18,11 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Btc, MilliBtc, Satoshi, SatoshiLong} -import fr.acinq.eclair._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.ChannelUpdate +import fr.acinq.eclair.{RealShortChannelId, _} import scala.annotation.tailrec import scala.collection.immutable.SortedMap @@ -310,8 +310,13 @@ object Graph { import RoutingHeuristics._ // Every edge is weighted by funding block height where older blocks add less weight. The window considered is 1 year. - val channelBlockHeight = ShortChannelId.coordinates(edge.desc.shortChannelId).blockHeight - val ageFactor = normalize(channelBlockHeight.toDouble, min = (currentBlockHeight - BLOCK_TIME_ONE_YEAR).toDouble, max = currentBlockHeight.toDouble) + val ageFactor = edge.desc.shortChannelId match { + 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 + case _: UnspecifiedShortChannelId => 1 + } // Every edge is weighted by channel capacity, larger channels add less weight val edgeMaxCapacity = edge.capacity.toMilliSatoshi @@ -610,7 +615,7 @@ object Graph { * * @param channels map of all known public channels in the network. */ - def makeGraph(channels: SortedMap[ShortChannelId, PublicChannel]): DirectedGraph = { + def makeGraph(channels: SortedMap[RealShortChannelId, PublicChannel]): DirectedGraph = { // initialize the map with the appropriate size to avoid resizing during the graph initialization val mutableMap = new mutable.HashMap[PublicKey, List[GraphEdge]](initialCapacity = channels.size + 1, mutable.HashMap.defaultLoadFactor) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala index 8d781dc7b9..f960bf8d6b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} @@ -37,7 +38,7 @@ case class SingleChannelDiscovered(ann: ChannelAnnouncement, capacity: Satoshi, case class ChannelsDiscovered(c: Iterable[SingleChannelDiscovered]) extends NetworkEvent -case class ChannelLost(shortChannelId: ShortChannelId) extends NetworkEvent +case class ChannelLost(shortChannelId: RealShortChannelId) extends NetworkEvent case class ChannelUpdatesReceived(ann: Iterable[ChannelUpdate]) extends NetworkEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 46cd566252..200a81ed2e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -71,8 +71,8 @@ object RouteCalculation { case _ => None } case Some(c: PrivateChannel) => currentNode match { - case c.nodeId1 => Some(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) - case c.nodeId2 => Some(ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1)) + case c.nodeId1 => Some(ChannelDesc(c.shortIds.localAlias, c.nodeId1, c.nodeId2)) + case c.nodeId2 => Some(ChannelDesc(c.shortIds.localAlias, c.nodeId2, c.nodeId1)) case _ => None } case None => assistedChannels.get(shortChannelId).flatMap(c => currentNode match { 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 6580c17831..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 @@ -24,7 +24,6 @@ import akka.event.Logging.MDC import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.Logs.LogCategory -import fr.acinq.eclair.ShortChannelId.outputIndex import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{ValidateResult, WatchExternalChannelSpent, WatchExternalChannelSpentTriggered} @@ -40,7 +39,6 @@ import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.wire.protocol._ -import kamon.context.Context import java.util.UUID import scala.collection.immutable.SortedMap @@ -60,6 +58,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm // we pass these to helpers classes so that they have the logging context implicit def implicitLog: DiagnosticLoggingAdapter = diagLog + context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[LocalChannelUpdate]) context.system.eventStream.subscribe(self, classOf[LocalChannelDown]) context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged]) @@ -229,14 +228,17 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm case Event(PeerRoutingMessage(peerConnection, remoteNodeId, n: NodeAnnouncement), d: Data) => stay() using Validation.handleNodeAnnouncement(d, nodeParams.db.network, Set(RemoteGossip(peerConnection, remoteNodeId)), n) - case Event(u: ChannelUpdate, d: Data) => + case Event(scia: ShortChannelIdAssigned, d) => + stay() using Validation.handleShortChannelIdAssigned(d, nodeParams.nodeId, scia) + + case Event(u: ChannelUpdate, d: Data) => // from payment lifecycle stay() using Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, Right(RemoteChannelUpdate(u, Set(LocalGossip)))) - case Event(PeerRoutingMessage(peerConnection, remoteNodeId, u: ChannelUpdate), d) => + case Event(PeerRoutingMessage(peerConnection, remoteNodeId, u: ChannelUpdate), d) => // from network (gossip or peer) stay() using Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, Right(RemoteChannelUpdate(u, Set(RemoteGossip(peerConnection, remoteNodeId))))) - case Event(lcu: LocalChannelUpdate, d: Data) => - stay() using Validation.handleLocalChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, nodeParams.nodeId, watcher, lcu) + case Event(lcu: LocalChannelUpdate, d: Data) => // from local channel + stay() using Validation.handleLocalChannelUpdate(d, nodeParams, watcher, lcu) case Event(lcd: LocalChannelDown, d: Data) => stay() using Validation.handleLocalChannelDown(d, nodeParams.nodeId, lcd) @@ -275,21 +277,21 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm override def mdc(currentMessage: Any): MDC = { val category_opt = LogCategory(currentMessage) - val remoteNodeId_opt = currentMessage match { - case s: SendChannelQuery => Some(s.remoteNodeId) - case prm: PeerRoutingMessage => Some(prm.remoteNodeId) - case lcu: LocalChannelUpdate => Some(lcu.remoteNodeId) - case _ => None + val (remoteNodeId_opt, channelId_opt) = currentMessage match { + case s: SendChannelQuery => (Some(s.remoteNodeId), None) + case prm: PeerRoutingMessage => (Some(prm.remoteNodeId), None) + case sca: ShortChannelIdAssigned => (Some(sca.remoteNodeId), Some(sca.channelId)) + case lcu: LocalChannelUpdate => (Some(lcu.remoteNodeId), Some(lcu.channelId)) + case lcd: LocalChannelDown => (Some(lcd.remoteNodeId), Some(lcd.channelId)) + case abc: AvailableBalanceChanged => (Some(abc.commitments.remoteNodeId), Some(abc.channelId)) + case _ => (None, None) } - Logs.mdc(category_opt, remoteNodeId_opt = remoteNodeId_opt, nodeAlias_opt = Some(nodeParams.alias)) + Logs.mdc(category_opt, remoteNodeId_opt = remoteNodeId_opt, channelId_opt = channelId_opt, nodeAlias_opt = Some(nodeParams.alias)) } } object Router { - val shortChannelIdKey = Context.key[ShortChannelId]("shortChannelId", ShortChannelId(0)) - val remoteNodeIdKey = Context.key[String]("remoteNodeId", "unknown") - def props(nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], initialized: Option[Promise[Done]] = None) = Props(new Router(nodeParams, watcher, initialized)) case class SearchBoundaries(maxFeeFlat: MilliSatoshi, @@ -335,7 +337,7 @@ object Router { } def apply(u: ChannelUpdate, pc: PrivateChannel): ChannelDesc = { // the least significant bit tells us if it is node1 or node2 - if (u.channelFlags.isNode1) ChannelDesc(u.shortChannelId, pc.nodeId1, pc.nodeId2) else ChannelDesc(u.shortChannelId, pc.nodeId2, pc.nodeId1) + if (u.channelFlags.isNode1) ChannelDesc(pc.shortIds.localAlias, pc.nodeId1, pc.nodeId2) else ChannelDesc(pc.shortIds.localAlias, pc.nodeId2, pc.nodeId1) } } case class ChannelMeta(balance1: MilliSatoshi, balance2: MilliSatoshi) @@ -356,8 +358,8 @@ object Router { val nodeId1: PublicKey = ann.nodeId1 val nodeId2: PublicKey = ann.nodeId2 - def shortChannelId: ShortChannelId = ann.shortChannelId - def channelId: ByteVector32 = toLongId(fundingTxid.reverse, outputIndex(ann.shortChannelId)) + def shortChannelId: RealShortChannelId = ann.shortChannelId + def channelId: ByteVector32 = toLongId(fundingTxid.reverse, ann.shortChannelId.outputIndex) def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (u.channelFlags.isNode1) ann.nodeId1 else ann.nodeId2 def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (u.channelFlags.isNode1) update_1_opt else update_2_opt def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] = if (u.channelFlags.isNode1) meta_opt.map(_.balance1) else meta_opt.map(_.balance2) @@ -372,7 +374,7 @@ object Router { case Right(rcu) => updateChannelUpdateSameSideAs(rcu.channelUpdate) } } - case class PrivateChannel(shortChannelId: ShortChannelId, channelId: ByteVector32, localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends KnownChannel { + case class PrivateChannel(channelId: ByteVector32, shortIds: ShortIds, localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends KnownChannel { val (nodeId1, nodeId2) = if (Announcements.isNode1(localNodeId, remoteNodeId)) (localNodeId, remoteNodeId) else (remoteNodeId, localNodeId) val capacity: Satoshi = (meta.balance1 + meta.balance2).truncateToSatoshi @@ -393,9 +395,14 @@ object Router { def toIncomingExtraHop: Option[ExtraHop] = { // we want the incoming channel_update val remoteUpdate_opt = if (localNodeId == nodeId1) update_2_opt else update_1_opt - remoteUpdate_opt.map { remoteUpdate => - ExtraHop(remoteNodeId, remoteUpdate.shortChannelId, remoteUpdate.feeBaseMsat, remoteUpdate.feeProportionalMillionths, remoteUpdate.cltvExpiryDelta) + // for incoming payments we preferably use the *remote alias*, otherwise the real scid if we have it + 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 => + ExtraHop(remoteNodeId, scid, remoteUpdate.feeBaseMsat, remoteUpdate.feeProportionalMillionths, remoteUpdate.cltvExpiryDelta) } + } } } // @formatter:on @@ -626,7 +633,7 @@ object Router { case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[GossipOrigin]], updates: Map[ChannelUpdate, Set[GossipOrigin]], nodes: Map[NodeAnnouncement, Set[GossipOrigin]]) // @formatter:on - case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, flag: Long) + case class ShortChannelIdAndFlag(shortChannelId: RealShortChannelId, flag: Long) /** * @param remainingQueries remaining queries to send, the next one will be popped after we receive a [[ReplyShortChannelIdsEnd]] @@ -637,28 +644,32 @@ object Router { } case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, PublicChannel], + channels: SortedMap[RealShortChannelId, PublicChannel], stash: Stash, rebroadcast: Rebroadcast, awaiting: Map[ChannelAnnouncement, Seq[GossipOrigin]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done privateChannels: Map[ByteVector32, PrivateChannel], // indexed by channel id - scid2PrivateChannels: Map[ShortChannelId, ByteVector32], // scid to channel_id, only to be used for private channels + scid2PrivateChannels: Map[Long, ByteVector32], // real scid or alias to channel_id, only to be used for private channels excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure graphWithBalances: GraphWithBalanceEstimates, sync: Map[PublicKey, Syncing] // keep tracks of channel range queries sent to each peer. If there is an entry in the map, it means that there is an ongoing query for which we have not yet received an 'end' message ) { def resolve(scid: ShortChannelId): Option[KnownChannel] = { // let's assume this is a real scid - channels.get(scid) match { + channels.get(RealShortChannelId(scid.toLong)) match { case Some(publicChannel) => Some(publicChannel) case None => // maybe it's an alias or a real scid - scid2PrivateChannels.get(scid).flatMap(privateChannels.get) match { + scid2PrivateChannels.get(scid.toLong).flatMap(privateChannels.get) match { case Some(privateChannel) => Some(privateChannel) case None => None } } } + + def resolve(channelId: ByteVector32, realScid_opt: Option[RealShortChannelId]): Option[KnownChannel] = { + privateChannels.get(channelId).orElse(realScid_opt.flatMap(channels.get)) + } } // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala index 91ce0d13f2..8905989562 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala @@ -21,7 +21,7 @@ import akka.event.LoggingAdapter import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.router.Router.{ChannelDesc, Data, PublicChannel, hasChannels} import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{BlockHeight, ShortChannelId, TimestampSecond, TxCoordinates} +import fr.acinq.eclair.{BlockHeight, RealShortChannelId, ShortChannelId, TimestampSecond, TxCoordinates} import scala.collection.mutable import scala.concurrent.duration._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala index 63e65b3dcc..4eadbb4988 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala @@ -20,6 +20,7 @@ import akka.actor.{ActorContext, ActorRef} import akka.event.LoggingAdapter import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Router._ @@ -69,13 +70,13 @@ object Sync { } } - def handleQueryChannelRange(channels: SortedMap[ShortChannelId, PublicChannel], routerConf: RouterConf, origin: RemoteGossip, q: QueryChannelRange)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { + def handleQueryChannelRange(channels: SortedMap[RealShortChannelId, PublicChannel], routerConf: RouterConf, origin: RemoteGossip, q: QueryChannelRange)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors ctx.sender() ! TransportHandler.ReadAck(q) Metrics.QueryChannelRange.Blocks.withoutTags().record(q.numberOfBlocks) log.info("received query_channel_range with firstBlockNum={} numberOfBlocks={} extendedQueryFlags_opt={}", q.firstBlock, q.numberOfBlocks, q.tlvStream) // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = channels.keySet.filter(keep(q.firstBlock, q.numberOfBlocks, _)) + val shortChannelIds: SortedSet[RealShortChannelId] = channels.keySet.filter(keep(q.firstBlock, q.numberOfBlocks, _)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, q.firstBlock, q.numberOfBlocks) val chunks = split(shortChannelIds, q.firstBlock, q.numberOfBlocks, routerConf.channelRangeChunkSize) Metrics.QueryChannelRange.Replies.withoutTags().record(chunks.size) @@ -107,7 +108,7 @@ object Sync { Metrics.ReplyChannelRange.ShortChannelIds.withTag(Tags.Direction, Tags.Directions.Incoming).record(r.shortChannelIds.array.size) @tailrec - def loop(ids: List[ShortChannelId], timestamps: List[ReplyChannelRangeTlv.Timestamps], checksums: List[ReplyChannelRangeTlv.Checksums], acc: List[ShortChannelIdAndFlag] = List.empty[ShortChannelIdAndFlag]): List[ShortChannelIdAndFlag] = { + def loop(ids: List[RealShortChannelId], timestamps: List[ReplyChannelRangeTlv.Timestamps], checksums: List[ReplyChannelRangeTlv.Checksums], acc: List[ShortChannelIdAndFlag] = List.empty[ShortChannelIdAndFlag]): List[ShortChannelIdAndFlag] = { ids match { case Nil => acc.reverse case head :: tail => @@ -158,7 +159,7 @@ object Sync { } } - def handleQueryShortChannelIds(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[ShortChannelId, PublicChannel], origin: RemoteGossip, q: QueryShortChannelIds)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { + def handleQueryShortChannelIds(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[RealShortChannelId, PublicChannel], origin: RemoteGossip, q: QueryShortChannelIds)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors ctx.sender() ! TransportHandler.ReadAck(q) @@ -218,7 +219,7 @@ object Sync { /** * Filters channels that we want to send to nodes asking for a channel range */ - def keep(firstBlock: BlockHeight, numberOfBlocks: Long, id: ShortChannelId): Boolean = { + def keep(firstBlock: BlockHeight, numberOfBlocks: Long, id: RealShortChannelId): Boolean = { val height = id.blockHeight height >= firstBlock && height < (firstBlock + numberOfBlocks) } @@ -251,8 +252,8 @@ object Sync { } } - def computeFlag(channels: SortedMap[ShortChannelId, PublicChannel])( - shortChannelId: ShortChannelId, + def computeFlag(channels: SortedMap[RealShortChannelId, PublicChannel])( + shortChannelId: RealShortChannelId, theirTimestamps_opt: Option[ReplyChannelRangeTlv.Timestamps], theirChecksums_opt: Option[ReplyChannelRangeTlv.Checksums], includeNodeAnnouncements: Boolean): Long = { @@ -291,8 +292,8 @@ object Sync { * */ def processChannelQuery(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, PublicChannel])( - ids: List[ShortChannelId], + channels: SortedMap[RealShortChannelId, PublicChannel])( + ids: List[RealShortChannelId], flags: List[Long], onChannel: ChannelAnnouncement => Unit, onUpdate: ChannelUpdate => Unit, @@ -302,7 +303,7 @@ object Sync { // we loop over channel ids and query flag. We track node Ids for node announcement // we've already sent to avoid sending them multiple times, as requested by the BOLTs @tailrec - def loop(ids: List[ShortChannelId], flags: List[Long], numca: Int = 0, numcu: Int = 0, nodesSent: Set[PublicKey] = Set.empty[PublicKey]): (Int, Int, Int) = ids match { + def loop(ids: List[RealShortChannelId], flags: List[Long], numca: Int = 0, numcu: Int = 0, nodesSent: Set[PublicKey] = Set.empty[PublicKey]): (Int, Int, Int) = ids match { case Nil => (numca, numcu, nodesSent.size) case head :: tail if !channels.contains(head) => log.warning("received query for shortChannelId={} that we don't have", head) @@ -369,7 +370,7 @@ object Sync { } } - def getChannelDigestInfo(channels: SortedMap[ShortChannelId, PublicChannel])(shortChannelId: ShortChannelId): (ReplyChannelRangeTlv.Timestamps, ReplyChannelRangeTlv.Checksums) = { + def getChannelDigestInfo(channels: SortedMap[RealShortChannelId, PublicChannel])(shortChannelId: RealShortChannelId): (ReplyChannelRangeTlv.Timestamps, ReplyChannelRangeTlv.Checksums) = { val c = channels(shortChannelId) val timestamp1 = c.update_1_opt.map(_.timestamp).getOrElse(0L unixsec) val timestamp2 = c.update_2_opt.map(_.timestamp).getOrElse(0L unixsec) @@ -390,7 +391,7 @@ object Sync { crc32c(data) } - case class ShortChannelIdsChunk(firstBlock: BlockHeight, numBlocks: Long, shortChannelIds: List[ShortChannelId]) { + case class ShortChannelIdsChunk(firstBlock: BlockHeight, numBlocks: Long, shortChannelIds: List[RealShortChannelId]) { /** * @param maximumSize maximum size of the short channel ids list * @return a chunk with at most `maximumSize` ids @@ -419,7 +420,7 @@ object Sync { * returned chunks may still contain more than `channelRangeChunkSize` elements * @return a list of short channel id chunks */ - def split(shortChannelIds: SortedSet[ShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, channelRangeChunkSize: Int): List[ShortChannelIdsChunk] = { + def split(shortChannelIds: SortedSet[RealShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, channelRangeChunkSize: Int): List[ShortChannelIdsChunk] = { // see BOLT7: MUST encode a short_channel_id for every open channel it knows in blocks first_blocknum to first_blocknum plus number_of_blocks minus one val it = shortChannelIds.iterator.dropWhile(_.blockHeight < firstBlock).takeWhile(_.blockHeight < firstBlock + numberOfBlocks) if (it.isEmpty) { @@ -429,7 +430,7 @@ object Sync { // ids that have the same block height must be grouped in the same chunk // chunk should contain `channelRangeChunkSize` ids @tailrec - def loop(currentChunk: List[ShortChannelId], acc: List[ShortChannelIdsChunk]): List[ShortChannelIdsChunk] = { + def loop(currentChunk: List[RealShortChannelId], acc: List[ShortChannelIdsChunk]): List[ShortChannelIdsChunk] = { if (it.hasNext) { val id = it.next() val currentHeight = currentChunk.head.blockHeight @@ -481,7 +482,7 @@ object Sync { * @param channels channels map * @return a ReplyChannelRange object */ - def buildReplyChannelRange(chunk: ShortChannelIdsChunk, syncComplete: Boolean, chainHash: ByteVector32, defaultEncoding: EncodingType, queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags], channels: SortedMap[ShortChannelId, PublicChannel]): ReplyChannelRange = { + def buildReplyChannelRange(chunk: ShortChannelIdsChunk, syncComplete: Boolean, chainHash: ByteVector32, defaultEncoding: EncodingType, queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags], channels: SortedMap[RealShortChannelId, PublicChannel]): ReplyChannelRange = { val encoding = if (chunk.shortChannelIds.isEmpty) EncodingType.UNCOMPRESSED else defaultEncoding val (timestamps, checksums) = queryFlags_opt match { case Some(extension) if extension.wantChecksums | extension.wantTimestamps => 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 a218504a1e..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 @@ -21,9 +21,11 @@ import akka.actor.{ActorContext, ActorRef, typed} import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.eclair.ShortChannelId.outputIndex import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} -import fr.acinq.eclair.channel.{AvailableBalanceChanged, LocalChannelDown, LocalChannelUpdate} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge @@ -31,7 +33,7 @@ import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, ShortChannelId, TxCoordinates, toLongId} +import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TxCoordinates, toLongId} object Validation { @@ -79,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) @@ -91,7 +92,7 @@ object Validation { val remoteOrigins = d0.awaiting.getOrElse(c, Set.empty).collect { case rg: RemoteGossip => rg } Logs.withMdc(log)(Logs.mdc(remoteNodeId_opt = remoteOrigins.headOption.map(_.nodeId))) { // in the MDC we use the node id that sent us the announcement first log.debug("got validation result for shortChannelId={} (awaiting={} stash.nodes={} stash.updates={})", c.shortChannelId, d0.awaiting.size, d0.stash.nodes.size, d0.stash.updates.size) - val publicChannel_opt = r match { + val d1_opt = r match { case ValidateResult(c, Left(t)) => log.warning("validation failure for shortChannelId={} reason={}", c.shortChannelId, t.getMessage) remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ValidationFailure(c))) @@ -109,39 +110,22 @@ object Validation { remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.InvalidAnnouncement(c))) None } else { - watcher ! WatchExternalChannelSpent(ctx.self, tx.txid, outputIndex, c.shortChannelId) - log.debug("added channel channelId={}", c.shortChannelId) + log.debug("validation successful for shortChannelId={}", c.shortChannelId) remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.Accepted(c))) val capacity = tx.txOut(outputIndex).amount - ctx.system.eventStream.publish(ChannelsDiscovered(SingleChannelDiscovered(c, capacity, None, None) :: Nil)) - db.addChannel(c, tx.txid, capacity) - // in case we just validated our first local channel, we announce the local node - if (!d0.nodes.contains(nodeParams.nodeId) && isRelatedTo(c, nodeParams.nodeId)) { - log.info("first local channel validated, announcing local node") - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures()) - ctx.self ! nodeAnn - } - // maybe this previously was a local unannounced channel - val channelId = toLongId(tx.txid.reverse, outputIndex) - val privateChannel_opt = d0.privateChannels.get(channelId) - Some(PublicChannel(c, - tx.txid, - capacity, - update_1_opt = privateChannel_opt.flatMap(_.update_1_opt), - update_2_opt = privateChannel_opt.flatMap(_.update_2_opt), - meta_opt = privateChannel_opt.map(_.meta))) + Some(addPublicChannel(d0, nodeParams, watcher, c, fundingTxid = tx.txid, capacity = capacity)) } 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 @@ -152,30 +136,19 @@ object Validation { // we remove channel from awaiting map val awaiting1 = d0.awaiting - c - publicChannel_opt match { - case Some(pc) => - // those updates are only defined if this was a previously an unannounced local channel, we broadcast them if they use the real scid - val updates1 = (pc.update_1_opt.toSet ++ pc.update_2_opt.toSet) - .map(u => u -> (if (pc.getNodeIdSameSideAs(u) == nodeParams.nodeId) Set[GossipOrigin](LocalGossip) else Set.empty[GossipOrigin])) - .toMap - val d1 = d0.copy( - channels = d0.channels + (c.shortChannelId -> pc), - privateChannels = d0.privateChannels - pc.channelId, // we remove the corresponding unannounced channel that we may have until now - rebroadcast = d0.rebroadcast.copy( - channels = d0.rebroadcast.channels + (c -> d0.awaiting.getOrElse(c, Nil).toSet), // we rebroadcast the channel to our peers - updates = d0.rebroadcast.updates ++ updates1 - ), // we also add the newly validated channels to the rebroadcast queue - stash = stash1, - awaiting = awaiting1) - // we only reprocess updates and nodes if validation succeeded - val d2 = reprocessUpdates.foldLeft(d1) { + d1_opt match { + case Some(d1) => + val d2 = d1.copy(stash = stash1, awaiting = awaiting1) + // we process channel updates and node announcements if validation succeeded + val d3 = reprocessUpdates.foldLeft(d2) { case (d, (u, origins)) => Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, Right(RemoteChannelUpdate(u, origins)), wasStashed = true) } - val d3 = reprocessNodes.foldLeft(d2) { + val d4 = reprocessNodes.foldLeft(d3) { case (d, (n, origins)) => Validation.handleNodeAnnouncement(d, nodeParams.db.network, origins, n, wasStashed = true) } - d3 + d4 case None => + // if validation failed we can fast-discard related announcements reprocessUpdates.foreach { case (u, origins) => origins.collect { case o: RemoteGossip => sendDecision(o.peerConnection, GossipDecision.NoRelatedChannel(u)) } } reprocessNodes.foreach { case (n, origins) => origins.collect { case o: RemoteGossip => sendDecision(o.peerConnection, GossipDecision.NoKnownChannel(n)) } } d0.copy(stash = stash1, awaiting = awaiting1) @@ -183,12 +156,74 @@ object Validation { } } - def handleChannelSpent(d: Data, db: NetworkDb, shortChannelId: ShortChannelId)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { + 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 + 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)) + 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( + ann = ann, + fundingTxid = fundingTxid, + capacity = capacity, + update_1_opt = privChan_opt.flatMap(_.update_1_opt), + update_2_opt = privChan_opt.flatMap(_.update_2_opt), + meta_opt = privChan_opt.map(_.meta) + ) + log.debug("adding public channel channelId={} realScid={} localChannel={} publicChannel={}", channelId, ann.shortChannelId, privChan_opt.isDefined, pubChan) + // if this is a local channel graduating from private to public, we need to update the graph because the edge + // identifiers change from alias to real scid, and we can also populate the metadata + val graph1 = privChan_opt match { + case Some(privateChannel) => + log.debug("updating the graph for shortChannelId={}", pubChan.shortChannelId) + // mutable variable is simpler here + var graph = d.graphWithBalances + // remove previous private edges + pubChan.update_1_opt.foreach(u => graph = graph.removeEdge(ChannelDesc(u, privateChannel))) + pubChan.update_2_opt.foreach(u => graph = graph.removeEdge(ChannelDesc(u, privateChannel))) + // add new public edges + pubChan.update_1_opt.foreach(u => graph = graph.addEdge(GraphEdge(u, pubChan))) + pubChan.update_2_opt.foreach(u => graph = graph.addEdge(GraphEdge(u, pubChan))) + graph + case None => d.graphWithBalances + } + // those updates are only defined if this was a previously an unannounced local channel, we broadcast them if they use the real scid + val rebroadcastUpdates1 = (pubChan.update_1_opt.toSet ++ pubChan.update_2_opt.toSet) + .filter(_.shortChannelId == pubChan.shortChannelId) + .map(u => u -> (if (pubChan.getNodeIdSameSideAs(u) == nodeParams.nodeId) Set[GossipOrigin](LocalGossip) else Set.empty[GossipOrigin])) + .toMap + val d1 = d.copy( + channels = d.channels + (pubChan.shortChannelId -> pubChan), + // we remove the corresponding unannounced channel that we may have until now + privateChannels = d.privateChannels - pubChan.channelId, + // we also remove the scid -> channelId mappings + scid2PrivateChannels = d.scid2PrivateChannels - pubChan.shortChannelId.toLong -- privChan_opt.map(_.shortIds.localAlias.toLong), + // we also add the newly validated channels to the rebroadcast queue + 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 was previously an unannounced local channel, we broadcast them + updates = d.rebroadcast.updates ++ rebroadcastUpdates1 + ), + graphWithBalances = graph1 + ) + // in case this was our first local channel, we make a node announcement + if (!d.nodes.contains(nodeParams.nodeId) && isRelatedTo(ann, nodeParams.nodeId)) { + log.info("first local channel validated, announcing local node") + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures()) + handleNodeAnnouncement(d1, nodeParams.db.network, Set(LocalGossip), nodeAnn) + } else d1 + } + + def handleChannelSpent(d: Data, db: NetworkDb, shortChannelId: RealShortChannelId)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors 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) @@ -205,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, _) => @@ -230,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 @@ -265,7 +300,7 @@ object Validation { def handleChannelUpdate(d: Data, db: NetworkDb, routerConf: RouterConf, update: Either[LocalChannelUpdate, RemoteChannelUpdate], 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 (pc_opt: Option[KnownChannel], u: ChannelUpdate, origins: Set[GossipOrigin]) = update match { - case Left(lcu) => (d.resolve(lcu.shortChannelId), lcu.channelUpdate, Set(LocalGossip)) + case Left(lcu) => (d.resolve(lcu.channelId, lcu.shortIds.real.toOption), lcu.channelUpdate, Set(LocalGossip)) case Right(rcu) => rcu.origins.collect { case RemoteGossip(peerConnection, _) if !wasStashed => // stashed changes have already been acknowledged @@ -282,15 +317,20 @@ 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)) 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 {} (duplicate)", u) sendDecision(origins, GossipDecision.Duplicate(u)) update match { @@ -330,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 @@ -338,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 @@ -382,23 +422,24 @@ object Validation { d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins))) } case None if db.isPruned(u.shortChannelId) && !StaleChannels.isStale(u) => + // only public channels are pruned + val realShortChannelId = RealShortChannelId(u.shortChannelId.toLong) // the channel was recently pruned, but if we are here, it means that the update is not stale so this is the case // of a zombie channel coming back from the dead. they probably sent us a channel_announcement right before this update, // 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=${u.shortChannelId} 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(u.shortChannelId) + 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(u.shortChannelId)), TlvStream.empty) + 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 @@ -418,66 +459,97 @@ object Validation { } } - def handleLocalChannelUpdate(d: Data, db: NetworkDb, routerConf: RouterConf, localNodeId: PublicKey, watcher: typed.ActorRef[ZmqWatcher.Command], lcu: LocalChannelUpdate)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { + /** + * We will receive this event before [[LocalChannelUpdate]] or [[ChannelUpdate]] + */ + def handleShortChannelIdAssigned(d: Data, localNodeId: PublicKey, scia: ShortChannelIdAssigned)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - d.channels.get(lcu.shortChannelId) match { + // NB: we don't map remote aliases because they are decided by our peer and could overlap with ours + val mappings = scia.shortIds.real.toOption match { + case Some(realScid) => Map(realScid.toLong -> scia.channelId, scia.shortIds.localAlias.toLong -> scia.channelId) + case None => Map(scia.shortIds.localAlias.toLong -> scia.channelId) + } + log.debug("handleShortChannelIdAssigned scia={} mappings={}", scia, mappings) + val d1 = d.copy(scid2PrivateChannels = d.scid2PrivateChannels ++ mappings) + d1.resolve(scia.channelId, scia.shortIds.real.toOption) match { case Some(_) => - // channel has already been announced and router knows about it, we can process the channel_update - handleChannelUpdate(d, db, routerConf, Left(lcu)) + // channel is known, nothing more to do + d1 case None => + // this is a local channel that hasn't yet been announced (maybe it is a private channel or maybe it is a public + // channel that doesn't yet have 6 confirmations), we create a corresponding private channel + val pc = PrivateChannel(scia.channelId, scia.shortIds, localNodeId, scia.remoteNodeId, None, None, ChannelMeta(0 msat, 0 msat)) + log.debug("adding unannounced local channel to remote={} channelId={} localAlias={}", scia.remoteNodeId, scia.channelId, scia.shortIds.localAlias) + d1.copy(privateChannels = d1.privateChannels + (scia.channelId -> pc)) + } + } + + def handleLocalChannelUpdate(d: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], lcu: LocalChannelUpdate)(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} + log.debug("handleLocalChannelUpdate lcu={}", lcu) + d.resolve(lcu.channelId, lcu.shortIds.real.toOption) match { + case Some(publicChannel: PublicChannel) => + // this a known public channel, we can process the channel_update + log.debug("this is a known public channel, processing channel_update publicChannel={}", publicChannel) + handleChannelUpdate(d, db, nodeParams.routerConf, Left(lcu)) + case Some(privateChannel: PrivateChannel) => lcu.channelAnnouncement_opt match { - case Some(c) if d.awaiting.contains(c) => - // channel is currently being verified, we can process the channel_update right away (it will be stashed) - handleChannelUpdate(d, db, routerConf, Left(lcu)) - case Some(c) => - // channel wasn't announced but here is the announcement, we will process it *before* the channel_update - watcher ! ValidateRequest(ctx.self, c) - val d1 = d.copy(awaiting = d.awaiting + (c -> Seq(LocalGossip))) // no origin + case Some(ann) => + log.debug("private channel graduating to public privateChannel={}", privateChannel) + // 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 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(c.shortChannelId) - handleChannelUpdate(d1, db, routerConf, Left(lcu)) - case None if d.privateChannels.contains(lcu.channelId) => - // channel isn't announced but we already know about it, we can process the channel_update - handleChannelUpdate(d, db, routerConf, Left(lcu)) + db.removeFromPruned(ann.shortChannelId) + log.debug("processing channel_update") + handleChannelUpdate(d1, db, nodeParams.routerConf, Left(lcu)) case None => - // channel isn't announced and we never heard of it (maybe it is a private channel or maybe it is a public channel that doesn't yet have 6 confirmations) - // let's create a corresponding private channel and process the channel_update - log.debug("adding unannounced local channel to remote={} shortChannelId={}", lcu.remoteNodeId, lcu.shortChannelId) - val pc = PrivateChannel(lcu.shortChannelId, lcu.channelId, localNodeId, lcu.remoteNodeId, None, None, ChannelMeta(0 msat, 0 msat)).updateBalances(lcu.commitments) - val d1 = d.copy( - privateChannels = d.privateChannels + (lcu.channelId -> pc), - scid2PrivateChannels = d.scid2PrivateChannels + (lcu.shortChannelId -> lcu.channelId) - ) - handleChannelUpdate(d1, db, routerConf, Left(lcu)) + log.debug("this is a known private channel, processing channel_update privateChannel={}", privateChannel) + // 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)) } + case None => + // should never happen, we log a warning and handle the update, it will be rejected since there is no related channel + log.warning("unrecognized local chanel update for channelId={} localAlias={}", lcu.channelId, lcu.shortIds.localAlias) + handleChannelUpdate(d, db, nodeParams.routerConf, Left(lcu)) } } def handleLocalChannelDown(d: Data, localNodeId: PublicKey, lcd: LocalChannelDown)(implicit log: LoggingAdapter): Data = { - import lcd.{channelId, remoteNodeId, shortChannelId} + import lcd.{channelId, remoteNodeId} + log.debug("handleLocalChannelDown lcd={}", lcd) + val scid2PrivateChannels1 = d.scid2PrivateChannels - lcd.shortIds.localAlias.toLong -- lcd.shortIds.real.toOption.map(_.toLong) // a local channel has permanently gone down - if (d.channels.contains(shortChannelId)) { + if (lcd.shortIds.real.toOption.exists(d.channels.contains)) { // the channel was public, we will receive (or have already received) a WatchEventSpentBasic event, that will trigger a clean up of the channel // so let's not do anything here - d - } else if (d.privateChannels.contains(channelId)) { + d.copy(scid2PrivateChannels = scid2PrivateChannels1) + } else if (d.privateChannels.contains(lcd.channelId)) { // the channel was private or public-but-not-yet-announced, let's do the clean up - log.info("removing private local channel and channel_update for channelId={} shortChannelId={}", channelId, shortChannelId) - val desc1 = ChannelDesc(shortChannelId, localNodeId, remoteNodeId) - val desc2 = ChannelDesc(shortChannelId, remoteNodeId, localNodeId) + val localAlias = d.privateChannels(channelId).shortIds.localAlias + log.info("removing private local channel and channel_update for channelId={} localAlias={}", channelId, localAlias) // we remove the corresponding updates from the graph val graphWithBalances1 = d.graphWithBalances - .removeEdge(desc1) - .removeEdge(desc2) + .removeEdge(ChannelDesc(localAlias, localNodeId, remoteNodeId)) + .removeEdge(ChannelDesc(localAlias, remoteNodeId, localNodeId)) // and we remove the channel and channel_update from our state - d.copy(privateChannels = d.privateChannels - channelId, graphWithBalances = graphWithBalances1) + d.copy(privateChannels = d.privateChannels - channelId, scid2PrivateChannels = scid2PrivateChannels1, graphWithBalances = graphWithBalances1) } else { d } } def handleAvailableBalanceChanged(d: Data, e: AvailableBalanceChanged)(implicit log: LoggingAdapter): Data = { - val (publicChannels1, graphWithBalances1) = d.channels.get(e.shortChannelId) match { + val (publicChannels1, graphWithBalances1) = e.shortIds.real.toOption.flatMap(d.channels.get) match { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("public channel balance updated: {}", pc1) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index fa23ee1f42..b2c491c0fd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -18,7 +18,6 @@ package fr.acinq.eclair.wire.internal.channel.version0 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} -import fr.acinq.eclair.{BlockHeight, TimestampSecond} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -27,6 +26,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, combinedFeaturesCodec} import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{BlockHeight, Alias, TimestampSecond} import scodec.Codec import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -349,30 +349,33 @@ private[channel] object ChannelCodecs0 { ("signature" | bytes64) :: ("tlvStream" | provide(TlvStream.empty[FundingSignedTlv]))).as[FundingSigned] - val fundingLockedCodec: Codec[FundingLocked] = ( + val channelReadyCodec: Codec[ChannelReady] = ( ("channelId" | bytes32) :: ("nextPerCommitmentPoint" | publicKey) :: - ("tlvStream" | provide(TlvStream.empty[FundingLockedTlv]))).as[FundingLocked] + ("tlvStream" | provide(TlvStream.empty[ChannelReadyTlv]))).as[ChannelReady] // this is a decode-only codec compatible with versions 997acee and below, with placeholders for new fields val DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | provide[Option[Transaction]](None)) :: ("waitingSince" | provide(BlockHeight(TimestampSecond.now().toLong))) :: - ("deferred" | optional(bool, fundingLockedCodec)) :: + ("deferred" | optional(bool, channelReadyCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool, fundingLockedCodec)) :: + ("deferred" | optional(bool, channelReadyCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | fundingLockedCodec)).as[DATA_WAIT_FOR_FUNDING_LOCKED].decodeOnly + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | channelReadyCodec)).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly val shutdownCodec: Codec[Shutdown] = ( ("channelId" | bytes32) :: @@ -382,23 +385,29 @@ private[channel] object ChannelCodecs0 { // this is a decode-only codec compatible with versions 9afb26e and below val DATA_NORMAL_COMPAT_03_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool) :: ("channelAnnouncement" | optional(bool, variableSizeBytes(noUnknownFieldsChannelAnnouncementSizeCodec, channelAnnouncementCodec))) :: ("channelUpdate" | variableSizeBytes(noUnknownFieldsChannelUpdateSizeCodec, channelUpdateCodec)) :: ("localShutdown" | optional(bool, shutdownCodec)) :: ("remoteShutdown" | optional(bool, shutdownCodec)) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL].decodeOnly + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool) :: ("channelAnnouncement" | optional(bool, variableSizeBytes(uint16, channelAnnouncementCodec))) :: ("channelUpdate" | variableSizeBytes(uint16, channelUpdateCodec)) :: ("localShutdown" | optional(bool, shutdownCodec)) :: ("remoteShutdown" | optional(bool, shutdownCodec)) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL].decodeOnly + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: @@ -457,7 +466,7 @@ private[channel] object ChannelCodecs0 { .typecase(0x09, Codecs.DATA_CLOSING_Codec) .typecase(0x08, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec) - .typecase(0x02, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) + .typecase(0x02, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) .typecase(0x03, Codecs.DATA_NORMAL_COMPAT_03_Codec) .typecase(0x04, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x05, Codecs.DATA_NEGOTIATING_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index d4173bef30..384947d98f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version1 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} -import fr.acinq.eclair.BlockHeight +import fr.acinq.eclair.{Alias, BlockHeight} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -236,23 +236,29 @@ private[channel] object ChannelCodecs1 { ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: @@ -287,7 +293,7 @@ private[channel] object ChannelCodecs1 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) .typecase(0x20, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x21, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) + .typecase(0x21, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) .typecase(0x22, Codecs.DATA_NORMAL_Codec) .typecase(0x23, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x24, Codecs.DATA_NEGOTIATING_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 4001e6e3bb..0e1c29770c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version2 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} -import fr.acinq.eclair.BlockHeight +import fr.acinq.eclair.{Alias, BlockHeight} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -271,23 +271,29 @@ private[channel] object ChannelCodecs2 { ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: @@ -321,7 +327,7 @@ private[channel] object ChannelCodecs2 { val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) .typecase(0x02, Codecs.DATA_NORMAL_Codec) .typecase(0x03, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) 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 be8ebdf99c..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 @@ -25,7 +25,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.UpdateMessage -import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, PermanentChannelFeature} +import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, Alias, PermanentChannelFeature} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -320,32 +320,55 @@ private[channel] object ChannelCodecs3 { ("fundingTx" | optional(bool8, txCodec)) :: // TODO: next time we define a new channel codec version, we should use the blockHeight codec here (32 bytes) ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly + + val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + ("commitments" | commitmentsCodec) :: + ("shortIds" | shortids) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).as[DATA_WAIT_FOR_CHANNEL_READY] val DATA_NORMAL_COMPAT_02_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_COMPAT_07_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closingFeerates" | optional(bool8, closingFeeratesCodec))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly + + val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + ("commitments" | commitmentsCodec) :: + ("shortids" | shortids) :: + ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: + ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: + ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("closingFeerates" | optional(bool8, closingFeeratesCodec))).as[DATA_NORMAL] val DATA_SHUTDOWN_COMPAT_03_Codec: Codec[DATA_SHUTDOWN] = ( @@ -387,14 +410,16 @@ private[channel] object ChannelCodecs3 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .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_Codec) + .typecase(0x07, Codecs.DATA_NORMAL_COMPAT_07_Codec) .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) .typecase(0x05, Codecs.DATA_CLOSING_Codec) .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) .typecase(0x03, Codecs.DATA_SHUTDOWN_COMPAT_03_Codec) .typecase(0x02, Codecs.DATA_NORMAL_COMPAT_02_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_COMPAT_01_Codec) .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index f49187f7a6..27a32898f7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -18,9 +18,10 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.channel.{ChannelType, ChannelTypes} +import fr.acinq.eclair.wire.protocol.ChannelTlv.ChannelTypeTlv import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream -import fr.acinq.eclair.{FeatureSupport, Features, UInt64} +import fr.acinq.eclair.{Alias, FeatureSupport, Features, ShortChannelId, UInt64} import scodec.Codec import scodec.bits.ByteVector import scodec.codecs._ @@ -107,10 +108,17 @@ object FundingSignedTlv { val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint)) } -sealed trait FundingLockedTlv extends Tlv +sealed trait ChannelReadyTlv extends Tlv -object FundingLockedTlv { - val fundingLockedTlvCodec: Codec[TlvStream[FundingLockedTlv]] = tlvStream(discriminated[FundingLockedTlv].by(varint)) +object ChannelReadyTlv { + + case class ShortChannelIdTlv(alias: Alias) extends ChannelReadyTlv + + val channelAliasTlvCodec: Codec[ShortChannelIdTlv] = variableSizeBytesLong(varintoverflow, "alias" | alias).as[ShortChannelIdTlv] + + val channelReadyTlvCodec: Codec[TlvStream[ChannelReadyTlv]] = tlvStream(discriminated[ChannelReadyTlv].by(varint) + .typecase(UInt64(1), channelAliasTlvCodec) + ) } sealed trait ChannelReestablishTlv extends Tlv diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 78ad89f00a..314fbaf4bf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelFlags +import fr.acinq.eclair.channel.{ChannelFlags, RealScidStatus, ShortIds} import fr.acinq.eclair.crypto.Mac32 -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -130,7 +130,22 @@ object CommonCodecs { // number of bytes we can just skip to the next field val listofnodeaddresses: Codec[List[NodeAddress]] = variableSizeBytes(uint16, list(nodeaddress)) - val shortchannelid: Codec[ShortChannelId] = int64.xmap(l => ShortChannelId(l), s => s.toLong) + val shortchannelid: Codec[ShortChannelId] = int64.xmap(l => UnspecifiedShortChannelId(l), s => s.toLong) + + val realshortchannelid: Codec[RealShortChannelId] = shortchannelid.narrow[RealShortChannelId](scid => Attempt.successful(RealShortChannelId(scid.toLong)), scid => scid) + + val alias: Codec[Alias] = shortchannelid.narrow[Alias](scid => Attempt.successful(Alias(scid.toLong)), scid => scid) + + val realShortChannelIdStatus: Codec[RealScidStatus] = discriminated[RealScidStatus].by(uint8) + .typecase(0, provide(RealScidStatus.Unknown)) + .typecase(1, realshortchannelid.as[RealScidStatus.Temporary]) + .typecase(2, realshortchannelid.as[RealScidStatus.Final]) + + val shortids: Codec[ShortIds] = ( + ("real" | realShortChannelIdStatus) :: + ("localAlias" | discriminated[Alias].by(uint16).typecase(1, alias)) :: // forward-compatible with listOfN(uint16, aliashortchannelid) in case we want to store a list of local aliases later + ("remoteAlias_opt" | optional(bool8, alias)) + ).as[ShortIds] val privateKey: Codec[PrivateKey] = Codec[PrivateKey]( (priv: PrivateKey) => bytes(32).encode(priv.value), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala index 07969828bb..1e9612c22a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala @@ -139,9 +139,11 @@ object FailureMessageCodecs { /** * An onion-encrypted failure from an intermediate node: + * {{{ * +----------------+----------------------------------+-----------------+----------------------+-----+ * | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | * +----------------+----------------------------------+-----------------+----------------------+-----+ + * }}} * with failure message length + pad length = 256 */ def failureOnionCodec(mac: Mac32): Codec[FailureMessage] = CommonCodecs.prependmac( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 770b5c0839..4ffa28885a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -164,10 +164,10 @@ object LightningMessageCodecs { ("signature" | bytes64) :: ("tlvStream" | FundingSignedTlv.fundingSignedTlvCodec)).as[FundingSigned] - val fundingLockedCodec: Codec[FundingLocked] = ( + val channelReadyCodec: Codec[ChannelReady] = ( ("channelId" | bytes32) :: ("nextPerCommitmentPoint" | publicKey) :: - ("tlvStream" | FundingLockedTlv.fundingLockedTlvCodec)).as[FundingLocked] + ("tlvStream" | ChannelReadyTlv.channelReadyTlvCodec)).as[ChannelReady] val txAddInputCodec: Codec[TxAddInput] = ( ("channelId" | bytes32) :: @@ -281,7 +281,7 @@ object LightningMessageCodecs { val announcementSignaturesCodec: Codec[AnnouncementSignatures] = ( ("channelId" | bytes32) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("nodeSignature" | bytes64) :: ("bitcoinSignature" | bytes64) :: ("tlvStream" | AnnouncementSignaturesTlv.announcementSignaturesTlvCodec)).as[AnnouncementSignatures] @@ -289,7 +289,7 @@ object LightningMessageCodecs { val channelAnnouncementWitnessCodec = ("features" | featuresCodec) :: ("chainHash" | bytes32) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("nodeId1" | publicKey) :: ("nodeId2" | publicKey) :: ("bitcoinKey1" | publicKey) :: @@ -368,10 +368,10 @@ object LightningMessageCodecs { .\(0) { case a@EncodedShortChannelIds(_, Nil) => a // empty list is always encoded with encoding type 'uncompressed' for compatibility with other implementations case a@EncodedShortChannelIds(EncodingType.UNCOMPRESSED, _) => a - }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(shortchannelid)).as[EncodedShortChannelIds]) + }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(realshortchannelid)).as[EncodedShortChannelIds]) .\(1) { case a@EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, _) => a - }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(shortchannelid))).as[EncodedShortChannelIds]) + }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(realshortchannelid))).as[EncodedShortChannelIds]) val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = ( ("chainHash" | bytes32) :: @@ -441,7 +441,7 @@ object LightningMessageCodecs { .typecase(33, acceptChannelCodec) .typecase(34, fundingCreatedCodec) .typecase(35, fundingSignedCodec) - .typecase(36, fundingLockedCodec) + .typecase(36, channelReadyCodec) .typecase(38, shutdownCodec) .typecase(39, closingSignedCodec) .typecase(64, openDualFundedChannelCodec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index c614990f12..7d8aa9f3e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -20,10 +20,11 @@ import com.google.common.base.Charsets import com.google.common.net.InetAddresses import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, ScriptWitness, Transaction} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} import fr.acinq.eclair.payment.relay.Relayer -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} +import fr.acinq.eclair.wire.protocol.ChannelReadyTlv.ShortChannelIdTlv import scodec.bits.ByteVector import java.net.{Inet4Address, Inet6Address, InetAddress} @@ -225,9 +226,11 @@ case class FundingSigned(channelId: ByteVector32, signature: ByteVector64, tlvStream: TlvStream[FundingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId -case class FundingLocked(channelId: ByteVector32, +case class ChannelReady(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, - tlvStream: TlvStream[FundingLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId + tlvStream: TlvStream[ChannelReadyTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + val alias_opt: Option[Alias] = tlvStream.get[ShortChannelIdTlv].map(_.alias) +} case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector, @@ -279,7 +282,7 @@ case class UpdateFee(channelId: ByteVector32, tlvStream: TlvStream[UpdateFeeTlv] = TlvStream.empty) extends ChannelMessage with UpdateMessage with HasChannelId case class AnnouncementSignatures(channelId: ByteVector32, - shortChannelId: ShortChannelId, + shortChannelId: RealShortChannelId, nodeSignature: ByteVector64, bitcoinSignature: ByteVector64, tlvStream: TlvStream[AnnouncementSignaturesTlv] = TlvStream.empty) extends RoutingMessage with HasChannelId @@ -290,7 +293,7 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64, bitcoinSignature2: ByteVector64, features: Features[Feature], chainHash: ByteVector32, - shortChannelId: ShortChannelId, + shortChannelId: RealShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, @@ -394,7 +397,7 @@ object EncodingType { } // @formatter:on -case class EncodedShortChannelIds(encoding: EncodingType, array: List[ShortChannelId]) { +case class EncodedShortChannelIds(encoding: EncodingType, array: List[RealShortChannelId]) { /** custom toString because it can get huge in logs */ override def toString: String = s"EncodedShortChannelIds($encoding,${array.headOption.getOrElse("")}->${array.lastOption.getOrElse("")} size=${array.size})" } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index 9154eb3154..ed46c2e0d8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair -import akka.actor.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} +import akka.actor.{ActorRef, Status} import akka.pattern.pipe import akka.testkit.TestProbe import akka.util.Timeout @@ -29,6 +29,7 @@ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ +import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer.OpenChannel import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment @@ -207,11 +208,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I ) val (a, b, c, d, e) = (a_priv.publicKey, b_priv.publicKey, c_priv.publicKey, d_priv.publicKey, e_priv.publicKey) - val ann_ab = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(1), a, b, a, b, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_ae = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(4), a, e, a, e, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_bc = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(2), b, c, b, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_cd = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(3), c, d, c, d, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_ec = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(7), e, c, e, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_ab = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(1), a, b, a, b, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_ae = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(4), a, e, a, e, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_bc = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(2), b, c, b, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_cd = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(3), c, d, c, d, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_ec = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(7), e, c, e, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) assert(Announcements.isNode1(a, b)) assert(Announcements.isNode1(b, c)) @@ -233,6 +234,25 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I assert(sender.expectMsgType[Iterable[ChannelUpdate]].map(_.shortChannelId).toSet == Set(ShortChannelId(2))) } + test("open with bad arguments") { f => + import f._ + + val eclair = new EclairImpl(kit) + + // option_scid_alias is not compatible with public channels + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + assert(sender.expectMsgType[Status.Failure].cause.getMessage.contains("option_scid_alias is not compatible with public channels")) + + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + switchboard.expectMsgType[Peer.OpenChannel] + + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + switchboard.expectMsgType[Peer.OpenChannel] + + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + switchboard.expectMsgType[Peer.OpenChannel] + } + test("close and forceclose should work both with channelId and shortChannelId") { f => import f._ @@ -616,7 +636,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val eclair = new EclairImpl(kit) eclair.channelBalances().pipeTo(sender.ref) - relayer.expectMsg(GetOutgoingChannels(enabledOnly=false)) + relayer.expectMsg(GetOutgoingChannels(enabledOnly = false)) eclair.usableBalances().pipeTo(sender.ref) relayer.expectMsg(GetOutgoingChannels()) } 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 e26254dddf..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,31 +20,29 @@ 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) -> ShortChannelId(0), - TxCoordinates(BlockHeight(42000), 27, 3) -> ShortChannelId(0x0000a41000001b0003L), - TxCoordinates(BlockHeight(1258612), 63, 0) -> ShortChannelId(0x13347400003f0000L), - TxCoordinates(BlockHeight(0xffffff), 0x000000, 0xffff) -> ShortChannelId(0xffffff000000ffffL), - TxCoordinates(BlockHeight(0x000000), 0xffffff, 0xffff) -> ShortChannelId(0x000000ffffffffffL), - TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0x0000) -> ShortChannelId(0xffffffffffff0000L), - TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0xffff) -> ShortChannelId(0xffffffffffffffffL) + TxCoordinates(BlockHeight(0), 0, 0) -> RealShortChannelId(0), + TxCoordinates(BlockHeight(42000), 27, 3) -> RealShortChannelId(0x0000a41000001b0003L), + TxCoordinates(BlockHeight(1258612), 63, 0) -> RealShortChannelId(0x13347400003f0000L), + TxCoordinates(BlockHeight(0xffffff), 0x000000, 0xffff) -> RealShortChannelId(0xffffff000000ffffL), + TxCoordinates(BlockHeight(0x000000), 0xffffff, 0xffff) -> RealShortChannelId(0x000000ffffffffffL), + TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0x0000) -> RealShortChannelId(0xffffffffffff0000L), + TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0xffff) -> RealShortChannelId(0xffffffffffffffffL) ) for ((coord, shortChannelId) <- expected) { - assert(shortChannelId == ShortChannelId(coord.blockHeight, coord.txIndex, coord.outputIndex)) + assert(shortChannelId == RealShortChannelId(coord.blockHeight, coord.txIndex, coord.outputIndex)) assert(coord == ShortChannelId.coordinates(shortChannelId)) } } test("human readable format as per spec") { - assert(ShortChannelId(0x0000a41000001b0003L).toString == "42000x27x3") + assert(RealShortChannelId(0x0000a41000001b0003L).toString == "42000x27x3") } - test("parse a short channel it") { + test("parse a short channel id") { assert(ShortChannelId("42000x27x3").toLong == 0x0000a41000001b0003L) } @@ -57,4 +55,19 @@ class ShortChannelIdSpec extends AnyFunSuite { assert(Try(ShortChannelId("42000x27")).isFailure) assert(Try(ShortChannelId("42000x")).isFailure) } + + 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/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index 2930d4c07a..e613e7ab1c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -71,7 +71,7 @@ object TestDatabases { // serialized and deserialized we need to turn "hot" payments into cold ones def freeze3(input: PersistentChannelData): PersistentChannelData = input match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = freeze2(d.commitments)) - case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = freeze2(d.commitments)) case d: DATA_NORMAL => d.copy(commitments = freeze2(d.commitments)) case d: DATA_CLOSING => d.copy(commitments = freeze2(d.commitments)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala index 4979d559af..c8012d440b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala @@ -16,17 +16,20 @@ package fr.acinq.eclair -import akka.actor.ActorRef -import akka.event.DiagnosticLoggingAdapter +import akka.actor.{ActorRef, ActorSystem} +import akka.event.{DiagnosticLoggingAdapter, EventStream} import akka.testkit.{TestActor, TestProbe} import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.io.Peer import fr.acinq.eclair.wire.protocol.LightningMessage +import org.scalatest.concurrent.Eventually.eventually +import org.scalatest.concurrent.PatienceConfiguration import java.io.File import java.net.ServerSocket import java.nio.file.Files import java.util.UUID +import scala.concurrent.duration.{DurationInt, FiniteDuration} object TestUtils { @@ -89,4 +92,22 @@ object TestUtils { seedFile } + /** + * Subscribing to [[EventStream]] is asynchronous, which can lead to race conditions. + * + * We use a dummy event subscription and poll until we receive a message, and rely on the fact that + * [[EventStream]] is an actor which means that all previous subscriptions have been taken into account. + */ + def waitEventStreamSynced(eventStream: EventStream)(implicit system: ActorSystem): Unit = { + val listener = TestProbe() + case class DummyEvent() + eventStream.subscribe(listener.ref, classOf[DummyEvent]) + eventually { + eventStream.publish(DummyEvent()) + assert(listener.msgAvailable) + } + } + + def waitFor(duration: FiniteDuration): Unit = Thread.sleep(duration.toMillis) + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index c37464d932..e9a8e7c740 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -61,7 +61,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) val walletPassword = Random.alphanumeric.take(8).mkString sender.send(bitcoincli, BitcoinReq("encryptwallet", walletPassword)) - sender.expectMsgType[JString] + sender.expectMsgType[JString](60 seconds) restartBitcoind(sender) val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index b521692e20..544b9481db 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{FundTransactio import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.blockchain.{CurrentBlockHeight, NewTransaction} -import fr.acinq.eclair.{BlockHeight, ShortChannelId, TestConstants, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, RealShortChannelId, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import grizzled.slf4j.Logging import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -108,8 +108,8 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val w1 = WatchFundingSpent(TestProbe().ref, txid, outputIndex, hints = Set.empty) val w2 = WatchFundingSpent(TestProbe().ref, txid, outputIndex, hints = Set.empty) - val w3 = WatchExternalChannelSpent(TestProbe().ref, txid, outputIndex, ShortChannelId(1)) - val w4 = WatchExternalChannelSpent(TestProbe().ref, randomBytes32(), 5, ShortChannelId(1)) + val w3 = WatchExternalChannelSpent(TestProbe().ref, txid, outputIndex, RealShortChannelId(1)) + val w4 = WatchExternalChannelSpent(TestProbe().ref, randomBytes32(), 5, RealShortChannelId(1)) val w5 = WatchFundingConfirmed(TestProbe().ref, txid, 3) // we test as if the collection was immutable @@ -210,7 +210,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val (tx1, tx2) = createUnspentTxChain(tx, priv) val listener = TestProbe() - watcher ! WatchExternalChannelSpent(listener.ref, tx.txid, outputIndex, ShortChannelId(5)) + watcher ! WatchExternalChannelSpent(listener.ref, tx.txid, outputIndex, RealShortChannelId(5)) watcher ! WatchFundingSpent(listener.ref, tx.txid, outputIndex, Set.empty) listener.expectNoMessage(1 second) @@ -221,7 +221,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind probe.expectMsg(tx1.txid) // tx and tx1 aren't confirmed yet, but we trigger the WatchEventSpent when we see tx1 in the mempool. listener.expectMsgAllOf( - WatchExternalChannelSpentTriggered(ShortChannelId(5)), + WatchExternalChannelSpentTriggered(RealShortChannelId(5)), WatchFundingSpentTriggered(tx1) ) // Let's confirm tx and tx1: seeing tx1 in a block should trigger WatchEventSpent again, but not WatchEventSpentBasic @@ -267,8 +267,8 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind listener.expectMsg(WatchOutputSpentTriggered(tx1)) watcher ! StopWatching(listener.ref) - watcher ! WatchExternalChannelSpent(listener.ref, tx1.txid, 0, ShortChannelId(1)) - listener.expectMsg(WatchExternalChannelSpentTriggered(ShortChannelId(1))) + watcher ! WatchExternalChannelSpent(listener.ref, tx1.txid, 0, RealShortChannelId(1)) + listener.expectMsg(WatchExternalChannelSpentTriggered(RealShortChannelId(1))) watcher ! WatchFundingSpent(listener.ref, tx1.txid, 0, Set.empty) listener.expectMsg(WatchFundingSpentTriggered(tx2)) }) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala index 3e0567eb4a..4211416a7b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala @@ -57,34 +57,34 @@ class FeeEstimatorSpec extends AnyFunSuite { feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == defaultMaxCommitFeerate / 2) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == defaultMaxCommitFeerate) assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == overrideMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == overrideMaxCommitFeerate) val currentFeerates1 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) val currentFeerates2 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 1.5, mempoolMinFee = FeeratePerKw(250 sat))) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) val highFeerates = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(mempoolMinFee = FeeratePerKw(10000 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(mempoolMinFee = FeeratePerKw(10000 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) } test("fee difference too high") { @@ -123,7 +123,7 @@ class FeeEstimatorSpec extends AnyFunSuite { ) testCases.foreach { case (networkFeerate, proposedFeerate) => assert(!tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputs, networkFeerate, proposedFeerate)) - assert(!tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, networkFeerate, proposedFeerate)) + assert(!tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), networkFeerate, proposedFeerate)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index ad3a4baeaf..d992176daa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -55,27 +55,34 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha } test("pick channel type based on local and remote features") { - case class TestCase(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], expectedChannelType: ChannelType) + case class TestCase(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean, expectedChannelType: ChannelType) val testCases = Seq( - TestCase(Features.empty, Features.empty, ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelTypes.Standard), - TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelTypes.Standard), - TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), + TestCase(Features.empty, Features.empty, announceChannel = true, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional), Features.empty, announceChannel = true, ChannelTypes.Standard), + TestCase(Features.empty, Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.Standard), + TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), announceChannel = true, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), announceChannel = true, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), announceChannel = true, ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), announceChannel = false, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Mandatory, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Mandatory, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional, Features.ZeroConf -> Optional), announceChannel = false, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), ) for (testCase <- testCases) { - assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures) == testCase.expectedChannelType, s"localFeatures=${testCase.localFeatures} remoteFeatures=${testCase.remoteFeatures}") + assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures, testCase.announceChannel) == testCase.expectedChannelType, s"localFeatures=${testCase.localFeatures} remoteFeatures=${testCase.remoteFeatures}") } } @@ -86,7 +93,10 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha TestCase(Features.empty, ChannelTypes.Standard), TestCase(Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory, ScidAlias -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory, ZeroConf -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory, ScidAlias -> Mandatory, ZeroConf -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), ) for (testCase <- validChannelTypes) { assert(ChannelTypes.fromFeatures(testCase.features) == testCase.expectedChannelType, testCase.features) @@ -121,9 +131,9 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional), Set(StaticRemoteKey, Wumbo)), TestCase(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputs)), TestCase(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputs, Wumbo)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(DualFunding -> Optional, Wumbo -> Optional), Features(DualFunding -> Optional, Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo, DualFunding)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(DualFunding -> Optional, Wumbo -> Optional), Features(DualFunding -> Optional, Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo, DualFunding)), ) testCases.foreach(t => assert(ChannelFeatures(t.channelType, t.localFeatures, t.remoteFeatures).features == t.expected, s"channelType=${t.channelType} localFeatures=${t.localFeatures} remoteFeatures=${t.remoteFeatures}")) } 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 47de53b8d3..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 @@ -27,7 +27,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong} +import fr.acinq.eclair.{BlockHeight, Features, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong} import org.scalatest.Tag import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -40,13 +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.minDepthForFunding(nodeParams.channelConf, Btc(1)) == 4) - assert(Helpers.minDepthForFunding(nodeParams.channelConf.copy(minDepthBlocks = 6), Btc(1)) == 6) // 4 conf would be enough but we use min-depth=6 - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(6.25)) == 16) // we use scaling_factor=15 and a fixed block reward of 6.25BTC - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(12.50)) == 31) - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(12.60)) == 32) - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(30)) == 73) - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(50)) == 121) + 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/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index 687cf9a917..256d4b90df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -5,10 +5,10 @@ import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit import akka.testkit.{TestActor, TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.ScriptFlags -import fr.acinq.bitcoin import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.fsm.Channel @@ -19,7 +19,7 @@ import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, DefaultCommitmentFormat, InputInfo, TxOwner} -import fr.acinq.eclair.wire.protocol.{ChannelReestablish, CommitSig, Error, FundingLocked, Init, RevokeAndAck} +import fr.acinq.eclair.wire.protocol.{ChannelReady, ChannelReestablish, ChannelUpdate, CommitSig, Error, Init, RevokeAndAck} import fr.acinq.eclair.{TestKitBaseClass, _} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -31,19 +31,16 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan type FixtureParam = SetupFixture override def withFixture(test: OneArgTest): Outcome = { - val setup = test.tags.contains("disable-offline-mismatch") match { - case false => init() - case true => init(nodeParamsA = Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(closeOnOfflineMismatch = false))) - } + val setup = init() within(30 seconds) { reachNormal(setup) withFixture(test.toNoArgTest(setup)) } } - def aliceInit = Init(Alice.nodeParams.features.initFeatures()) + private def aliceInit = Init(Alice.nodeParams.features.initFeatures()) - def bobInit = Init(Bob.nodeParams.features.initFeatures()) + private def bobInit = Init(Bob.nodeParams.features.initFeatures()) test("use funding pubkeys from publish commitment to spend our output") { f => import f._ @@ -136,14 +133,14 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan /** We are only interested in channel updates from Alice, we use the channel flag to discriminate */ def aliceChannelUpdateListener(channelUpdateListener: TestProbe): TestProbe = { val aliceListener = TestProbe() - channelUpdateListener.setAutoPilot(new testkit.TestActor.AutoPilot { - override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match { + channelUpdateListener.setAutoPilot { + (sender: ActorRef, msg: Any) => msg match { case u: ChannelUpdateParametersChanged if u.channelUpdate.channelFlags.isNode1 == Announcements.isNode1(Alice.nodeParams.nodeId, Bob.nodeParams.nodeId) => aliceListener.ref.tell(msg, sender) TestActor.KeepRunning case _ => TestActor.KeepRunning } - }) + } aliceListener } @@ -206,6 +203,10 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan // and we terminate Alice alice.stop() + // there should ne no pending messages + alice2bob.expectNoMessage() + bob2alice.expectNoMessage() + // we restart Alice with different configurations Seq( Alice.nodeParams @@ -234,10 +235,15 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan bob2alice.expectMsgType[ChannelReestablish] alice2bob.forward(bob) bob2alice.forward(newAlice) - alice2bob.expectMsgType[FundingLocked] - bob2alice.expectMsgType[FundingLocked] + alice2bob.expectMsgType[ChannelReady] + bob2alice.expectMsgType[ChannelReady] alice2bob.forward(bob) bob2alice.forward(newAlice) + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + alice2bob.expectNoMessage() + bob2alice.expectNoMessage() + awaitCond(newAlice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 3d136c5ae7..1dc458a684 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -131,7 +131,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val aliceNodeParams = TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight) val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), walletClient) val testTags = channelType match { - case ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) + case _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) case ChannelTypes.AnchorOutputs => Set(ChannelStateTestsTags.AnchorOutputs) case ChannelTypes.StaticRemoteKey => Set(ChannelStateTestsTags.StaticRemoteKey) case _ => Set.empty[String] @@ -168,7 +168,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate @@ -183,7 +183,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) @@ -200,7 +200,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough and commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate @@ -218,7 +218,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -236,7 +236,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx published, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -255,7 +255,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx replaces local commit tx, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -291,7 +291,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("funding tx not found, skipping anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (_, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) @@ -307,7 +307,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("not enough funds to increase commit tx feerate") { - withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // close channel and wait for the commit tx to be published, anchor will not be published because we don't have enough funds @@ -326,7 +326,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate too low, spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -356,7 +356,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not published, publishing it and spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 32) @@ -392,7 +392,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 22000 sat, 15000 sat ) - withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. @@ -420,7 +420,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx fees not increased when confirmation target is far and feerate hasn't changed") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -443,7 +443,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, lowering anchor output amount") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -481,7 +481,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, adding other wallet inputs") { - withFixture(Seq(10.5 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.5 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -520,7 +520,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, not enough funds to increase fees") { - withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -554,7 +554,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, cannot use new unconfirmed inputs to increase fees") { - withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -585,7 +585,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, updating confirmation target") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -628,7 +628,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when anchor tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -660,7 +660,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock anchor utxos when stopped before completion") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -679,7 +679,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx confirmed, not publishing htlc tx") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // Add htlcs in both directions and ensure that preimages are available. @@ -768,7 +768,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("not enough funds to increase htlc tx feerate") { - withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight()) @@ -866,7 +866,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate zero, adding wallet inputs") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(15_000 sat) @@ -883,7 +883,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate zero, high commit feerate, adding wallet inputs") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate @@ -915,7 +915,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 5200 sat, 5100 sat ) - withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(8_000 sat) @@ -930,7 +930,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx not confirming, lowering output amount") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val initialFeerate = FeeratePerKw(15_000 sat) @@ -966,7 +966,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx not confirming, adding other wallet inputs") { - withFixture(Seq(10.2 millibtc, 2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.2 millibtc, 2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val initialFeerate = FeeratePerKw(15_000 sat) @@ -1002,7 +1002,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx confirmation target reached, increasing fees") { - withFixture(Seq(50 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(50 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val initialFeerate = FeeratePerKw(10_000 sat) @@ -1034,7 +1034,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc timeout tx not confirming, increasing fees") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val feerate = FeeratePerKw(15_000 sat) @@ -1072,7 +1072,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("utxos count too low, setting short confirmation target") { - withFixture(Seq(15 millibtc, 10 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(15 millibtc, 10 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 144) @@ -1094,7 +1094,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when htlc tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(5_000 sat) @@ -1129,7 +1129,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock htlc utxos when stopped before completion") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ setFeerate(FeeratePerKw(5_000 sat)) @@ -1146,7 +1146,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("local commit tx confirmed, not publishing claim htlc tx") { - withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // Add htlcs in both directions and ensure that preimages are available. @@ -1283,7 +1283,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("claim htlc tx feerate high enough, not changing output amount") { - withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val currentFeerate = alice.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(2) 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 5caba83160..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 @@ -72,6 +72,10 @@ object ChannelStateTestsTags { val HighDustLimitDifferenceBobAlice = "high_dust_limit_difference_bob_alice" /** If set, channels will use option_channel_type. */ val ChannelType = "option_channel_type" + /** If set, channels will use option_zeroconf. */ + val ZeroConf = "zeroconf" + /** If set, channels will use option_scid_alias. */ + val ScidAlias = "scid_alias" } trait ChannelStateTestsBase extends Assertions with Eventually { @@ -146,7 +150,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, alice2relayer, bob2relayer, channelUpdateListener, wallet, alicePeer, bobPeer) } - def computeFeatures(setup: SetupFixture, tags: Set[String]): (LocalParams, LocalParams, SupportedChannelType) = { + def computeFeatures(setup: SetupFixture, tags: Set[String], channelFlags: ChannelFlags): (LocalParams, LocalParams, SupportedChannelType) = { import setup._ val aliceInitFeatures = Alice.nodeParams.features @@ -157,6 +161,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .initFeatures() val bobInitFeatures = Bob.nodeParams.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) @@ -166,9 +172,15 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .initFeatures() - val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures) + val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, channelFlags.announceChannel) + + // those features can only be enabled with AnchorOutputsZeroFeeHtlcTxs, this is to prevent incompatible test configurations + if (tags.contains(ChannelStateTestsTags.ZeroConf)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") + if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(channelType.features.contains(Features.ScidAlias), "invalid test configuration") implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global val aliceParams = Alice.channelParams @@ -188,13 +200,13 @@ trait ChannelStateTestsBase extends Assertions with Eventually { (aliceParams, bobParams, channelType) } - def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Transaction = { + def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty, interceptChannelUpdates: Boolean = true): Transaction = { import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags) val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) + val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags, channelFlags) val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) { (TestConstants.fundingSatoshis, 0.msat) @@ -202,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) @@ -218,21 +233,29 @@ trait ChannelStateTestsBase extends Assertions with Eventually { bob2alice.forward(alice) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) alice2blockchain.expectMsgType[WatchFundingSpent] - alice2blockchain.expectMsgType[WatchFundingConfirmed] assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) bob2blockchain.expectMsgType[WatchFundingSpent] - bob2blockchain.expectMsgType[WatchFundingConfirmed] - - eventually(assert(alice.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) + 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[FundingLocked] + alice2bob.expectMsgType[ChannelReady] alice2bob.forward(bob) - bob2alice.expectMsgType[FundingLocked] + bob2alice.expectMsgType[ChannelReady] bob2alice.forward(alice) + if (interceptChannelUpdates) { + // we don't forward the channel updates, in reality they would be processed by the router + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + } alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2blockchain.expectMsgType[WatchFundingDeeplyBuried] eventually(assert(alice.stateName == NORMAL)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 040e255f29..3ed6f1429b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -56,14 +56,15 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags, channelFlags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType.isInstanceOf[ChannelTypes.AnchorOutputsZeroFeeHtlcTx]) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val fundingAmount = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -96,10 +97,20 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + aliceOrigin.expectNoMessage() + } + + test("recv AcceptChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)) aliceOrigin.expectNoMessage() } @@ -118,7 +129,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (channel type not set but feature bit set)", Tag(ChannelStateTestsTags.ChannelType), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) bob2alice.forward(alice, accept.copy(tlvStream = TlvStream.empty)) alice2bob.expectMsg(Error(accept.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing")) awaitCond(alice.stateName == CLOSED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index b76db60e96..13ac83a34c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} -import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -50,13 +50,14 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags, channelFlags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType.isInstanceOf[ChannelTypes.AnchorOutputsZeroFeeHtlcTx]) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) @@ -87,10 +88,19 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + } + + test("recv OpenChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) + alice2bob.forward(bob) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)) } test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => @@ -287,6 +297,14 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui awaitCond(bob.stateName == CLOSED) } + test("recv OpenChannel (zeroconf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob, open) + val accept = bob2alice.expectMsgType[AcceptChannel] + assert(accept.minimumDepth == 0) + } + test("recv Error") { f => import f._ bob ! Error(ByteVector32.Zeroes, "oops") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 84bb3b43e1..ff298ee255 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -60,11 +60,12 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index 7fa5845ec6..23419edfda 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -43,11 +43,12 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val setup = init(wallet = new NoOpOnChainWallet()) import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 9596b3c33b..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 @@ -59,11 +59,12 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -91,7 +92,19 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(txPublished.tx.txid == fundingTxId) assert(txPublished.miningFee > 0.sat) val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth == Alice.nodeParams.channelConf.minDepthBlocks) + assert(watchConfirmed.minDepth == 1) // when funder we trust ourselves so we never wait more than 1 block + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] + } + + test("recv FundingSigned with valid signature (zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + bob2alice.expectMsgType[FundingSigned] + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) + // alice doesn't watch for the funding tx to confirm + alice2blockchain.expectMsgType[WatchFundingSpent] + alice2blockchain.expectMsgType[WatchFundingLost] + alice2blockchain.expectNoMessage(100 millis) aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } @@ -102,8 +115,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) alice2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - // when we are funder, we keep our regular min depth even for wumbo channels - assert(watchConfirmed.minDepth == Alice.nodeParams.channelConf.minDepthBlocks) + assert(watchConfirmed.minDepth == 1) // when funder we trust ourselves so we never wait more than 1 block 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 new file mode 100644 index 0000000000..167c75563d --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -0,0 +1,255 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.c + +import akka.testkit.{TestFSMRef, TestProbe} +import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +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.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +import scala.concurrent.duration._ + +/** + * Created by PM on 05/07/2016. + */ + +class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + val relayFees: RelayFees = RelayFees(999 msat, 1234) + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init() + import setup._ + val channelConfig = ChannelConfig.standard + val channelFlags = ChannelFlags(announceChannel = test.tags.contains(ChannelStateTestsTags.ChannelsPublic)) + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) + val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + + within(30 seconds) { + alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice2blockchain.expectMsgType[TxPublisher.SetChannelId] + bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob2blockchain.expectMsgType[TxPublisher.SetChannelId] + alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptChannel] + bob2alice.forward(alice) + alice2bob.expectMsgType[FundingCreated] + alice2bob.forward(bob) + bob2alice.expectMsgType[FundingSigned] + bob2alice.forward(alice) + alice2blockchain.expectMsgType[TxPublisher.SetChannelId] + alice2blockchain.expectMsgType[WatchFundingSpent] + bob2blockchain.expectMsgType[TxPublisher.SetChannelId] + bob2blockchain.expectMsgType[WatchFundingSpent] + 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] + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) + awaitCond(bob.stateName == WAIT_FOR_CHANNEL_READY) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router))) + } + } + + test("recv ChannelReady") { f => + import f._ + // we have a real scid at this stage, because this isn't a zero-conf channel + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + val channelReady = bob2alice.expectMsgType[ChannelReady] + bob2alice.forward(alice) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // we have a real scid, but the channel is not announced so alice uses bob's alias + assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (no alias)") { f => + import f._ + // we have a real scid at this stage, because this isn't a zero-conf channel + val realScid = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val channelReady = bob2alice.expectMsgType[ChannelReady] + val channelReadyNoAlias = channelReady.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelReadyTlv.ShortChannelIdTlv])) + bob2alice.forward(alice, channelReadyNoAlias) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // the channel is not announced but bob didn't send an alias so we use the real scid + assert(initialChannelUpdate.shortChannelId == realScid) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // zero-conf channel: we don't have a real scid + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + val channelReady = bob2alice.expectMsgType[ChannelReady] + bob2alice.forward(alice) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // the channel is not announced so alice uses bob's alias (we have a no real scid anyway) + assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (zero-conf, no alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // zero-conf channel: we don't have a real scid + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + val channelReady = bob2alice.expectMsgType[ChannelReady] + val channelReadyNoAlias = channelReady.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelReadyTlv.ShortChannelIdTlv])) + bob2alice.forward(alice, channelReadyNoAlias) + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].desc == "commit-tx") + assert(alice2blockchain.expectMsgType[TxPublisher.PublishTx].desc == "local-anchor") + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].desc == "local-main-delayed") + alice2blockchain.expectMsgType[WatchTxConfirmed] + } + + test("recv ChannelReady (public)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + // we have a real scid at this stage, because this isn't a zero-conf channel + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.channelFlags.announceChannel) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.channelFlags.announceChannel) + val channelReady = bob2alice.expectMsgType[ChannelReady] + bob2alice.forward(alice) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // we have a real scid, but it is not the final one (less than 6 confirmations) so alice uses bob's alias + assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (public, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // zero-conf channel: we don't have a real scid + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + val channelReady = bob2alice.expectMsgType[ChannelReady] + bob2alice.forward(alice) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // the channel is not announced, so alice uses bob's alias (we have a no real scid anyway) + assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv WatchFundingSpentTriggered (remote commit)") { f => + import f._ + // bob publishes his commitment tx + val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(tx) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + awaitCond(alice.stateName == CLOSING) + } + + test("recv WatchFundingSpentTriggered (other commit)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + alice2bob.expectMsgType[Error] + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + + test("recv Error") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! Error(ByteVector32.Zeroes, "oops") + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } + + test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + bob ! Error(ByteVector32.Zeroes, "funding double-spent") + awaitCond(bob.stateName == CLOSING) + assert(bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } + + test("recv CMD_CLOSE") { f => + import f._ + val sender = TestProbe() + val c = CMD_CLOSE(sender.ref, None, None) + alice ! c + sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_CHANNEL_READY))) + } + + test("recv CMD_FORCECLOSE") { f => + import f._ + val sender = TestProbe() + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! CMD_FORCECLOSE(sender.ref) + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } +} 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 149f4d9255..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 @@ -26,10 +26,10 @@ 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, Error, FundingCreated, FundingLocked, 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.{Outcome, Tag} import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ @@ -47,15 +47,16 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF import setup._ val channelConfig = ChannelConfig.standard + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val listener = TestProbe() alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -79,26 +80,55 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF } } - test("recv FundingLocked") { f => + test("recv ChannelReady (funder, with remote alias)") { f => import f._ - // we create a new listener that registers after alice has published the funding tx - val listener = TestProbe() - bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) - // make bob send a FundingLocked msg + // 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 txPublished = listener.expectMsgType[TransactionPublished] - assert(txPublished.tx == fundingTx) - assert(txPublished.miningFee == 0.sat) // bob is fundee - assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) - val msg = bob2alice.expectMsgType[FundingLocked] + val bobChannelReady = bob2alice.expectMsgType[ChannelReady] + assert(bobChannelReady.alias_opt.isDefined) + // test starts here bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(msg)) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + // 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 => + 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 channelReadyNoAlias = bob2alice.expectMsgType[ChannelReady].copy(tlvStream = TlvStream.empty) + // test starts here + bob2alice.forward(alice, channelReadyNoAlias) + // alice keeps bob's channel_ready for later processing + eventually { + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(channelReadyNoAlias)) + } + alice2blockchain.expectNoMessage() + } + + test("recv ChannelReady (fundee)") { f => + import f._ + // make alice send a ChannelReady msg + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + val channelReady = alice2bob.expectMsgType[ChannelReady] + // test starts here + alice2bob.forward(bob) + // alice keeps bob's channel_ready for later processing + eventually { + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(channelReady)) + } + // bob is fundee, he doesn't trust alice and won't create a zero-conf watch + bob2blockchain.expectNoMessage() } - test("recv WatchFundingConfirmedTriggered") { f => + test("recv WatchFundingConfirmedTriggered (funder)") { f => import f._ // we create a new listener that registers after alice has published the funding tx val listener = TestProbe() @@ -107,9 +137,31 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) alice2blockchain.expectMsgType[WatchFundingLost] - alice2bob.expectMsgType[FundingLocked] + val channelReady = alice2bob.expectMsgType[ChannelReady] + // we always send an alias + assert(channelReady.alias_opt.isDefined) + } + + test("recv WatchFundingConfirmedTriggered (fundee)") { f => + import f._ + // we create a new listener that registers after alice has published the funding tx + val listener = TestProbe() + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) + // 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 txPublished = listener.expectMsgType[TransactionPublished] + assert(txPublished.tx == fundingTx) + assert(txPublished.miningFee == 0.sat) // bob is fundee + assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) + awaitCond(bob.stateName == WAIT_FOR_CHANNEL_READY) + bob2blockchain.expectMsgType[WatchFundingLost] + val channelReady = bob2alice.expectMsgType[ChannelReady] + // we always send an alias + assert(channelReady.alias_opt.isDefined) } test("recv WatchFundingConfirmedTriggered (bad funding pubkey script)") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala deleted file mode 100644 index 01345af1e2..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.channel.states.c - -import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.fsm.Channel -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.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} -import org.scalatest.funsuite.FixtureAnyFunSuiteLike -import org.scalatest.{Outcome, Tag} - -import scala.concurrent.duration._ - -/** - * Created by PM on 05/07/2016. - */ - -class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - - val relayFees: RelayFees = RelayFees(999 msat, 1234) - - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe) - - override def withFixture(test: OneArgTest): Outcome = { - val setup = init() - import setup._ - val channelConfig = ChannelConfig.standard - val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) - - within(30 seconds) { - alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) - alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) - bob2blockchain.expectMsgType[TxPublisher.SetChannelId] - alice2bob.expectMsgType[OpenChannel] - alice2bob.forward(bob) - bob2alice.expectMsgType[AcceptChannel] - bob2alice.forward(alice) - alice2bob.expectMsgType[FundingCreated] - alice2bob.forward(bob) - bob2alice.expectMsgType[FundingSigned] - bob2alice.forward(alice) - alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - alice2blockchain.expectMsgType[WatchFundingSpent] - alice2blockchain.expectMsgType[WatchFundingConfirmed] - bob2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob2blockchain.expectMsgType[WatchFundingSpent] - bob2blockchain.expectMsgType[WatchFundingConfirmed] - awaitCond(alice.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[FundingLocked] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) - awaitCond(bob.stateName == WAIT_FOR_FUNDING_LOCKED) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router))) - } - } - - test("recv FundingLocked") { f => - import f._ - bob2alice.expectMsgType[FundingLocked] - bob2alice.forward(alice) - awaitCond(alice.stateName == NORMAL) - val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) - assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) - bob2alice.expectNoMessage(200 millis) - } - - test("recv WatchFundingSpentTriggered (remote commit)") { f => - import f._ - // bob publishes his commitment tx - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! WatchFundingSpentTriggered(tx) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) - awaitCond(alice.stateName == CLOSING) - } - - test("recv WatchFundingSpentTriggered (other commit)") { f => - import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) - alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - awaitCond(alice.stateName == ERR_INFORMATION_LEAK) - } - - test("recv Error") { f => - import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! Error(ByteVector32.Zeroes, "oops") - awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) - } - - test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => - import f._ - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - bob ! Error(ByteVector32.Zeroes, "funding double-spent") - awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) - } - - test("recv CMD_CLOSE") { f => - import f._ - val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None, None) - alice ! c - sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_LOCKED))) - } - - test("recv CMD_FORCECLOSE") { f => - import f._ - val sender = TestProbe() - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! CMD_FORCECLOSE(sender.ref) - awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) - } -} 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 2930c973ba..8ff235ff85 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 @@ -18,9 +18,9 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.TestProbe +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction} -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ @@ -41,7 +41,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits._ @@ -3400,78 +3400,157 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(addSettled.htlc == htlc1) } - test("recv WatchFundingDeeplyBuriedTriggered", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + test("recv WatchFundingDeeplyBuriedTriggered (public channel)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] - // public channel: we don't send the channel_update directly to the peer - alice2bob.expectNoMessage(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - // we don't re-publish the same channel_update if there was no change - channelUpdateListener.expectNoMessage(1 second) + assert(annSigs.shortChannelId == realShortChannelId) + // alice updates her internal state + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // public channel: alice will update the channel update to use the real scid when she receives her peer's announcement_signatures + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) } - test("recv WatchFundingDeeplyBuriedTriggered (short channel id changed)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + test("recv WatchFundingDeeplyBuriedTriggered (public channel, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400001), 22, null) + // in zero-conf channel we don't have a real short channel id when going to NORMAL state + assert(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // funding tx coordinates (unknown before) + val (blockHeight, txIndex) = (BlockHeight(400000), 42) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) + val realShortChannelId = RealShortChannelId(blockHeight, txIndex, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] - // public channel: we don't send the channel_update directly to the peer - alice2bob.expectNoMessage(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) - channelUpdateListener.expectNoMessage(1 second) + assert(annSigs.shortChannelId == realShortChannelId) + // alice updates her internal state + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // public channel: alice will update the channel update to use the real scid when she receives her peer's announcement_signatures + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + } + + test("recv WatchFundingDeeplyBuriedTriggered (public channel, short channel id changed)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + // new funding tx coordinates (there was a reorg) + val (blockHeight1, txIndex1) = (blockHeight + 10, txIndex + 10) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight1, txIndex1, null) + val newRealShortChannelId = RealShortChannelId(blockHeight1, txIndex1, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) + val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] + assert(annSigs.shortChannelId == newRealShortChannelId) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(newRealShortChannelId)) + // public channel: alice will update the channel update to use the real scid when she receives her peer's announcement_signatures + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) } test("recv WatchFundingDeeplyBuriedTriggered (private channel)") { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - // private channel: we send the channel_update directly to the peer - val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - // we don't re-publish the same channel_update if there was no change - channelUpdateListener.expectNoMessage(1 second) + val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // private channel: we prefer the remote alias, so there is no change in the channel_update, and we don't send a new one + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + } + + test("recv WatchFundingDeeplyBuriedTriggered (private channel, zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // 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 + assert(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42000), 42, null) + val realShortChannelId = RealShortChannelId(BlockHeight(42000), 42, 0) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // private channel: we prefer the remote alias, so there is no change in the channel_update, and we don't send a new one + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + // this is the first time we know the funding tx has been confirmed + listener.expectMsgType[TransactionConfirmed] } test("recv WatchFundingDeeplyBuriedTriggered (private channel, short channel id changed)") { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400001), 22, null) - // private channel: we send the channel_update directly to the peer - val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - // LocalChannelUpdate should not be published - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) - channelUpdateListener.expectNoMessage(1 second) + val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + // new funding tx coordinates (there was a reorg) + val (blockHeight1, txIndex1) = (blockHeight + 10, txIndex + 10) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight1, txIndex1, null) + val newRealShortChannelId = RealShortChannelId(blockHeight1, txIndex1, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(newRealShortChannelId)) + // private channel: we prefer the remote alias, so there is no change in the channel_update, and we don't send a new one + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) } test("recv AnnouncementSignatures", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + val realShortChannelId = initialState.shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] - bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + bob ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.channelKeyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) // actual test starts here - bob2alice.forward(alice) - awaitCond({ + bob2alice.forward(alice, annSigsB) + awaitAssert { val normal = alice.stateData.asInstanceOf[DATA_NORMAL] - normal.shortChannelId == annSigsA.shortChannelId && normal.buried && normal.channelAnnouncement.contains(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId - }) - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt == Some(channelAnn)) + assert(normal.shortIds.real == RealScidStatus.Final(annSigsA.shortChannelId) && normal.channelAnnouncement.contains(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId) + } + // we use the real scid instead of remote alias as soon as the channel is announced + val lcu = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(lcu.channelUpdate.shortChannelId == realShortChannelId) + assert(lcu.channelAnnouncement_opt.contains(channelAnn)) + // we don't send directly the channel_update to our peer, public announcements are handled by the router + alice2bob.expectNoMessage(100 millis) } test("recv AnnouncementSignatures (re-send)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42), 10, null) + val realShortChannelId = initialState.shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] - bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42), 10, null) + bob ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.channelKeyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement == Some(channelAnn)) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.contains(channelAnn)) // actual test starts here // simulate bob re-sending its sigs @@ -3527,9 +3606,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv INPUT_DISCONNECTED") { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - val update1a = alice2bob.expectMsgType[ChannelUpdate] - assert(update1a.channelFlags.isEnabled) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) // actual test starts here alice ! INPUT_DISCONNECTED @@ -3540,10 +3617,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv INPUT_DISCONNECTED (with pending unsigned htlcs)") { f => import f._ + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) val sender = TestProbe() - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - val update1a = alice2bob.expectMsgType[ChannelUpdate] - assert(update1a.channelFlags.isEnabled) val (_, htlc1) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val (_, htlc2) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index cca34ac7d0..6a66d8a789 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -18,9 +18,10 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} +import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw @@ -31,7 +32,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx} import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestFeeEstimator, TestKitBaseClass, TestUtils, TimestampMilli, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -45,14 +46,22 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with type FixtureParam = SetupFixture + /** If set, do not close channel in case of a fee mismatch when disconnected */ + val DisableOfflineMismatch = "disable_offline_mismatch" + /** If set, channel_update will be ignored */ + val IgnoreChannelUpdates = "ignore_channel_updates" + override def withFixture(test: OneArgTest): Outcome = { - val setup = test.tags.contains("disable-offline-mismatch") match { - case false => init() - case true => init(nodeParamsA = Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(closeOnOfflineMismatch = false))) - } + val aliceParams = Alice.nodeParams + .modify(_.onChainFeeConf.closeOnOfflineMismatch).setToIf(test.tags.contains(DisableOfflineMismatch))(false) + val setup = init(nodeParamsA = aliceParams) import setup._ within(30 seconds) { reachNormal(setup) + if (test.tags.contains(IgnoreChannelUpdates)) { + setup.alice2bob.ignoreMsg({ case _: ChannelUpdate => true }) + setup.bob2alice.ignoreMsg({ case _: ChannelUpdate => true }) + } awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) withFixture(test.toNoArgTest(setup)) @@ -62,7 +71,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val aliceInit = Init(TestConstants.Alice.nodeParams.features.initFeatures()) val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures()) - test("re-send lost htlc and signature after first commitment") { f => + test("re-send lost htlc and signature after first commitment", Tag(IgnoreChannelUpdates)) { f => import f._ // alice bob // | | @@ -85,9 +94,9 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob, reestablishA) bob2alice.forward(alice, reestablishB) - // both nodes will send the funding_locked message because all updates have been cancelled - alice2bob.expectMsgType[FundingLocked] - bob2alice.expectMsgType[FundingLocked] + // both nodes will send the channel_ready message because all updates have been cancelled + alice2bob.expectMsgType[ChannelReady] + bob2alice.expectMsgType[ChannelReady] // alice will re-send the update and the sig alice2bob.expectMsg(htlc) @@ -115,7 +124,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("re-send lost revocation") { f => + test("re-send lost revocation", Tag(IgnoreChannelUpdates)) { f => import f._ // alice bob // | | @@ -159,7 +168,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("re-send lost signature after revocation") { f => + test("re-send lost signature after revocation", Tag(IgnoreChannelUpdates)) { f => import f._ // alice bob // | | @@ -232,7 +241,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("resume htlc settlement") { f => + test("resume htlc settlement", Tag(IgnoreChannelUpdates)) { f => import f._ // Successfully send a first payment. @@ -279,7 +288,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 4) } - test("reconnect with an outdated commitment") { f => + test("reconnect with an outdated commitment", Tag(IgnoreChannelUpdates)) { f => import f._ val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) @@ -329,7 +338,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transaction.correctlySpends(claimMainOutput, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - test("reconnect with an outdated commitment (but counterparty can't tell)") { f => + test("reconnect with an outdated commitment (but counterparty can't tell)", Tag(IgnoreChannelUpdates)) { f => import f._ // we start by storing the current state @@ -384,7 +393,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transaction.correctlySpends(claimMainOutput, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - test("counterparty lies about having a more recent commitment") { f => + test("counterparty lies about having a more recent commitment", Tag(IgnoreChannelUpdates)) { f => import f._ val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx @@ -406,7 +415,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(error == Error(channelId(alice), InvalidRevokedCommitProof(channelId(alice), 0, 42, invalidReestablish.yourLastPerCommitmentSecret).getMessage)) } - test("change relay fee while offline") { f => + test("change relay fee while offline", Tag(IgnoreChannelUpdates)) { f => import f._ val sender = TestProbe() @@ -461,7 +470,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(!update.channelUpdate.channelFlags.isEnabled) } - test("replay pending commands when going back to NORMAL") { f => + test("replay pending commands when going back to NORMAL", Tag(IgnoreChannelUpdates)) { f => import f._ val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -613,7 +622,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("handle feerate changes while offline (don't close on mismatch)", Tag("disable-offline-mismatch")) { f => + test("handle feerate changes while offline (don't close on mismatch)", Tag(DisableOfflineMismatch)) { f => import f._ // we only close channels on feerate mismatch if there are HTLCs at risk in the commitment @@ -660,7 +669,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[ChannelReestablish] bob2alice.forward(alice) - alice2bob.expectMsgType[FundingLocked] // since the channel's commitment hasn't been updated, we re-send funding_locked + alice2bob.expectMsgType[ChannelReady] // since the channel's commitment hasn't been updated, we re-send channel_ready if (shouldUpdateFee) { alice2bob.expectMsg(UpdateFee(channelId(alice), networkFeeratePerKw)) } else { @@ -669,11 +678,11 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("handle feerate changes while offline (update at reconnection)") { f => + test("handle feerate changes while offline (update at reconnection)", Tag(IgnoreChannelUpdates)) { f => testUpdateFeeOnReconnect(f, shouldUpdateFee = true) } - test("handle feerate changes while offline (shutdown sent, don't update at reconnection)") { f => + test("handle feerate changes while offline (shutdown sent, don't update at reconnection)", Tag(IgnoreChannelUpdates)) { f => import f._ // alice initiates a shutdown @@ -722,20 +731,27 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("re-send channel_update at reconnection for private channels") { f => + test("re-send channel_update at reconnection for unannounced channels") { f => import f._ // we simulate a disconnection / reconnection disconnect(alice, bob) + // we wait 1s so the new channel_update doesn't have the same timestamp + TestUtils.waitFor(1 second) reconnect(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[ChannelReestablish] bob2alice.expectMsgType[ChannelReestablish] bob2alice.forward(alice) alice2bob.forward(bob) - // at this point the channel isn't deeply buried: channel_update isn't sent again - alice2bob.expectMsgType[FundingLocked] - bob2alice.expectMsgType[FundingLocked] + // alice and bob resend their channel_ready because there hasn't been payments on the channel + alice2bob.expectMsgType[ChannelReady] + bob2alice.expectMsgType[ChannelReady] + + // alice and bob resend their channel update at reconnection (unannounced channel) + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + alice2bob.expectNoMessage() bob2alice.expectNoMessage() @@ -745,24 +761,31 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // we simulate a disconnection / reconnection disconnect(alice, bob) + // we wait 1s so the new channel_update doesn't have the same timestamp + TestUtils.waitFor(1 second) reconnect(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[ChannelReestablish] bob2alice.expectMsgType[ChannelReestablish] bob2alice.forward(alice) alice2bob.forward(bob) - // at this point the channel still isn't deeply buried: channel_update isn't sent again + // alice and bob resend their channel update at reconnection (unannounced channel) + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + alice2bob.expectNoMessage() bob2alice.expectNoMessage() - // funding tx gets 6 confirmations, channel is private so there is no announcement sigs + // funding tx gets 6 confirmations alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - alice2bob.expectMsgType[ChannelUpdate] - bob2alice.expectMsgType[ChannelUpdate] + // channel is private so there is no announcement sigs + // we use aliases so there is no need to resend a channel_update // we get disconnected again disconnect(alice, bob) + // we wait 1s so the new channel_update doesn't have the same timestamp + TestUtils.waitFor(1 second) reconnect(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[ChannelReestablish] bob2alice.expectMsgType[ChannelReestablish] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 52052b9e67..556b453386 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -66,10 +66,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with if (unconfirmedFundingTx) { within(30 seconds) { val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index 39506f983f..9ef2ca1d01 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -845,7 +845,7 @@ class AuditDbSpec extends AnyFunSuite { val scid = ShortChannelId(123) val remoteNodeId = randomKey().publicKey val u = Announcements.makeChannelUpdate(randomBytes32(), randomKey(), remoteNodeId, scid, CltvExpiryDelta(56), 2000 msat, 1000 msat, 999, 1000000000 msat) - dbs.audit.addChannelUpdate(ChannelUpdateParametersChanged(null, channelId, scid, remoteNodeId, u)) + dbs.audit.addChannelUpdate(ChannelUpdateParametersChanged(null, channelId, remoteNodeId, u)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala index 5d4c5dede7..1a8c6e75d4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.db import com.softwaremill.quicklens._ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck} +import fr.acinq.eclair.channel.RealScidStatus import fr.acinq.eclair.db.ChannelsDbSpec.{getPgTimestamp, getTimestamp, testCases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.jdbc.JdbcUtils.using @@ -28,7 +29,7 @@ import fr.acinq.eclair.db.sqlite.SqliteChannelsDb import fr.acinq.eclair.db.sqlite.SqliteUtils.ExtendedResultSet._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.{CltvExpiry, ShortChannelId, TestDatabases, randomBytes32} +import fr.acinq.eclair.{CltvExpiry, RealShortChannelId, TestDatabases, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.ByteVector @@ -60,7 +61,7 @@ class ChannelsDbSpec extends AnyFunSuite { val channel1 = ChannelCodecsSpec.normal val channel2a = ChannelCodecsSpec.normal.modify(_.commitments.channelId).setTo(randomBytes32()) - val channel2b = channel2a.modify(_.shortChannelId).setTo(ShortChannelId(189371)) + val channel2b = channel2a.modify(_.shortIds.real).setTo(RealScidStatus.Final(RealShortChannelId(189371))) val commitNumber = 42 val paymentHash1 = ByteVector32.Zeroes diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala index a1e39298f9..3d56d767fb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.PublicChannel import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, TestDatabases, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TestDatabases, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite import scala.collection.{SortedMap, mutable} @@ -81,7 +81,7 @@ class NetworkDbSpec extends AnyFunSuite { forAllDbs { dbs => val db = dbs.network val sig = ByteVector64.Zeroes - val c = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val c = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) val txid = ByteVector32.fromValidHex("0001" * 16) db.addChannel(c, txid, Satoshi(42)) assert(db.listChannels() == SortedMap(c.shortChannelId -> PublicChannel(c, txid, Satoshi(42), None, None, None))) @@ -104,9 +104,9 @@ class NetworkDbSpec extends AnyFunSuite { val b = generatePubkeyHigherThan(a) val c = generatePubkeyHigherThan(b) - val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), a.publicKey, b.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) - val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(43), a.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) - val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(44), b.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), a.publicKey, b.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(43), a.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(44), b.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) val txid_1 = randomBytes32() val txid_2 = randomBytes32() @@ -194,7 +194,7 @@ class NetworkDbSpec extends AnyFunSuite { } } - val shortChannelIds: Seq[ShortChannelId] = (42 to (5000 + 42)).map(i => ShortChannelId(i)) + val shortChannelIds: Seq[RealShortChannelId] = (42 to (5000 + 42)).map(i => RealShortChannelId(i)) test("remove many channels") { forAllDbs { dbs => @@ -224,7 +224,7 @@ class NetworkDbSpec extends AnyFunSuite { db.addToPruned(shortChannelIds) shortChannelIds.foreach { id => assert(db.isPruned(id)) } - db.removeFromPruned(ShortChannelId(5)) + db.removeFromPruned(RealShortChannelId(5)) assert(!db.isPruned(ShortChannelId(5))) } } @@ -381,7 +381,7 @@ object NetworkDbSpec { val channelTestCases: Seq[ChannelTestCase] = for (_ <- 0 until 10) yield { val a = randomKey() val b = generatePubkeyHigherThan(a) - val channel = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(Random.nextInt(1_000_000)), a.publicKey, a.publicKey, randomKey().publicKey, randomKey().publicKey, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val channel = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(Random.nextInt(1_000_000)), a.publicKey, a.publicKey, randomKey().publicKey, randomKey().publicKey, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) val channel_update_1_opt = if (Random.nextBoolean()) { Some(Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, a, b.publicKey, channel.shortChannelId, CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, Random.nextBoolean())) } else None diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index c778b391ab..ff22a84393 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -495,7 +495,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { val channelId = sender.expectMsgType[RES_GET_CHANNEL_DATA[PersistentChannelData]].data.channelId awaitCond({ funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) - sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_FUNDING_LOCKED + sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_CHANNEL_READY }) generateBlocks(6) @@ -506,7 +506,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { // after 8 blocks the funder is still waiting for funding_locked from the fundee funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) - assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_FUNDING_LOCKED) + assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_CHANNEL_READY) // simulate a disconnection sender.send(funder.switchboard, Peer.Disconnect(fundee.nodeParams.nodeId)) @@ -520,7 +520,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { fundeeState == OFFLINE && funderState == OFFLINE }) - // reconnect and check the fundee is waiting for more conf, funder is waiting for fundee to send funding_locked + // reconnect and check the fundee is waiting for more conf, funder is waiting for fundee to send channel_ready awaitCond({ // reconnection sender.send(fundee.switchboard, Peer.Connect( @@ -535,7 +535,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state - fundeeState == WAIT_FOR_FUNDING_CONFIRMED && funderState == WAIT_FOR_FUNDING_LOCKED + fundeeState == WAIT_FOR_FUNDING_CONFIRMED && funderState == WAIT_FOR_CHANNEL_READY }, max = 30 seconds, interval = 10 seconds) // 5 extra blocks make it 13, just the amount of confirmations needed @@ -829,7 +829,7 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte } test("open channel C <-> F, send payments and close (anchor outputs zero fee htlc txs)") { - testOpenPayClose(ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + testOpenPayClose(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) } test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs zero fee htlc txs)") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 0b540986b5..1e383811e0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -28,7 +28,6 @@ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.RouteCalculation.ROUTE_MAX_LENGTH import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, SearchBoundaries, NORMAL => _, State => _} -import fr.acinq.eclair.wire.protocol.NodeAddress import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Kit, MilliSatoshi, MilliSatoshiLong, Setup, TestKitBaseClass} import grizzled.slf4j.Logging import org.json4s.{DefaultFormats, Formats} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala index 836470bf9b..6c19262e89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala @@ -24,6 +24,7 @@ import akka.util.Timeout import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.Transaction import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.eclair.TestUtils.waitEventStreamSynced import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient @@ -303,11 +304,12 @@ class MessageIntegrationSpec extends IntegrationSpec { val probe = TestProbe() val eventListener = TestProbe() nodes("C").system.eventStream.subscribe(eventListener.ref, classOf[OnionMessages.ReceiveMessage]) + waitEventStreamSynced(nodes("C").system.eventStream) alice.sendOnionMessage(nodes("B").nodeParams.nodeId :: Nil, Left(nodes("C").nodeParams.nodeId), None, hex"7300").pipeTo(probe.ref) assert(probe.expectMsgType[SendOnionMessageResponse].sent) val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds) - assert(r.pathId == None) + assert(r.pathId.isEmpty) assert(r.finalPayload.records.unknown.toSet == Set(GenericTlv(UInt64(115), hex""))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 976dfcbbef..e0d7bcca0b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -32,7 +32,6 @@ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.relay.Relayer @@ -41,7 +40,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTra import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel} import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router} -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails, NodeAnnouncement} +import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails} import fr.acinq.eclair.{CltvExpiryDelta, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32} import org.json4s.JsonAST.{JString, JValue} import scodec.bits.ByteVector @@ -114,22 +113,19 @@ class PaymentIntegrationSpec extends IntegrationSpec { } } - def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, channels: Int, updates: Int): Unit = { + def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, privateChannels: Int, publicChannels: Int, privateUpdates: Int, publicUpdates: Int): Unit = { val sender = TestProbe() subset.foreach { case (node, setup) => withClue(node) { awaitAssert({ - sender.send(setup.router, Router.GetNodes) - assert(sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes) - }, max = 10 seconds, interval = 1 second) - awaitAssert({ - sender.send(setup.router, Router.GetChannels) - sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels - }, max = 10 seconds, interval = 1 second) - awaitAssert({ - sender.send(setup.router, Router.GetChannelUpdates) - sender.expectMsgType[Iterable[ChannelUpdate]].size == updates + sender.send(setup.router, Router.GetRouterData) + val data = sender.expectMsgType[Router.Data] + assert(data.nodes.size == nodes) + assert(data.privateChannels.size == privateChannels) + assert(data.channels.size == publicChannels) + assert(data.privateChannels.values.flatMap(pc => pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).size == privateUpdates) + assert(data.channels.values.flatMap(pc => pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).size == publicUpdates) }, max = 10 seconds, interval = 1 second) } } @@ -141,8 +137,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { // A requires private channels, as a consequence: // - only A and B know about channel A-B (and there is no channel_announcement) // - A is not announced (no node_announcement) - awaitAnnouncements(nodes.view.filterKeys(key => List("A", "B").contains(key)).toMap, 6, 8, 18) - awaitAnnouncements(nodes.view.filterKeys(key => List("C", "D", "E", "G").contains(key)).toMap, 6, 8, 16) + awaitAnnouncements(nodes.view.filterKeys(key => List("A", "B").contains(key)).toMap, nodes = 6, privateChannels = 1, publicChannels = 8, privateUpdates = 2, publicUpdates = 16) + awaitAnnouncements(nodes.view.filterKeys(key => List("C", "D", "E", "G").contains(key)).toMap, nodes = 6, privateChannels = 0, publicChannels = 8, privateUpdates = 0, publicUpdates = 16) } test("wait for channels balance") { @@ -153,7 +149,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val routingState = sender.expectMsgType[Router.RoutingState] val publicChannels = routingState.channels.filter(pc => Set(pc.ann.nodeId1, pc.ann.nodeId2).contains(nodeId)) assert(publicChannels.nonEmpty) - publicChannels.foreach(pc => assert(pc.meta_opt.map(m => m.balance1 > 0.msat || m.balance2 > 0.msat) == Some(true), pc)) + publicChannels.foreach(pc => assert(pc.meta_opt.exists(m => m.balance1 > 0.msat || m.balance2 > 0.msat), pc)) } test("send an HTLC A->D") { @@ -183,11 +179,11 @@ class PaymentIntegrationSpec extends IntegrationSpec { val shortIdBC = sender.expectMsgType[Iterable[ChannelAnnouncement]].find(c => Set(c.nodeId1, c.nodeId2) == Set(nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)).get.shortChannelId // we also need the full commitment nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) - val commitmentBC = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL].commitments + val normalBC = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL] // we then forge a new channel_update for B-C... val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.channelConf.expiryDelta + 1, nodes("C").nodeParams.channelConf.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) // ...and notify B's relayer - nodes("B").system.eventStream.publish(LocalChannelUpdate(system.deadLetters, commitmentBC.channelId, shortIdBC, commitmentBC.remoteParams.nodeId, None, channelUpdateBC, commitmentBC)) + nodes("B").system.eventStream.publish(LocalChannelUpdate(system.deadLetters, normalBC.channelId, normalBC.shortIds, normalBC.commitments.remoteParams.nodeId, normalBC.channelAnnouncement, channelUpdateBC, normalBC.commitments)) // we retrieve a payment hash from D val amountMsat = 4200000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala index f6c2eedcc4..a1c8306f80 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala @@ -1,10 +1,9 @@ package fr.acinq.eclair.integration.basic import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} -import fr.acinq.eclair.ShortChannelId.txIndex +import fr.acinq.eclair.MilliSatoshiLong import fr.acinq.eclair.integration.basic.fixtures.ThreeNodesFixture import fr.acinq.eclair.testutils.FixtureSpec -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong} import org.scalatest.TestData import org.scalatest.concurrent.IntegrationPatience import scodec.bits.HexStringSyntax @@ -36,33 +35,20 @@ class ThreeNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { connect(alice, bob) connect(bob, carol) - val channelIdAB = openChannel(alice, bob, 100_000 sat).channelId - val channelIdBC = openChannel(bob, carol, 100_000 sat).channelId + // we put watchers on auto pilot to confirm funding txs + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) + carol.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) - val fundingTxAB = fundingTx(alice, channelIdAB) - val fundingTxBC = fundingTx(bob, channelIdBC) - - val shortIdAB = confirmChannel(alice, bob, channelIdAB, BlockHeight(420_000), 21) - val shortIdBC = confirmChannel(bob, carol, channelIdBC, BlockHeight(420_001), 22) - - val fundingTxs = Map( - shortIdAB -> fundingTxAB, - shortIdBC -> fundingTxBC - ) - - // auto-validate channel announcements - alice.watcher.setAutoPilot(autoValidatePublicChannels(fundingTxs)) - bob.watcher.setAutoPilot(autoValidatePublicChannels(fundingTxs)) - - confirmChannelDeep(alice, bob, channelIdAB, shortIdAB.blockHeight, txIndex(shortIdAB)) - confirmChannelDeep(bob, carol, channelIdBC, shortIdBC.blockHeight, txIndex(shortIdBC)) + openChannel(alice, bob, 100_000 sat).channelId + openChannel(bob, carol, 100_000 sat).channelId // alice now knows about bob-carol eventually { val routerData = getRouterData(alice) - //prettyPrint(routerData, alice, bob, carol) + prettyPrint(routerData, alice, bob, carol) assert(routerData.channels.size == 2) // 2 channels - assert(routerData.channels.values.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 3) // only 3 channel_updates because c->b is disabled (all funds on b) + assert(routerData.channels.values.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 4) // 2 channel_updates per channel } sendPayment(alice, carol, 100_000 msat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala index ae2ab79092..89598db9e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.integration.basic import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} -import fr.acinq.eclair.channel.{DATA_NORMAL, NORMAL} +import fr.acinq.eclair.channel.{DATA_NORMAL, NORMAL, RealScidStatus} import fr.acinq.eclair.integration.basic.fixtures.TwoNodesFixture import fr.acinq.eclair.testutils.FixtureSpec import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong} @@ -45,8 +45,8 @@ class TwoNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { test("open a channel alice-bob (autoconfirm)") { f => import f._ - alice.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) - bob.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) connect(alice, bob) val channelId = openChannel(alice, bob, 100_000 sat).channelId eventually { @@ -61,19 +61,19 @@ class TwoNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { val channelId = openChannel(alice, bob, 100_000 sat).channelId confirmChannel(alice, bob, channelId, BlockHeight(420_000), 21) confirmChannelDeep(alice, bob, channelId, BlockHeight(420_000), 21) - assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].buried) - assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].buried) + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) } test("open a channel alice-bob and confirm deeply (autoconfirm)") { f => import f._ - alice.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) - bob.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) connect(alice, bob) val channelId = openChannel(alice, bob, 100_000 sat).channelId eventually { - assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].buried) - assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].buried) + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala new file mode 100644 index 0000000000..23313dffe9 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala @@ -0,0 +1,79 @@ +package fr.acinq.eclair.integration.basic + +import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.ZeroConf +import fr.acinq.eclair.channel.ChannelTypes.AnchorOutputsZeroFeeHtlcTx +import fr.acinq.eclair.channel.PersistentChannelData +import fr.acinq.eclair.integration.basic.fixtures.TwoNodesFixture +import fr.acinq.eclair.testutils.FixtureSpec +import org.scalatest.concurrent.IntegrationPatience +import org.scalatest.{Tag, TestData} +import scodec.bits.HexStringSyntax + +/** + * Test the activation of zero-conf option, via features or channel type. + */ +class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = TwoNodesFixture + + val ZeroConfBob = "zero_conf_bob" + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + override def createFixture(testData: TestData): FixtureParam = { + // seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc. + val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + .modify(_.features.activated).using(_ - ZeroConf) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBob))(_ + (ZeroConf -> Optional)) + TwoNodesFixture(aliceParams, bobParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + test("open a channel alice-bob (zero-conf disabled on both sides)") { f => + import f._ + + assert(!alice.nodeParams.features.activated.contains(ZeroConf)) + assert(!bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + + assert(!getChannelData(alice, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + assert(!getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + } + + test("open a channel alice-bob (zero-conf disabled on both sides, requested via channel type by alice)") { f => + import f._ + + assert(!alice.nodeParams.features.activated.contains(ZeroConf)) + assert(!bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelType = AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true) + // bob rejects the channel + intercept[AssertionError] { + openChannel(alice, bob, 100_000 sat, channelType_opt = Some(channelType)).channelId + } + } + + test("open a channel alice-bob (zero-conf enabled on bob, requested via channel type by alice)", Tag(ZeroConfBob)) { f => + import f._ + + assert(!alice.nodeParams.features.activated.contains(ZeroConf)) + assert(bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelType = AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true) + val channelId = openChannel(alice, bob, 100_000 sat, channelType_opt = Some(channelType)).channelId + + assert(getChannelData(alice, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + assert(getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala new file mode 100644 index 0000000000..71c26c03b0 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala @@ -0,0 +1,231 @@ +package fr.acinq.eclair.integration.basic + +import com.softwaremill.quicklens._ +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{ScidAlias, ZeroConf} +import fr.acinq.eclair.channel.{DATA_NORMAL, RealScidStatus} +import fr.acinq.eclair.integration.basic.fixtures.ThreeNodesFixture +import fr.acinq.eclair.payment.PaymentSent +import fr.acinq.eclair.testutils.FixtureSpec +import fr.acinq.eclair.{MilliSatoshiLong, RealShortChannelId} +import org.scalatest.OptionValues.convertOptionToValuable +import org.scalatest.concurrent.IntegrationPatience +import org.scalatest.{Tag, TestData} +import scodec.bits.HexStringSyntax + +class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = ThreeNodesFixture + + val ZeroConfBobCarol = "zeroconf_bob_carol" + val ScidAliasBobCarol = "scid_alias_bob_carol" + val PublicBobCarol = "public_bob_carol" + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + override def createFixture(testData: TestData): FixtureParam = { + // seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc. + val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + .modify(_.features.activated).using(_ - ZeroConf - ScidAlias) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBobCarol))(_ + (ZeroConf -> Optional)) + .modify(_.features.activated).usingIf(testData.tags.contains(ScidAliasBobCarol))(_ + (ScidAlias -> Optional)) + .modify(_.channelConf.channelFlags.announceChannel).setTo(testData.tags.contains(PublicBobCarol)) + val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc")) + .modify(_.features.activated).using(_ - ZeroConf - ScidAlias) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBobCarol))(_ + (ZeroConf -> Mandatory)) + .modify(_.features.activated).usingIf(testData.tags.contains(ScidAliasBobCarol))(_ + (ScidAlias -> Mandatory)) + .modify(_.channelConf.channelFlags.announceChannel).setTo(testData.tags.contains(PublicBobCarol)) + ThreeNodesFixture(aliceParams, bobParams, carolParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + private def createChannels(f: FixtureParam)(deepConfirm: Boolean): (ByteVector32, ByteVector32) = { + import f._ + + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol), deepConfirm = deepConfirm)) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol), deepConfirm = deepConfirm)) + carol.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol), deepConfirm = deepConfirm)) + + connect(alice, bob) + connect(bob, carol) + + val channelId_ab = openChannel(alice, bob, 100_000 sat).channelId + val channelId_bc = openChannel(bob, carol, 100_000 sat).channelId + + (channelId_ab, channelId_bc) + } + + private def sendPaymentAliceToCarol(f: FixtureParam, useHint: Boolean = false, overrideHintScid_opt: Option[RealShortChannelId] = None): PaymentSent = { + import f._ + val hint = if (useHint) { + val Some(carolHint) = getRouterData(carol).privateChannels.values.head.toIncomingExtraHop + // due to how node ids are built, bob < carol so carol is always the node 2 + val bobAlias = getRouterData(bob).privateChannels.values.find(_.nodeId2 == carol.nodeParams.nodeId).value.shortIds.localAlias + // the hint is always using the alias + assert(carolHint.shortChannelId == bobAlias) + Seq(carolHint.modify(_.shortChannelId).setToIfDefined(overrideHintScid_opt)) + } else Seq.empty + sendPayment(alice, carol, 100_000 msat, hints = Seq(hint)) + } + + private def internalTest(f: FixtureParam, + deepConfirm: Boolean, + bcPublic: Boolean, + bcZeroConf: Boolean, + bcScidAlias: Boolean, + paymentWorksWithoutHint: Boolean, + paymentWorksWithHint_opt: Option[Boolean], + paymentWorksWithRealScidHint_opt: Option[Boolean]): Unit = { + import f._ + + val (_, channelId_bc) = createChannels(f)(deepConfirm = deepConfirm) + + eventually { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ZeroConf) == bcZeroConf) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ScidAlias) == bcScidAlias) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFlags.announceChannel == bcPublic) + if (deepConfirm) { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + } else if (bcZeroConf) { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + } else { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + } + } + + if (bcPublic && deepConfirm) { + // if channel bob-carol is public, we wait for alice to learn about it + eventually { + val data = getRouterData(alice) + assert(data.channels.size == 2) + assert(data.channels.values.forall(pc => pc.update_1_opt.isDefined && pc.update_2_opt.isDefined)) + } + } + + if (paymentWorksWithoutHint) { + sendPaymentAliceToCarol(f) + } else { + intercept[AssertionError] { + sendPaymentAliceToCarol(f) + } + } + + paymentWorksWithHint_opt match { + case Some(true) => sendPaymentAliceToCarol(f, useHint = true) + case Some(false) => intercept[AssertionError] { + sendPaymentAliceToCarol(f, useHint = true) + } + case None => // skipped + } + + paymentWorksWithRealScidHint_opt match { + // if alice uses the real scid instead of the bob-carol alias, it still works + case Some(true) => sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) + case Some(false) => intercept[AssertionError] { + sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) + } + case None => // skipped + } + } + + test("a->b->c (b-c private)") { f => + import f._ + + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = false, + bcScidAlias = false, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works (and it will use the alias, even if the feature isn't enabled) + paymentWorksWithRealScidHint_opt = Some(true) // if alice uses the real scid instead of the bob-carol alias, it still works + ) + } + + test("a->b->c (b-c scid-alias private)", Tag(ScidAliasBobCarol)) { f => + import f._ + + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = false, + bcScidAlias = true, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + paymentWorksWithRealScidHint_opt = Some(false) // if alice uses the real scid instead of the bob-carol alias, it doesn't work due to option_scid_alias + ) + } + + test("a->b->c (b-c zero-conf unconfirmed private)", Tag(ZeroConfBobCarol)) { f => + import f._ + + internalTest(f, + deepConfirm = false, + bcPublic = false, + bcZeroConf = true, + bcScidAlias = false, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + paymentWorksWithRealScidHint_opt = None // there is no real scid for bob-carol yet + ) + } + + test("a->b->c (b-c zero-conf deeply confirmed private)", Tag(ZeroConfBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = true, + bcScidAlias = false, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + // TODO: we should be able to send payments with the real scid in the routing hint, but this currently doesn't work, + // because the ChannelRelayer relies on the the LocalChannelUpdate event to maintain its scid resolution map, and + // the channel doesn't emit a new one when a real scid is assigned, because we use the remote alias for the + // channel_update, not the real scid. So the channel_update remains the same. We used to have the ChannelRelayer + // also listen to ShortChannelIdAssigned event, but it's doesn't seem worth it here. + paymentWorksWithRealScidHint_opt = None + ) + } + + test("a->b->c (b-c zero-conf scid-alias deeply confirmed private)", Tag(ZeroConfBobCarol), Tag(ScidAliasBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = true, + bcScidAlias = true, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + paymentWorksWithRealScidHint_opt = Some(false) // if alice uses the real scid instead of the b-c alias, it doesn't work due to option_scid_alias + ) + } + + test("a->b->c (b-c zero-conf unconfirmed public)", Tag(ZeroConfBobCarol), Tag(PublicBobCarol)) { f => + internalTest(f, + deepConfirm = false, + bcPublic = true, + bcZeroConf = true, + bcScidAlias = false, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced yet + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + paymentWorksWithRealScidHint_opt = None // there is no real scid for bob-carol yet + ) + } + + test("a->b->c (b-c zero-conf deeply confirmed public)", Tag(ZeroConfBobCarol), Tag(PublicBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = true, + bcZeroConf = true, + bcScidAlias = false, + paymentWorksWithoutHint = true, + paymentWorksWithHint_opt = None, // there is no routing hints for public channels + paymentWorksWithRealScidHint_opt = None // there is no routing hints for public channels + ) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 9962d5972b..491a3ea271 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -6,6 +6,7 @@ import akka.testkit.{TestActor, TestProbe} import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, Transaction} +import fr.acinq.eclair.ShortChannelId.txIndex import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchFundingConfirmedTriggered, WatchFundingDeeplyBuried, WatchFundingDeeplyBuriedTriggered} @@ -16,13 +17,14 @@ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.PeerConnection.ConnectionResult import fr.acinq.eclair.io.{Peer, PeerConnection, Switchboard} +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.receive.{MultiPartHandler, PaymentHandler} import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.send.PaymentInitiator import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSent} import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IPAddress -import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} +import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} import org.scalatest.Assertions import org.scalatest.concurrent.Eventually.eventually @@ -97,7 +99,13 @@ object MinimalNodeFixture extends Assertions { ) } - def connect(node1: MinimalNodeFixture, node2: MinimalNodeFixture)(implicit system: ActorSystem): ConnectionResult.Connected = { + /** + * Connect node1 to node2, using a real [[PeerConnection]] and a fake transport layer. + * + * @param mutate12 a method to alter messages from node1 to node2 mid-flight for testing purposes + * @param mutate21 a method to alter messages from node2 to node1 mid-flight for testing purposes + */ + def connect(node1: MinimalNodeFixture, node2: MinimalNodeFixture, mutate12: Any => Any = identity, mutate21: Any => Any = identity)(implicit system: ActorSystem): ConnectionResult.Connected = { val sender = TestProbe("sender") val connection1 = TestProbe("connection-1")(node1.system) @@ -114,7 +122,7 @@ object MinimalNodeFixture extends Assertions { case _: TransportHandler.Listener => TestActor.KeepRunning case _: TransportHandler.ReadAck => TestActor.KeepRunning case _ => - peerConnection2.tell(msg, transport2.ref) + peerConnection2.tell(mutate12(msg), transport2.ref) TestActor.KeepRunning } } @@ -124,7 +132,7 @@ object MinimalNodeFixture extends Assertions { case _: TransportHandler.Listener => TestActor.KeepRunning case _: TransportHandler.ReadAck => TestActor.KeepRunning case _ => - peerConnection1.tell(msg, transport1.ref) + peerConnection1.tell(mutate21(msg), transport1.ref) TestActor.KeepRunning } } @@ -141,9 +149,9 @@ object MinimalNodeFixture extends Assertions { sender.expectMsgType[ConnectionResult.Connected] } - def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi)(implicit system: ActorSystem): ChannelOpened = { + def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi, channelType_opt: Option[SupportedChannelType] = None)(implicit system: ActorSystem): ChannelOpened = { val sender = TestProbe("sender") - sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, 0L msat, None, None, None, None)) + sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, 0L msat, channelType_opt, None, None, None)) sender.expectMsgType[ChannelOpened] } @@ -152,7 +160,7 @@ object MinimalNodeFixture extends Assertions { node.wallet.funded(fundingTxid) } - def confirmChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): ShortChannelId = { + def confirmChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): Option[RealScidStatus.Temporary] = { assert(getChannelState(node1, channelId) == WAIT_FOR_FUNDING_CONFIRMED) val data1Before = getChannelData(node1, channelId).asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED] val fundingTx = data1Before.fundingTx.get @@ -170,13 +178,13 @@ object MinimalNodeFixture extends Assertions { val data1After = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] val data2After = getChannelData(node2, channelId).asInstanceOf[DATA_NORMAL] - assert(data1After.shortChannelId == data2After.shortChannelId) - assert(!data1After.buried && !data2After.buried) - - data1After.shortChannelId + val realScid1 = data1After.shortIds.real.asInstanceOf[RealScidStatus.Temporary] + val realScid2 = data2After.shortIds.real.asInstanceOf[RealScidStatus.Temporary] + assert(realScid1 == realScid2) + Some(realScid1) } - def confirmChannelDeep(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): ShortChannelId = { + def confirmChannelDeep(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): RealScidStatus.Final = { assert(getChannelState(node1, channelId) == NORMAL) val data1Before = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] val fundingTxid = data1Before.commitments.commitInput.outPoint.txid @@ -193,10 +201,10 @@ object MinimalNodeFixture extends Assertions { val data1After = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] val data2After = getChannelData(node2, channelId).asInstanceOf[DATA_NORMAL] - assert(data1After.shortChannelId == data2After.shortChannelId) - assert(data1After.buried && data2After.buried) - - data1After.shortChannelId + val realScid1 = data1After.shortIds.real.asInstanceOf[RealScidStatus.Final] + val realScid2 = data2After.shortIds.real.asInstanceOf[RealScidStatus.Final] + assert(realScid1 == realScid2) + realScid1 } /** Utility method to make sure that the channel has processed all previous messages */ @@ -222,41 +230,54 @@ object MinimalNodeFixture extends Assertions { sender.expectMsgType[Router.Data] } - private def deterministicShortId(txId: ByteVector32): (BlockHeight, Int) = { + /** + * Computes a deterministic [[RealShortChannelId]] based on a txid. We need this so that watchers can verify + * transactions in a independent and stateless fashion, since there is no actual blockchain in those tests. + */ + def deterministicShortId(txId: ByteVector32): RealShortChannelId = { val blockHeight = txId.take(3).toInt(signed = false) val txIndex = txId.takeRight(2).toInt(signed = false) - (BlockHeight(blockHeight), txIndex) + val outputIndex = 0 // funding txs created by the dummy wallet used in tests only have one output + RealShortChannelId(BlockHeight(blockHeight), txIndex, outputIndex) } - /** An autopilot method for the watcher, that handled funding confirmation requests from the channel */ - def autoConfirmLocalChannels(fundingTxs: collection.concurrent.Map[ByteVector32, Transaction]): TestActor.AutoPilot = (_, msg) => msg match { + /** All known funding txs (we don't evaluate immediately because new ones could be created) */ + def knownFundingTxs(nodes: MinimalNodeFixture*): () => Iterable[Transaction] = () => nodes.map(_.wallet.funded.values).reduce(_ ++ _) + + /** + * An autopilot method for the watcher, that handled funding confirmation requests from the channel and channel + * validation requests from the router + */ + def watcherAutopilot(knownFundingTxs: () => Iterable[Transaction], deepConfirm: Boolean = true): TestActor.AutoPilot = (_, msg) => msg match { case watch: ZmqWatcher.WatchFundingConfirmed => - val (blockHeight, txIndex) = deterministicShortId(watch.txId) - watch.replyTo ! ZmqWatcher.WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTxs(watch.txId)) + val realScid = deterministicShortId(watch.txId) + val fundingTx = knownFundingTxs().find(_.txid == watch.txId) + .getOrElse(throw new RuntimeException(s"unknown fundingTxId=${watch.txId}, known=${knownFundingTxs().map(_.txid).mkString(",")}")) + watch.replyTo ! ZmqWatcher.WatchFundingConfirmedTriggered(realScid.blockHeight, txIndex(realScid), fundingTx) TestActor.KeepRunning - case watch: ZmqWatcher.WatchFundingDeeplyBuried => - val (blockHeight, txIndex) = deterministicShortId(watch.txId) - watch.replyTo ! ZmqWatcher.WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTxs(watch.txId)) + case watch: ZmqWatcher.WatchFundingDeeplyBuried if deepConfirm => + val realScid = deterministicShortId(watch.txId) + val fundingTx = knownFundingTxs().find(_.txid == watch.txId).get + watch.replyTo ! ZmqWatcher.WatchFundingDeeplyBuriedTriggered(realScid.blockHeight, txIndex(realScid), fundingTx) TestActor.KeepRunning - case _ => TestActor.KeepRunning - } - - /** An autopilot method for the watcher, that handles channel validation requests from the router */ - def autoValidatePublicChannels(fundingTxs: Map[ShortChannelId, Transaction]): TestActor.AutoPilot = (_, msg: Any) => msg match { case vr: ZmqWatcher.ValidateRequest => - val res = Try(fundingTxs(vr.ann.shortChannelId), ZmqWatcher.UtxoStatus.Unspent).toEither + val res = Try { + val fundingTx = knownFundingTxs().find(tx => deterministicShortId(tx.txid) == vr.ann.shortChannelId) + .getOrElse(throw new RuntimeException(s"unknown realScid=${vr.ann.shortChannelId}, known=${knownFundingTxs().map(tx => deterministicShortId(tx.txid)).mkString(",")}")) + (fundingTx, ZmqWatcher.UtxoStatus.Unspent) + }.toEither vr.replyTo ! ZmqWatcher.ValidateResult(vr.ann, res) TestActor.KeepRunning case _ => TestActor.KeepRunning } - def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi)(implicit system: ActorSystem): PaymentSent = { + def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentSent = { val sender = TestProbe("sender") sender.send(node2.paymentHandler, MultiPartHandler.ReceivePayment(Some(amount), Left("test payment"))) val invoice = sender.expectMsgType[Bolt11Invoice] val routeParams = node1.nodeParams.routerConf.pathFindingExperimentConf.experiments.values.head.getDefaultRouteParams - sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = routeParams, blockUntilComplete = true)) + sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = routeParams, assistedRoutes = hints, blockUntilComplete = true)) sender.expectMsgType[PaymentSent] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index fffe22d180..5985109de1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -23,6 +23,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.TestConstants._ +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} @@ -333,7 +334,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val query = QueryShortChannelIds( Alice.nodeParams.chainHash, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42000))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(42000))), TlvStream.empty) // make sure that routing messages go through @@ -423,7 +424,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) probe.send(peerConnection, message) assert(peerConnection.stateName == PeerConnection.CONNECTED) - probe.send(peerConnection, FundingLocked(ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), randomKey().publicKey)) + probe.send(peerConnection, ChannelReady(ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), randomKey().publicKey)) peerConnection.stateData match { case d: PeerConnection.ConnectedData => assert(d.isPersistent) case _ => fail() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index e557c5b3e0..77d0623c0e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -341,7 +341,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } // They only support anchor outputs with zero fee htlc txs and we don't. { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs_zero_fee_htlc_tx, expected channel_type=standard")) } @@ -432,7 +432,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) assert(init.fundingAmount == 15000.sat) assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) assert(init.fundingTxFeerate == feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) @@ -450,6 +450,14 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle assert(init.localParams.defaultFinalScriptPubKey == Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get))) } + test("do not allow option_scid_alias with public channel") { f => + import f._ + + intercept[IllegalArgumentException] { + Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, Some(ChannelFlags(announceChannel = true)), None) + } + } + test("set origin_opt when spawning a channel") { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index 1b3708bd4c..0249b9a53f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -202,7 +202,7 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { toLocal = Map(ByteVector32(hex"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247") -> Btc(0.1))) )) ) - JsonSerializers.serialization.write(gb)(JsonSerializers.formats) shouldBe """{"total":1.0,"onChain":{"confirmed":0.4,"unconfirmed":0.05},"offChain":{"waitForFundingConfirmed":0.0,"waitForFundingLocked":0.0,"normal":{"toLocal":0.2,"htlcs":0.05},"shutdown":{"toLocal":0.0,"htlcs":0.0},"negotiating":0.0,"closing":{"localCloseBalance":{"toLocal":{"4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2":0.1},"htlcs":{"94b70cec5a98d67d17c6e3de5c7697f8a6cab4f698df91e633ce35efa3574d71":0.03,"a844edd41ce8503864f3c95d89d850b177a09d7d35e950a7d27c14abb63adb13":0.06},"htlcsUnpublished":0.01},"remoteCloseBalance":{"toLocal":{},"htlcs":{},"htlcsUnpublished":0.0},"mutualCloseBalance":{"toLocal":{"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247":0.1}},"unknownCloseBalance":{"toLocal":0.0,"htlcs":0.0}},"waitForPublishFutureCommitment":0.0}}""" + JsonSerializers.serialization.write(gb)(JsonSerializers.formats) shouldBe """{"total":1.0,"onChain":{"confirmed":0.4,"unconfirmed":0.05},"offChain":{"waitForFundingConfirmed":0.0,"waitForChannelReady":0.0,"normal":{"toLocal":0.2,"htlcs":0.05},"shutdown":{"toLocal":0.0,"htlcs":0.0},"negotiating":0.0,"closing":{"localCloseBalance":{"toLocal":{"4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2":0.1},"htlcs":{"94b70cec5a98d67d17c6e3de5c7697f8a6cab4f698df91e633ce35efa3574d71":0.03,"a844edd41ce8503864f3c95d89d850b177a09d7d35e950a7d27c14abb63adb13":0.06},"htlcsUnpublished":0.01},"remoteCloseBalance":{"toLocal":{},"htlcs":{},"htlcsUnpublished":0.0},"mutualCloseBalance":{"toLocal":{"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247":0.1}},"unknownCloseBalance":{"toLocal":0.0,"htlcs":0.0}},"waitForPublishFutureCommitment":0.0}}""" } test("type hints") { @@ -306,6 +306,20 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { JsonSerializers.serialization.write(map)(JsonSerializers.formats) shouldBe s"""{"e2fc57221cfb1942224082174022f3f70a32005aa209956f9c94c6903f7669ff":"ok","8e3ec6e16436b7dc61b86340192603d05f16d4f8e06c8aaa02fbe2ad63209af3":"cannot execute command=CMD_UPDATE_RELAY_FEE in state=CLOSING","74ca7a86e52d597aa2248cd2ff3b24428ede71345262be7fb31afddfe18dc0d8":"channel 74ca7a86e52d597aa2248cd2ff3b24428ede71345262be7fb31afddfe18dc0d8 not found"}""" } + test("serialize short ids") { + val testCases = Map( + ShortIds(real = RealScidStatus.Unknown, localAlias = Alias(0x4455), remoteAlias_opt = Some(Alias(0x88888888L))) -> + """{"real":{"status":"unknown"},"localAlias":"0x4455","remoteAlias":"0x88888888"}""", + ShortIds(real = RealScidStatus.Temporary(RealShortChannelId(BlockHeight(500000), 42, 1)), localAlias = Alias(0x4455), remoteAlias_opt = None) -> + """{"real":{"status":"temporary","realScid":"500000x42x1"},"localAlias":"0x4455"}""", + ShortIds(real = RealScidStatus.Final(RealShortChannelId(BlockHeight(500000), 42, 1)), localAlias = Alias(0x4455), remoteAlias_opt = None) -> + """{"real":{"status":"final","realScid":"500000x42x1"},"localAlias":"0x4455"}""", + ) + for ((obj, json) <- testCases) { + JsonSerializers.serialization.write(obj)(JsonSerializers.formats) shouldBe json + } + } + /** utility method that strips line breaks in the expected json */ def assertJsonEquals(actual: String, expected: String) = { val cleanedExpected = expected diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index c7c010a55c..13a45c188b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -714,7 +714,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // | | and b -> h has fees = 0 // +---(5)--> g ---(6)--> h // and e --(4)--> f (we are a) - val channelId_bh = ShortChannelId(BlockHeight(420000), 100, 0) + val channelId_bh = RealShortChannelId(BlockHeight(420000), 100, 0) val chan_bh = channelAnnouncement(channelId_bh, priv_b, priv_h, priv_funding_b, priv_funding_h) val channelUpdate_bh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, h, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 0 msat, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000 msat) val channelUpdate_hb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, b, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 491f200789..1a3edce695 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -368,12 +368,12 @@ object PaymentPacketSpec { Sphinx.create(sessionKey, packetPayloadLength, nodes, payloadsBin, Some(associatedData)).get.packet } - def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat): Commitments = { + def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat, channelFeatures: ChannelFeatures = ChannelFeatures()): Commitments = { val params = LocalParams(null, null, null, null, None, null, null, 0, isInitiator = true, null, None, null) val remoteParams = RemoteParams(randomKey().publicKey, null, null, None, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) val commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(testCapacity, Nil), Nil) val channelFlags = ChannelFlags.Private - new Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), params, remoteParams, channelFlags, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { + new Commitments(channelId, ChannelConfig.standard, channelFeatures, params, remoteParams, channelFlags, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { override lazy val availableBalanceForSend: MilliSatoshi = testAvailableBalanceForSend.max(0 msat) override lazy val availableBalanceForReceive: MilliSatoshi = testAvailableBalanceForReceive.max(0 msat) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index b3096820c1..b123d35c51 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -20,10 +20,13 @@ import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.TypedActorRefOps +import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.eclair.Features.ScidAlias import fr.acinq.eclair.TestConstants.emptyOnionPacket +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.IncomingPaymentPacket.ChannelRelayPacket @@ -32,7 +35,7 @@ import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket, Pa import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayPayload, ChannelRelayTlvPayload, RelayLegacyPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, NodeParams, TestConstants, randomBytes32, _} +import fr.acinq.eclair.{CltvExpiry, NodeParams, RealShortChannelId, TestConstants, randomBytes32, _} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -57,13 +60,14 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: channel.Command): Register.Forward[channel.Command] = { val fwd = register.expectMessageType[Register.Forward[channel.Command]] - assert(fwd.channelId == channelId) assert(fwd.message == cmd) + assert(fwd.channelId == channelId) fwd } def expectFwdAdd(register: TestProbe[Any], channelId: ByteVector32, outAmount: MilliSatoshi, outExpiry: CltvExpiry): Register.Forward[CMD_ADD_HTLC] = { val fwd = register.expectMessageType[Register.Forward[CMD_ADD_HTLC]] + assert(fwd.message.isInstanceOf[CMD_ADD_HTLC]) // the line above doesn't check the type due to type erasure assert(fwd.channelId == channelId) assert(fwd.message.amount == outAmount) assert(fwd.message.cltvExpiry == outExpiry) @@ -73,97 +77,161 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd } - test("relay htlc-add") { f => + def basicRelaytest(f: FixtureParam)(relayPayloadScid: ShortChannelId, lcu: LocalChannelUpdate, success: Boolean): Unit = { import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = RelayLegacyPayload(relayPayloadScid, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) - channelRelayer ! WrappedLocalChannelUpdate(u) + channelRelayer ! WrappedLocalChannelUpdate(lcu) channelRelayer ! Relay(r) - expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + if (success) { + expectFwdAdd(register, lcu.channelId, outgoingAmount, outgoingExpiry) + } else { + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) + } + } + + test("relay with real scid (channel update uses real scid)") { f => + basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1), success = true) + } + + test("relay with real scid (channel update uses remote alias)") { f => + basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(remoteAlias1)), success = true) + } + + test("relay with local alias (channel update uses real scid)") { f => + basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1), success = true) + } + + test("relay with local alias (channel update uses remote alias)") { f => + // we use a random value to simulate a remote alias + basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(remoteAlias1)), success = true) + } + + test("fail to relay with real scid when option_scid_alias is enabled (channel update uses real scid)") { f => + basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = false) + } + + test("fail to relay with real scid when option_scid_alias is enabled (channel update uses remote alias)") { f => + basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(remoteAlias1)), success = false) } - test("relay an htlc-add with onion tlv payload") { f => + test("relay with local alias when option_scid_alias is enabled (channel update uses real scid)") { f => + basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = true) + } + + test("relay with local alias when option_scid_alias is enabled (channel update uses remote alias)") { f => + // we use a random value to simulate a remote alias + basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(remoteAlias1)), success = true) + } + + test("relay with new real scid after reorg") { f => + import f._ + + // initial channel update + val lcu1 = createLocalUpdate(channelId1) + val payload1 = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r1 = createValidIncomingPacket(payload1) + channelRelayer ! WrappedLocalChannelUpdate(lcu1) + channelRelayer ! Relay(r1) + expectFwdAdd(register, lcu1.channelId, outgoingAmount, outgoingExpiry) + + // reorg happens + val realScid1AfterReorg = RealShortChannelId(111112) + val lcu2 = createLocalUpdate(channelId1).modify(_.shortIds.real).setTo(RealScidStatus.Final(realScid1AfterReorg)) + val payload2 = RelayLegacyPayload(realScid1AfterReorg, outgoingAmount, outgoingExpiry) + val r2 = createValidIncomingPacket(payload2) + channelRelayer ! WrappedLocalChannelUpdate(lcu2) + + // both old and new real scids work + channelRelayer ! Relay(r1) + expectFwdAdd(register, lcu1.channelId, outgoingAmount, outgoingExpiry) + // new real scid works + channelRelayer ! Relay(r2) + expectFwdAdd(register, lcu2.channelId, outgoingAmount, outgoingExpiry) + } + + + test("relay with onion tlv payload") { f => import f._ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ - val payload = ChannelRelayTlvPayload(TlvStream[OnionPaymentPayloadTlv](AmountToForward(outgoingAmount), OutgoingCltv(outgoingExpiry), OutgoingChannelId(shortId1))) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = ChannelRelayTlvPayload(TlvStream[OnionPaymentPayloadTlv](AmountToForward(outgoingAmount), OutgoingCltv(outgoingExpiry), OutgoingChannelId(realScid1))) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) } - test("relay an htlc-add with retries") { f => + test("relay with retries") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) // we tell the relayer about the first channel - val u1 = createLocalUpdate(shortId1) + val u1 = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u1) // this is another channel, with less balance (it will be preferred) - val u2 = createLocalUpdate(shortId2, 8000000 msat) + val u2 = createLocalUpdate(channelId2, balance = 8000000 msat) channelRelayer ! WrappedLocalChannelUpdate(u2) channelRelayer ! Relay(r) // first try - val fwd1 = expectFwdAdd(register, channelIds(shortId2), outgoingAmount, outgoingExpiry) + val fwd1 = expectFwdAdd(register, channelIds(realScId2), outgoingAmount, outgoingExpiry) // channel returns an error - fwd1.message.replyTo ! RES_ADD_FAILED(fwd1.message, HtlcValueTooHighInFlight(channelIds(shortId2), UInt64(1000000000L), 1516977616L msat), Some(u2.channelUpdate)) + fwd1.message.replyTo ! RES_ADD_FAILED(fwd1.message, HtlcValueTooHighInFlight(channelIds(realScId2), UInt64(1000000000L), 1516977616L msat), Some(u2.channelUpdate)) // second try - val fwd2 = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd2 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) // failure again - fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(shortId1), UInt64(1000000000L), 1516977616L msat), Some(u1.channelUpdate)) + fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(realScid1), UInt64(1000000000L), 1516977616L msat), Some(u1.channelUpdate)) // the relayer should give up expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(TemporaryNodeFailure), commit = true)) } - test("fail to relay an htlc-add when we have no channel_update for the next channel") { f => + test("fail to relay when we have no channel_update for the next channel") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) channelRelayer ! Relay(r) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } - test("fail to relay an htlc-add when register returns an error") { f => + test("fail to relay when register returns an error") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.replyTo ! Register.ForwardFailure(fwd) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } - test("fail to relay an htlc-add when the channel is advertised as unusable (down)") { f => + test("fail to relay when the channel is advertised as unusable (down)") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) - val d = LocalChannelDown(null, channelId = channelIds(shortId1), shortId1, outgoingNodeId) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) + val d = LocalChannelDown(null, channelId1, createShortIds(channelId1), outgoingNodeId) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! WrappedLocalChannelDown(d) @@ -172,12 +240,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } - test("fail to relay an htlc-add (channel disabled)") { f => + test("fail to relay when channel is disabled") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1, enabled = false) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1, enabled = false) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -185,12 +253,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(u.channelUpdate.messageFlags, u.channelUpdate.channelFlags, u.channelUpdate)), commit = true)) } - test("fail to relay an htlc-add (amount below minimum)") { f => + test("fail to relay when amount is below minimum") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1, htlcMinimum = outgoingAmount + 1.msat) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1, htlcMinimum = outgoingAmount + 1.msat) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -198,25 +266,25 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(AmountBelowMinimum(outgoingAmount, u.channelUpdate)), commit = true)) } - test("relay an htlc-add (expiry larger than our requirements)") { f => + test("relay when expiry larger than our requirements") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val u = createLocalUpdate(shortId1) - val r = createValidIncomingPacket(1100000 msat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta + CltvExpiryDelta(1), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val u = createLocalUpdate(channelId1) + val r = createValidIncomingPacket(payload, expiryIn = outgoingExpiry + u.channelUpdate.cltvExpiryDelta + CltvExpiryDelta(1)) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdAdd(register, channelIds(shortId1), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelIds(realScid1), payload.amountToForward, payload.outgoingCltv).message } - test("fail to relay an htlc-add (expiry too small)") { f => + test("fail to relay when expiry is too small") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val u = createLocalUpdate(shortId1) - val r = createValidIncomingPacket(1100000 msat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta - CltvExpiryDelta(1), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val u = createLocalUpdate(channelId1) + val r = createValidIncomingPacket(payload, expiryIn = outgoingExpiry + u.channelUpdate.cltvExpiryDelta - CltvExpiryDelta(1)) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -224,12 +292,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, u.channelUpdate)), commit = true)) } - test("fail to relay an htlc-add (fee insufficient)") { f => + test("fail to relay when fee is insufficient") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(outgoingAmount + 1.msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload, amountIn = outgoingAmount + 1.msat) + val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -237,28 +305,28 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u.channelUpdate)), commit = true)) } - test("relay an htlc-add that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => + test("relay that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(outgoingAmount + 1.msat, CltvExpiry(400100), payload) - val u1 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now(), feeBaseMsat = 1 msat, feeProportionalMillionths = 0) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload, amountIn = outgoingAmount + 1.msat) + val u1 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now(), feeBaseMsat = 1 msat, feeProportionalMillionths = 0) channelRelayer ! WrappedLocalChannelUpdate(u1) channelRelayer ! Relay(r) // relay succeeds with current channel update (u1) with lower fees - expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) - val u2 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now() - 530) + val u2 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now() - 530) channelRelayer ! WrappedLocalChannelUpdate(u2) channelRelayer ! Relay(r) // relay succeeds because the current update (u2) with higher fees occurred less than 10 minutes ago - expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) - val u3 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now() - 601) + val u3 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now() - 601) channelRelayer ! WrappedLocalChannelUpdate(u1) channelRelayer ! WrappedLocalChannelUpdate(u3) @@ -268,14 +336,14 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u3.channelUpdate)), commit = true)) } - test("fail to relay an htlc-add (local error)") { f => + test("fail to relay when there is a local error") { f => import f._ - val channelId1 = channelIds(shortId1) - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) - val u_disabled = createLocalUpdate(shortId1, enabled = false) + val channelId1 = channelIds(realScid1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) + val u_disabled = createLocalUpdate(channelId1, enabled = false) case class TestCase(exc: ChannelException, update: ChannelUpdate, failure: FailureMessage) @@ -290,7 +358,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, testCase.exc, Some(testCase.update)) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(testCase.failure), commit = true)) } @@ -300,29 +368,30 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a import f._ /** This is just a simplified helper function with random values for fields we are not using here */ - def dummyLocalUpdate(shortChannelId: ShortChannelId, remoteNodeId: PublicKey, availableBalanceForSend: MilliSatoshi, capacity: Satoshi) = { + def dummyLocalUpdate(shortChannelId: RealShortChannelId, remoteNodeId: PublicKey, availableBalanceForSend: MilliSatoshi, capacity: Satoshi) = { val channelId = randomBytes32() val update = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey(), remoteNodeId, shortChannelId, CltvExpiryDelta(10), 100 msat, 1000 msat, 100, capacity.toMilliSatoshi) val commitments = PaymentPacketSpec.makeCommitments(channelId, availableBalanceForSend, testCapacity = capacity) - LocalChannelUpdate(null, channelId, shortChannelId, remoteNodeId, None, update, commitments) + val shortIds = ShortIds(real = RealScidStatus.Final(shortChannelId), localAlias = ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + LocalChannelUpdate(null, channelId, shortIds, remoteNodeId, None, update, commitments) } val (a, b) = (randomKey().publicKey, randomKey().publicKey) val channelUpdates = Map( - ShortChannelId(11111) -> dummyLocalUpdate(ShortChannelId(11111), a, 100000000 msat, 200000 sat), - ShortChannelId(12345) -> dummyLocalUpdate(ShortChannelId(12345), a, 10000000 msat, 200000 sat), - ShortChannelId(22222) -> dummyLocalUpdate(ShortChannelId(22222), a, 10000000 msat, 100000 sat), - ShortChannelId(22223) -> dummyLocalUpdate(ShortChannelId(22223), a, 9000000 msat, 50000 sat), - ShortChannelId(33333) -> dummyLocalUpdate(ShortChannelId(33333), a, 100000 msat, 50000 sat), - ShortChannelId(44444) -> dummyLocalUpdate(ShortChannelId(44444), b, 1000000 msat, 10000 sat), + ShortChannelId(11111) -> dummyLocalUpdate(RealShortChannelId(11111), a, 100000000 msat, 200000 sat), + ShortChannelId(12345) -> dummyLocalUpdate(RealShortChannelId(12345), a, 10000000 msat, 200000 sat), + ShortChannelId(22222) -> dummyLocalUpdate(RealShortChannelId(22222), a, 10000000 msat, 100000 sat), + ShortChannelId(22223) -> dummyLocalUpdate(RealShortChannelId(22223), a, 9000000 msat, 50000 sat), + ShortChannelId(33333) -> dummyLocalUpdate(RealShortChannelId(33333), a, 100000 msat, 50000 sat), + ShortChannelId(44444) -> dummyLocalUpdate(RealShortChannelId(44444), b, 1000000 msat, 10000 sat), ) channelUpdates.values.foreach(u => channelRelayer ! WrappedLocalChannelUpdate(u)) { val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) // select the channel to the same node, with the lowest capacity and balance but still high enough to handle the payment val cmd1 = expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, payload.amountToForward, payload.outgoingCltv).message @@ -342,35 +411,35 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a { // higher amount payment (have to increased incoming htlc amount for fees to be sufficient) val payload = RelayLegacyPayload(ShortChannelId(12345), 50000000 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(60000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // lower amount payment val payload = RelayLegacyPayload(ShortChannelId(12345), 1000 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(60000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdAdd(register, channelUpdates(ShortChannelId(33333)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // payment too high, no suitable channel found, we keep the requested one val payload = RelayLegacyPayload(ShortChannelId(12345), 1000000000 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(1010000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1010000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdAdd(register, channelUpdates(ShortChannelId(12345)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // cltv expiry larger than our requirements val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(50)) - val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // cltv expiry too small, no suitable channel found val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(61)) - val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(CltvExpiry(61), channelUpdates(ShortChannelId(12345)).channelUpdate)), commit = true)) } @@ -379,11 +448,11 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a test("settlement failure") { f => import f._ - val channelId1 = channelIds(shortId1) - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) - val u_disabled = createLocalUpdate(shortId1, enabled = false) + val channelId1 = channelIds(realScid1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload, 1100000 msat, CltvExpiry(400100)) + val u = createLocalUpdate(channelId1) + val u_disabled = createLocalUpdate(channelId1, enabled = false) val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket) case class TestCase(result: HtlcResult, cmd: channel.HtlcSettlementCommand) @@ -399,7 +468,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.message.replyTo ! RES_SUCCESS(fwd.message, channelId1) fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, downstream_htlc, testCase.result) expectFwdFail(register, r.add.channelId, testCase.cmd) @@ -411,10 +480,10 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val eventListener = TestProbe[ChannelPaymentRelayed]() system.eventStream ! EventStream.Subscribe(eventListener.ref) - val channelId1 = channelIds(shortId1) - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val channelId1 = channelIds(realScid1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket) case class TestCase(result: HtlcResult) @@ -428,7 +497,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd1 = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd1 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd1.message.replyTo ! RES_SUCCESS(fwd1.message, channelId1) fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, downstream_htlc, testCase.result) @@ -447,6 +516,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a import f._ val channelId_ab = randomBytes32() val channelId_bc = randomBytes32() + val shortIds_ab = ShortIds(RealScidStatus.Final(RealShortChannelId(channelUpdate_ab.shortChannelId.toLong)), ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + val shortIds_bc = ShortIds(RealScidStatus.Final(RealShortChannelId(channelUpdate_bc.shortChannelId.toLong)), ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) val a = PaymentPacketSpec.a val sender = TestProbe[Relayer.OutgoingChannels]() @@ -457,8 +528,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channels } - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000 msat, 300000 msat))) - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000 msat, -5000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, shortIds_ab, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000 msat, 300000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_bc, shortIds_bc, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000 msat, -5000 msat))) val channels1 = getOutgoingChannels(true) assert(channels1.size == 2) @@ -467,33 +538,24 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a assert(channels1.last.channelUpdate == channelUpdate_bc) assert(channels1.last.toChannelBalance == Relayer.ChannelBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) - channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) + channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, shortIds_ab, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) val channels2 = getOutgoingChannels(true) assert(channels2.last.commitments.availableBalanceForReceive == 500000.msat && channels2.last.commitments.availableBalanceForSend == 200000.msat) - channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) - channelRelayer ! WrappedLocalChannelDown(LocalChannelDown(null, channelId_bc, channelUpdate_bc.shortChannelId, c)) + channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_ab, shortIds_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) + channelRelayer ! WrappedLocalChannelDown(LocalChannelDown(null, channelId_bc, shortIds_ab, c)) val channels3 = getOutgoingChannels(true) assert(channels3.size == 1 && channels3.head.commitments.availableBalanceForSend == 100000.msat) - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab.copy(channelFlags = ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = true)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, shortIds_ab, a, None, channelUpdate_ab.copy(channelFlags = ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = true)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) val channels4 = getOutgoingChannels(true) assert(channels4.isEmpty) val channels5 = getOutgoingChannels(false) assert(channels5.size == 1) - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, shortIds_ab, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) val channels6 = getOutgoingChannels(true) assert(channels6.size == 1) - - // Simulate a chain re-org that changes the shortChannelId: - channelRelayer ! WrappedShortChannelIdAssigned(ShortChannelIdAssigned(null, channelId_ab, ShortChannelId(42), Some(channelUpdate_ab.shortChannelId))) - - // We should receive the updated channel update containing the new shortChannelId: - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, ShortChannelId(42), a, None, channelUpdate_ab.copy(shortChannelId = ShortChannelId(42)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) - val channels7 = getOutgoingChannels(true) - assert(channels7.size == 1) - assert(channels7.head.channelUpdate.shortChannelId == ShortChannelId(42)) } } @@ -506,23 +568,41 @@ object ChannelRelayerSpec { val outgoingExpiry: CltvExpiry = CltvExpiry(400000) val outgoingNodeId: PublicKey = randomKey().publicKey - val shortId1: ShortChannelId = ShortChannelId(111111) - val shortId2: ShortChannelId = ShortChannelId(222222) + val realScid1: RealShortChannelId = RealShortChannelId(111111) + val realScId2: RealShortChannelId = RealShortChannelId(222222) + + val localAlias1: Alias = Alias(111000) + val localAlias2: Alias = Alias(222000) + + val remoteAlias1: ShortChannelId = ShortChannelId(111999) + + val channelId1: ByteVector32 = randomBytes32() + val channelId2: ByteVector32 = randomBytes32() val channelIds = Map( - shortId1 -> randomBytes32(), - shortId2 -> randomBytes32() + realScid1 -> channelId1, + realScId2 -> channelId2, + localAlias1 -> channelId1, + localAlias2 -> channelId2, ) - def createValidIncomingPacket(amountIn: MilliSatoshi, expiryIn: CltvExpiry, payload: ChannelRelayPayload): IncomingPaymentPacket.ChannelRelayPacket = { + def createValidIncomingPacket(payload: ChannelRelayPayload, amountIn: MilliSatoshi = 1_100_000 msat, expiryIn: CltvExpiry = CltvExpiry(400_100)): IncomingPaymentPacket.ChannelRelayPacket = { val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, amountIn, paymentHash, expiryIn, emptyOnionPacket) ChannelRelayPacket(add_ab, payload, emptyOnionPacket) } - def createLocalUpdate(shortChannelId: ShortChannelId, balance: MilliSatoshi = 10000000 msat, capacity: Satoshi = 500000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat, timestamp: TimestampSecond = 0 unixsec, feeBaseMsat: MilliSatoshi = 1000 msat, feeProportionalMillionths: Long = 100): LocalChannelUpdate = { - val channelId = channelIds(shortChannelId) - val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, shortChannelId, timestamp, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, feeBaseMsat, feeProportionalMillionths, Some(capacity.toMilliSatoshi)) - val commitments = PaymentPacketSpec.makeCommitments(channelId, testAvailableBalanceForSend = balance, testCapacity = capacity) - LocalChannelUpdate(null, channelId, shortChannelId, outgoingNodeId, None, update, commitments) + def createShortIds(channelId: ByteVector32) = { + val realScid = channelIds.collectFirst { case (realScid: RealShortChannelId, cid) if cid == channelId => realScid }.get + val localAlias = channelIds.collectFirst { case (localAlias: Alias, cid) if cid == channelId => localAlias }.get + ShortIds(real = RealScidStatus.Final(realScid), localAlias, remoteAlias_opt = None) + } + + def createLocalUpdate(channelId: ByteVector32, channelUpdateScid_opt: Option[ShortChannelId] = None, balance: MilliSatoshi = 10000000 msat, capacity: Satoshi = 500000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat, timestamp: TimestampSecond = 0 unixsec, feeBaseMsat: MilliSatoshi = 1000 msat, feeProportionalMillionths: Long = 100, optionScidAlias: Boolean = false): LocalChannelUpdate = { + val shortIds = createShortIds(channelId) + val channelUpdateScid = channelUpdateScid_opt.getOrElse(shortIds.real.toOption.get) + val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, channelUpdateScid, timestamp, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, feeBaseMsat, feeProportionalMillionths, Some(capacity.toMilliSatoshi)) + val channelFeatures = if (optionScidAlias) ChannelFeatures(ScidAlias) else ChannelFeatures() + val commitments = PaymentPacketSpec.makeCommitments(channelId, testAvailableBalanceForSend = balance, testCapacity = capacity, channelFeatures = channelFeatures) + LocalChannelUpdate(null, channelId, shortIds, outgoingNodeId, None, update, commitments) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 0df90707cb..89aaef7e17 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.{OutgoingPaymentPacket, PaymentPacketSpec} import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate -import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} +import fr.acinq.eclair.router.Router.NodeHop import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _} import org.scalatest.concurrent.PatienceConfiguration @@ -78,7 +78,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat assert(sender.expectMessageType[Relayer.OutgoingChannels].channels.isEmpty) // We publish a channel update, that should be picked up by the channel relayer - system.eventStream ! EventStream.Publish(LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))) + val shortIds_bc = ShortIds(RealScidStatus.Final(RealShortChannelId(channelUpdate_bc.shortChannelId.toLong)), ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + system.eventStream ! EventStream.Publish(LocalChannelUpdate(null, channelId_bc, shortIds_bc, c, None, channelUpdate_bc, makeCommitments(channelId_bc))) eventually(PatienceConfiguration.Timeout(30 seconds), PatienceConfiguration.Interval(1 second)) { childActors.channelRelayer ! ChannelRelayer.GetOutgoingChannels(sender.ref.toClassic, GetOutgoingChannels()) val channels = sender.expectMessageType[Relayer.OutgoingChannels].channels diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index b87f78b3f6..b5d7775a4f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, Bitco import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshiLong, RealShortChannelId, ShortChannelId, randomKey} import org.json4s.JsonAST.JString import org.scalatest.funsuite.AnyFunSuite @@ -64,10 +64,10 @@ class AnnouncementsBatchValidationSpec extends AnyFunSuite { bitcoinClient.validate(announcements(0)).pipeTo(sender.ref) sender.expectMsgType[ValidateResult].fundingTx.isRight - bitcoinClient.validate(announcements(1).copy(shortChannelId = ShortChannelId(Long.MaxValue))).pipeTo(sender.ref) // invalid block height + bitcoinClient.validate(announcements(1).copy(shortChannelId = RealShortChannelId(Long.MaxValue))).pipeTo(sender.ref) // invalid block height sender.expectMsgType[ValidateResult].fundingTx.isRight - bitcoinClient.validate(announcements(2).copy(shortChannelId = ShortChannelId(BlockHeight(500), 1000, 0))).pipeTo(sender.ref) // invalid tx index + bitcoinClient.validate(announcements(2).copy(shortChannelId = RealShortChannelId(BlockHeight(500), 1000, 0))).pipeTo(sender.ref) // invalid tx index sender.expectMsgType[ValidateResult].fundingTx.isRight } @@ -101,7 +101,7 @@ object AnnouncementsBatchValidationSpec { def makeChannelAnnouncement(c: SimulatedChannel, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): ChannelAnnouncement = { val (blockHeight, txIndex) = Await.result(bitcoinClient.getTransactionShortId(c.fundingTx.txid), 10 seconds) - val shortChannelId = ShortChannelId(blockHeight, txIndex, c.fundingOutputIndex) + val shortChannelId = RealShortChannelId(blockHeight, txIndex, c.fundingOutputIndex) val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, shortChannelId, c.node1Key.publicKey, c.node2Key.publicKey, c.node1FundingKey.publicKey, c.node2FundingKey.publicKey, Features.empty) val channelAnnNodeSig1 = Announcements.signChannelAnnouncement(witness, c.node1Key) val channelAnnBitcoinSig1 = Announcements.signChannelAnnouncement(witness, c.node1FundingKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index b7e4b52f35..b82360bc0f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.TestConstants.Alice +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.router.Announcements._ import fr.acinq.eclair.wire.protocol.ChannelUpdate.ChannelFlags @@ -43,12 +44,12 @@ class AnnouncementsSpec extends AnyFunSuite { test("create valid signed channel announcement") { val (node_a, node_b, bitcoin_a, bitcoin_b) = (randomKey(), randomKey(), randomKey(), randomKey()) - val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, Features.empty) + val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, Features.empty) val node_a_sig = Announcements.signChannelAnnouncement(witness, node_a) val bitcoin_a_sig = Announcements.signChannelAnnouncement(witness, bitcoin_a) val node_b_sig = Announcements.signChannelAnnouncement(witness, node_b) val bitcoin_b_sig = Announcements.signChannelAnnouncement(witness, bitcoin_b) - val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig) + val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig) assert(checkSigs(ann)) assert(checkSigs(ann.copy(nodeId1 = randomKey().publicKey)) == false) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index 625f814d44..6cc08c32b2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} -import fr.acinq.eclair.channel.{CommitmentsSpec, LocalChannelUpdate} +import fr.acinq.eclair.channel.{CommitmentsSpec, LocalChannelUpdate, RealScidStatus, ShortChannelIdAssigned, ShortIds} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.Peer.PeerRoutingMessage @@ -33,7 +33,7 @@ import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{TestKitBaseClass, randomKey, _} +import fr.acinq.eclair._ import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.ByteVector @@ -73,15 +73,21 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val node_g = makeNodeAnnouncement(priv_g, "node-G", Color(30, 10, -50), Nil, Features.empty) val node_h = makeNodeAnnouncement(priv_h, "node-H", Color(30, 10, -50), Nil, Features.empty) - val scid_ab = ShortChannelId(BlockHeight(420000), 1, 0) - val scid_bc = ShortChannelId(BlockHeight(420000), 2, 0) - val scid_cd = ShortChannelId(BlockHeight(420000), 3, 0) - val scid_ef = ShortChannelId(BlockHeight(420000), 4, 0) - val scid_ag_private = ShortChannelId(BlockHeight(420000), 5, 0) - val scid_gh = ShortChannelId(BlockHeight(420000), 6, 0) + val scid_ab = RealShortChannelId(BlockHeight(420000), 1, 0) + val scid_bc = RealShortChannelId(BlockHeight(420000), 2, 0) + val scid_cd = RealShortChannelId(BlockHeight(420000), 3, 0) + val scid_ef = RealShortChannelId(BlockHeight(420000), 4, 0) + val scid_ag_private = RealShortChannelId(BlockHeight(420000), 5, 0) + val scid_gh = RealShortChannelId(BlockHeight(420000), 6, 0) val channelId_ag_private = randomBytes32() + val alias_ab = ShortChannelId.generateLocalAlias() + val alias_ag_private = ShortChannelId.generateLocalAlias() + + val scids_ab = ShortIds(RealScidStatus.Final(scid_ab), alias_ab, None) + val scids_ag_private = ShortIds(RealScidStatus.Final(scid_ag_private), alias_ag_private, None) + val chan_ab = channelAnnouncement(scid_ab, priv_a, priv_b, priv_funding_a, priv_funding_b) val chan_bc = channelAnnouncement(scid_bc, priv_b, priv_c, priv_funding_b, priv_funding_c) val chan_cd = channelAnnouncement(scid_cd, priv_c, priv_d, priv_funding_c, priv_funding_d) @@ -96,8 +102,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val update_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, scid_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) val update_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, scid_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) val update_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, scid_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) - val update_ag_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scid_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_ga_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, a, scid_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_ag_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scids_ag_private.localAlias, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_ga_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, a, scids_ag_private.localAlias, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) val update_gh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, h, scid_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) val update_hg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, g, scid_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) @@ -112,8 +118,9 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi assert(ChannelDesc(update_bc, chan_bc) == ChannelDesc(chan_bc.shortChannelId, b, c)) assert(ChannelDesc(update_cd, chan_cd) == ChannelDesc(chan_cd.shortChannelId, c, d)) assert(ChannelDesc(update_ef, chan_ef) == ChannelDesc(chan_ef.shortChannelId, e, f)) - assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) == ChannelDesc(scid_ag_private, a, g)) - assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) == ChannelDesc(scid_ag_private, a, g)) + val privateChannel_ag = PrivateChannel(channelId_ag_private, scids_ag_private, a, g, None, None, ChannelMeta(1000 msat, 1000 msat)) + assert(ChannelDesc(update_ag_private, privateChannel_ag) == ChannelDesc(scids_ag_private.localAlias, a, g)) + assert(ChannelDesc(update_ga_private, privateChannel_ag) == ChannelDesc(scids_ag_private.localAlias, g, a)) assert(ChannelDesc(update_gh, chan_gh) == ChannelDesc(chan_gh.shortChannelId, g, h)) // let's set up the router @@ -151,7 +158,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_gh)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_hg)) // then private channels - sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scid_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) + sender.send(router, ShortChannelIdAssigned(sender.ref, channelId_ag_private, scids_ag_private, remoteNodeId = g)) + sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scids_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) // watcher receives the get tx requests assert(watcher.expectMsgType[ValidateRequest].ann == chan_ab) assert(watcher.expectMsgType[ValidateRequest].ann == chan_bc) @@ -214,13 +222,13 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi } object BaseRouterSpec { - def channelAnnouncement(channelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { - val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, channelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, Features.empty) + def channelAnnouncement(shortChannelId: RealShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { + val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, Features.empty) val node1_sig = Announcements.signChannelAnnouncement(witness, node1_priv) val funding1_sig = Announcements.signChannelAnnouncement(witness, funding1_priv) val node2_sig = Announcements.signChannelAnnouncement(witness, node2_priv) val funding2_sig = Announcements.signChannelAnnouncement(witness, funding2_priv) - makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig) + makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig) } def channelHopFromUpdate(nodeId: PublicKey, nextNodeId: PublicKey, channelUpdate: ChannelUpdate): ChannelHop = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 15cbbc832f..2baf31a3a8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.router.Router.{ChannelMeta, PublicChannel} import fr.acinq.eclair.router.Sync._ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.QueryFlags @@ -130,8 +131,8 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) } - def makeShortChannelIds(height: BlockHeight, count: Int): List[ShortChannelId] = { - val output = ArrayBuffer.empty[ShortChannelId] + def makeShortChannelIds(height: BlockHeight, count: Int): List[RealShortChannelId] = { + val output = ArrayBuffer.empty[RealShortChannelId] var txIndex = 0 var outputIndex = 0 while (output.size < count) { @@ -141,7 +142,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } else { outputIndex = outputIndex + 1 } - output += ShortChannelId(height, txIndex, outputIndex) + output += RealShortChannelId(height, txIndex, outputIndex) } output.toList } @@ -152,7 +153,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { // check that chunks contain exactly the ids they were built from are are consistent i.e each chunk covers a range that immediately follows // the previous one even if there are gaps in block heights - def validate(ids: SortedSet[ShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, chunks: List[ShortChannelIdsChunk]): Unit = { + def validate(ids: SortedSet[RealShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, chunks: List[ShortChannelIdsChunk]): Unit = { @tailrec def noOverlap(chunks: List[ShortChannelIdsChunk]): Boolean = chunks match { @@ -189,14 +190,14 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { test("split short channel ids correctly (basic tests") { - def id(blockHeight: Int, txIndex: Int = 0, outputIndex: Int = 0) = ShortChannelId(BlockHeight(blockHeight), txIndex, outputIndex) + def id(blockHeight: Int, txIndex: Int = 0, outputIndex: Int = 0) = RealShortChannelId(BlockHeight(blockHeight), txIndex, outputIndex) // no ids to split { val ids = Nil val firstBlock = BlockHeight(10) val numberOfBlocks = 100 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, Nil) :: Nil) } @@ -205,7 +206,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(10) val numberOfBlocks = 100 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, Nil) :: Nil) } @@ -214,7 +215,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(1100) val numberOfBlocks = 100 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, Nil) :: Nil) } @@ -223,7 +224,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, ids) :: Nil) } @@ -233,7 +234,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000, 0), id(1000, 1), id(1000, 2), id(1000, 3), id(1000, 4), id(1000, 5)) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, ids) :: Nil) } @@ -242,7 +243,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050)) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 100 + 6, List(ids(0), ids(1))), ShortChannelIdsChunk(BlockHeight(1006), 8, List(ids(2), ids(3))), @@ -255,7 +256,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050)) val firstBlock = BlockHeight(1001) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 12, List(ids(1), ids(2))), ShortChannelIdsChunk(BlockHeight(1013), 1040 - 1013 + 1, List(ids(3), ids(4))), @@ -268,7 +269,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(900) val numberOfBlocks = 105 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 100 + 2, List(ids(0), ids(1))), ShortChannelIdsChunk(BlockHeight(1002), 2, List(ids(2), ids(3))), @@ -281,7 +282,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(1001) val numberOfBlocks = 4 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 2, List(ids(1), ids(2))), ShortChannelIdsChunk(BlockHeight(1003), 2, List(ids(3), ids(4))) @@ -293,13 +294,13 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = makeShortChannelIds(BlockHeight(1000), 100) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 10) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 10) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, ids) :: Nil) } } test("split short channel ids correctly") { - val ids = SortedSet.empty[ShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(44), 50) ++ makeShortChannelIds(BlockHeight(45), 30) ++ makeShortChannelIds(BlockHeight(50), 120) + val ids = SortedSet.empty[RealShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(44), 50) ++ makeShortChannelIds(BlockHeight(45), 30) ++ makeShortChannelIds(BlockHeight(50), 120) val firstBlock = BlockHeight(0) val numberOfBlocks = 1000 @@ -311,7 +312,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } test("split short channel ids correctly (comprehensive tests)") { - val ids = SortedSet.empty[ShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(45), 50) ++ makeShortChannelIds(BlockHeight(47), 30) ++ makeShortChannelIds(BlockHeight(50), 120) + val ids = SortedSet.empty[RealShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(45), 50) ++ makeShortChannelIds(BlockHeight(47), 30) ++ makeShortChannelIds(BlockHeight(50), 120) for (firstBlock <- 0 to 60) { for (numberOfBlocks <- 1 to 60) { for (chunkSize <- 1 :: 2 :: 20 :: 50 :: 100 :: 1000 :: Nil) { @@ -357,7 +358,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } test("encode maximum size reply_channel_range") { - val scids = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => ShortChannelId(i)).toList + val scids = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => RealShortChannelId(i)).toList val timestamps = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => Timestamps(i.unixsec, (i + 1).unixsec)).toList val checksums = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => Checksums(i, i + 1)).toList val reply = ReplyChannelRange(Block.RegtestGenesisBlock.hash, BlockHeight(0), 100, 0, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, scids), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, timestamps)), Some(EncodedChecksums(checksums))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala index daa0d8ed65..ebe90ec8f5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala @@ -2,23 +2,31 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingDeeplyBuriedTriggered -import fr.acinq.eclair.channel.DATA_NORMAL +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchExternalChannelSpent, WatchExternalChannelSpentTriggered, WatchFundingDeeplyBuriedTriggered} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.channel.{CMD_CLOSE, DATA_NORMAL} import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.router.Router.{GossipOrigin, LocalGossip} -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate} +import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge +import fr.acinq.eclair.router.Router._ +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, Shutdown} import fr.acinq.eclair.{BlockHeight, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} +import scala.concurrent.duration.DurationInt + /** * This test checks the integration between Channel and Router (events, etc.) */ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(router: TestFSMRef[Router.State, Router.Data, Router], rebroadcastListener: TestProbe, channels: SetupFixture, testTags: Set[String]) + case class FixtureParam(router: TestFSMRef[Router.State, Router.Data, Router], rebroadcastListener: TestProbe, channels: SetupFixture, testTags: Set[String]) { + //@formatter:off + /** there is only one channel here */ + def privateChannel: PrivateChannel = router.stateData.privateChannels.values.head + def publicChannel: PublicChannel = router.stateData.channels.values.head + //@formatter:on + } implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging @@ -34,92 +42,164 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu withFixture(test.toNoArgTest(FixtureParam(router, rebroadcastListener, channels, test.tags))) } - test("private local channel") { f => + private def internalTest(f: FixtureParam): Unit = { import f._ - reachNormal(channels, testTags) - - awaitAssert(router.stateData.privateChannels.size == 1) - - { - // only the local channel_update is known (bob won't send his before the channel is deeply buried) - val pc = router.stateData.privateChannels.values.head - assert(pc.update_1_opt.isDefined ^ pc.update_2_opt.isDefined) - } - + val (aliceNodeId, bobNodeId) = (channels.alice.underlyingActor.nodeParams.nodeId, channels.bob.underlyingActor.nodeParams.nodeId) + reachNormal(channels, testTags, interceptChannelUpdates = false) + + // the router learns about the local, still unannounced, channel + awaitCond(router.stateData.privateChannels.size == 1) + + // only alice's channel_update is known (NB : due to how node ids are constructed, 1 = alice and 2 = bob) + assert(privateChannel.update_1_opt.isDefined) + assert(privateChannel.update_2_opt.isEmpty) + // alice will only have a real scid if this is not a zeroconf channel + assert(channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.toOption.isEmpty == f.testTags.contains(ChannelStateTestsTags.ZeroConf)) + assert(channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.remoteAlias_opt.isDefined) + // alice uses bob's alias for her channel update + assert(privateChannel.update_1_opt.get.shortChannelId != privateChannel.shortIds.localAlias) + assert(privateChannel.update_1_opt.get.shortChannelId == channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.remoteAlias_opt.get) + + // alice and bob send their channel_updates using remote alias when they go to NORMAL state + val aliceChannelUpdate1 = channels.alice2bob.expectMsgType[ChannelUpdate] + val bobChannelUpdate1 = channels.bob2alice.expectMsgType[ChannelUpdate] + // alice's channel_update uses bob's alias, and vice versa + assert(aliceChannelUpdate1.shortChannelId == channels.bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias) + assert(bobChannelUpdate1.shortChannelId == channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias) + // channel_updates are handled by the peer connection and sent to the router val peerConnection = TestProbe() - // bob hasn't yet sent his channel_update but we can get it by looking at its internal data - val bobChannelUpdate = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - router ! PeerRoutingMessage(peerConnection.ref, channels.bob.underlyingActor.nodeParams.nodeId, bobChannelUpdate) + router ! PeerRoutingMessage(peerConnection.ref, bobNodeId, bobChannelUpdate1) - awaitAssert { - val pc = router.stateData.privateChannels.values.head - // both channel_updates are known - pc.update_1_opt.isDefined && pc.update_2_opt.isDefined + // router processes bob's channel_update and now knows both channel updates + awaitCond { + privateChannel.update_1_opt.contains(aliceChannelUpdate1) && privateChannel.update_2_opt.contains(bobChannelUpdate1) } - // manual rebroadcast - router ! Router.TickBroadcast - rebroadcastListener.expectNoMessage() - - } - - test("public local channel", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => - import f._ - - val fundingTx = reachNormal(channels, testTags) - - awaitAssert(router.stateData.privateChannels.size == 1) - - { - val pc = router.stateData.privateChannels.values.head - // only the local channel_update is known - assert(pc.update_1_opt.isDefined ^ pc.update_2_opt.isDefined) + // there is nothing for the router to rebroadcast, channel is not announced + assert(router.stateData.rebroadcast == Rebroadcast(Map.empty, Map.empty, Map.empty)) + + // router graph contains a single channel + assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceChannelUpdate1, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) + + if (testTags.contains(ChannelStateTestsTags.ChannelsPublic)) { + // this is a public channel + // funding tx reaches 6 blocks, announcements are exchanged + channels.alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.alice2bob.expectMsgType[AnnouncementSignatures] + channels.alice2bob.forward(channels.bob) + + channels.bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.bob2alice.expectMsgType[AnnouncementSignatures] + channels.bob2alice.forward(channels.alice) + + // the router learns about the announcement and channel graduates from private to public + awaitCond { + router.stateData.privateChannels.isEmpty && router.stateData.channels.size == 1 + } + + // router has cleaned up the scid mapping + assert(router.stateData.scid2PrivateChannels.isEmpty) + + // alice and bob won't send their channel_update directly to each other because the channel has been announced + // but we can get the update from their data + awaitCond { + channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.isDefined && + channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.isDefined + } + val aliceChannelUpdate2 = channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + val bobChannelUpdate2 = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + // this time, they use the real scid + val aliceAnn = channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.get + val bobAnn = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.get + assert(aliceAnn == bobAnn) + assert(aliceChannelUpdate2.shortChannelId == aliceAnn.shortChannelId) + assert(bobChannelUpdate2.shortChannelId == bobAnn.shortChannelId) + + // the router has already processed the new local channel update from alice which uses the real scid, and keeps bob's previous channel update + assert(publicChannel.update_1_opt.contains(aliceChannelUpdate2) && publicChannel.update_2_opt.contains(bobChannelUpdate1)) + + // the router prepares to rebroadcast the channel announcement, the local update which uses the real scid, and the first node announcement + assert(router.stateData.rebroadcast == Rebroadcast( + channels = Map(aliceAnn -> Set[GossipOrigin](LocalGossip)), + updates = Map(aliceChannelUpdate2 -> Set[GossipOrigin](LocalGossip)), + nodes = Map(router.stateData.nodes.values.head -> Set[GossipOrigin](LocalGossip))) + ) + + // bob's channel_update reaches the router + router ! PeerRoutingMessage(peerConnection.ref, bobNodeId, bobChannelUpdate2) + + // router processes bob's channel_update and now knows both channel updates with real scids + awaitCond { + publicChannel.update_1_opt.contains(aliceChannelUpdate2) && publicChannel.update_2_opt.contains(bobChannelUpdate2) + } + + // router is now ready to rebroadcast both channel updates + assert(router.stateData.rebroadcast == Rebroadcast( + channels = Map(aliceAnn -> Set[GossipOrigin](LocalGossip)), + updates = Map( + aliceChannelUpdate2 -> Set[GossipOrigin](LocalGossip), + bobChannelUpdate2 -> Set[GossipOrigin](RemoteGossip(peerConnection.ref, bobNodeId)) + ), + nodes = Map(router.stateData.nodes.values.head -> Set[GossipOrigin](LocalGossip))) + ) + + // router graph contains a single channel + assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) + assert(router.stateData.graphWithBalances.graph.edgeSet().size == 2) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceChannelUpdate2, publicChannel), GraphEdge(bobChannelUpdate2, publicChannel))) + } else { + // this is a private channel + // funding tx reaches 6 blocks, no announcements are exchanged because the channel is private + channels.alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + + // alice and bob won't send their channel_update directly to each other because they haven't changed + channels.alice2bob.expectNoMessage(100 millis) + channels.bob2alice.expectNoMessage(100 millis) + + // router graph contains a single channel + assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceChannelUpdate1, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) } - - val peerConnection = TestProbe() - // alice and bob haven't yet sent their channel_updates but we can get them by looking at their internal data - val aliceChannelUpdate = channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - val bobChannelUpdate = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - router ! PeerRoutingMessage(peerConnection.ref, channels.bob.underlyingActor.nodeParams.nodeId, bobChannelUpdate) - - awaitAssert { - val pc = router.stateData.privateChannels.values.head - // both channel_updates are known - pc.update_1_opt.isDefined && pc.update_2_opt.isDefined - } - - // funding tx reaches 6 blocks, announcements are exchanged - channels.alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - channels.alice2bob.expectMsgType[AnnouncementSignatures] + // channel closes + channels.alice ! CMD_CLOSE(TestProbe().ref, scriptPubKey = None, feerates = None) + channels.alice2bob.expectMsgType[Shutdown] channels.alice2bob.forward(channels.bob) - - channels.bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - channels.bob2alice.expectMsgType[AnnouncementSignatures] + channels.bob2alice.expectMsgType[Shutdown] channels.bob2alice.forward(channels.alice) - - // router gets notified and attempts to validate the local channel - val vr = channels.alice2blockchain.expectMsgType[ZmqWatcher.ValidateRequest] - vr.replyTo ! ZmqWatcher.ValidateResult(vr.ann, Right((fundingTx, ZmqWatcher.UtxoStatus.Unspent))) - - awaitAssert { - router.stateData.privateChannels.isEmpty && router.stateData.channels.size == 1 + if (testTags.contains(ChannelStateTestsTags.ChannelsPublic)) { + // if the channel was public, the router asked the watcher to watch the funding tx and will be notified + val watchSpentBasic = channels.alice2blockchain.expectMsgType[WatchExternalChannelSpent] + watchSpentBasic.replyTo ! WatchExternalChannelSpentTriggered(watchSpentBasic.shortChannelId) } - + // router cleans up the channel awaitAssert { - val pc = router.stateData.channels.values.head - // both channel updates are preserved - pc.update_1_opt.isDefined && pc.update_2_opt.isDefined + assert(router.stateData.nodes == Map.empty) + assert(router.stateData.channels == Map.empty) + assert(router.stateData.privateChannels == Map.empty) + assert(router.stateData.scid2PrivateChannels == Map.empty) + assert(router.stateData.graphWithBalances.graph.edgeSet().isEmpty) + // TODO: we're not currently pruning nodes without channels from the graph, but we should! + // assert(router.stateData.graphWithBalances.graph.vertexSet().isEmpty) } + } + + test("private local channel") { f => + internalTest(f) + } - // manual rebroadcast - router ! Router.TickBroadcast - rebroadcastListener.expectMsg(Router.Rebroadcast( - channels = Map(vr.ann -> Set[GossipOrigin](LocalGossip)), - updates = Map(aliceChannelUpdate -> Set[GossipOrigin](LocalGossip), bobChannelUpdate -> Set.empty[GossipOrigin]), // broadcast the channel_updates (they were previously unannounced) - nodes = Map(router.underlyingActor.stateData.nodes.values.head -> Set[GossipOrigin](LocalGossip)), // new node_announcement - )) + test("private local channel (zeroconf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + internalTest(f) + } + + test("public local channel", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + internalTest(f) + } + test("public local channel (zeroconf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + internalTest(f) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index a44a9afb14..aca83a802d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.router import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate @@ -29,7 +30,7 @@ import fr.acinq.eclair.router.RouteCalculation._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.{ParallelTestExecution, Tag} import scodec.bits._ @@ -915,7 +916,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // This test have a channel (542280x2156x0) that according to heuristics is very convenient but actually useless to reach the target, // then if the cost function is not monotonic the path-finding breaks because the result path contains a loop. val updates = SortedMap( - ShortChannelId("565643x1216x0") -> PublicChannel( + RealShortChannelId(BlockHeight(565643), 1216, 0) -> PublicChannel( ann = makeChannel(ShortChannelId("565643x1216x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, @@ -923,7 +924,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))), meta_opt = None ), - ShortChannelId("542280x2156x0") -> PublicChannel( + RealShortChannelId(BlockHeight(542280), 2156, 0) -> PublicChannel( ann = makeChannel(ShortChannelId("542280x2156x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, @@ -931,7 +932,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))), meta_opt = None ), - ShortChannelId("565779x2711x0") -> PublicChannel( + RealShortChannelId(BlockHeight(565779), 2711, 0) -> PublicChannel( ann = makeChannel(ShortChannelId("565779x2711x0").toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, @@ -1948,7 +1949,7 @@ object RouteCalculationSpec { def makeChannel(shortChannelId: Long, nodeIdA: PublicKey, nodeIdB: PublicKey): ChannelAnnouncement = { val (nodeId1, nodeId2) = if (Announcements.isNode1(nodeIdA, nodeIdB)) (nodeIdA, nodeIdB) else (nodeIdB, nodeIdA) - ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, Features.empty, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey().publicKey, randomKey().publicKey) + ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, Features.empty, Block.RegtestGenesisBlock.hash, RealShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey().publicKey, randomKey().publicKey) } def makeEdge(shortChannelId: Long, @@ -1962,7 +1963,7 @@ object RouteCalculationSpec { capacity: Satoshi = DEFAULT_CAPACITY, balance_opt: Option[MilliSatoshi] = None): GraphEdge = { val update = makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBase, feeProportionalMillionth, minHtlc, maxHtlc, cltvDelta) - GraphEdge(ChannelDesc(ShortChannelId(shortChannelId), nodeId1, nodeId2), ChannelRelayParams.FromAnnouncement(update), capacity, balance_opt) + GraphEdge(ChannelDesc(RealShortChannelId(shortChannelId), nodeId1, nodeId2), ChannelRelayParams.FromAnnouncement(update), capacity, balance_opt) } def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0), timestamp: TimestampSecond = 0 unixsec): ChannelUpdate = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 4c5b9d0eb8..fcd54e96cc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -22,8 +22,9 @@ import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong, Transaction, TxOut} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel.{AvailableBalanceChanged, CommitmentsSpec, LocalChannelUpdate} +import fr.acinq.eclair.channel.{AvailableBalanceChanged, CommitmentsSpec, LocalChannelUpdate, RealScidStatus, ShortIds} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop @@ -34,7 +35,7 @@ import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, nodeFee, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TestConstants, TimestampSecond, nodeFee, randomKey} import scodec.bits._ import scala.concurrent.duration._ @@ -54,7 +55,7 @@ class RouterSpec extends BaseRouterSpec { { // valid channel announcement, no stashing - val chan_ac = channelAnnouncement(ShortChannelId(BlockHeight(420000), 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c) + val chan_ac = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c) val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, chan_ac.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac)) @@ -84,7 +85,7 @@ class RouterSpec extends BaseRouterSpec { // valid channel announcement, stashing while validating channel announcement val priv_u = randomKey() val priv_funding_u = randomKey() - val chan_uc = channelAnnouncement(ShortChannelId(BlockHeight(420000), 100, 0), priv_u, priv_c, priv_funding_u, priv_funding_c) + val chan_uc = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 100, 0), priv_u, priv_c, priv_funding_u, priv_funding_c) val update_uc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_u, c, chan_uc.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) val node_u = makeNodeAnnouncement(priv_u, "node-U", Color(-120, -20, 60), Nil, Features.empty) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_uc)) @@ -129,7 +130,7 @@ class RouterSpec extends BaseRouterSpec { { // invalid signatures val invalid_node_b = node_b.copy(timestamp = node_b.timestamp + 10) - val invalid_chan_ac = channelAnnouncement(ShortChannelId(BlockHeight(420000), 101, 1), priv_a, priv_c, priv_funding_a, priv_funding_c).copy(nodeId1 = randomKey().publicKey) + val invalid_chan_ac = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 101, 1), priv_a, priv_c, priv_funding_a, priv_funding_c).copy(nodeId1 = randomKey().publicKey) val invalid_update_ab = update_ab.copy(cltvExpiryDelta = CltvExpiryDelta(21), timestamp = update_ab.timestamp + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, invalid_node_b)) peerConnection.expectMsg(TransportHandler.ReadAck(invalid_node_b)) @@ -149,7 +150,7 @@ class RouterSpec extends BaseRouterSpec { // pruned channel val priv_v = randomKey() val priv_funding_v = randomKey() - val chan_vc = channelAnnouncement(ShortChannelId(BlockHeight(420000), 102, 0), priv_v, priv_c, priv_funding_v, priv_funding_c) + val chan_vc = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 102, 0), priv_v, priv_c, priv_funding_v, priv_funding_c) nodeParams.db.network.addToPruned(chan_vc.shortChannelId :: Nil) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_vc)) peerConnection.expectMsg(TransportHandler.ReadAck(chan_vc)) @@ -190,7 +191,7 @@ class RouterSpec extends BaseRouterSpec { // invalid announcement + reject stashed val priv_y = randomKey() val priv_funding_y = randomKey() // a-y will have an invalid script - val chan_ay = channelAnnouncement(ShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y) + val chan_ay = channelAnnouncement(RealShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y) val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ay)) @@ -212,7 +213,7 @@ class RouterSpec extends BaseRouterSpec { { // validation failure val priv_x = randomKey() - val chan_ax = channelAnnouncement(ShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey()) + val chan_ax = channelAnnouncement(RealShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ax)) assert(watcher.expectMsgType[ValidateRequest].ann == chan_ax) watcher.send(router, ValidateResult(chan_ax, Left(new RuntimeException("funding tx not found")))) @@ -227,7 +228,7 @@ class RouterSpec extends BaseRouterSpec { // funding tx spent (funding tx not confirmed) val priv_z = randomKey() val priv_funding_z = randomKey() - val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) + val chan_az = channelAnnouncement(RealShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)) assert(watcher.expectMsgType[ValidateRequest].ann == chan_az) watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = false)))) @@ -242,7 +243,7 @@ class RouterSpec extends BaseRouterSpec { // funding tx spent (funding tx confirmed) val priv_z = randomKey() val priv_funding_z = randomKey() - val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) + val chan_az = channelAnnouncement(RealShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)) assert(watcher.expectMsgType[ValidateRequest].ann == chan_az) watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = true)))) @@ -285,7 +286,7 @@ class RouterSpec extends BaseRouterSpec { test("handle bad signature for ChannelAnnouncement") { fixture => import fixture._ val peerConnection = TestProbe() - val channelId_ac = ShortChannelId(BlockHeight(420000), 105, 0) + val channelId_ac = RealShortChannelId(BlockHeight(420000), 105, 0) val chan_ac = channelAnnouncement(channelId_ac, priv_a, priv_c, priv_funding_a, priv_funding_c) val buggy_chan_ac = chan_ac.copy(nodeSignature1 = chan_ac.nodeSignature2) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, buggy_chan_ac)) @@ -388,8 +389,8 @@ class RouterSpec extends BaseRouterSpec { assert(res.routes.head.hops.map(_.nodeId).toList == a :: g :: Nil) assert(res.routes.head.hops.last.nextNodeId == h) - val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scid_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) - sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) + val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, alias_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) + sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scids_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) } @@ -410,7 +411,7 @@ class RouterSpec extends BaseRouterSpec { sender.send(router, RouteRequest(a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] val commitments1 = CommitmentsSpec.makeCommitments(10000000 msat, 20000000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments1)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments1)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(BalanceTooLow)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) @@ -484,6 +485,18 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() { + // using the channel alias + val preComputedRoute = PredefinedChannelRoute(g, Seq(alias_ag_private)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + val response = sender.expectMsgType[RouteResponse] + assert(response.routes.length == 1) + val route = response.routes.head + assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) + assert(route.hops.head.nodeId == a) + assert(route.hops.head.nextNodeId == g) + } + { + // using the real scid val preComputedRoute = PredefinedChannelRoute(g, Seq(scid_ag_private)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] @@ -511,7 +524,7 @@ class RouterSpec extends BaseRouterSpec { val targetNodeId = randomKey().publicKey { - val invoiceRoutingHint = ExtraHop(b, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) + val invoiceRoutingHint = ExtraHop(b, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ab, invoiceRoutingHint.shortChannelId)) val amount = 10_000.msat // the amount affects the way we estimate the channel capacity of the hinted channel @@ -526,7 +539,7 @@ class RouterSpec extends BaseRouterSpec { assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint, RoutingHeuristics.CAPACITY_CHANNEL_LOW + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, RoutingHeuristics.CAPACITY_CHANNEL_LOW))) } { - val invoiceRoutingHint = ExtraHop(h, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) + val invoiceRoutingHint = ExtraHop(h, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ag_private, scid_gh, invoiceRoutingHint.shortChannelId)) val amount = RoutingHeuristics.CAPACITY_CHANNEL_LOW * 2 // the amount affects the way we estimate the channel capacity of the hinted channel @@ -571,7 +584,7 @@ class RouterSpec extends BaseRouterSpec { test("ask for channels that we marked as stale for which we receive a new update") { fixture => import fixture._ val blockHeight = BlockHeight(400000) - 2020 - val channelId = ShortChannelId(blockHeight, 5, 0) + val channelId = RealShortChannelId(blockHeight, 5, 0) val announcement = channelAnnouncement(channelId, priv_a, priv_c, priv_funding_a, priv_funding_c) val oldTimestamp = TimestampSecond.now() - 14.days - 1.day val staleUpdate = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 5 msat, timestamp = oldTimestamp) @@ -611,7 +624,7 @@ class RouterSpec extends BaseRouterSpec { // When the local channel comes back online, it will send a LocalChannelUpdate to the router. val balances = Set[Option[MilliSatoshi]](Some(10000 msat), Some(15000 msat)) val commitments = CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) @@ -634,7 +647,7 @@ class RouterSpec extends BaseRouterSpec { // Then we update the balance without changing the contents of the channel update; the graph should still be updated. val balances = Set[Option[MilliSatoshi]](Some(11000 msat), Some(14000 msat)) val commitments = CommitmentsSpec.makeCommitments(11000 msat, 14000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) @@ -652,7 +665,7 @@ class RouterSpec extends BaseRouterSpec { // When HTLCs are relayed through the channel, balance changes are sent to the router. val balances = Set[Option[MilliSatoshi]](Some(12000 msat), Some(13000 msat)) val commitments = CommitmentsSpec.makeCommitments(12000 msat, 13000 msat, a, b, announceChannel = true) - sender.send(router, AvailableBalanceChanged(sender.ref, null, scid_ab, commitments)) + sender.send(router, AvailableBalanceChanged(sender.ref, null, scids_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) @@ -670,13 +683,13 @@ class RouterSpec extends BaseRouterSpec { // Private channels should also update the graph when HTLCs are relayed through them. val balances = Set(33000000 msat, 5000000 msat) val commitments = CommitmentsSpec.makeCommitments(33000000 msat, 5000000 msat, a, g, announceChannel = false) - sender.send(router, AvailableBalanceChanged(sender.ref, channelId_ag_private, scid_ag_private, commitments)) + sender.send(router, AvailableBalanceChanged(sender.ref, channelId_ag_private, scids_ab, commitments)) sender.send(router, Router.GetRouterData) val data = sender.expectMsgType[Data] val channel_ag = data.privateChannels(channelId_ag_private) assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) == balances) // And the graph should be updated too. - val edge_ag = data.graphWithBalances.graph.getEdge(ChannelDesc(scid_ag_private, a, g)).get + val edge_ag = data.graphWithBalances.graph.getEdge(ChannelDesc(alias_ag_private, a, g)).get assert(edge_ag.capacity == channel_ag.capacity) assert(edge_ag.balance_opt == Some(33000000 msat)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 5abee91aba..987c5f22d2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -22,6 +22,7 @@ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult} import fr.acinq.eclair.crypto.TransportHandler @@ -35,7 +36,7 @@ import fr.acinq.eclair.wire.protocol._ import org.scalatest.ParallelTestExecution import org.scalatest.funsuite.AnyFunSuiteLike -import scala.collection.immutable.{TreeMap, SortedSet} +import scala.collection.immutable.{SortedSet, TreeMap} import scala.collection.mutable import scala.concurrent.duration._ @@ -45,10 +46,10 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle // this map will store private keys so that we can sign new announcements at will val pub2priv: mutable.Map[PublicKey, PrivateKey] = mutable.HashMap.empty - val fakeRoutingInfo: TreeMap[ShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)] = RoutingSyncSpec + val fakeRoutingInfo: TreeMap[RealShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)] = RoutingSyncSpec .shortChannelIds .take(60) - .foldLeft(TreeMap.empty[ShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)]) { + .foldLeft(TreeMap.empty[RealShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)]) { case (m, shortChannelId) => m + (shortChannelId -> makeFakeRoutingInfo(pub2priv)(shortChannelId)) } @@ -127,7 +128,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle SyncResult(rcrs, queries, channels, updates, nodes) } - def countUpdates(channels: Map[ShortChannelId, PublicChannel]): Int = channels.values.foldLeft(0) { + def countUpdates(channels: Map[RealShortChannelId, PublicChannel]): Int = channels.values.foldLeft(0) { case (count, pc) => count + pc.update_1_opt.map(_ => 1).getOrElse(0) + pc.update_2_opt.map(_ => 1).getOrElse(0) } @@ -310,7 +311,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle test("sync progress") { - def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42))), TlvStream.empty) + def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(42))), TlvStream.empty) val nodeIdA = randomKey().publicKey val nodeIdB = randomKey().publicKey @@ -331,15 +332,15 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle object RoutingSyncSpec { - lazy val shortChannelIds: SortedSet[ShortChannelId] = (for { + lazy val shortChannelIds: SortedSet[RealShortChannelId] = (for { block <- 400000 to 420000 txindex <- 0 to 5 outputIndex <- 0 to 1 - } yield ShortChannelId(BlockHeight(block), txindex, outputIndex)).foldLeft(SortedSet.empty[ShortChannelId])(_ + _) + } yield RealShortChannelId(BlockHeight(block), txindex, outputIndex)).foldLeft(SortedSet.empty[RealShortChannelId])(_ + _) val unused: PrivateKey = randomKey() - def makeFakeRoutingInfo(pub2priv: mutable.Map[PublicKey, PrivateKey])(shortChannelId: ShortChannelId): (PublicChannel, NodeAnnouncement, NodeAnnouncement) = { + def makeFakeRoutingInfo(pub2priv: mutable.Map[PublicKey, PrivateKey])(shortChannelId: RealShortChannelId): (PublicChannel, NodeAnnouncement, NodeAnnouncement) = { val timestamp = TimestampSecond.now() val (priv1, priv2) = { val (priv_a, priv_b) = (randomKey(), randomKey()) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index fe3e7663f7..0d82508f50 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire.internal.channel import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxIn} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding @@ -124,7 +125,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // and we encode with new codec val newbin = channelDataCodec.encode(oldnormal).require.bytes // make sure that encoding used the new codec - assert(newbin.startsWith(hex"030007")) + assert(newbin.startsWith(hex"030009")) // make sure that round-trip yields the same data val newnormal = channelDataCodec.decode(newbin.bits).require.value assert(newnormal == oldnormal) @@ -135,11 +136,11 @@ class ChannelCodecsSpec extends AnyFunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"requestedChannelReserve_opt":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"requestedChannelReserve_opt":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"requestedChannelReserve_opt":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"requestedChannelReserve_opt":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortIds":{"real":{"status":"final","realScid":"1513532x23x1"},"localAlias":"0x17183c0000170001"},"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortIds":{"real":{"status":"final","realScid":"1413373x969x0"},"localAlias":"0x1590fd0003c90000"},"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortIds":{"real":{"status":"final","realScid":"2026958x1x0"},"localAlias":"0x1eedce0000010000"},"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => @@ -328,7 +329,7 @@ object ChannelCodecsSpec { commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init) - DATA_NORMAL(commitments, ShortChannelId(42), buried = true, None, channelUpdate, None, None, None) + DATA_NORMAL(commitments, ShortIds(RealScidStatus.Final(RealShortChannelId(42)), ShortChannelId.generateLocalAlias(), None), None, channelUpdate, None, None, None) } } 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 18f8a86de7..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 @@ -23,7 +23,7 @@ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs._ import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.channelDataCodec -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, UInt64, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, RealShortChannelId, ShortChannelId, UInt64, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -160,7 +160,7 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(decoded1.asInstanceOf[DATA_NORMAL].closingFeerates == None) val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec - assert(newBin.startsWith(hex"0007")) + assert(newBin.startsWith(hex"0009")) val decoded2 = channelDataCodec.decode(newBin.bits).require.value assert(decoded1 == decoded2) } @@ -250,4 +250,24 @@ class ChannelCodecs3Spec extends AnyFunSuite { } } + test("backwards compatibility with codecs pre-alias") { + { + val bin = hex"0001000000000000000000000000000000000000000000000000000000000000000001010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000010000002a00000000000002220000000002faf0800000000000002710000000000000271000900032ff0000000000022cd2a00cddf20323d320bd14ce0e59b00d62def4d853b88e8bf7dc44c556fc07000000000000022200000000004c4b400000000000002710000000000000138800900032031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00000000000100000000000000000005fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc0000000002faf08000000000042c1d8024bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752aefd010f02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848900000000000000000000040047304402202148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab71702202bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d0147304402206cb12624b253adeb0a41210d63ac6280154923c502202ea16a581bc1839e1e610220178e31542e4a7735d9e243927a5aac00bae1b2889cb9eb785c4d182f3b22a87d01475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000002148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d00000000000000000000000500fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc000000000000c35000000000000aae60030303030303030303030303030303030303030303030303030303030303030303462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b000000000000000000000000000000000000002000000000000000040002000000000000002a00036e9078b299874e92af44d59fcbc9d2190000000000003a9800022a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000002b0000000000a7d8c00000000000989680ff023730c8e0fc52aff6ba2f618f29e5ebd551c0129e13ce20312df76e4403c5abbc24bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae00000000000000075bcd1541dd93fe76288e183498a7d57065cf472a4413c643730f9c117ca806be6b57f75303a153d48f51439f168ca85ef2fd6f3e0642fc7132f7a530cd50d880ec05cb4c17" + val data = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY] + assert(data.shortIds.localAlias == ShortChannelId(123456789L)) + assert(data.shortIds.real == RealScidStatus.Temporary(RealShortChannelId(123456789L))) + val binMigrated = channelDataCodec.encode(data).require.toHex + assert(binMigrated.startsWith("000a")) // NB: 01 -> 0a + } + + { + val bin = hex"0007000000000000000000000000000000000000000000000000000000000000000001010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000010000002a00000000000002220000000002faf0800000000000002710000000000000271000900032ff0000000000022cd2a00cddf20323d320bd14ce0e59b00d62def4d853b88e8bf7dc44c556fc07000000000000022200000000004c4b400000000000002710000000000000138800900032031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00000000000100000000000000000005fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc0000000002faf08000000000042c1d8024bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752aefd010f02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848900000000000000000000040047304402202148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab71702202bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d0147304402206cb12624b253adeb0a41210d63ac6280154923c502202ea16a581bc1839e1e610220178e31542e4a7735d9e243927a5aac00bae1b2889cb9eb785c4d182f3b22a87d01475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000002148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d00000000000000000000000500fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc000000000000c35000000000000aae60030303030303030303030303030303030303030303030303030303030303030303462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b000000000000000000000000000000000000002000000000000000040002000000000000002a00036e9078b299874e92af44d59fcbc9d2190000000000003a9800022a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000002b0000000000a7d8c00000000000989680ff023730c8e0fc52aff6ba2f618f29e5ebd551c0129e13ce20312df76e4403c5abbc24bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000000000000000002aff008821c93fbf280b2391826bc70ae858cf815d4afc1816f85445364f188e635d4ae64dfe1c58bedb017dd6f267452444d991b66fcfc638396f72fa6926f69d6125ff01010101010101010101010101010101010101010101010101010101010101010000000000022cd962975eec0101002a000000000000000f0000023f0000003500000003e8000000000000" + val data = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_NORMAL] + assert(data.shortIds.localAlias == ShortChannelId(42)) + assert(data.shortIds.real == RealScidStatus.Final(RealShortChannelId(42))) + val binMigrated = channelDataCodec.encode(data).require.toHex + assert(binMigrated.startsWith("0009")) + } + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala index b405869f1e..9d8e643d62 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala @@ -17,6 +17,8 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64} +import fr.acinq.eclair.RealShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.router.Sync import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ @@ -29,7 +31,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { test("encode a list of short channel ids") { { // encode/decode with encoding 'uncompressed' - val ids = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))) + val ids = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))) val encoded = encodedShortChannelIdsCodec.encode(ids).require val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value assert(decoded == ids) @@ -37,7 +39,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { { // encode/decode with encoding 'zlib' - val ids = EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))) + val ids = EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))) val encoded = encodedShortChannelIdsCodec.encode(ids).require val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value assert(decoded == ids) @@ -65,7 +67,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { test("encode query_short_channel_ids (no optional data)") { val query_short_channel_id = QueryShortChannelIds( Block.RegtestGenesisBlock.blockId, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty) val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require @@ -76,7 +78,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { test("encode query_short_channel_ids (with optional data)") { val query_short_channel_id = QueryShortChannelIds( Block.RegtestGenesisBlock.blockId, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1.toByte, 2.toByte, 3.toByte, 4.toByte, 5.toByte)))) val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require @@ -87,7 +89,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { test("encode query_short_channel_ids (with optional data including unknown data)") { val query_short_channel_id = QueryShortChannelIds( Block.RegtestGenesisBlock.blockId, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream( QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1.toByte, 2.toByte, 3.toByte, 4.toByte, 5.toByte)) :: Nil, GenericTlv(UInt64(43), ByteVector.fromValidHex("deadbeef")) :: Nil @@ -104,7 +106,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { Block.RegtestGenesisBlock.blockId, BlockHeight(1), 100, 1.toByte, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), None, None) val encoded = replyChannelRangeCodec.encode(replyChannelRange).require @@ -117,7 +119,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { Block.RegtestGenesisBlock.blockId, BlockHeight(1), 100, 1.toByte, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec)))), None) @@ -131,7 +133,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { Block.RegtestGenesisBlock.blockId, BlockHeight(1), 100, 1.toByte, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream( List( EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 9893c6b268..064dfbf758 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -221,7 +221,13 @@ class LightningMessageCodecsSpec extends AnyFunSuite { defaultEncoded ++ hex"0000" ++ hex"01021000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey))), // non-empty upfront_shutdown_script + channel type defaultEncoded ++ hex"0004 01abcdef" ++ hex"0103101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))), - defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))), + defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)))), + // empty upfront_shutdown_script + channel type (scid-alias) + defaultEncoded ++ hex"0000" ++ hex"0106 400000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)))), + // empty upfront_shutdown_script + channel type (zeroconf) + defaultEncoded ++ hex"0000" ++ hex"0107 04000000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)))), + // empty upfront_shutdown_script + channel type (scid-alias + zeroconf) + defaultEncoded ++ hex"0000" ++ hex"0107 04400000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)))), ) for ((encoded, expected) <- testCases) { @@ -255,8 +261,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val defaultEncoded = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 01" val testCases = Seq( defaultOpen -> defaultEncoded, - defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"0103401000"), - defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), + defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(false, false)))) -> (defaultEncoded ++ hex"0103401000"), + defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(false, false)))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), ) testCases.foreach { case (open, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require.value @@ -347,7 +353,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { AcceptChannel(randomBytes32(), 3 sat, UInt64(4), 5 sat, 6 msat, 7, CltvExpiryDelta(8), 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)), FundingCreated(randomBytes32(), bin32(0), 3, randomBytes64()), FundingSigned(randomBytes32(), randomBytes64()), - FundingLocked(randomBytes32(), point(2)), + ChannelReady(randomBytes32(), point(2)), + ChannelReady(randomBytes32(), point(2), TlvStream(ChannelReadyTlv.ShortChannelIdTlv(Alias(123456)))), UpdateFee(randomBytes32(), FeeratePerKw(2 sat)), Shutdown(randomBytes32(), bin(47, 0)), ClosingSigned(randomBytes32(), 2 sat, randomBytes64()), @@ -357,19 +364,19 @@ class LightningMessageCodecsSpec extends AnyFunSuite { UpdateFailMalformedHtlc(randomBytes32(), 2, randomBytes32(), 1111), CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil), RevokeAndAck(randomBytes32(), scalar(0), point(1)), - ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey), + ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, RealShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey), NodeAnnouncement(randomBytes64(), Features(DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil), ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None), - AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64()), + AnnouncementSignatures(randomBytes32(), RealShortChannelId(42), randomBytes64(), randomBytes64()), GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500), - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty), QueryChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(100000), 1500, TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL) :: Nil, unknownTlv :: Nil)), ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(100000), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream( EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, unknownTlv :: Nil) @@ -402,13 +409,13 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("non-reg encoding type") { val refs = Map( hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4010400010204" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3010c01789c6364620100000e0008" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) ) refs.forall { @@ -430,28 +437,28 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL))) -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000088b800000064010103", ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(756230), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None, None) -> + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), None, None) -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000b8a06000005dc01001900000000000000008e0000000000003c69000000000045a6c4", ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(1600), 110, 1, - EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(265462))), None, None) -> + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(265462))), None, None) -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000006400000006e01001601789c636000833e08659309a65878be010010a9023a", ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(12355), RealShortChannelId(489686), RealShortChannelId(4645313))), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001900000000000000304300000000000778d6000000000046e1c1011900000282c1000e77c5000778ad00490ab00000b57800955bff031800000457000008ae00000d050000115c000015b300001a0a", ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, - EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(12355), RealShortChannelId(489686), RealShortChannelId(4645313))), Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001801789c63600001036730c55e710d4cbb3d3c080017c303b1012201789c63606a3ac8c0577e9481bd622d8327d7060686ad150c53a3ff0300554707db031800000457000008ae00000d050000115c000015b300001a0a", - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) -> + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty) -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4", - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(4564), ShortChannelId(178622), ShortChannelId(4564676))), TlvStream.empty) -> + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(4564), RealShortChannelId(178622), RealShortChannelId(4564676))), TlvStream.empty) -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001c12b608a69e73e30edbaec0800203b040e", - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12232), ShortChannelId(15556), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(12232), RealShortChannelId(15556), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060019000000000000002fc80000000000003cc4000000000045a6c4010c01789c6364620100000e0008", - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(14200), ShortChannelId(46645), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(14200), RealShortChannelId(46645), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001f30a30c5b0cd144cb92e3b020017c6034a010c01789c6364620100000e0008" ) diff --git a/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala b/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala index aed2186f54..ed1f868cb3 100644 --- a/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala +++ b/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala @@ -23,6 +23,7 @@ import akka.event.LoggingAdapter import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.router.Router._ @@ -110,7 +111,7 @@ class FrontRouter(routerConf: RouterConf, remoteRouter: ActorRef, initialized: O origin.peerConnection ! TransportHandler.ReadAck(ann) Metrics.gossipDropped(ann).increment() d - case u: ChannelUpdate if d.channels.contains(u.shortChannelId) && d.channels(u.shortChannelId).getChannelUpdateSameSideAs(u).contains(u) => + case u: ChannelUpdate if d.channels.get(RealShortChannelId(u.shortChannelId.toLong)).exists(_.getChannelUpdateSameSideAs(u).contains(u)) => origin.peerConnection ! TransportHandler.ReadAck(ann) Metrics.gossipDropped(ann).increment() d @@ -213,7 +214,7 @@ object FrontRouter { // @formatter:on case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, PublicChannel], + channels: SortedMap[RealShortChannelId, PublicChannel], processing: Map[AnnouncementMessage, Set[RemoteGossip]], accepted: Map[AnnouncementMessage, Set[RemoteGossip]], rebroadcast: Rebroadcast) @@ -263,7 +264,7 @@ object FrontRouter { case ChannelsDiscovered(channels) => log.debug("adding {} channels", channels.size) - val channels1 = channels.foldLeft(SortedMap.empty[ShortChannelId, PublicChannel]) { + val channels1 = channels.foldLeft(SortedMap.empty[RealShortChannelId, PublicChannel]) { case (channels, sc) => channels + (sc.ann.shortChannelId -> PublicChannel(sc.ann, ByteVector32.Zeroes, sc.capacity, sc.u1_opt, sc.u2_opt, None)) } val d1 = d.copy(channels = d.channels ++ channels1) @@ -280,7 +281,7 @@ object FrontRouter { case ChannelUpdatesReceived(updates) => log.debug("adding/updating {} channel_updates", updates.size) val channels1 = updates.foldLeft(d.channels) { - case (channels, u) => channels.get(u.shortChannelId) match { + case (channels, u) => channels.get(RealShortChannelId(u.shortChannelId.toLong)) match { case Some(c) => channels + (c.ann.shortChannelId -> c.updateChannelUpdateSameSideAs(u)) case None => channels } @@ -309,7 +310,7 @@ object FrontRouter { case n: NodeAnnouncement => d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins)) case c: ChannelAnnouncement => d.rebroadcast.copy(channels = d.rebroadcast.channels + (c -> origins)) case u: ChannelUpdate => - if (d.channels.contains(u.shortChannelId)) { + if (d.channels.contains(RealShortChannelId(u.shortChannelId.toLong))) { d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)) } else { d.rebroadcast // private channel, we don't rebroadcast the channel_update diff --git a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala index a72d82c11b..0501dfcf67 100644 --- a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala +++ b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala @@ -340,12 +340,12 @@ object FrontRouterSpec { val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features.empty) val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features.empty) - val channelId_ab = ShortChannelId(BlockHeight(420000), 1, 0) - val channelId_bc = ShortChannelId(BlockHeight(420000), 2, 0) - val channelId_cd = ShortChannelId(BlockHeight(420000), 3, 0) - val channelId_ef = ShortChannelId(BlockHeight(420000), 4, 0) + val channelId_ab = RealShortChannelId(BlockHeight(420000), 1, 0) + val channelId_bc = RealShortChannelId(BlockHeight(420000), 2, 0) + val channelId_cd = RealShortChannelId(BlockHeight(420000), 3, 0) + val channelId_ef = RealShortChannelId(BlockHeight(420000), 4, 0) - def channelAnnouncement(shortChannelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { + def channelAnnouncement(shortChannelId: RealShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, Features.empty) val node1_sig = Announcements.signChannelAnnouncement(witness, node1_priv) val funding1_sig = Announcements.signChannelAnnouncement(witness, funding1_priv) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index 61fece11a3..f7a04b0263 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -32,19 +32,28 @@ trait Channel { import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} + val supportedChannelTypes = Set( + ChannelTypes.Standard, + ChannelTypes.StaticRemoteKey, + ChannelTypes.AnchorOutputs, + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true), + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false), + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true) + ).map(ct => ct.toString -> ct).toMap // we use the toString method as name in the api + val open: Route = postRequest("open") { implicit t => formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "channelType".?, "fundingFeerateSatByte".as[FeeratePerByte].?, "announceChannel".as[Boolean].?, "openTimeoutSeconds".as[Timeout].?) { - (nodeId, fundingSatoshis, pushMsat, channelType, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) => - val (channelTypeOk, channelType_opt) = channelType match { - case Some(str) if str == ChannelTypes.Standard.toString => (true, Some(ChannelTypes.Standard)) - case Some(str) if str == ChannelTypes.StaticRemoteKey.toString => (true, Some(ChannelTypes.StaticRemoteKey)) - case Some(str) if str == ChannelTypes.AnchorOutputs.toString => (true, Some(ChannelTypes.AnchorOutputs)) - case Some(str) if str == ChannelTypes.AnchorOutputsZeroFeeHtlcTx.toString => (true, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) - case Some(_) => (false, None) + (nodeId, fundingSatoshis, pushMsat, channelTypeName_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) => + val (channelTypeOk, channelType_opt) = channelTypeName_opt match { + case Some(channelTypeName) => supportedChannelTypes.get(channelTypeName) match { + case Some(channelType) => (true, Some(channelType)) + case None => (false, None) // invalid channel type name + } case None => (true, None) } if (!channelTypeOk) { - reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be ${ChannelTypes.Standard.toString}, ${ChannelTypes.StaticRemoteKey.toString}, ${ChannelTypes.AnchorOutputs.toString} or ${ChannelTypes.AnchorOutputsZeroFeeHtlcTx.toString}")) + reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be one of ${supportedChannelTypes.keys.mkString(",")}")) } else { complete { eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index 4843701bd6..5f1bc093d8 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec -import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} +import fr.acinq.eclair.{UnspecifiedShortChannelId, MilliSatoshi, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector import java.util.UUID diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index de7394b551..e134c67b7a 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -374,7 +374,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx), None, None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), None, None, None)(any[Timeout]).wasCalled(once) } } @@ -994,7 +994,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val mockChannelUpdate1 = ChannelUpdate( signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), chainHash = ByteVector32.fromValidHex("024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2"), - shortChannelId = ShortChannelId(BlockHeight(1), 2, 3), + shortChannelId = RealShortChannelId(BlockHeight(1), 2, 3), timestamp = 0 unixsec, channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(0), @@ -1003,8 +1003,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM feeProportionalMillionths = 1, htlcMaximumMsat = None ) - val mockChannelUpdate2 = mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 4)) - val mockChannelUpdate3 = mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 5)) + val mockChannelUpdate2 = mockChannelUpdate1.copy(shortChannelId = RealShortChannelId(BlockHeight(1), 2, 4)) + val mockChannelUpdate3 = mockChannelUpdate1.copy(shortChannelId = RealShortChannelId (BlockHeight(1), 2, 5)) val mockHop1 = Router.ChannelHop(mockChannelUpdate1.shortChannelId, PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate1)) val mockHop2 = Router.ChannelHop(mockChannelUpdate2.shortChannelId, mockHop1.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate2)) diff --git a/pom.xml b/pom.xml index 48f3e25e5f..adb6006aea 100644 --- a/pom.xml +++ b/pom.xml @@ -132,7 +132,7 @@ net.alchim31.maven scala-maven-plugin - 4.6.1 + 4.6.2 -feature @@ -228,7 +228,7 @@ org.scalatest scalatest-maven-plugin - 2.0.0 + 2.0.2 true I