diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index d1bd1d07ae..8abe2c1514 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -9,6 +9,23 @@ We remove the code used to support legacy channels that don't use anchor outputs or taproot. If you still have such channels, eclair won't start: you will need to close those channels, and will only be able to update eclair once they have been successfully closed. +### Channel lifecyle events rework + +Eclair emits several events during a channel lifecycle, which can be received by plugins or through the websocket. +We reworked these events to be compatible with splicing and consistent with 0-conf: + +- we removed the `channel-opened` event +- we introduced a `channel-confirmed` event +- we introduced a `channel-ready` event + +The `channel-confirmed` event is emitted when the funding transaction or a splice transaction has enough confirmations. +Listeners can use the `fundingTxIndex` to detect whether this is the initial channel funding (`fundingTxIndex = 0`) or a splice (`fundingTxIndex > 0`). + +The `channel-ready` event is emitted when the channel is ready to process payments, which generally happens after the `channel-confirmed` event. +However, when using zero-conf, this event may be emitted before the `channel-confirmed` event. + +See #3237 for more details. + ### Channel jamming accountability We update our channel jamming mitigation to match the latest draft of the [spec](https://github.com/lightning/bolts/pull/1280). @@ -29,6 +46,7 @@ eclair.relay.reserved-for-accountable = 0.0 ### API changes - `findroute`, `findroutetonode` and `findroutebetweennodes` now include a `maxCltvExpiryDelta` parameter (#3234) +- `channel-opened` was removed from the websocket in favor of `channel-confirmed` and `channel-ready` (#3237) ### Miscellaneous improvements and bug fixes 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 8a3b7bdd98..0c1dc2f31f 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 @@ -765,14 +765,7 @@ object DATA_CLOSED { commitmentFormat = d.commitments.latest.commitmentFormat.toString, announced = d.commitments.latest.channelParams.announceChannel, capacity = d.commitments.latest.capacity, - closingTxId = closingType match { - case Closing.MutualClose(closingTx) => closingTx.tx.txid - case Closing.LocalClose(_, localCommitPublished) => localCommitPublished.commitTx.txid - case Closing.CurrentRemoteClose(_, remoteCommitPublished) => remoteCommitPublished.commitTx.txid - case Closing.NextRemoteClose(_, remoteCommitPublished) => remoteCommitPublished.commitTx.txid - case Closing.RecoveryClose(remoteCommitPublished) => remoteCommitPublished.commitTx.txid - case Closing.RevokedClose(revokedCommitPublished) => revokedCommitPublished.commitTx.txid - }, + closingTxId = closingType.closingTxId, closingType = closingType.toString, closingScript = d.finalScriptPubKey, localBalance = closingType match { 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 806b5835fa..aa719f54e0 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 @@ -30,6 +30,7 @@ import fr.acinq.eclair.{BlockHeight, Features, MilliSatoshi, RealShortChannelId, trait ChannelEvent +/** This event is sent when a channel has been created: however, it may not be ready to process payments yet (see [[ChannelReadyForPayments]]). */ case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isOpener: Boolean, temporaryChannelId: ByteVector32, commitTxFeerate: FeeratePerKw, fundingTxFeerate: Option[FeeratePerKw]) extends ChannelEvent // This trait can be used by non-standard channels to inject themselves into Register actor and thus make them usable for routing @@ -50,11 +51,11 @@ case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, an /** This event will be sent if a channel was aborted before completing the opening flow. */ case class ChannelAborted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32) extends ChannelEvent -/** This event will be sent once a channel has been successfully opened and is ready to process payments. */ -case class ChannelOpened(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32) extends ChannelEvent +/** This event is sent once a funding transaction (channel creation or splice) has been confirmed. */ +case class ChannelFundingConfirmed(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, fundingTxId: TxId, fundingTxIndex: Long, blockHeight: BlockHeight, commitments: Commitments) extends ChannelEvent -/** This event is sent once channel_ready or splice_locked have been exchanged. */ -case class ChannelReadyForPayments(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, fundingTxIndex: Long) extends ChannelEvent +/** This event is sent once channel_ready or splice_locked have been exchanged: the channel is ready to process payments. */ +case class ChannelReadyForPayments(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, fundingTxId: TxId, fundingTxIndex: Long) extends ChannelEvent case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, aliases: ShortIdAliases, remoteNodeId: PublicKey, announcement_opt: Option[AnnouncedCommitment], channelUpdate: ChannelUpdate, commitments: Commitments) extends ChannelEvent { /** @@ -103,7 +104,7 @@ case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelI case class LocalCommitConfirmed(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, refundAtBlock: BlockHeight) extends ChannelEvent -case class ChannelClosed(channel: ActorRef, channelId: ByteVector32, closingType: ClosingType, commitments: Commitments) extends ChannelEvent +case class ChannelClosed(channel: ActorRef, channelId: ByteVector32, closingType: ClosingType, closingTxId: TxId, commitments: Commitments) extends ChannelEvent case class OutgoingHtlcAdded(add: UpdateAddHtlc, remoteNodeId: PublicKey, fee: MilliSatoshi) 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 012d3e85f5..b50b4a15f6 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 @@ -603,14 +603,30 @@ object Helpers { object Closing { // @formatter:off - sealed trait ClosingType - case class MutualClose(tx: ClosingTx) extends ClosingType { override def toString: String = "mutual-close" } - case class LocalClose(localCommit: LocalCommit, localCommitPublished: LocalCommitPublished) extends ClosingType { override def toString: String = "local-close" } - sealed trait RemoteClose extends ClosingType { def remoteCommit: RemoteCommit; def remoteCommitPublished: RemoteCommitPublished } + sealed trait ClosingType { def closingTxId: TxId } + case class MutualClose(tx: ClosingTx) extends ClosingType { + override def closingTxId: TxId = tx.tx.txid + override def toString: String = "mutual-close" + } + case class LocalClose(localCommit: LocalCommit, localCommitPublished: LocalCommitPublished) extends ClosingType { + override def closingTxId: TxId = localCommitPublished.commitTx.txid + override def toString: String = "local-close" + } + sealed trait RemoteClose extends ClosingType { + def remoteCommit: RemoteCommit + def remoteCommitPublished: RemoteCommitPublished + override def closingTxId: TxId = remoteCommitPublished.commitTx.txid + } case class CurrentRemoteClose(remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished) extends RemoteClose { override def toString: String = "remote-close" } case class NextRemoteClose(remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished) extends RemoteClose { override def toString: String = "next-remote-close" } - case class RecoveryClose(remoteCommitPublished: RemoteCommitPublished) extends ClosingType { override def toString: String = "recovery-close" } - case class RevokedClose(revokedCommitPublished: RevokedCommitPublished) extends ClosingType { override def toString: String = "revoked-close" } + case class RecoveryClose(remoteCommitPublished: RemoteCommitPublished) extends ClosingType { + override def closingTxId: TxId = remoteCommitPublished.commitTx.txid + override def toString: String = "recovery-close" + } + case class RevokedClose(revokedCommitPublished: RevokedCommitPublished) extends ClosingType { + override def closingTxId: TxId = revokedCommitPublished.commitTx.txid + override def toString: String = "revoked-close" + } // @formatter:on /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Monitoring.scala index 648a0ce927..f7d8062a05 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Monitoring.scala @@ -94,6 +94,7 @@ object Monitoring { object Events { val Created = "created" + val Spliced = "spliced" val Closing = "closing" val Closed = "closed" } 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 b845af376d..f3e10e09dd 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 @@ -2281,7 +2281,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall Closing.isClosed(d1, Some(tx)) match { case Some(closingType) => log.info("channel closed (type={})", EventType.Closed(closingType).label) - context.system.eventStream.publish(ChannelClosed(self, d.channelId, closingType, d.commitments)) + context.system.eventStream.publish(ChannelClosed(self, d.channelId, closingType, closingType.closingTxId, d.commitments)) goto(CLOSED) using DATA_CLOSED(d1, closingType) case None => stay() using d1 storing() @@ -2746,8 +2746,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // We tell the peer that the channel is ready to process payments that may be queued. if (!shutdownInProgress) { - val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min - peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex) + val c = commitments1.active.minBy(_.fundingTxIndex) + peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, c.fundingTxId, c.fundingTxIndex) } goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1, localShutdown = shutdown_opt) sending sendQueue @@ -3033,7 +3033,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall val closingTx = d.findClosingTx(tx).get val closingType = MutualClose(closingTx) log.info("channel closed (type={})", EventType.Closed(closingType).label) - context.system.eventStream.publish(ChannelClosed(self, d.channelId, closingType, d.commitments)) + context.system.eventStream.publish(ChannelClosed(self, d.channelId, closingType, closingType.closingTxId, d.commitments)) goto(CLOSED) using DATA_CLOSED(d, closingTx) case Event(WatchFundingSpentTriggered(tx), d: ChannelDataWithCommitments) => @@ -3339,8 +3339,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall if (oldCommitments.active.size != newCommitments.active.size) { // Some commitments have been deactivated, which means our available balance changed, which may allow forwarding // payments that couldn't be forwarded before. - val fundingTxIndex = newCommitments.active.map(_.fundingTxIndex).min - peer ! ChannelReadyForPayments(self, remoteNodeId, newCommitments.channelId, fundingTxIndex) + val c = newCommitments.active.minBy(_.fundingTxIndex) + peer ! ChannelReadyForPayments(self, remoteNodeId, newCommitments.channelId, c.fundingTxId, c.fundingTxIndex) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala index 2490e86a13..dad00af1d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala @@ -102,6 +102,8 @@ trait CommonFundingHandlers extends CommonHandlers { // Children splice transactions may already spend that confirmed funding transaction. val spliceSpendingTxs = commitments1.all.collect { case c if c.fundingTxIndex == commitment.fundingTxIndex + 1 => c.fundingTxId } watchFundingSpent(commitment, additionalKnownSpendingTxs = spliceSpendingTxs.toSet, None) + // We notify listeners that this funding transaction is now confirmed. + context.system.eventStream.publish(ChannelFundingConfirmed(self, d.channelId, remoteNodeId, w.tx.txid, c.fundingTxIndex, w.blockHeight, commitments1)) // We can unwatch the previous funding transaction(s), which have been spent by this splice transaction. d.commitments.all.collect { case c if c.fundingTxIndex < commitment.fundingTxIndex => blockchain ! UnwatchFundingSpent(c.fundingTxId, c.fundingInput.index.toInt) } // In the dual-funding/splicing case we can forget all other transactions (RBF attempts), they have been @@ -144,7 +146,8 @@ trait CommonFundingHandlers extends CommonHandlers { aliases1.remoteAlias_opt.foreach(_ => context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, None, aliases1, remoteNodeId))) log.info("shortIds: real={} localAlias={} remoteAlias={}", commitments.latest.shortChannelId_opt.getOrElse("none"), aliases1.localAlias, aliases1.remoteAlias_opt.getOrElse("none")) // We notify that the channel is now ready to route payments. - context.system.eventStream.publish(ChannelOpened(self, remoteNodeId, commitments.channelId)) + context.system.eventStream.publish(ChannelReadyForPayments(self, remoteNodeId, commitments.channelId, commitments.latest.fundingTxId, fundingTxIndex = 0)) + peer ! ChannelReadyForPayments(self, remoteNodeId, commitments.channelId, commitments.latest.fundingTxId, fundingTxIndex = 0) // 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, aliases1.localAlias) log.info("using shortChannelId={} for initial channel_update", scidForChannelUpdate) @@ -161,7 +164,6 @@ trait CommonFundingHandlers extends CommonHandlers { remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint) ) channelReady.nextCommitNonce_opt.foreach(nonce => remoteNextCommitNonces = remoteNextCommitNonces + (commitments.latest.fundingTxId -> nonce)) - peer ! ChannelReadyForPayments(self, remoteNodeId, commitments.channelId, fundingTxIndex = 0) DATA_NORMAL(commitments1, aliases1, None, initialChannelUpdate, SpliceStatus.NoSplice, None, None, None) } 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 116e70faea..bc50eda18e 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 @@ -22,7 +22,7 @@ import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{Actor, DiagnosticActorLogging, Props} import akka.event.Logging.MDC import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, TxId} import fr.acinq.eclair.channel.Helpers.Closing._ import fr.acinq.eclair.channel.Monitoring.{Metrics => ChannelMetrics, Tags => ChannelTags} import fr.acinq.eclair.channel._ @@ -51,6 +51,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL context.system.eventStream.subscribe(self, classOf[TransactionConfirmed]) context.system.eventStream.subscribe(self, classOf[ChannelErrorOccurred]) context.system.eventStream.subscribe(self, classOf[ChannelStateChanged]) + context.system.eventStream.subscribe(self, classOf[ChannelFundingConfirmed]) context.system.eventStream.subscribe(self, classOf[ChannelClosed]) context.system.eventStream.subscribe(self, classOf[ChannelUpdateParametersChanged]) context.system.eventStream.subscribe(self, classOf[PathFindingExperimentMetrics]) @@ -123,7 +124,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_CHANNEL_READY | WAIT_FOR_DUAL_FUNDING_READY, NORMAL, Some(commitments)) => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Created).increment() val event = ChannelEvent.EventType.Created - auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.latest.capacity, commitments.localChannelParams.isChannelOpener, !commitments.announceChannel, event)) + auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.latest.fundingTxId, commitments.latest.capacity, commitments.localChannelParams.isChannelOpener, !commitments.announceChannel, event)) channelsDb.updateChannelMeta(channelId, event) case ChannelStateChanged(_, channelId, _, _, OFFLINE, SYNCING, _) => channelsDb.updateChannelMeta(channelId, ChannelEvent.EventType.Connected) @@ -132,11 +133,24 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case _ => () } + case e: ChannelFundingConfirmed => + if (e.fundingTxIndex > 0) { + ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Spliced).increment() + } + val event = e.fundingTxIndex match { + case 0 => ChannelEvent.EventType.Confirmed + case _ => ChannelEvent.EventType.Spliced + } + auditDb.add(ChannelEvent(e.channelId, e.remoteNodeId, e.fundingTxId, e.commitments.latest.capacity, e.commitments.localChannelParams.isChannelOpener, !e.commitments.announceChannel, event)) + case e: ChannelClosed => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Closed).increment() val event = ChannelEvent.EventType.Closed(e.closingType) + // We use the latest state of the channel (in case it has been spliced since it was opened), which is the state + // spent by the closing transaction. val capacity = e.commitments.latest.capacity - auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteNodeId, capacity, e.commitments.localChannelParams.isChannelOpener, !e.commitments.announceChannel, event)) + val fundingTxId = e.commitments.latest.fundingTxId + auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteNodeId, fundingTxId, capacity, e.commitments.localChannelParams.isChannelOpener, !e.commitments.announceChannel, event)) channelsDb.updateChannelMeta(e.channelId, event) case u: ChannelUpdateParametersChanged => @@ -164,11 +178,13 @@ object DbEventHandler { def props(nodeParams: NodeParams): Props = Props(new DbEventHandler(nodeParams)) // @formatter:off - case class ChannelEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacity: Satoshi, isChannelOpener: Boolean, isPrivate: Boolean, event: ChannelEvent.EventType) + case class ChannelEvent(channelId: ByteVector32, remoteNodeId: PublicKey, fundingTxId: TxId, capacity: Satoshi, isChannelOpener: Boolean, isPrivate: Boolean, event: ChannelEvent.EventType) object ChannelEvent { sealed trait EventType { def label: String } object EventType { object Created extends EventType { override def label: String = "created" } + object Confirmed extends EventType { override def label: String = "confirmed" } + object Spliced extends EventType { override def label: String = "spliced" } object Connected extends EventType { override def label: String = "connected" } object PaymentSent extends EventType { override def label: String = "sent" } object PaymentReceived extends EventType { override def label: String = "received" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/IncomingConnectionsTracker.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/IncomingConnectionsTracker.scala index ff3602c4da..574ba689b8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/IncomingConnectionsTracker.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/IncomingConnectionsTracker.scala @@ -6,8 +6,8 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory +import fr.acinq.eclair.channel.ChannelReadyForPayments import fr.acinq.eclair.{Logs, NodeParams} -import fr.acinq.eclair.channel.ChannelOpened import fr.acinq.eclair.io.IncomingConnectionsTracker.Command import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.io.Peer.{Disconnect, DisconnectResponse} @@ -46,7 +46,7 @@ object IncomingConnectionsTracker { Behaviors.setup { context => Behaviors.withMdc(Logs.mdc(category_opt = Some(LogCategory.CONNECTION))) { context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[PeerDisconnected](c => ForgetIncomingConnection(c.nodeId))) - context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelOpened](c => ForgetIncomingConnection(c.remoteNodeId))) + context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelReadyForPayments](c => ForgetIncomingConnection(c.remoteNodeId))) new IncomingConnectionsTracker(nodeParams, switchboard, context).tracking(Map.empty) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PendingChannelsRateLimiter.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PendingChannelsRateLimiter.scala index 44ccb6f366..7dcc9d0ad4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PendingChannelsRateLimiter.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PendingChannelsRateLimiter.scala @@ -40,7 +40,7 @@ object PendingChannelsRateLimiter { def apply(nodeParams: NodeParams, router: ActorRef[Router.GetNode], channels: Seq[PersistentChannelData]): Behavior[Command] = { Behaviors.setup { context => context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelIdAssigned](c => ReplaceChannelId(c.remoteNodeId, c.temporaryChannelId, c.channelId))) - context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelOpened](c => RemoveChannelId(c.remoteNodeId, c.channelId))) + context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelReadyForPayments](c => RemoveChannelId(c.remoteNodeId, c.channelId))) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelClosed](c => RemoveChannelId(c.commitments.remoteNodeId, c.channelId))) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelAborted](c => RemoveChannelId(c.remoteNodeId, c.channelId))) val pendingChannels = filterPendingChannels(nodeParams, channels) 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 f447fabb23..f37d66e595 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 @@ -553,10 +553,20 @@ object ChannelEventSerializer extends MinimalSerializer({ JField("commitTxFeeratePerKw", JLong(e.commitTxFeerate.toLong)), JField("fundingTxFeeratePerKw", e.fundingTxFeerate.map(f => JLong(f.toLong)).getOrElse(JNothing)) ) - case e: ChannelOpened => JObject( - JField("type", JString("channel-opened")), + case e: ChannelFundingConfirmed => JObject( + JField("type", JString("channel-confirmed")), JField("remoteNodeId", JString(e.remoteNodeId.toString())), JField("channelId", JString(e.channelId.toHex)), + JField("fundingTxId", JString(e.fundingTxId.value.toHex)), + JField("fundingTxIndex", JLong(e.fundingTxIndex)), + JField("blockHeight", JLong(e.blockHeight.toLong)), + ) + case e: ChannelReadyForPayments => JObject( + JField("type", JString("channel-ready")), + JField("remoteNodeId", JString(e.remoteNodeId.toString())), + JField("channelId", JString(e.channelId.toHex)), + JField("fundingTxId", JString(e.fundingTxId.value.toHex)), + JField("fundingTxIndex", JLong(e.fundingTxIndex)), ) case e: ChannelStateChanged => JObject( JField("type", JString("channel-state-changed")), @@ -568,7 +578,8 @@ object ChannelEventSerializer extends MinimalSerializer({ case e: ChannelClosed => JObject( JField("type", JString("channel-closed")), JField("channelId", JString(e.channelId.toHex)), - JField("closingType", JString(e.closingType.getClass.getSimpleName)) + JField("closingType", JString(e.closingType.getClass.getSimpleName)), + JField("closingTxId", JString(e.closingTxId.value.toHex)), ) }) 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 51fbdd8840..0c725edc27 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 @@ -108,10 +108,10 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu val channelReady = bob2alice.expectMsgType[ChannelReady] assert(channelReady.alias_opt.contains(bobIds.localAlias)) val listener = TestProbe() - alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelOpened]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelReadyForPayments]) bob2alice.forward(alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.remoteFundingStatus == RemoteFundingStatus.Locked) - listener.expectMsg(ChannelOpened(alice, bob.underlyingActor.nodeParams.nodeId, channelId(alice))) + assert(listener.expectMsgType[ChannelReadyForPayments].channelId == channelId(alice)) val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) @@ -156,9 +156,9 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu val channelReady = bob2alice.expectMsgType[ChannelReady] assert(channelReady.alias_opt.contains(bobIds.localAlias)) val listener = TestProbe() - alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelOpened]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelReadyForPayments]) bob2alice.forward(alice) - listener.expectMsg(ChannelOpened(alice, bob.underlyingActor.nodeParams.nodeId, channelId(alice))) + assert(listener.expectMsgType[ChannelReadyForPayments].channelId == channelId(alice)) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.remoteFundingStatus == RemoteFundingStatus.Locked) val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala index 25a7022584..c12210112e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala @@ -114,18 +114,18 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF bob.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(alice.underlyingActor.nodeParams.nodeId, RelayFees(25 msat, 90)) val listenerA = TestProbe() - alice.underlying.system.eventStream.subscribe(listenerA.ref, classOf[ChannelOpened]) + alice.underlying.system.eventStream.subscribe(listenerA.ref, classOf[ChannelReadyForPayments]) val listenerB = TestProbe() - bob.underlying.system.eventStream.subscribe(listenerB.ref, classOf[ChannelOpened]) + bob.underlying.system.eventStream.subscribe(listenerB.ref, classOf[ChannelReadyForPayments]) val aliceChannelReady = alice2bob.expectMsgType[ChannelReady] alice2bob.forward(bob, aliceChannelReady) - listenerB.expectMsg(ChannelOpened(bob, alice.underlyingActor.nodeParams.nodeId, channelId(bob))) + assert(listenerB.expectMsgType[ChannelReadyForPayments].channelId == channelId(bob)) awaitCond(bob.stateName == NORMAL) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.remoteFundingStatus == RemoteFundingStatus.Locked) val bobChannelReady = bob2alice.expectMsgType[ChannelReady] bob2alice.forward(alice, bobChannelReady) - listenerA.expectMsg(ChannelOpened(alice, bob.underlyingActor.nodeParams.nodeId, channelId(alice))) + assert(listenerA.expectMsgType[ChannelReadyForPayments].channelId == channelId(alice)) awaitCond(alice.stateName == NORMAL) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.remoteFundingStatus == RemoteFundingStatus.Locked) @@ -172,18 +172,18 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF import f._ val listenerA = TestProbe() - alice.underlying.system.eventStream.subscribe(listenerA.ref, classOf[ChannelOpened]) + alice.underlying.system.eventStream.subscribe(listenerA.ref, classOf[ChannelReadyForPayments]) val listenerB = TestProbe() - bob.underlying.system.eventStream.subscribe(listenerB.ref, classOf[ChannelOpened]) + bob.underlying.system.eventStream.subscribe(listenerB.ref, classOf[ChannelReadyForPayments]) val aliceChannelReady = alice2bob.expectMsgType[ChannelReady] alice2bob.forward(bob, aliceChannelReady) - listenerB.expectMsg(ChannelOpened(bob, alice.underlyingActor.nodeParams.nodeId, channelId(bob))) + assert(listenerB.expectMsgType[ChannelReadyForPayments].channelId == channelId(bob)) awaitCond(bob.stateName == NORMAL) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.remoteFundingStatus == RemoteFundingStatus.Locked) val bobChannelReady = bob2alice.expectMsgType[ChannelReady] bob2alice.forward(alice, bobChannelReady) - listenerA.expectMsg(ChannelOpened(alice, bob.underlyingActor.nodeParams.nodeId, channelId(alice))) + assert(listenerA.expectMsgType[ChannelReadyForPayments].channelId == channelId(alice)) awaitCond(alice.stateName == NORMAL) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.remoteFundingStatus == RemoteFundingStatus.Locked) 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 0fabf79b01..94a095ec8a 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 @@ -19,6 +19,7 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} +import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel._ @@ -75,7 +76,7 @@ class AuditDbSpec extends AnyFunSuite { val e5 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84100 msat, randomKey().publicKey, pp5a :: pp5b :: Nil, None) val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = now + 10.minutes) val e6 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, randomKey().publicKey, pp6 :: Nil, None) - val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, 456123000 sat, isChannelOpener = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null))) + val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, randomTxId(), 456123000 sat, isChannelOpener = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null))) val e10 = TrampolinePaymentRelayed(randomBytes32(), Seq( PaymentRelayed.IncomingPart(20000 msat, randomBytes32(), now - 7.seconds), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/IncomingConnectionsTrackerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/IncomingConnectionsTrackerSpec.scala index 0943bff037..8efa6bd815 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/IncomingConnectionsTrackerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/IncomingConnectionsTrackerSpec.scala @@ -23,7 +23,8 @@ import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.Crypto import fr.acinq.eclair.TestConstants.Alice.nodeParams -import fr.acinq.eclair.channel.ChannelOpened +import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.channel.ChannelReadyForPayments import fr.acinq.eclair.io.Peer.Disconnect import fr.acinq.eclair.{randomBytes32, randomKey} import org.scalatest.Outcome @@ -96,7 +97,7 @@ class IncomingConnectionsTrackerSpec extends ScalaTestWithActorTestKit(ConfigFac } // Untrack a node when a channel with it is confirmed on-chain. - system.eventStream ! EventStream.Publish(ChannelOpened(system.deadLetters.toClassic, connection1, randomBytes32())) + system.eventStream ! EventStream.Publish(ChannelReadyForPayments(system.deadLetters.toClassic, connection1, randomBytes32(), randomTxId(), 0)) eventually { tracker ! IncomingConnectionsTracker.CountIncomingConnections(probe.ref) probe.expectMessage(1) 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 b1b794ba60..af9a1630bc 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 @@ -24,6 +24,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants._ +import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{CurrentFeerates, DummyOnChainWallet} @@ -752,7 +753,7 @@ class PeerSpec extends FixtureSpec { val peerConnection1 = peerConnection nodeParams.db.peers.updateStorage(remoteNodeId, hex"abcdef") connect(remoteNodeId, peer, peerConnection1, switchboard, channels = Set(channel), peerStorage = Some(hex"abcdef")) - peer ! ChannelReadyForPayments(ActorRef.noSender, channel.remoteNodeId, channel.channelId, channel.commitments.latest.fundingTxIndex) + peer ! ChannelReadyForPayments(ActorRef.noSender, channel.remoteNodeId, channel.channelId, channel.commitments.latest.fundingTxId, channel.commitments.latest.fundingTxIndex) peerConnection1.send(peer, PeerStorageStore(hex"deadbeef")) peerConnection1.send(peer, PeerStorageStore(hex"0123456789")) @@ -798,7 +799,7 @@ class PeerSpec extends FixtureSpec { // A channel is created, so we update the remote features in our DB. // We don't update the address because this was an incoming connection. - peer ! ChannelReadyForPayments(ActorRef.noSender, remoteNodeId, randomBytes32(), 0) + peer ! ChannelReadyForPayments(ActorRef.noSender, remoteNodeId, randomBytes32(), randomTxId(), 0) probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) assert(probe.expectMsgType[Peer.PeerInfo].features.contains(remoteFeatures2)) assert(nodeParams.db.peers.getPeer(remoteNodeId).contains(nodeInfo1.copy(features = remoteFeatures2))) @@ -827,7 +828,7 @@ class PeerSpec extends FixtureSpec { // The channel confirms, so we store the remote features in our DB. // We don't store the remote address because this was an incoming connection. - peer ! ChannelReadyForPayments(ActorRef.noSender, remoteNodeId, randomBytes32(), 0) + peer ! ChannelReadyForPayments(ActorRef.noSender, remoteNodeId, randomBytes32(), randomTxId(), 0) probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) assert(probe.expectMsgType[Peer.PeerInfo].state == Peer.DISCONNECTED) assert(nodeParams.db.peers.getPeer(remoteNodeId).contains(NodeInfo(remoteInit.features, None))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala index 713b399db9..2a60cd78c2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala @@ -22,6 +22,7 @@ import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong, Transaction, TxId, TxOut} +import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair.channel._ import fr.acinq.eclair.io.PendingChannelsRateLimiter.filterPendingChannels import fr.acinq.eclair.router.Router.{GetNode, PublicNode, UnknownNode} @@ -188,8 +189,8 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac system.eventStream ! Publish(ChannelIdAssigned(null, randomKey().publicKey, channelIdPrivate1, newChannelIdPrivate1)) // ignore confirm/close/abort events for channels not tracked for a public peer - system.eventStream ! Publish(ChannelOpened(null, peerAtLimit1, newChannelId1)) - system.eventStream ! Publish(ChannelClosed(null, channelIdAtLimit1, null, commitments(peerBelowLimit1, randomBytes32()))) + system.eventStream ! Publish(ChannelReadyForPayments(null, peerAtLimit1, newChannelId1, randomTxId(), 0)) + system.eventStream ! Publish(ChannelClosed(null, channelIdAtLimit1, null, randomTxId(), commitments(peerBelowLimit1, randomBytes32()))) system.eventStream ! Publish(ChannelAborted(null, peerBelowLimit2, randomBytes32())) // after channel events for untracked channels, new channel requests for public peers are still rejected @@ -202,8 +203,8 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac // stop tracking channels that are confirmed/closed/aborted for a public peer limiter ! PendingChannelsRateLimiter.CountOpenChannelRequests(requests.ref, publicPeers = true) val pendingChannels = requests.expectMessageType[Int] - system.eventStream ! Publish(ChannelOpened(null, peerAtLimit1, channelIdAtLimit1)) - system.eventStream ! Publish(ChannelClosed(null, newChannelId1, null, commitments(peerBelowLimit1, newChannelId1))) + system.eventStream ! Publish(ChannelReadyForPayments(null, peerAtLimit1, channelIdAtLimit1, randomTxId(), 0)) + system.eventStream ! Publish(ChannelClosed(null, newChannelId1, null, randomTxId(), commitments(peerBelowLimit1, newChannelId1))) system.eventStream ! Publish(ChannelAborted(null, peerBelowLimit2, newChannelId2)) eventually { limiter ! PendingChannelsRateLimiter.CountOpenChannelRequests(requests.ref, publicPeers = true) @@ -260,8 +261,8 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac system.eventStream ! Publish(ChannelIdAssigned(null, peerBelowLimit2, channelIdBelowLimit2, newChannelId2)) // ignore confirm/close/abort events for node/channel pairs not tracked for a private peer - system.eventStream ! Publish(ChannelOpened(null, privatePeer1, newChannelId1)) - system.eventStream ! Publish(ChannelClosed(null, newChannelId1, null, commitments(privatePeer2, newChannelId1))) + system.eventStream ! Publish(ChannelReadyForPayments(null, privatePeer1, newChannelId1, randomTxId(), 0)) + system.eventStream ! Publish(ChannelClosed(null, newChannelId1, null, randomTxId(), commitments(privatePeer2, newChannelId1))) system.eventStream ! Publish(ChannelAborted(null, peerBelowLimit2, newChannelIdPrivate1)) // after channel events for untracked channels, new channel requests for private peers are still rejected @@ -272,8 +273,8 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac // stop tracking channels that are confirmed/closed/aborted for a private peer limiter ! PendingChannelsRateLimiter.CountOpenChannelRequests(requests.ref, publicPeers = false) requests.expectMessage(2) - system.eventStream ! Publish(ChannelOpened(null, privatePeer1, newChannelIdPrivate1)) - system.eventStream ! Publish(ChannelClosed(null, channelIdPrivate2, null, commitments(privatePeer2, channelIdPrivate2))) + system.eventStream ! Publish(ChannelReadyForPayments(null, privatePeer1, newChannelIdPrivate1, randomTxId(), 0)) + system.eventStream ! Publish(ChannelClosed(null, channelIdPrivate2, null, randomTxId(), commitments(privatePeer2, channelIdPrivate2))) eventually { limiter ! PendingChannelsRateLimiter.CountOpenChannelRequests(requests.ref, publicPeers = false) requests.expectMessage(0) @@ -384,7 +385,7 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac // when the first pending channel request is confirmed, the first tracked private channel is removed // AND the peer becomes public, but still has a tracked channel request as a private node - system.eventStream ! Publish(ChannelOpened(null, peer, channelId1)) + system.eventStream ! Publish(ChannelReadyForPayments(null, peer, channelId1, randomTxId(), 0)) eventually { limiter ! PendingChannelsRateLimiter.CountOpenChannelRequests(requests.ref, publicPeers = false) requests.expectMessage(1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala index 124d8ffc13..4b77e96e7f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala @@ -31,7 +31,6 @@ import fr.acinq.eclair.crypto.keymanager.ChannelKeys import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.io.PendingChannelsRateLimiter.AddOrRejectChannel import fr.acinq.eclair.io.{Peer, PeerConnection, PendingChannelsRateLimiter} -import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilli, ToMilliSatoshiConversion, UInt64, randomBytes, randomBytes32, randomKey, randomLong} @@ -65,7 +64,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { case class FixtureParam(nodeParams: NodeParams, remoteNodeId: PublicKey, peer: TestFSMRef[Peer.State, Peer.Data, Peer], peerConnection: TestProbe, channel: TestProbe, register: TestProbe, rateLimiter: TestProbe, probe: TestProbe) { // Shared secrets used for the outgoing will_add_htlc onion. - val onionSharedSecrets = Sphinx.SharedSecret(randomBytes32(), remoteNodeId) :: Nil + val onionSharedSecrets: Seq[Sphinx.SharedSecret] = Sphinx.SharedSecret(randomBytes32(), remoteNodeId) :: Nil def connect(peer: TestFSMRef[Peer.State, Peer.Data, Peer], remoteInit: protocol.Init = protocol.Init(remoteFeatures.initFeatures()), channelCount: Int = 0): Unit = { val localInit = protocol.Init(nodeParams.features.initFeatures()) @@ -825,7 +824,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // Once the channel is ready to relay payments, we forward HTLCs matching the proposed will_add_htlc. // We have two distinct payment hashes that are relayed independently. val channelData = makeChannelData(htlcMinimum) - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, fundingTxIndex = 0) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, purchase.txId, fundingTxIndex = 0) val channelInfo = Seq( channel.expectMsgType[CMD_GET_CHANNEL_INFO], channel.expectMsgType[CMD_GET_CHANNEL_INFO], @@ -892,9 +891,9 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // Once the splice with the right funding index is locked, we forward HTLCs matching the proposed will_add_htlc. val channelData = makeChannelData(htlcMinimum) - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, fundingTxIndex = 4) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, randomTxId(), fundingTxIndex = 4) channel.expectNoMessage(100 millis) - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, fundingTxIndex = 5) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, purchase.txId, fundingTxIndex = 5) channel.expectMsgType[CMD_GET_CHANNEL_INFO].replyTo ! RES_GET_CHANNEL_INFO(remoteNodeId, channelId, channel.ref, NORMAL, channelData) val adds1 = Seq( channel.expectMsgType[CMD_ADD_HTLC], @@ -921,7 +920,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { register.expectNoMessage(100 millis) // When the next splice completes, we retry the payment. - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, fundingTxIndex = 6) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, randomTxId(), fundingTxIndex = 6) channel.expectMsgType[CMD_GET_CHANNEL_INFO].replyTo ! RES_GET_CHANNEL_INFO(remoteNodeId, channelId, channel.ref, NORMAL, channelData) val adds2 = Seq( channel.expectMsgType[CMD_ADD_HTLC], @@ -950,7 +949,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { awaitCond(nodeParams.db.liquidity.listPendingOnTheFlyFunding(remoteNodeId).isEmpty, interval = 100 millis) // We don't retry anymore. - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, fundingTxIndex = 7) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, randomTxId(), fundingTxIndex = 7) channel.expectNoMessage(100 millis) } @@ -966,7 +965,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { proposeFunding(50_000_000 msat, expiryOut, paymentHash, upstream) val fees = LiquidityAds.Fees(1000 sat, 1000 sat) val purchase = signLiquidityPurchase(200_000 sat, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(paymentHash :: Nil), channelId, fees, fundingTxIndex = 1) - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, fundingTxIndex = 1) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, purchase.txId, fundingTxIndex = 1) val channelData1 = makeChannelData() channel.expectMsgType[CMD_GET_CHANNEL_INFO].replyTo ! RES_GET_CHANNEL_INFO(remoteNodeId, channelId, channel.ref, NORMAL, channelData1) // We don't collect additional fees if they were paid from our peer's channel balance already. @@ -979,7 +978,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // We disconnect: on reconnection, we don't attempt the payment again since it's already pending. disconnect(channelCount = 1) connect(peer, channelCount = 1) - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, fundingTxIndex = 1) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, purchase.txId, fundingTxIndex = 1) channel.expectNoMessage(100 millis) register.expectNoMessage(100 millis) @@ -987,14 +986,14 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val peerAfterRestart = TestFSMRef(new Peer(nodeParams, remoteNodeId, new DummyOnChainWallet(), FakeChannelFactory(remoteNodeId, channel), TestProbe().ref, register.ref, TestProbe().ref, TestProbe().ref)) peerAfterRestart ! Peer.Init(Set.empty, nodeParams.db.liquidity.listPendingOnTheFlyFunding(remoteNodeId)) connect(peerAfterRestart) - peerAfterRestart ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, fundingTxIndex = 1) + peerAfterRestart ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, purchase.txId, fundingTxIndex = 1) val channelData2 = makeChannelData(localChanges = LocalChanges(Nil, htlc :: Nil, Nil)) channel.expectMsgType[CMD_GET_CHANNEL_INFO].replyTo ! RES_GET_CHANNEL_INFO(remoteNodeId, channelId, channel.ref, NORMAL, channelData2) channel.expectNoMessage(100 millis) // The payment is failed by our peer but we don't see it (it's a cold origin): we attempt it again. val channelData3 = makeChannelData() - peerAfterRestart ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, fundingTxIndex = 1) + peerAfterRestart ! ChannelReadyForPayments(channel.ref, remoteNodeId, channelId, purchase.txId, fundingTxIndex = 1) channel.expectMsgType[CMD_GET_CHANNEL_INFO].replyTo ! RES_GET_CHANNEL_INFO(remoteNodeId, channelId, channel.ref, NORMAL, channelData3) val cmd2 = channel.expectMsgType[CMD_ADD_HTLC] cmd2.replyTo ! RES_SUCCESS(cmd2, purchase.channelId) @@ -1035,7 +1034,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // Once the channel is ready to relay payments, we forward the remaining HTLC. // We collect the liquidity fees that weren't paid by the fee credit. val channelData = makeChannelData() - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, fundingTxIndex = 0) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, purchase.txId, fundingTxIndex = 0) channel.expectMsgType[CMD_GET_CHANNEL_INFO].replyTo ! RES_GET_CHANNEL_INFO(remoteNodeId, purchase.channelId, channel.ref, NORMAL, channelData) val cmd = channel.expectMsgType[CMD_ADD_HTLC] assert(cmd.amount == 10_000_000.msat) @@ -1064,7 +1063,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val purchase = signLiquidityPurchase(200_000 sat, LiquidityAds.PaymentDetails.FromFutureHtlc(List(paymentHash)), fees = fees) // Once the channel is ready to relay payments, we forward HTLCs matching the proposed will_add_htlc. - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, fundingTxIndex = 0) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, purchase.txId, fundingTxIndex = 0) channel.expectMsgType[CMD_GET_CHANNEL_INFO] // Our peer rejects the HTLC, so we automatically disable from_future_htlc. @@ -1107,14 +1106,14 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // We've fulfilled the upstream HTLCs, so we're earning more than our expected fees. val purchase = signLiquidityPurchase(requestFunding.requestedAmount, requestFunding.paymentDetails, fees = requestFunding.fees(open.fundingFeerate, isChannelCreation = true)) awaitCond(nodeParams.db.liquidity.getFeeCredit(remoteNodeId) == 0.msat, interval = 100 millis) - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, fundingTxIndex = 0) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, purchase.txId, fundingTxIndex = 0) channel.expectNoMessage(100 millis) // We don't relay the payment on reconnection either. disconnect(channelCount = 1) connect(peer, protocol.Init(remoteFeaturesWithFeeCredit.initFeatures())) assert(peerConnection.expectMsgType[CurrentFeeCredit].amount == 0.msat) - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, fundingTxIndex = 0) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, purchase.txId, fundingTxIndex = 0) channel.expectNoMessage(100 millis) peerConnection.expectNoMessage(100 millis) } @@ -1129,7 +1128,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val purchase = signLiquidityPurchase(200_000 sat, LiquidityAds.PaymentDetails.FromFutureHtlc(List(paymentHash))) // We're too close the HTLC expiry to relay it. - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, fundingTxIndex = 0) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, purchase.txId, fundingTxIndex = 0) channel.expectNoMessage(100 millis) peer ! CurrentBlockHeight(BlockHeight(TestConstants.defaultBlockHeight)) val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] @@ -1150,7 +1149,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // We've already relayed that payment and have the matching preimage in our DB. // We don't relay it again to avoid paying our peer twice. nodeParams.db.liquidity.addOnTheFlyFundingPreimage(preimage) - peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, fundingTxIndex = 0) + peer ! ChannelReadyForPayments(channel.ref, remoteNodeId, purchase.channelId, purchase.txId, fundingTxIndex = 0) channel.expectMsgType[CMD_GET_CHANNEL_INFO].replyTo ! RES_GET_CHANNEL_INFO(remoteNodeId, purchase.channelId, channel.ref, NORMAL, makeChannelData()) channel.expectNoMessage(100 millis) @@ -1253,11 +1252,11 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { object OnTheFlyFundingSpec { - val expiryIn = CltvExpiry(TestConstants.defaultBlockHeight + 750) - val expiryOut = CltvExpiry(TestConstants.defaultBlockHeight + 500) + val expiryIn: CltvExpiry = CltvExpiry(TestConstants.defaultBlockHeight + 750) + val expiryOut: CltvExpiry = CltvExpiry(TestConstants.defaultBlockHeight + 500) - val preimage = randomBytes32() - val paymentHash = Crypto.sha256(preimage) + val preimage: ByteVector32 = randomBytes32() + val paymentHash: ByteVector32 = Crypto.sha256(preimage) def randomOnion(): OnionRoutingPacket = OnionRoutingPacket(0, randomKey().publicKey.value, randomBytes(1300), randomBytes32()) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/WebSocket.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/WebSocket.scala index 120e2706ab..1698e67dd9 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/WebSocket.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/WebSocket.scala @@ -52,7 +52,8 @@ trait WebSocket { override def preStart(): Unit = { context.system.eventStream.subscribe(self, classOf[PaymentEvent]) context.system.eventStream.subscribe(self, classOf[ChannelCreated]) - context.system.eventStream.subscribe(self, classOf[ChannelOpened]) + context.system.eventStream.subscribe(self, classOf[ChannelFundingConfirmed]) + context.system.eventStream.subscribe(self, classOf[ChannelReadyForPayments]) context.system.eventStream.subscribe(self, classOf[ChannelStateChanged]) context.system.eventStream.subscribe(self, classOf[ChannelClosed]) context.system.eventStream.subscribe(self, classOf[OnionMessages.ReceiveMessage]) @@ -61,7 +62,8 @@ trait WebSocket { def receive: Receive = { case message: PaymentEvent => flowInput.offer(serialization.write(message)) case message: ChannelCreated => flowInput.offer(serialization.write(message)) - case message: ChannelOpened => flowInput.offer(serialization.write(message)) + case message: ChannelFundingConfirmed => flowInput.offer(serialization.write(message)) + case message: ChannelReadyForPayments => flowInput.offer(serialization.write(message)) case message: ChannelStateChanged => if (message.previousState != WAIT_FOR_INIT_INTERNAL) { flowInput.offer(serialization.write(message)) 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 da1ae28874..f71e03e1d4 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 @@ -1115,6 +1115,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("the websocket should return typed objects") { val mockService = new MockService(mock[Eclair]) val fixedUUID = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") + val fundingTxId = TxId.fromValidHex("9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692") val wsClient = WSProbe() WS("/ws", wsClient.flow) ~> @@ -1163,11 +1164,17 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM system.eventStream.publish(chcr) wsClient.expectMessage(expectedSerializedChcr) - val chop = ChannelOpened(system.deadLetters, bobNodeId, ByteVector32.One) - val expectedSerializedChop = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","channelId":"0100000000000000000000000000000000000000000000000000000000000000"}""" - assert(serialization.write(chop) == expectedSerializedChop) - system.eventStream.publish(chop) - wsClient.expectMessage(expectedSerializedChop) + val chfc = ChannelFundingConfirmed(system.deadLetters, ByteVector32.One, bobNodeId, fundingTxId, 0, BlockHeight(900000), null) + val expectedSerializedChfc = """{"type":"channel-confirmed","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","channelId":"0100000000000000000000000000000000000000000000000000000000000000","fundingTxId":"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692","fundingTxIndex":0,"blockHeight":900000}""" + assert(serialization.write(chfc) == expectedSerializedChfc) + system.eventStream.publish(chfc) + wsClient.expectMessage(expectedSerializedChfc) + + val chrp = ChannelReadyForPayments(system.deadLetters, bobNodeId, ByteVector32.One, fundingTxId, 1) + val expectedSerializedChrp = """{"type":"channel-ready","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","channelId":"0100000000000000000000000000000000000000000000000000000000000000","fundingTxId":"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692","fundingTxIndex":1}""" + assert(serialization.write(chrp) == expectedSerializedChrp) + system.eventStream.publish(chrp) + wsClient.expectMessage(expectedSerializedChrp) val chsc = ChannelStateChanged(system.deadLetters, ByteVector32.One, system.deadLetters, bobNodeId, OFFLINE, NORMAL, null) val expectedSerializedChsc = """{"type":"channel-state-changed","channelId":"0100000000000000000000000000000000000000000000000000000000000000","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","previousState":"OFFLINE","currentState":"NORMAL"}""" @@ -1175,8 +1182,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM system.eventStream.publish(chsc) wsClient.expectMessage(expectedSerializedChsc) - val chcl = ChannelClosed(system.deadLetters, ByteVector32.One, Closing.NextRemoteClose(null, null), null) - val expectedSerializedChcl = """{"type":"channel-closed","channelId":"0100000000000000000000000000000000000000000000000000000000000000","closingType":"NextRemoteClose"}""" + val chcl = ChannelClosed(system.deadLetters, ByteVector32.One, Closing.NextRemoteClose(null, null), fundingTxId, null) + val expectedSerializedChcl = """{"type":"channel-closed","channelId":"0100000000000000000000000000000000000000000000000000000000000000","closingType":"NextRemoteClose","closingTxId":"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692"}""" assert(serialization.write(chcl) == expectedSerializedChcl) system.eventStream.publish(chcl) wsClient.expectMessage(expectedSerializedChcl)