From 69f1e0730ef778557e7107b08d4473a2e4cb3e8b Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 20 Feb 2025 18:09:02 +0100 Subject: [PATCH 1/2] Use default confirmations for single-funded channel For single-funded channels where we are the funder, we previously only waited for 1 confirmation before marking the funding transaction as confirmed. This is fine because we trust ourselves to not double-spend our own channels. However, in #2969, we started assigning the `short_channel_id` when we consider the funding transaction confirmed, and not after 6 blocks. We thus now risk creating a `short_channel_id` after only 1 confirmation, which is too early in case a reorg happens. This wouldn't cause loss of funds because we would create invalid `channel_update`s and the channel would likely not be usable. --- .../scala/fr/acinq/eclair/channel/Commitments.scala | 12 ++++++------ .../eclair/channel/fsm/ChannelOpenSingleFunded.scala | 2 +- .../eclair/channel/fsm/SingleFundingHandlers.scala | 2 +- .../states/b/WaitForFundingSignedStateSpec.scala | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 675fdd758e..9a6458bac5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -49,16 +49,16 @@ case class ChannelParams(channelId: ByteVector32, ) /** - * As funder we trust ourselves to not double spend funding txs: we could always use a zero-confirmation watch, - * but we need a scid to send the initial channel_update and remote may not provide an alias. That's why we always - * wait for one conf, except if the channel has the zero-conf feature (because presumably the peer will send an - * alias in that case). + * Returns the number of confirmations needed to safely handle a funding transaction that we unilaterally funded. + * As funder we trust ourselves to not double spend funding txs, so we don't need to scale the number of confirmations + * based on the funding amount. We want to wait a few blocks though to ensure that the short_channel_id we obtain will + * not be invalidated by a reorg. */ - def minDepthFunder: Option[Long] = { + def minDepthFunder(defaultMinDepth: Int): Option[Long] = { if (localParams.initFeatures.hasFeature(Features.ZeroConf)) { None } else { - Some(1) + Some(defaultMinDepth.toLong) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 944c6885c9..e14d949e19 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -342,7 +342,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val blockHeight = nodeParams.currentBlockHeight context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) log.info(s"publishing funding tx fundingTxid=${commitment.fundingTxId}") - watchFundingConfirmed(commitment.fundingTxId, params.minDepthFunder, delay_opt = None) + watchFundingConfirmed(commitment.fundingTxId, params.minDepthFunder(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 // make sure we first persist the commitment that returns back the funds to us in case of problem goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx(d.channelId, fundingTx, fundingTxFee, d.replyTo) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala index e7d4eb5dad..2842be4bd5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala @@ -116,7 +116,7 @@ trait SingleFundingHandlers extends CommonFundingHandlers { def singleFundingMinDepth(d: ChannelDataWithCommitments): Long = { val minDepth_opt = if (d.commitments.params.localParams.isChannelOpener) { - d.commitments.params.minDepthFunder + d.commitments.params.minDepthFunder(nodeParams.channelConf.minDepth) } else { // When we're not the channel initiator we scale the min_depth confirmations depending on the funding amount. d.commitments.params.minDepthFundee(nodeParams.channelConf.minDepth, d.commitments.latest.commitInput.txOut.amount) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 33ef3074c0..fd778546c9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -93,7 +93,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] val fundingTxId = watchConfirmed.txId - assert(watchConfirmed.minDepth == 1) // when funder we trust ourselves so we never wait more than 1 block + assert(watchConfirmed.minDepth == 6) val txPublished = listener.expectMsgType[TransactionPublished] assert(txPublished.tx.txid == fundingTxId) assert(txPublished.miningFee > 0.sat) @@ -117,7 +117,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth == 1) // when funder we trust ourselves so we never wait more than 1 block + assert(watchConfirmed.minDepth == 6) // when funder we don't scale the number of confirmations based on the funding amount aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Created] } From c99851386fee322e0bf72d943313366512e3b5b5 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 20 Feb 2025 18:45:58 +0100 Subject: [PATCH 2/2] fixup! Use default confirmations for single-funded channel --- .../eclair/integration/ChannelIntegrationSpec.scala | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 30734814e7..8b6c939404 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -124,13 +124,10 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { connect(nodes("C"), nodes("F"), 5000000 sat, 500000000 msat) awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds) awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds) - generateBlocks(1, Some(minerAddress)) - // the funder sends its channel_ready after only one block - awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_CHANNEL_READY, max = 30 seconds) - generateBlocks(5, Some(minerAddress)) - // the fundee sends its channel_ready after 6 blocks - awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NORMAL, max = 30 seconds) + // we exchange channel_ready and move to the NORMAL state after 6 blocks + generateBlocks(6, Some(minerAddress)) awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NORMAL, max = 30 seconds) + awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NORMAL, max = 30 seconds) awaitAnnouncements(2) // first we make sure we are in sync with current blockchain height val currentBlockHeight = getBlockHeight()