diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 33cdb33d0b..e956f945ea 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -89,8 +89,9 @@ upgrading to this release. ### API changes -- `channelbalances` Retrieves information about the balances of all local channels. (#2196) -- `stop` Stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233) +- `channelbalances`: retrieves information about the balances of all local channels (#2196) +- `stop`: stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233) +- `channelbalances` and `usablebalances` return a `shortIds` object instead of a single `shortChannelId` (#2323) ### Miscellaneous improvements and bug fixes @@ -112,8 +113,8 @@ to your node. 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` +- `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. @@ -124,8 +125,8 @@ Expired incoming invoices that are unpaid will be searched for and purged from t 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` +- `eclair.purge-expired-invoices.enabled = true +- `eclair.purge-expired-invoices.interval = 24 hours` ## Verifying signatures 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 357f418a62..8cdb82327a 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 @@ -22,7 +22,6 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, REFRESH_CHANNEL_UPDATE_INTERVAL} @@ -39,7 +38,6 @@ import fr.acinq.eclair.wire.protocol._ import scodec.bits.ByteVector import scala.concurrent.duration._ -import scala.concurrent.{Await, ExecutionContext} import scala.util.{Failure, Success, Try} /** @@ -186,27 +184,30 @@ object Helpers { } /** - * 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 + * We use the real scid if the channel has been announced, otherwise we use our local alias. */ - 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(channelAnnouncement_opt: Option[ChannelAnnouncement], localAlias: Alias): ShortChannelId = { + channelAnnouncement_opt.map(_.shortChannelId).getOrElse(localAlias) } - def scidForChannelUpdate(d: DATA_NORMAL): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds) + def scidForChannelUpdate(d: DATA_NORMAL): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds.localAlias) + + /** + * If our peer sent us an alias, that's what we must use in the channel_update we send them to ensure they're able to + * match this update with the corresponding local channel. If they didn't send us an alias, it means we're not using + * 0-conf and we'll use the real scid. + */ + def channelUpdateForDirectPeer(nodeParams: NodeParams, channelUpdate: ChannelUpdate, shortIds: ShortIds): ChannelUpdate = { + shortIds.remoteAlias_opt match { + case Some(remoteAlias) => Announcements.updateScid(nodeParams.privateKey, channelUpdate, remoteAlias) + case None => shortIds.real.toOption match { + case Some(realScid) => Announcements.updateScid(nodeParams.privateKey, channelUpdate, realScid) + // This case is a spec violation: this is a 0-conf channel, so our peer MUST send their alias. + // They won't be able to match our channel_update with their local channel, too bad for them. + case None => channelUpdate + } + } + } /** * Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by 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 bab02b5eae..9decc58b0d 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 @@ -622,7 +622,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // 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) + val scidForChannelUpdate = Helpers.scidForChannelUpdate(d.channelAnnouncement, shortIds1.localAlias) // 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") @@ -659,7 +659,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val 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 scidForChannelUpdate = Helpers.scidForChannelUpdate(Some(channelAnn), d.shortIds.localAlias) 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() @@ -1640,7 +1640,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val 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) + send(Helpers.channelUpdateForDirectPeer(nodeParams, d.channelUpdate, d.shortIds)) } case EmitLocalChannelDown(d) => log.debug(s"emitting channel down event") 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 8966b25364..14722ddfe6 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 @@ -421,7 +421,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { } 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 scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, shortIds1) + val scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, shortIds1.localAlias) 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)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 621e6b62a5..226ad8544c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -20,6 +20,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Invoice.{BasicEdge, ExtraEdge} +import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore} @@ -154,6 +155,7 @@ object PaymentFailure { def transformForUser(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = { failures match { case previousFailures :+ LocalFailure(_, _, RouteNotFound) if previousFailures.nonEmpty => previousFailures + case previousFailures :+ LocalFailure(_, _, RetryExhausted) if previousFailures.nonEmpty => previousFailures case other => other } } 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 c811d03a84..3ba8c507f4 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 @@ -191,7 +191,10 @@ class ChannelRelay private(nodeParams: NodeParams, private val nextNodeId_opt = channels.headOption.map(_._2.nextNodeId) /** channel id explicitly requested in the onion payload */ - private val requestedChannelId_opt = channels.find(_._2.channelUpdate.shortChannelId == r.payload.outgoingChannelId).map(_._1) + private val requestedChannelId_opt = channels.collectFirst { + case (channelId, channel) if channel.shortIds.localAlias == r.payload.outgoingChannelId => channelId + case (channelId, channel) if channel.shortIds.real.toOption.contains(r.payload.outgoingChannelId) => channelId + } /** * Select a channel to the same node to relay the payment to, that has the lowest capacity and balance and is 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 1a9bfc2fc1..59eb7b58b0 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 @@ -95,7 +95,7 @@ object ChannelRelayer { 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 channel = Relayer.OutgoingChannel(shortIds, remoteNodeId, channelUpdate, prevChannelUpdate, commitments) val channels1 = channels + (channelId -> channel) val mappings = lcu.scidsForRouting.map(_ -> channelId).toMap context.log.debug("adding mappings={} to channelId={}", mappings.keys.mkString(","), channelId) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index 1996927e8c..c7341e7fab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams, ShortChannelId} +import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams} import grizzled.slf4j.Logging import scala.concurrent.Promise @@ -133,9 +133,10 @@ object Relayer extends Logging { } case class RelayForward(add: UpdateAddHtlc) - case class ChannelBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean) + case class ChannelBalance(remoteNodeId: PublicKey, shortIds: ShortIds, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean) sealed trait OutgoingChannelParams { + def channelId: ByteVector32 def channelUpdate: ChannelUpdate def prevChannelUpdate: Option[ChannelUpdate] } @@ -146,11 +147,11 @@ object Relayer extends Logging { * @param enabledOnly if true, filter out disabled channels. */ case class GetOutgoingChannels(enabledOnly: Boolean = true) - case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) extends OutgoingChannelParams { - val channelId: ByteVector32 = commitments.channelId + case class OutgoingChannel(shortIds: ShortIds, nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) extends OutgoingChannelParams { + override val channelId: ByteVector32 = commitments.channelId def toChannelBalance: ChannelBalance = ChannelBalance( remoteNodeId = nextNodeId, - shortChannelId = channelUpdate.shortChannelId, + shortIds = shortIds, canSend = commitments.availableBalanceForSend, canReceive = commitments.availableBalanceForReceive, isPublic = commitments.announceChannel, 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 da8381840c..5441a3407f 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,9 +18,8 @@ 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 fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector import shapeless.HNil @@ -118,7 +117,6 @@ object Announcements { def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, enable: Boolean = true, timestamp: TimestampSecond = TimestampSecond.now()): ChannelUpdate = { val channelFlags = ChannelUpdate.ChannelFlags(isNode1 = isNode1(nodeSecret.publicKey, remoteNodeId), isEnabled = enable) val htlcMaximumMsatOpt = Some(htlcMaximumMsat) - val witness = channelUpdateWitnessEncode(chainHash, shortChannelId, timestamp, channelFlags, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, htlcMaximumMsatOpt, TlvStream.empty) val sig = Crypto.sign(witness, nodeSecret) ChannelUpdate( @@ -135,6 +133,14 @@ object Announcements { ) } + def updateScid(nodeSecret: PrivateKey, u: ChannelUpdate, scid: ShortChannelId): ChannelUpdate = { + // NB: we don't update the timestamp as we're not changing any parameter. + val u1 = u.copy(shortChannelId = scid) + val witness = channelUpdateWitnessEncode(u.chainHash, scid, u.timestamp, u.channelFlags, u.cltvExpiryDelta, u.htlcMinimumMsat, u.feeBaseMsat, u.feeProportionalMillionths, u.htlcMaximumMsat, u.tlvStream) + val sig = Crypto.sign(witness, nodeSecret) + u1.copy(signature = sig) + } + def checkSigs(ann: ChannelAnnouncement): Boolean = { val witness = channelAnnouncementWitnessEncode(ann.chainHash, ann.shortChannelId, ann.nodeId1, ann.nodeId2, ann.bitcoinKey1, ann.bitcoinKey2, ann.features, ann.tlvStream) verifySignature(witness, ann.nodeSignature1, ann.nodeId1) && 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 48b031fd52..6386271f94 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 @@ -181,7 +181,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { // 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") + if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global val aliceParams = Alice.channelParams diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index 167c75563d..e08f1c3879 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -25,6 +25,7 @@ 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.router.Announcements import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -91,16 +92,22 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu 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 aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real.isInstanceOf[RealScidStatus.Temporary]) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real.isInstanceOf[RealScidStatus.Temporary]) val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) 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) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + // we have a real scid, but the channel is not announced so alice uses bob's alias + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) @@ -109,16 +116,20 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu 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 aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + val realScid = aliceIds.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) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + // the channel is not announced but bob didn't send an alias so we use the real scid + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + assert(channelUpdateSentToPeer.shortChannelId == realScid) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) @@ -127,16 +138,22 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu 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 aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real == RealScidStatus.Unknown) val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) 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) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // the channel is not announced so alice uses bob's alias (we have a no real scid anyway) + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) @@ -145,33 +162,48 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu 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 aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.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] + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // the channel is 0-conf but bob didn't provide an alias: it's a spec violation, so we use our local alias and if + // they can't understand it, too bad for them + assert(channelUpdateSentToPeer.shortChannelId == aliceIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) } 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]) + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.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]) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real.isInstanceOf[RealScidStatus.Temporary]) assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.channelFlags.announceChannel) val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) 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) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // we have a real scid, but it is not the final one (less than 6 confirmations) so alice uses bob's alias + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) @@ -180,16 +212,22 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu 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 aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real == RealScidStatus.Unknown) val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) 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) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // the channel is not announced, so alice uses bob's alias (we have a no real scid anyway) + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) 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 8ff235ff85..5ec60cd941 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 @@ -844,7 +844,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap == false) + assert(!waitForRevocation.reSignAsap) // actual test starts here alice ! CMD_SIGN() @@ -861,7 +861,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap == false) + assert(!waitForRevocation.reSignAsap) // actual test starts here addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1221,7 +1221,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() sender.expectNoMessage(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap == true) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap) // actual test starts here bob2alice.expectMsgType[RevokeAndAck] @@ -1364,7 +1364,6 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob)) { f => import f._ - val sender = TestProbe() assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) testRevokeAndAckDustOverflowSingleCommit(f) @@ -1372,7 +1371,6 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice)) { f => import f._ - val sender = TestProbe() assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) testRevokeAndAckDustOverflowSingleCommit(f) @@ -3402,8 +3400,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingDeeplyBuriedTriggered (public channel)", 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 + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid // existing funding tx coordinates val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) @@ -3411,17 +3409,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv WatchFundingDeeplyBuriedTriggered (public channel, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // 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 + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) // funding tx coordinates (unknown before) val (blockHeight, txIndex) = (BlockHeight(400000), 42) alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) @@ -3430,16 +3427,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } 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 + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid // existing funding tx coordinates val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) // new funding tx coordinates (there was a reorg) @@ -3450,25 +3446,24 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv WatchFundingDeeplyBuriedTriggered (private channel)") { f => import f._ + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds 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 + // private channel: we'll use the remote alias in the channel_update we sent to our peer, so there is no change 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) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv WatchFundingDeeplyBuriedTriggered (private channel, zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => @@ -3477,24 +3472,24 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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 + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) 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 + // private channel: we'll use the remote alias in the channel_update we sent to our peer, so there is no change 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) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) // 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._ - val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid - val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid // existing funding tx coordinates val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) // new funding tx coordinates (there was a reorg) @@ -3503,10 +3498,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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 + // private channel: we'll use the remote alias in the channel_update we sent to our peer, so there is no change 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) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv AnnouncementSignatures", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => @@ -3579,14 +3574,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - bob2alice.expectMsgType[AnnouncementSignatures] + val realScid = bob2alice.expectMsgType[AnnouncementSignatures].shortChannelId bob2alice.forward(alice) val update1 = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(update1.channelUpdate.shortChannelId == realScid) // actual test starts here Thread.sleep(1100) alice ! BroadcastChannelUpdate(PeriodicRefresh) val update2 = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(update2.channelUpdate.shortChannelId == realScid) assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) } 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 72da991d06..c0cc5482f8 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 @@ -51,7 +51,7 @@ class ThreeNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { 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) + sendSuccessfulPayment(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 89598db9e5..1e8a87c32f 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 @@ -87,7 +87,7 @@ class TwoNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { getRouterData(alice).privateChannels.size == 1 } - sendPayment(alice, bob, 10_000 msat) + sendSuccessfulPayment(alice, bob, 10_000 msat) } } 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 index 05af0ee034..33bdf0d4c9 100644 --- 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 @@ -5,9 +5,13 @@ 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.crypto.Sphinx import fr.acinq.eclair.integration.basic.fixtures.ThreeNodesFixture -import fr.acinq.eclair.payment.PaymentSent +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.RouteNotFound import fr.acinq.eclair.testutils.FixtureSpec +import fr.acinq.eclair.wire.protocol.{FailureMessage, UnknownNextPeer, Update} import fr.acinq.eclair.{MilliSatoshiLong, RealShortChannelId} import org.scalatest.OptionValues.convertOptionToValuable import org.scalatest.concurrent.IntegrationPatience @@ -60,17 +64,35 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience (channelId_ab, channelId_bc) } - private def sendPaymentAliceToCarol(f: FixtureParam, useHint: Boolean = false, overrideHintScid_opt: Option[RealShortChannelId] = None): PaymentSent = { + private def createBobToCarolTestHint(f: FixtureParam, useHint: Boolean, overrideHintScid_opt: Option[RealShortChannelId]): Seq[ExtraHop] = { import f._ - val hint = if (useHint) { + 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)) + } else { + Seq.empty + } + } + + case object Ok + + private def sendPaymentAliceToCarol(f: FixtureParam, expected: Either[Either[Throwable, FailureMessage], Ok.type], useHint: Boolean = false, overrideHintScid_opt: Option[RealShortChannelId] = None): Unit = { + import f._ + val result = sendPayment(alice, carol, 100_000 msat, hints = Seq(createBobToCarolTestHint(f, useHint, overrideHintScid_opt))) match { + case Left(paymentFailed) => + Left(PaymentFailure.transformForUser(paymentFailed.failures).last match { + case LocalFailure(_, _, t) => Left(t) + case RemoteFailure(_, _, e) => Right(e.failureMessage) + case _: UnreadableRemoteFailure => fail("received unreadable remote failure") + }) + case Right(_) => Right(Ok) + } + + assert(result == expected) } private def internalTest(f: FixtureParam, @@ -78,9 +100,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience bcPublic: Boolean, bcZeroConf: Boolean, bcScidAlias: Boolean, - paymentWorksWithoutHint: Boolean, - paymentWorksWithHint_opt: Option[Boolean], - paymentWorksWithRealScidHint_opt: Option[Boolean]): Unit = { + paymentWithoutHint: Either[Either[Throwable, FailureMessage], Ok.type], + paymentWithHint_opt: Option[Either[Either[Throwable, FailureMessage], Ok.type]], + paymentWithRealScidHint_opt: Option[Either[Either[Throwable, FailureMessage], Ok.type]]): Unit = { import f._ val (_, channelId_bc) = createChannels(f)(deepConfirm = deepConfirm) @@ -108,33 +130,18 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience } eventually { - if (paymentWorksWithoutHint) { - sendPaymentAliceToCarol(f) - } else { - intercept[AssertionError] { - sendPaymentAliceToCarol(f) - } - } + sendPaymentAliceToCarol(f, paymentWithoutHint) } - eventually { - paymentWorksWithHint_opt match { - case Some(true) => sendPaymentAliceToCarol(f, useHint = true) - case Some(false) => intercept[AssertionError] { - sendPaymentAliceToCarol(f, useHint = true) - } - case None => // skipped + paymentWithHint_opt.foreach { paymentWithHint => + eventually { + sendPaymentAliceToCarol(f, paymentWithHint, useHint = true) } } - eventually { - 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 + paymentWithRealScidHint_opt.foreach { paymentWithRealScidHint => + eventually { + sendPaymentAliceToCarol(f, paymentWithRealScidHint, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) } } } @@ -145,9 +152,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience 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 + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works (and it will use the alias, even if the feature isn't enabled) + paymentWithRealScidHint_opt = Some(Right(Ok)) // if alice uses the real scid instead of the bob-carol alias, it still works ) } @@ -157,9 +164,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience 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 + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = Some(Left(Right(UnknownNextPeer))) // if alice uses the real scid instead of the bob-carol alias, it doesn't work due to option_scid_alias ) } @@ -169,9 +176,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience 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 + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = None // there is no real scid for bob-carol yet ) } @@ -181,14 +188,14 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience 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 + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // 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 + paymentWithRealScidHint_opt = None ) } @@ -198,9 +205,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience 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 + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = Some(Left(Right(UnknownNextPeer))) // if alice uses the real scid instead of the b-c alias, it doesn't work due to option_scid_alias ) } @@ -210,9 +217,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience 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 + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced yet + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = None // there is no real scid for bob-carol yet ) } @@ -222,10 +229,36 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience 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 + paymentWithoutHint = Right(Ok), + paymentWithHint_opt = None, // there is no routing hints for public channels + paymentWithRealScidHint_opt = None // there is no routing hints for public channels ) } + test("temporary channel failures don't leak the real scid", Tag(ScidAliasBobCarol), Tag(ZeroConfBobCarol)) { f => + import f._ + + val (_, channelId_bc) = createChannels(f)(deepConfirm = false) + + eventually { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ZeroConf)) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ScidAlias)) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + assert(getRouterData(bob).privateChannels.values.exists(_.nodeId2 == carol.nodeParams.nodeId)) + } + + val Some(carolHint) = getRouterData(carol).privateChannels.values.head.toIncomingExtraHop + val bobAlias = getRouterData(bob).privateChannels.values.find(_.nodeId2 == carol.nodeParams.nodeId).value.shortIds.localAlias + assert(carolHint.shortChannelId == bobAlias) + + // We make sure Bob won't have enough liquidity to relay another payment. + sendSuccessfulPayment(bob, carol, 60_000_000 msat) + + // The channel update returned in failures doesn't leak the real scid. + val failure = sendFailingPayment(alice, carol, 50_000_000 msat, hints = Seq(Seq(carolHint))) + val failureWithChannelUpdate = failure.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f: Update)) => f } + assert(failureWithChannelUpdate.length == 1) + assert(failureWithChannelUpdate.head.update.shortChannelId == bobAlias) + } + } 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 cd6ba696e9..0667d515e1 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 @@ -21,12 +21,12 @@ import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.receive.{MultiPartHandler, PaymentHandler} import fr.acinq.eclair.payment.relay.{ChannelRelayer, Relayer} import fr.acinq.eclair.payment.send.PaymentInitiator -import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSent} +import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent, PaymentFailed, PaymentSent} import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IPAddress import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, SubscriptionsComplete, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} -import org.scalatest.Assertions import org.scalatest.concurrent.Eventually.eventually +import org.scalatest.{Assertions, EitherValues} import java.net.InetAddress import java.util.UUID @@ -51,7 +51,7 @@ case class MinimalNodeFixture private(nodeParams: NodeParams, watcher: TestProbe, wallet: DummyOnChainWallet) -object MinimalNodeFixture extends Assertions { +object MinimalNodeFixture extends Assertions with EitherValues { def nodeParamsFor(alias: String, seed: ByteVector32): NodeParams = { NodeParams.makeNodeParams( @@ -278,14 +278,26 @@ object MinimalNodeFixture extends Assertions { case _ => TestActor.KeepRunning } - def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentSent = { + def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): Either[PaymentFailed, 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, extraEdges = hints.flatMap(Bolt11Invoice.toExtraEdges(_, node2.nodeParams.nodeId)), blockUntilComplete = true)) - sender.expectMsgType[PaymentSent] + sender.expectMsgType[PaymentEvent] match { + case e: PaymentSent => Right(e) + case e: PaymentFailed => Left(e) + case e => fail(s"unexpected event $e") + } + } + + def sendSuccessfulPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentSent = { + sendPayment(node1, node2, amount, hints).value + } + + def sendFailingPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentFailed = { + sendPayment(node1, node2, amount, hints).left.value } def prettyPrint(routerData: Router.Data, nodes: MinimalNodeFixture*): Unit = { 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 912569ce3c..695a9cf96b 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 @@ -78,7 +78,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd } - def basicRelaytest(f: FixtureParam)(relayPayloadScid: ShortChannelId, lcu: LocalChannelUpdate, success: Boolean): Unit = { + def basicRelayTest(f: FixtureParam)(relayPayloadScid: ShortChannelId, lcu: LocalChannelUpdate, success: Boolean): Unit = { import f._ val payload = RelayLegacyPayload(relayPayloadScid, outgoingAmount, outgoingExpiry) @@ -95,37 +95,35 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a } test("relay with real scid (channel update uses real scid)") { f => - basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1), success = true) + 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 real scid (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(localAlias1)), success = true) } test("relay with local alias (channel update uses real scid)") { f => - basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1), success = true) + 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("relay with local alias (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(localAlias1)), 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) + 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("fail to relay with real scid when option_scid_alias is enabled (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(localAlias1)), success = false) } 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) + 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 local alias when option_scid_alias is enabled (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(localAlias1)), success = true) } test("relay with new real scid after reorg") { f => @@ -154,7 +152,6 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdAdd(register, lcu2.channelId, outgoingAmount, outgoingExpiry) } - test("relay with onion tlv payload") { f => import f._ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ @@ -535,9 +532,9 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val channels1 = getOutgoingChannels(true) assert(channels1.size == 2) assert(channels1.head.channelUpdate == channelUpdate_ab) - assert(channels1.head.toChannelBalance == Relayer.ChannelBalance(a, channelUpdate_ab.shortChannelId, 0 msat, 300000 msat, isPublic = false, isEnabled = true)) + assert(channels1.head.toChannelBalance == Relayer.ChannelBalance(a, shortIds_ab, 0 msat, 300000 msat, isPublic = false, isEnabled = true)) assert(channels1.last.channelUpdate == channelUpdate_bc) - assert(channels1.last.toChannelBalance == Relayer.ChannelBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) + assert(channels1.last.toChannelBalance == Relayer.ChannelBalance(c, shortIds_bc, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, shortIds_ab, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) val channels2 = getOutgoingChannels(true) @@ -575,8 +572,6 @@ object ChannelRelayerSpec { val localAlias1: Alias = Alias(111000) val localAlias2: Alias = Alias(222000) - val remoteAlias1: ShortChannelId = ShortChannelId(111999) - val channelId1: ByteVector32 = randomBytes32() val channelId2: ByteVector32 = randomBytes32() 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 ebe90ec8f5..643e682d5c 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 @@ -10,6 +10,7 @@ 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.OptionValues.convertOptionToValuable import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -57,9 +58,9 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu // 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 uses her alias for her internal channel update + val aliceInitialChannelUpdate = privateChannel.update_1_opt.value + assert(aliceInitialChannelUpdate.shortChannelId == privateChannel.shortIds.localAlias) // alice and bob send their channel_updates using remote alias when they go to NORMAL state val aliceChannelUpdate1 = channels.alice2bob.expectMsgType[ChannelUpdate] @@ -73,7 +74,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu // 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) + privateChannel.update_1_opt.contains(aliceInitialChannelUpdate) && privateChannel.update_2_opt.contains(bobChannelUpdate1) } // there is nothing for the router to rebroadcast, channel is not announced @@ -81,7 +82,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu // 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))) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceInitialChannelUpdate, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) if (testTags.contains(ChannelStateTestsTags.ChannelsPublic)) { // this is a public channel @@ -161,7 +162,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu // 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))) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceInitialChannelUpdate, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) } // channel closes channels.alice ! CMD_CLOSE(TestProbe().ref, scriptPubKey = None, feerates = None) diff --git a/eclair-node/src/test/resources/api/channelbalances b/eclair-node/src/test/resources/api/channelbalances index 962b4c89aa..bbfc7619b3 100644 --- a/eclair-node/src/test/resources/api/channelbalances +++ b/eclair-node/src/test/resources/api/channelbalances @@ -1 +1 @@ -[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":0,"canReceive":30000000,"isPublic":false,"isEnabled":false}] \ No newline at end of file +[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"temporary","realScid":"0x0x1"},"localAlias":"0x2"},"canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"final","realScid":"0x0x3"},"localAlias":"0x3","remoteAlias":"0x3"},"canSend":0,"canReceive":30000000,"isPublic":false,"isEnabled":false}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/usablebalances b/eclair-node/src/test/resources/api/usablebalances index 5613aaf151..2876388234 100644 --- a/eclair-node/src/test/resources/api/usablebalances +++ b/eclair-node/src/test/resources/api/usablebalances @@ -1 +1 @@ -[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":400000000,"canReceive":30000000,"isPublic":false,"isEnabled":true}] \ No newline at end of file +[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"unknown"},"localAlias":"0x1"},"canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"final","realScid":"0x0x2"},"localAlias":"0x3","remoteAlias":"0x4"},"canSend":400000000,"canReceive":30000000,"isPublic":false,"isEnabled":true}] \ No newline at end of file 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 36f3d7c37f..dba2e0834a 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 @@ -205,8 +205,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'usablebalances' returns expected balance json only for enabled channels") { val eclair = mock[Eclair] eclair.usableBalances()(any[Timeout]) returns Future.successful(List( - ChannelBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), - ChannelBalance(aliceNodeId, ShortChannelId(2), 400000000 msat, 30000000 msat, isPublic = false, isEnabled = true) + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Unknown, Alias(1), None), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Final(RealShortChannelId(2)), Alias(3), Some(Alias(4))), 400000000 msat, 30000000 msat, isPublic = false, isEnabled = true) )) val mockService = mockApi(eclair) @@ -225,8 +225,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'channelbalances' returns expected balance json for all channels") { val eclair = mock[Eclair] eclair.channelBalances()(any[Timeout]) returns Future.successful(List( - ChannelBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), - ChannelBalance(aliceNodeId, ShortChannelId(2), 0 msat, 30000000 msat, isPublic = false, isEnabled = false) + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Temporary(RealShortChannelId(1)), Alias(2), None), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Final(RealShortChannelId(3)), Alias(3), Some(Alias(3))), 0 msat, 30000000 msat, isPublic = false, isEnabled = false) )) val mockService = mockApi(eclair)