Add previous commitments to ChannelFundingConfirmed event (#3303)
What changed, and why it matters
This commit adds an extra data field to an internal event notification so that subscribers can see information about older channel states that may have been removed from the current state. It is a small, informational change to an event object and does not appear to fix an active security bug.
No immediate action required. Review whether event-stream subscribers that receive previousCommitments handle the data with appropriate access controls, since it exposes historical channel state.
Security signals we found
Adds historical commitment data to an event stream message
No validation, authorization, or cryptographic logic changed
No memory-safety, input-parsing, or transaction-handling changes
Test fixture updated only to provide the new constructor argument
Evidence from the diff
The patch extends the ChannelFundingConfirmed case class with a previousCommitments field and populates it with d.commitments when a funding transaction confirms. This allows event-stream consumers to inspect prior commitment data after a splice or RBF. The change is additive and does not alter channel-state logic, validation, or cryptographic checks.
Changed components
eclair-core ChannelEvents.scalaeclair-core CommonFundingHandlers.scalaeclair-node ApiServiceSpec.scalaInspect captured patch +3 / −3
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 971e315..da43d3a 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
@@ -58,7 +58,7 @@ case class ChannelFundingCreated(channel: ActorRef, channelId: ByteVector32, rem
}
/** 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
+case class ChannelFundingConfirmed(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, fundingTxId: TxId, fundingTxIndex: Long, blockHeight: BlockHeight, commitments: Commitments, previousCommitments: Commitments) 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
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 dad00af..648b1fb 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
@@ -103,7 +103,7 @@ trait CommonFundingHandlers extends CommonHandlers {
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))
+ context.system.eventStream.publish(ChannelFundingConfirmed(self, d.channelId, remoteNodeId, w.tx.txid, c.fundingTxIndex, w.blockHeight, commitments1, d.commitments))
// 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
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 2456d87..f320559 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
@@ -1166,7 +1166,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
system.eventStream.publish(chfcr)
wsClient.expectMessage(expectedSerializedChfcr)
- val chfc = ChannelFundingConfirmed(system.deadLetters, ByteVector32.One, bobNodeId, fundingTxId, 0, BlockHeight(900000), null)
+ val chfc = ChannelFundingConfirmed(system.deadLetters, ByteVector32.One, bobNodeId, fundingTxId, 0, BlockHeight(900000), null, 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)
Why this scored 17/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.