Skip to content

Commit 7b4c86b

Browse files
committed
Add ChannelFundingCreated event
We add a `ChannelFundingCreated` event that is emitted when the funding transaction or a splice transaction has been signed and can now be published, either by us or by our peer (depending on who funds). This can be handy to detect peers that are using black-listed inputs and immediately close the channel before it confirms and can be used.
1 parent 5f934ea commit 7b4c86b

File tree

12 files changed

+57
-6
lines changed

12 files changed

+57
-6
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ Eclair emits several events during a channel lifecycle, which can be received by
1515
We reworked these events to be compatible with splicing and consistent with 0-conf:
1616

1717
- we removed the `channel-opened` event
18+
- we introduced a `channel-funding-created` event
1819
- we introduced a `channel-confirmed` event
1920
- we introduced a `channel-ready` event
2021

22+
The `channel-funding-created` event is emitted when the funding transaction or a splice transaction has been signed and can be published.
23+
Listeners can use the `fundingTxIndex` to detect whether this is the initial channel funding (`fundingTxIndex = 0`) or a splice (`fundingTxIndex > 0`).
24+
2125
The `channel-confirmed` event is emitted when the funding transaction or a splice transaction has enough confirmations.
2226
Listeners can use the `fundingTxIndex` to detect whether this is the initial channel funding (`fundingTxIndex = 0`) or a splice (`fundingTxIndex > 0`).
2327

@@ -46,7 +50,7 @@ eclair.relay.reserved-for-accountable = 0.0
4650
### API changes
4751

4852
- `findroute`, `findroutetonode` and `findroutebetweennodes` now include a `maxCltvExpiryDelta` parameter (#3234)
49-
- `channel-opened` was removed from the websocket in favor of `channel-confirmed` and `channel-ready` (#3237)
53+
- `channel-opened` was removed from the websocket in favor of `channel-funding-created`, `channel-confirmed` and `channel-ready` (#3237 and #3256)
5054

5155
### Miscellaneous improvements and bug fixes
5256

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, an
5252
/** This event will be sent if a channel was aborted before completing the opening flow. */
5353
case class ChannelAborted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32) extends ChannelEvent
5454

55+
/** This event is sent once a funding transaction (channel creation or splice) is ready to be published. */
56+
case class ChannelFundingCreated(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, fundingTx: Either[TxId, Transaction], fundingTxIndex: Long, commitments: Commitments) extends ChannelEvent {
57+
val fundingTxId: TxId = fundingTx.fold(txId => txId, tx => tx.txid)
58+
}
59+
5560
/** This event is sent once a funding transaction (channel creation or splice) has been confirmed. */
5661
case class ChannelFundingConfirmed(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, fundingTxId: TxId, fundingTxIndex: Long, blockHeight: BlockHeight, commitments: Commitments) extends ChannelEvent
5762

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
685685
val minDepth_opt = d.commitments.channelParams.minDepth(nodeParams.channelConf.minDepth)
686686
watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
687687
val commitments1 = d.commitments.add(signingSession1.commitment)
688+
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(signingSession1.fundingTx.signedTx_opt.getOrElse(signingSession1.fundingTx.sharedTx.tx.buildUnsignedTx())), signingSession1.commitment.fundingTxIndex, commitments1))
688689
val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice)
689690
stay() using d1 storing() sending signingSession1.localSigs calling endQuiescence(d1)
690691
}
@@ -1457,6 +1458,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
14571458
val minDepth_opt = d.commitments.channelParams.minDepth(nodeParams.channelConf.minDepth)
14581459
watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
14591460
val commitments1 = d.commitments.add(signingSession1.commitment)
1461+
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(signingSession1.fundingTx.signedTx_opt.getOrElse(signingSession1.fundingTx.sharedTx.tx.buildUnsignedTx())), signingSession1.commitment.fundingTxIndex, commitments1))
14601462
val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice)
14611463
log.info("publishing funding tx for channelId={} fundingTxId={}", d.channelId, signingSession1.fundingTx.sharedTx.txId)
14621464
Metrics.recordSplice(signingSession1.fundingTx.fundingParams, signingSession1.fundingTx.sharedTx.tx)

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
383383
remotePerCommitmentSecrets = ShaChain.init,
384384
originChannels = Map.empty
385385
)
386+
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(signingSession1.fundingTx.signedTx_opt.getOrElse(signingSession1.fundingTx.sharedTx.tx.buildUnsignedTx())), signingSession1.commitment.fundingTxIndex, commitments))
386387
val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, d.localPushAmount, d.remotePushAmount, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, DualFundingStatus.WaitingForConfirmations, None)
387388
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d1 storing() sending signingSession1.localSigs
388389
}
@@ -406,14 +407,15 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
406407
remotePerCommitmentSecrets = ShaChain.init,
407408
originChannels = Map.empty
408409
)
410+
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(signingSession.fundingTx.signedTx_opt.getOrElse(signingSession.fundingTx.sharedTx.tx.buildUnsignedTx())), signingSession.commitment.fundingTxIndex, commitments))
409411
val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, d.localPushAmount, d.remotePushAmount, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, DualFundingStatus.WaitingForConfirmations, None)
410412
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d1 storing() sending signingSession.localSigs calling publishFundingTx(signingSession.fundingTx)
411413
}
412414
case msg: TxAbort =>
413415
log.info("our peer aborted the dual funding flow: ascii='{}' bin={}", msg.toAscii, msg.data)
414416
rollbackFundingAttempt(d.signingSession.fundingTx.tx, Nil)
415417
d.signingSession.liquidityPurchase_opt.collect {
416-
case purchase if !d.signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseAborted(d.channelId, d.signingSession.fundingTx.txId, d.signingSession.fundingTxIndex)
418+
case _ if !d.signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseAborted(d.channelId, d.signingSession.fundingTx.txId, d.signingSession.fundingTxIndex)
417419
}
418420
goto(CLOSED) using IgnoreClosedData(d) sending TxAbort(d.channelId, DualFundingAborted(d.channelId).getMessage)
419421
case msg: InteractiveTxConstructionMessage =>

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
334334
txPublisher ! SetChannelId(remoteNodeId, channelId)
335335
context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
336336
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
337+
context.system.eventStream.publish(ChannelFundingCreated(self, channelId, remoteNodeId, Left(commitment.fundingTxId), commitment.fundingTxIndex, commitments))
337338
// NB: we don't send a ChannelSignatureSent for the first commit
338339
log.info("waiting for them to publish the funding tx for channelId={} fundingTxid={}", channelId, commitment.fundingTxId)
339340
watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None)
@@ -391,6 +392,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
391392
originChannels = Map.empty)
392393
val blockHeight = nodeParams.currentBlockHeight
393394
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
395+
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(d.fundingTx), commitment.fundingTxIndex, commitments))
394396
log.info("publishing funding tx fundingTxId={}", commitment.fundingTxId)
395397
watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None)
396398
// we will publish the funding tx only after the channel state has been written to disk because we want to

eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,13 @@ object ChannelEventSerializer extends MinimalSerializer({
560560
JField("commitTxFeeratePerKw", JLong(e.commitTxFeerate.toLong)),
561561
JField("fundingTxFeeratePerKw", e.fundingTxFeerate.map(f => JLong(f.toLong)).getOrElse(JNothing))
562562
)
563+
case e: ChannelFundingCreated => JObject(
564+
JField("type", JString("channel-funding-created")),
565+
JField("remoteNodeId", JString(e.remoteNodeId.toString())),
566+
JField("channelId", JString(e.channelId.toHex)),
567+
JField("fundingTxId", JString(e.fundingTxId.value.toHex)),
568+
JField("fundingTxIndex", JLong(e.fundingTxIndex)),
569+
)
563570
case e: ChannelFundingConfirmed => JObject(
564571
JField("type", JString("channel-confirmed")),
565572
JField("remoteNodeId", JString(e.remoteNodeId.toString())),

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,26 +97,31 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
9797
test("complete interactive-tx protocol", Tag(ChannelStateTestsTags.DualFunding)) { f =>
9898
import f._
9999

100-
val listener = TestProbe()
101-
alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
100+
val listenerA = TestProbe()
101+
alice.underlyingActor.context.system.eventStream.subscribe(listenerA.ref, classOf[TransactionPublished])
102+
alice.underlyingActor.context.system.eventStream.subscribe(listenerA.ref, classOf[ChannelFundingCreated])
103+
val listenerB = TestProbe()
104+
bob.underlyingActor.context.system.eventStream.subscribe(listenerB.ref, classOf[ChannelFundingCreated])
102105

103106
bob2alice.expectMsgType[CommitSig]
104107
bob2alice.forward(alice)
105108
alice2bob.expectMsgType[CommitSig]
106109
alice2bob.forward(bob)
107110

108111
// Bob sends its signatures first as he contributed less than Alice.
109-
bob2alice.expectMsgType[TxSignatures]
112+
bob2alice.expectMsgType[TxSignatures].txId
110113
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
111114
val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED]
112115
assert(bobData.commitments.channelParams.channelFeatures.hasFeature(Features.DualFunding))
113116
assert(bobData.latestFundingTx.sharedTx.isInstanceOf[PartiallySignedSharedTransaction])
114117
val fundingTxId = bobData.latestFundingTx.sharedTx.asInstanceOf[PartiallySignedSharedTransaction].txId
115118
assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTxId)
119+
assert(listenerB.expectMsgType[ChannelFundingCreated].fundingTxId == fundingTxId)
116120

117121
// Alice receives Bob's signatures and sends her own signatures.
118122
bob2alice.forward(alice)
119-
assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTxId)
123+
assert(listenerA.expectMsgType[ChannelFundingCreated].fundingTxId == fundingTxId)
124+
assert(listenerA.expectMsgType[TransactionPublished].tx.txid == fundingTxId)
120125
assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTxId)
121126
alice2bob.expectMsgType[TxSignatures]
122127
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,16 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun
7070

7171
test("recv FundingCreated") { f =>
7272
import f._
73+
bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelFundingCreated])
7374
alice2bob.expectMsgType[FundingCreated]
7475
alice2bob.forward(bob)
7576
awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED)
7677
bob2alice.expectMsgType[FundingSigned]
7778
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
7879
val watchConfirmed = bob2blockchain.expectMsgType[WatchFundingConfirmed]
7980
assert(watchConfirmed.minDepth == Bob.nodeParams.channelConf.minDepth)
81+
val fundingCreated = listener.expectMsgType[ChannelFundingCreated]
82+
assert(fundingCreated.fundingTx == Left(watchConfirmed.txId))
8083
}
8184

8285
test("recv FundingCreated (funder can't pay fees)", Tag(FunderBelowCommitFees)) { f =>

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,16 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS
8080
import f._
8181
val listener = TestProbe()
8282
alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
83+
alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelFundingCreated])
8384
bob2alice.expectMsgType[FundingSigned]
8485
bob2alice.forward(alice)
8586
awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)
8687
val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed]
8788
val fundingTxId = watchConfirmed.txId
8889
assert(watchConfirmed.minDepth == 6)
90+
val fundingCreated = listener.expectMsgType[ChannelFundingCreated]
91+
assert(fundingCreated.fundingTx.isRight)
92+
assert(fundingCreated.fundingTx.toOption.map(_.txid).contains(fundingTxId))
8993
val txPublished = listener.expectMsgType[TransactionPublished]
9094
assert(txPublished.tx.txid == fundingTxId)
9195
assert(txPublished.miningFee > 0.sat)

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
13941394
systemA.eventStream.subscribe(aliceEvents.ref, classOf[AvailableBalanceChanged])
13951395
systemA.eventStream.subscribe(aliceEvents.ref, classOf[LocalChannelUpdate])
13961396
systemA.eventStream.subscribe(aliceEvents.ref, classOf[LocalChannelDown])
1397+
systemA.eventStream.subscribe(aliceEvents.ref, classOf[ChannelFundingCreated])
13971398
systemB.eventStream.subscribe(bobEvents.ref, classOf[ForgetHtlcInfos])
13981399
systemB.eventStream.subscribe(bobEvents.ref, classOf[AvailableBalanceChanged])
13991400
systemB.eventStream.subscribe(bobEvents.ref, classOf[LocalChannelUpdate])
@@ -1402,6 +1403,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
14021403
val fundingInput = alice.commitments.latest.fundingInput
14031404
val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)))
14041405
checkWatchConfirmed(f, fundingTx1)
1406+
inside(aliceEvents.expectMsgType[ChannelFundingCreated]) { e =>
1407+
assert(e.fundingTx == Right(fundingTx1))
1408+
assert(e.fundingTxIndex == 1)
1409+
}
14051410

14061411
bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1)
14071412
bob2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx1.txid)
@@ -1413,6 +1418,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
14131418

14141419
val fundingTx2 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)))
14151420
checkWatchConfirmed(f, fundingTx2)
1421+
inside(aliceEvents.expectMsgType[ChannelFundingCreated]) { e =>
1422+
assert(e.fundingTx == Right(fundingTx2))
1423+
assert(e.fundingTxIndex == 2)
1424+
}
14161425

14171426
alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1)
14181427
alice2bob.expectMsgType[SpliceLocked]

0 commit comments

Comments
 (0)