1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-13 11:35:47 +01:00

Abort incoming channel after timeout

If a remote node starts opening a channel to us and then becomes
unresponsive, we abort the channel. This is particularly useful
when they're purchasing liquidity and we've locked utxos.
This commit is contained in:
t-bast 2025-01-16 18:31:46 +01:00
parent 68c6720c8f
commit 64a12b8028
No known key found for this signature in database
GPG key ID: 34F377B0100ED6BB
6 changed files with 23 additions and 2 deletions

View file

@ -109,6 +109,9 @@ eclair {
max-attempts = 5 // maximum number of RBF attempts our peer is allowed to make
attempt-delta-blocks = 6 // minimum number of blocks between RBF attempts
}
// Duration after which we abort a channel creation. If our peer seems unresponsive and doesn't complete the
// funding protocol in time, they're likely buggy or malicious.
timeout = 60 seconds
}
dust-limit-satoshis = 546

View file

@ -591,6 +591,7 @@ object NodeParams extends Logging {
channelOpenerWhitelist = channelOpenerWhitelist,
maxPendingChannelsPerPeer = maxPendingChannelsPerPeer,
maxTotalPendingChannelsPrivateNodes = maxTotalPendingChannelsPrivateNodes,
channelFundingTimeout = FiniteDuration(config.getDuration("channel.funding.timeout").getSeconds, TimeUnit.SECONDS),
remoteRbfLimits = Channel.RemoteRbfLimits(config.getInt("channel.funding.remote-rbf-limits.max-attempts"), config.getInt("channel.funding.remote-rbf-limits.attempt-delta-blocks")),
quiescenceTimeout = FiniteDuration(config.getDuration("channel.quiescence-timeout").getSeconds, TimeUnit.SECONDS),
balanceThresholds = config.getConfigList("channel.channel-update.balance-thresholds").asScala.map(conf => BalanceThreshold(Satoshi(conf.getLong("available-sat")), Satoshi(conf.getLong("max-htlc-sat")))).toSeq,
@ -683,7 +684,7 @@ object NodeParams extends Logging {
batchSize = config.getInt("db.revoked-htlc-info-cleaner.batch-size"),
interval = FiniteDuration(config.getDuration("db.revoked-htlc-info-cleaner.interval").getSeconds, TimeUnit.SECONDS)
),
liquidityAdsConfig = LiquidityAds.Config(willFundRates_opt, lockUtxos = config.getBoolean("liquidity-ads.lock-utxos-during-funding")),
liquidityAdsConfig = LiquidityAds.Config(rates_opt = willFundRates_opt, lockUtxos = config.getBoolean("liquidity-ads.lock-utxos-during-funding")),
peerWakeUpConfig = PeerReadyNotifier.WakeUpConfig(
enabled = config.getBoolean("peer-wake-up.enabled"),
timeout = FiniteDuration(config.getDuration("peer-wake-up.timeout").getSeconds, TimeUnit.SECONDS),

View file

@ -102,6 +102,7 @@ object Channel {
channelOpenerWhitelist: Set[PublicKey],
maxPendingChannelsPerPeer: Int,
maxTotalPendingChannelsPrivateNodes: Int,
channelFundingTimeout: FiniteDuration,
remoteRbfLimits: RemoteRbfLimits,
quiescenceTimeout: FiniteDuration,
balanceThresholds: Seq[BalanceThreshold],

View file

@ -198,7 +198,7 @@ class Peer(val nodeParams: NodeParams,
case Event(SpawnChannelInitiator(replyTo, c, channelConfig, channelType, localParams), d: ConnectedData) =>
val channel = spawnChannel()
c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher))
context.system.scheduler.scheduleOnce(c.timeout_opt.map(_.duration).getOrElse(nodeParams.channelConf.channelFundingTimeout), channel, Channel.TickChannelOpenTimeout)(context.dispatcher)
val dualFunded = Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.DualFunding)
val requireConfirmedInputs = c.requireConfirmedInputsOverride_opt.getOrElse(nodeParams.channelConf.requireConfirmedInputsForDualFunding)
val temporaryChannelId = if (dualFunded) {
@ -252,6 +252,7 @@ class Peer(val nodeParams: NodeParams,
stay()
case accept: OnTheFlyFunding.ValidationResult.Accept =>
val channel = spawnChannel()
context.system.scheduler.scheduleOnce(nodeParams.channelConf.channelFundingTimeout, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)
log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams")
open match {
case Left(open) =>

View file

@ -145,6 +145,7 @@ object TestConstants {
channelOpenerWhitelist = Set.empty,
maxPendingChannelsPerPeer = 3,
maxTotalPendingChannelsPrivateNodes = 99,
channelFundingTimeout = 30 seconds,
remoteRbfLimits = RemoteRbfLimits(5, 0),
quiescenceTimeout = 2 minutes,
balanceThresholds = Nil,
@ -323,6 +324,7 @@ object TestConstants {
channelOpenerWhitelist = Set.empty,
maxPendingChannelsPerPeer = 3,
maxTotalPendingChannelsPrivateNodes = 99,
channelFundingTimeout = 30 seconds,
remoteRbfLimits = RemoteRbfLimits(10, 0),
quiescenceTimeout = 2 minutes,
balanceThresholds = Nil,

View file

@ -28,6 +28,7 @@ import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw}
import fr.acinq.eclair.blockchain.{CurrentFeerates, DummyOnChainWallet}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.states.ChannelStateTestsTags
import fr.acinq.eclair.io.Peer._
import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage}
@ -76,6 +77,7 @@ class PeerSpec extends FixtureSpec {
.modify(_.features).setToIf(testData.tags.contains(ChannelStateTestsTags.DualFunding))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))
.modify(_.channelConf.maxHtlcValueInFlightMsat).setToIf(testData.tags.contains("max-htlc-value-in-flight-percent"))(100_000_000 msat)
.modify(_.channelConf.maxHtlcValueInFlightPercent).setToIf(testData.tags.contains("max-htlc-value-in-flight-percent"))(25)
.modify(_.channelConf.channelFundingTimeout).setToIf(testData.tags.contains("channel_funding_timeout"))(100 millis)
.modify(_.autoReconnect).setToIf(testData.tags.contains("auto_reconnect"))(true)
if (testData.tags.contains("with_node_announcement")) {
@ -718,6 +720,17 @@ class PeerSpec extends FixtureSpec {
assert(channelAborted.channelId == open.temporaryChannelId)
}
test("abort incoming channel after funding timeout", Tag(ChannelStateTestsTags.DualFunding), Tag("channel_funding_timeout")) { f =>
import f._
connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional)))
val open = createOpenDualFundedChannelMessage()
peerConnection.send(peer, open)
assert(channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR].dualFunded)
channel.expectMsg(open)
channel.expectMsg(Channel.TickChannelOpenTimeout)
}
test("kill peer with no channels if connection dies before receiving `ConnectionReady`") { f =>
import f._
val probe = TestProbe()