What changed, and why it matters
This commit is a routine internal refactoring of Eclair's channel-lifecycle event system. It renames and splits events (for example replacing 'channel-opened' with 'channel-confirmed' and 'channel-ready') so the software can correctly track normal channels, zero-confirmation channels, and splice transactions. It also records more details in the audit database, such as the funding transaction ID for each splice. There is no direct security vulnerability in the diff; it is a maintainability and observability improvement.
Treat as a normal feature/refactoring commit. Review release notes and update any plugins or external tools that subscribed to the old 'channel-opened' websocket event. No security patch or incident response is indicated.
Security signals we found
Event schema change only: no privilege escalation, memory safety, or consensus bug evident
Audit logging is expanded (more fields persisted), which improves traceability
One removed event (ChannelOpened) and two new events (ChannelFundingConfirmed, ChannelReadyForPayments) may require downstream consumers/plugins to update, but this is an API compatibility note, not a vulnerability
ClosingType now exposes closingTxId consistently; this is a data-quality improvement
Evidence from the diff
The patch reworks channel lifecycle events to handle 0-conf, splicing, and on-the-fly funding consistently. Key changes: ChannelOpened is removed and replaced by ChannelReadyForPayments; a new ChannelFundingConfirmed event is emitted when a funding or splice transaction reaches sufficient confirmations; ChannelReadyForPayments now carries fundingTxId and fundingTxIndex; ChannelClosed carries closingTxId via a new closingTxId field on ClosingType; the audit DB records Confirmed/Spliced events and stores fundingTxId on channel events; WebSocket/API event serialization is updated accordingly. Tests are updated to match the new event types and fields. No cryptographic, consensus, or authorization logic is changed.
Changed components
eclair-core channel event definitions (ChannelEvents.scala)eclair-core channel FSM (Channel.scala, CommonFundingHandlers.scala)Audit database event handler (DbEventHandler.scala)Incoming connection tracking (IncomingConnectionsTracker.scala)Pending channel rate limiter (PendingChannelsRateLimiter.scala)JSON/WebSocket API serializers (JsonSerializers.scala, WebSocket.scala)Related unit testsInspect captured patch +164 / −94
diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md
index d1bd1d0..8abe2c1 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 8a3b7bd..0c1dc2f 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 806b583..aa719f5 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 012d3e8..b50b4a1 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 648a0ce..f7d8062 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 b845af3..f3e10e0 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 2490e86..dad00af 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 116e70f..bc50eda 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 ff3602c..574ba68 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 44ccb6f..7dcc9d0 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 f447fab..f37d66e 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 51fbdd8..0c725ed 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 25a7022..c122101 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 0fabf79..94a095e 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 0943bff..8efa6bd 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 b1b794b..af9a163 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 713b399..2a60cd7 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 124d8ff..4b77e96 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 120e270..1698e67 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 da1ae28..f71e03e 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)
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.