Skip to content

Commit

Permalink
Increase default max-cltv value (#2677)
Browse files Browse the repository at this point in the history
LND and CLN already use 2016 blocks. The network is generally raising the
values of `cltv_expiry_delta` to account for high on-chain fees, so we'll
need to allow longer maximum deltas to avoid rejecting payments.
  • Loading branch information
t-bast committed Jun 2, 2023
1 parent 5505967 commit ef277f0
Show file tree
Hide file tree
Showing 9 changed files with 31 additions and 27 deletions.
3 changes: 2 additions & 1 deletion eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ eclair {
max-to-local-delay-blocks = 2016 // maximum number of blocks that we are ready to accept for our own delayed outputs (2016 ~ 2 weeks)
mindepth-blocks = 3
expiry-delta-blocks = 144
max-expiry-delta-blocks = 2016 // we won't forward HTLCs with timeouts greater than this delta
// When we receive the preimage for an HTLC and want to fulfill it but the upstream peer stops responding, we want to
// avoid letting its HTLC-timeout transaction become enforceable on-chain (otherwise there is a race condition between
// our HTLC-success and their HTLC-timeout).
Expand Down Expand Up @@ -312,7 +313,7 @@ eclair {

boundaries {
max-route-length = 6 // max route length for the 'first pass', if none is found then a second pass is made with no limit
max-cltv = 1008 // max acceptable cltv expiry for the payment (1008 ~ 1 week)
max-cltv = 2016 // max acceptable cltv expiry for the payment (2016 ~ 2 weeks)
// the route must have a fee that's below max-fee-flat-sat OR below max-fee-proportional-percent / 100 * total-amount
max-fee-flat-sat = 21
max-fee-proportional-percent = 3 // that's 3%
Expand Down
4 changes: 3 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,10 @@ object NodeParams extends Logging {

val maxToLocalCLTV = CltvExpiryDelta(config.getInt("channel.max-to-local-delay-blocks"))
val offeredCLTV = CltvExpiryDelta(config.getInt("channel.to-remote-delay-blocks"))
require(maxToLocalCLTV <= Channel.MAX_TO_SELF_DELAY && offeredCLTV <= Channel.MAX_TO_SELF_DELAY, s"CLTV delay values too high, max is ${Channel.MAX_TO_SELF_DELAY}")

val expiryDelta = CltvExpiryDelta(config.getInt("channel.expiry-delta-blocks"))
val maxExpiryDelta = CltvExpiryDelta(config.getInt("channel.max-expiry-delta-blocks"))
require(expiryDelta < maxExpiryDelta, "channel.max-expiry-delta-blocks must be at least a few times larger than channel.expiry-delta-blocks, otherwise you will fail to relay payments")
val fulfillSafetyBeforeTimeout = CltvExpiryDelta(config.getInt("channel.fulfill-safety-before-timeout-blocks"))
require(fulfillSafetyBeforeTimeout * 2 < expiryDelta, "channel.fulfill-safety-before-timeout-blocks must be smaller than channel.expiry-delta-blocks / 2 because it effectively reduces that delta; if you want to increase this value, you may want to increase expiry-delta-blocks as well")
val minFinalExpiryDelta = CltvExpiryDelta(config.getInt("channel.min-final-expiry-delta-blocks"))
Expand Down Expand Up @@ -479,6 +480,7 @@ object NodeParams extends Logging {
maxToLocalDelay = maxToLocalCLTV,
minDepthBlocks = config.getInt("channel.mindepth-blocks"),
expiryDelta = expiryDelta,
maxExpiryDelta = maxExpiryDelta,
fulfillSafetyBeforeTimeout = fulfillSafetyBeforeTimeout,
minFinalExpiryDelta = minFinalExpiryDelta,
maxBlockProcessingDelay = FiniteDuration(config.getDuration("channel.max-block-processing-delay").getSeconds, TimeUnit.SECONDS),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, OnChainFeeConf}
import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel.Monitoring.Metrics
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.ChannelConf
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.SharedTransaction
import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
import fr.acinq.eclair.crypto.{Generators, ShaChain}
Expand Down Expand Up @@ -808,15 +809,15 @@ case class Commitments(params: ChannelParams,
* @param cmd add HTLC command
* @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right(new commitments, updateAddHtlc)
*/
def sendAdd(cmd: CMD_ADD_HTLC, currentHeight: BlockHeight, feeConf: OnChainFeeConf): Either[ChannelException, (Commitments, UpdateAddHtlc)] = {
def sendAdd(cmd: CMD_ADD_HTLC, currentHeight: BlockHeight, channelConf: ChannelConf, feeConf: OnChainFeeConf): Either[ChannelException, (Commitments, UpdateAddHtlc)] = {
// we must ensure we're not relaying htlcs that are already expired, otherwise the downstream channel will instantly close
// NB: we add a 3 blocks safety to reduce the probability of running into this when our bitcoin node is slightly outdated
val minExpiry = CltvExpiry(currentHeight + 3)
if (cmd.cltvExpiry < minExpiry) {
return Left(ExpiryTooSmall(channelId, minimum = minExpiry, actual = cmd.cltvExpiry, blockHeight = currentHeight))
}
// we don't want to use too high a refund timeout, because our funds will be locked during that time if the payment is never fulfilled
val maxExpiry = Channel.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(currentHeight)
val maxExpiry = channelConf.maxExpiryDelta.toCltvExpiry(currentHeight)
if (cmd.cltvExpiry >= maxExpiry) {
return Left(ExpiryTooBig(channelId, maximum = maxExpiry, actual = cmd.cltvExpiry, blockHeight = currentHeight))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ object Helpers {
if (open.pushMsat > open.fundingSatoshis) return Left(InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi))

// BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large.
if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))
if (open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))

// BOLT #2: The receiving node MUST fail the channel if: max_accepted_htlcs is greater than 483.
if (open.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
Expand Down Expand Up @@ -144,7 +144,7 @@ object Helpers {
if (open.pushAmount > open.fundingAmount) return Left(InvalidPushAmount(open.temporaryChannelId, open.pushAmount, open.fundingAmount.toMilliSatoshi))

// BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large.
if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))
if (open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))

// BOLT #2: The receiving node MUST fail the channel if: max_accepted_htlcs is greater than 483.
if (open.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
Expand Down Expand Up @@ -198,7 +198,7 @@ object Helpers {

// if minimum_depth is unreasonably large:
// MAY reject the channel.
if (accept.toSelfDelay > Channel.MAX_TO_SELF_DELAY || accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))
if (accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))

// if channel_reserve_satoshis is less than dust_limit_satoshis within the open_channel message:
// MUST reject the channel.
Expand Down Expand Up @@ -236,7 +236,7 @@ object Helpers {

// if minimum_depth is unreasonably large:
// MAY reject the channel.
if (accept.toSelfDelay > Channel.MAX_TO_SELF_DELAY || accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))
if (accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))

val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ object Channel {
maxToLocalDelay: CltvExpiryDelta,
minDepthBlocks: Int,
expiryDelta: CltvExpiryDelta,
maxExpiryDelta: CltvExpiryDelta,
fulfillSafetyBeforeTimeout: CltvExpiryDelta,
minFinalExpiryDelta: CltvExpiryDelta,
maxBlockProcessingDelay: FiniteDuration,
Expand Down Expand Up @@ -127,10 +128,6 @@ object Channel {
val MAX_NEGOTIATION_ITERATIONS = 20

val MIN_CLTV_EXPIRY_DELTA: CltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_CLTV_EXPIRY_DELTA
val MAX_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(7 * 144) // one week

// since BOLT 1.1, there is a max value for the refund delay of the main commitment tx
val MAX_TO_SELF_DELAY: CltvExpiryDelta = CltvExpiryDelta(2016)

// as a non-initiator, we will wait that many blocks for the funding tx to confirm (initiator will rely on the funding tx being double-spent)
val FUNDING_TIMEOUT_FUNDEE = 2016
Expand Down Expand Up @@ -387,7 +384,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
handleAddHtlcCommandError(c, error, Some(d.channelUpdate))

case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) =>
d.commitments.sendAdd(c, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) match {
d.commitments.sendAdd(c, nodeParams.currentBlockHeight, nodeParams.channelConf, nodeParams.onChainFeeConf) match {
case Right((commitments1, add)) =>
if (c.commit) self ! CMD_SIGN()
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ object RouteCalculation {
/** https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#clarifications */
val ROUTE_MAX_LENGTH = 20

/** Max allowed CLTV for a route (one week) */
val DEFAULT_ROUTE_MAX_CLTV = CltvExpiryDelta(1008)
/** Max allowed CLTV for a route (two weeks) */
val DEFAULT_ROUTE_MAX_CLTV = CltvExpiryDelta(2016)

/** The default number of routes we'll search for when findRoute is called with randomize = true */
val DEFAULT_ROUTES_COUNT = 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ object TestConstants {
maxHtlcValueInFlightPercent = 100,
maxAcceptedHtlcs = 100,
expiryDelta = CltvExpiryDelta(144),
maxExpiryDelta = CltvExpiryDelta(2016),
fulfillSafetyBeforeTimeout = CltvExpiryDelta(6),
minFinalExpiryDelta = CltvExpiryDelta(18),
maxBlockProcessingDelay = 10 millis,
Expand Down Expand Up @@ -265,6 +266,7 @@ object TestConstants {
maxHtlcValueInFlightPercent = 100,
maxAcceptedHtlcs = 30,
expiryDelta = CltvExpiryDelta(144),
maxExpiryDelta = CltvExpiryDelta(2016),
fulfillSafetyBeforeTimeout = CltvExpiryDelta(6),
minFinalExpiryDelta = CltvExpiryDelta(18),
maxBlockProcessingDelay = 10 millis,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(bc0.availableBalanceForReceive == a)

val (payment_preimage, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Right((ac1, add)) = ac0.sendAdd(cmdAdd, currentBlockHeight, alice.underlyingActor.nodeParams.onChainFeeConf)
val Right((ac1, add)) = ac0.sendAdd(cmdAdd, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.onChainFeeConf)
assert(ac1.availableBalanceForSend == a - p - htlcOutputFee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees)
assert(ac1.availableBalanceForReceive == b)

Expand Down Expand Up @@ -164,7 +164,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(bc0.availableBalanceForReceive == a)

val (_, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Right((ac1, add)) = ac0.sendAdd(cmdAdd, currentBlockHeight, alice.underlyingActor.nodeParams.onChainFeeConf)
val Right((ac1, add)) = ac0.sendAdd(cmdAdd, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.onChainFeeConf)
assert(ac1.availableBalanceForSend == a - p - htlcOutputFee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees)
assert(ac1.availableBalanceForReceive == b)

Expand Down Expand Up @@ -252,17 +252,17 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(bc0.availableBalanceForReceive == a)

val (payment_preimage1, cmdAdd1) = makeCmdAdd(p1, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Right((ac1, add1)) = ac0.sendAdd(cmdAdd1, currentBlockHeight, alice.underlyingActor.nodeParams.onChainFeeConf)
val Right((ac1, add1)) = ac0.sendAdd(cmdAdd1, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.onChainFeeConf)
assert(ac1.availableBalanceForSend == a - p1 - htlcOutputFee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees)
assert(ac1.availableBalanceForReceive == b)

val (_, cmdAdd2) = makeCmdAdd(p2, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Right((ac2, add2)) = ac1.sendAdd(cmdAdd2, currentBlockHeight, alice.underlyingActor.nodeParams.onChainFeeConf)
val Right((ac2, add2)) = ac1.sendAdd(cmdAdd2, currentBlockHeight, alice.underlyingActor.nodeParams.channelConf, alice.underlyingActor.nodeParams.onChainFeeConf)
assert(ac2.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees)
assert(ac2.availableBalanceForReceive == b)

val (payment_preimage3, cmdAdd3) = makeCmdAdd(p3, alice.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Right((bc1, add3)) = bc0.sendAdd(cmdAdd3, currentBlockHeight, bob.underlyingActor.nodeParams.onChainFeeConf)
val Right((bc1, add3)) = bc0.sendAdd(cmdAdd3, currentBlockHeight, bob.underlyingActor.nodeParams.channelConf, bob.underlyingActor.nodeParams.onChainFeeConf)
assert(bc1.availableBalanceForSend == b - p3) // bob doesn't pay the fee
assert(bc1.availableBalanceForReceive == a)

Expand Down Expand Up @@ -383,22 +383,22 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val isInitiator = true
val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, FeeratePerKw(2500 sat), 546 sat, isInitiator)
val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight)
val Right((c1, _)) = c.sendAdd(cmdAdd, f.currentBlockHeight, feeConfNoMismatch)
val Right((c1, _)) = c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch)
assert(c1.availableBalanceForSend == 0.msat)

// We should be able to handle a fee increase.
val Right((c2, _)) = c1.sendFee(CMD_UPDATE_FEE(FeeratePerKw(3000 sat)), feeConfNoMismatch)

// Now we shouldn't be able to send until we receive enough to handle the updated commit tx fee (even trimmed HTLCs shouldn't be sent).
val (_, cmdAdd1) = makeCmdAdd(100 msat, randomKey().publicKey, f.currentBlockHeight)
val Left(_: InsufficientFunds) = c2.sendAdd(cmdAdd1, f.currentBlockHeight, feeConfNoMismatch)
val Left(_: InsufficientFunds) = c2.sendAdd(cmdAdd1, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch)
}

test("can send availableForSend") { f =>
for (isInitiator <- Seq(true, false)) {
val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, FeeratePerKw(2679 sat), 546 sat, isInitiator)
val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight)
val result = c.sendAdd(cmdAdd, f.currentBlockHeight, feeConfNoMismatch)
val result = c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch)
assert(result.isRight, result)
}
}
Expand Down Expand Up @@ -428,14 +428,14 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
for (_ <- 1 to t.pendingHtlcs) {
val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat)
val (_, cmdAdd) = makeCmdAdd(amount, randomKey().publicKey, f.currentBlockHeight)
c.sendAdd(cmdAdd, f.currentBlockHeight, feeConfNoMismatch) match {
c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch) match {
case Right((cc, _)) => c = cc
case Left(e) => ignore(s"$t -> could not setup initial htlcs: $e")
}
}
if (c.availableBalanceForSend > 0.msat) {
val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight)
val result = c.sendAdd(cmdAdd, f.currentBlockHeight, feeConfNoMismatch)
val result = c.sendAdd(cmdAdd, f.currentBlockHeight, TestConstants.Alice.nodeParams.channelConf, feeConfNoMismatch)
assert(result.isRight, s"$t -> $result")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val expiryTooBig = (Channel.MAX_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight)
val maxAllowedExpiryDelta = alice.underlyingActor.nodeParams.channelConf.maxExpiryDelta
val expiryTooBig = (maxAllowedExpiryDelta + 1).toCltvExpiry(currentBlockHeight)
val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), expiryTooBig, TestConstants.emptyOnionPacket, None, localOrigin(sender.ref))
alice ! add
val error = ExpiryTooBig(channelId(alice), maximum = Channel.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(currentBlockHeight), actual = expiryTooBig, blockHeight = currentBlockHeight)
val error = ExpiryTooBig(channelId(alice), maximum = maxAllowedExpiryDelta.toCltvExpiry(currentBlockHeight), actual = expiryTooBig, blockHeight = currentBlockHeight)
sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate)))
alice2bob.expectNoMessage(200 millis)
}
Expand Down

0 comments on commit ef277f0

Please sign in to comment.