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
6 changes: 5 additions & 1 deletion docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ Eclair emits several events during a channel lifecycle, which can be received by
We reworked these events to be compatible with splicing and consistent with 0-conf:

- we removed the `channel-opened` event
- we introduced a `channel-funding-created` event
- we introduced a `channel-confirmed` event
- we introduced a `channel-ready` event

The `channel-funding-created` event is emitted when the funding transaction or a splice transaction has been signed and can be published.
Listeners can use the `fundingTxIndex` to detect whether this is the initial channel funding (`fundingTxIndex = 0`) or a splice (`fundingTxIndex > 0`).

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`).

Expand Down Expand Up @@ -46,7 +50,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)
- `channel-opened` was removed from the websocket in favor of `channel-funding-created`, `channel-confirmed` and `channel-ready` (#3237 and #3256)

### Miscellaneous improvements and bug fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,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 is sent once a funding transaction (channel creation or splice) is ready to be published. */
case class ChannelFundingCreated(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, fundingTx: Either[TxId, Transaction], fundingTxIndex: Long, commitments: Commitments) extends ChannelEvent {
val fundingTxId: TxId = fundingTx.fold(txId => txId, tx => tx.txid)
}

/** 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val minDepth_opt = d.commitments.channelParams.minDepth(nodeParams.channelConf.minDepth)
watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
val commitments1 = d.commitments.add(signingSession1.commitment)
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(signingSession1.fundingTx.signedTx_opt.getOrElse(signingSession1.fundingTx.sharedTx.tx.buildUnsignedTx())), signingSession1.commitment.fundingTxIndex, commitments1))
val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice)
stay() using d1 storing() sending signingSession1.localSigs calling endQuiescence(d1)
}
Expand Down Expand Up @@ -1457,6 +1458,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val minDepth_opt = d.commitments.channelParams.minDepth(nodeParams.channelConf.minDepth)
watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
val commitments1 = d.commitments.add(signingSession1.commitment)
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(signingSession1.fundingTx.signedTx_opt.getOrElse(signingSession1.fundingTx.sharedTx.tx.buildUnsignedTx())), signingSession1.commitment.fundingTxIndex, commitments1))
val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice)
log.info("publishing funding tx for channelId={} fundingTxId={}", d.channelId, signingSession1.fundingTx.sharedTx.txId)
Metrics.recordSplice(signingSession1.fundingTx.fundingParams, signingSession1.fundingTx.sharedTx.tx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
remotePerCommitmentSecrets = ShaChain.init,
originChannels = Map.empty
)
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(signingSession1.fundingTx.signedTx_opt.getOrElse(signingSession1.fundingTx.sharedTx.tx.buildUnsignedTx())), signingSession1.commitment.fundingTxIndex, commitments))
val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, d.localPushAmount, d.remotePushAmount, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, DualFundingStatus.WaitingForConfirmations, None)
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d1 storing() sending signingSession1.localSigs
}
Expand All @@ -406,14 +407,15 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
remotePerCommitmentSecrets = ShaChain.init,
originChannels = Map.empty
)
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(signingSession.fundingTx.signedTx_opt.getOrElse(signingSession.fundingTx.sharedTx.tx.buildUnsignedTx())), signingSession.commitment.fundingTxIndex, commitments))
val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, d.localPushAmount, d.remotePushAmount, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, DualFundingStatus.WaitingForConfirmations, None)
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d1 storing() sending signingSession.localSigs calling publishFundingTx(signingSession.fundingTx)
}
case msg: TxAbort =>
log.info("our peer aborted the dual funding flow: ascii='{}' bin={}", msg.toAscii, msg.data)
rollbackFundingAttempt(d.signingSession.fundingTx.tx, Nil)
d.signingSession.liquidityPurchase_opt.collect {
case purchase if !d.signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseAborted(d.channelId, d.signingSession.fundingTx.txId, d.signingSession.fundingTxIndex)
case _ if !d.signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseAborted(d.channelId, d.signingSession.fundingTx.txId, d.signingSession.fundingTxIndex)
}
goto(CLOSED) using IgnoreClosedData(d) sending TxAbort(d.channelId, DualFundingAborted(d.channelId).getMessage)
case msg: InteractiveTxConstructionMessage =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
txPublisher ! SetChannelId(remoteNodeId, channelId)
context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
context.system.eventStream.publish(ChannelFundingCreated(self, channelId, remoteNodeId, Left(commitment.fundingTxId), commitment.fundingTxIndex, commitments))
// NB: we don't send a ChannelSignatureSent for the first commit
log.info("waiting for them to publish the funding tx for channelId={} fundingTxid={}", channelId, commitment.fundingTxId)
watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None)
Expand Down Expand Up @@ -391,6 +392,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
originChannels = Map.empty)
val blockHeight = nodeParams.currentBlockHeight
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
context.system.eventStream.publish(ChannelFundingCreated(self, d.channelId, remoteNodeId, Right(d.fundingTx), commitment.fundingTxIndex, commitments))
log.info("publishing funding tx fundingTxId={}", commitment.fundingTxId)
watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None)
// we will publish the funding tx only after the channel state has been written to disk because we want to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,13 @@ object ChannelEventSerializer extends MinimalSerializer({
JField("commitTxFeeratePerKw", JLong(e.commitTxFeerate.toLong)),
JField("fundingTxFeeratePerKw", e.fundingTxFeerate.map(f => JLong(f.toLong)).getOrElse(JNothing))
)
case e: ChannelFundingCreated => JObject(
JField("type", JString("channel-funding-created")),
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: ChannelFundingConfirmed => JObject(
JField("type", JString("channel-confirmed")),
JField("remoteNodeId", JString(e.remoteNodeId.toString())),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,31 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
test("complete interactive-tx protocol", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._

val listener = TestProbe()
alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
val listenerA = TestProbe()
alice.underlyingActor.context.system.eventStream.subscribe(listenerA.ref, classOf[TransactionPublished])
alice.underlyingActor.context.system.eventStream.subscribe(listenerA.ref, classOf[ChannelFundingCreated])
val listenerB = TestProbe()
bob.underlyingActor.context.system.eventStream.subscribe(listenerB.ref, classOf[ChannelFundingCreated])

bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
alice2bob.expectMsgType[CommitSig]
alice2bob.forward(bob)

// Bob sends its signatures first as he contributed less than Alice.
bob2alice.expectMsgType[TxSignatures]
bob2alice.expectMsgType[TxSignatures].txId
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED]
assert(bobData.commitments.channelParams.channelFeatures.hasFeature(Features.DualFunding))
assert(bobData.latestFundingTx.sharedTx.isInstanceOf[PartiallySignedSharedTransaction])
val fundingTxId = bobData.latestFundingTx.sharedTx.asInstanceOf[PartiallySignedSharedTransaction].txId
assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTxId)
assert(listenerB.expectMsgType[ChannelFundingCreated].fundingTxId == fundingTxId)

// Alice receives Bob's signatures and sends her own signatures.
bob2alice.forward(alice)
assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTxId)
assert(listenerA.expectMsgType[ChannelFundingCreated].fundingTxId == fundingTxId)
assert(listenerA.expectMsgType[TransactionPublished].tx.txid == fundingTxId)
assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTxId)
alice2bob.expectMsgType[TxSignatures]
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun

test("recv FundingCreated") { f =>
import f._
bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelFundingCreated])
alice2bob.expectMsgType[FundingCreated]
alice2bob.forward(bob)
awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED)
bob2alice.expectMsgType[FundingSigned]
bob2blockchain.expectMsgType[TxPublisher.SetChannelId]
val watchConfirmed = bob2blockchain.expectMsgType[WatchFundingConfirmed]
assert(watchConfirmed.minDepth == Bob.nodeParams.channelConf.minDepth)
val fundingCreated = listener.expectMsgType[ChannelFundingCreated]
assert(fundingCreated.fundingTx == Left(watchConfirmed.txId))
}

test("recv FundingCreated (funder can't pay fees)", Tag(FunderBelowCommitFees)) { f =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,16 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS
import f._
val listener = TestProbe()
alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished])
alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelFundingCreated])
bob2alice.expectMsgType[FundingSigned]
bob2alice.forward(alice)
awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)
val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed]
val fundingTxId = watchConfirmed.txId
assert(watchConfirmed.minDepth == 6)
val fundingCreated = listener.expectMsgType[ChannelFundingCreated]
assert(fundingCreated.fundingTx.isRight)
assert(fundingCreated.fundingTx.toOption.map(_.txid).contains(fundingTxId))
val txPublished = listener.expectMsgType[TransactionPublished]
assert(txPublished.tx.txid == fundingTxId)
assert(txPublished.miningFee > 0.sat)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
systemA.eventStream.subscribe(aliceEvents.ref, classOf[AvailableBalanceChanged])
systemA.eventStream.subscribe(aliceEvents.ref, classOf[LocalChannelUpdate])
systemA.eventStream.subscribe(aliceEvents.ref, classOf[LocalChannelDown])
systemA.eventStream.subscribe(aliceEvents.ref, classOf[ChannelFundingCreated])
systemB.eventStream.subscribe(bobEvents.ref, classOf[ForgetHtlcInfos])
systemB.eventStream.subscribe(bobEvents.ref, classOf[AvailableBalanceChanged])
systemB.eventStream.subscribe(bobEvents.ref, classOf[LocalChannelUpdate])
Expand All @@ -1402,6 +1403,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
val fundingInput = alice.commitments.latest.fundingInput
val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)))
checkWatchConfirmed(f, fundingTx1)
inside(aliceEvents.expectMsgType[ChannelFundingCreated]) { e =>
assert(e.fundingTx == Right(fundingTx1))
assert(e.fundingTxIndex == 1)
}

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

val fundingTx2 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)))
checkWatchConfirmed(f, fundingTx2)
inside(aliceEvents.expectMsgType[ChannelFundingCreated]) { e =>
assert(e.fundingTx == Right(fundingTx2))
assert(e.fundingTxIndex == 2)
}

alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1)
alice2bob.expectMsgType[SpliceLocked]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ 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[ChannelFundingCreated])
context.system.eventStream.subscribe(self, classOf[ChannelFundingConfirmed])
context.system.eventStream.subscribe(self, classOf[ChannelReadyForPayments])
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
Expand All @@ -62,6 +63,7 @@ trait WebSocket {
def receive: Receive = {
case message: PaymentEvent => flowInput.offer(serialization.write(message))
case message: ChannelCreated => flowInput.offer(serialization.write(message))
case message: ChannelFundingCreated => flowInput.offer(serialization.write(message))
case message: ChannelFundingConfirmed => flowInput.offer(serialization.write(message))
case message: ChannelReadyForPayments => flowInput.offer(serialization.write(message))
case message: ChannelStateChanged =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,12 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
system.eventStream.publish(chcr)
wsClient.expectMessage(expectedSerializedChcr)

val chfcr = ChannelFundingCreated(system.deadLetters, ByteVector32.One, bobNodeId, Left(fundingTxId), 0, null)
val expectedSerializedChfcr = """{"type":"channel-funding-created","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","channelId":"0100000000000000000000000000000000000000000000000000000000000000","fundingTxId":"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692","fundingTxIndex":0}"""
assert(serialization.write(chfcr) == expectedSerializedChfcr)
system.eventStream.publish(chfcr)
wsClient.expectMessage(expectedSerializedChfcr)

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)
Expand Down