Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
/**
Expand Down Expand Up @@ -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)

Expand Down
28 changes: 22 additions & 6 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ object Monitoring {

object Events {
val Created = "created"
val Spliced = "spliced"
val Closing = "closing"
val Closed = "closed"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}

Expand Down
24 changes: 20 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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)
Expand All @@ -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 =>
Expand Down Expand Up @@ -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" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)
}
}
Expand Down
Loading