What changed, and why it matters
This commit fixes a bug in Eclair's on-the-fly funding feature where a payment fee could be calculated as a negative number. The patch hard-codes a minimum HTLC value used by Phoenix and prevents the fee from dropping below zero. It is a defensive fix that avoids later validation failures rather than a clear exploitable vulnerability.
Review whether hard-coding 1000 msat is appropriate for all future consumers of on-the-fly funding, and consider passing the actual remote htlc_minimum to the Peer actor for more robust validation. Monitor for any related channel failures or splice rejections.
Security signals we found
Negative funding fee calculation prevented by clamping to zero
Missing remote channel parameter (htlc_minimum) addressed with hard-coded Phoenix value
On-the-fly splice validation logic changed
Log message corrected from 'open_channel2' to 'splice_init'
Evidence from the diff
In OnTheFlyFunding.scala, maxFees() previously returned htlc.amount - htlcMinimum, which could be negative if the HTLC amount was below the minimum. The patch clamps the result to a minimum of 0 msat. In Peer.scala, the remote htlc_minimum is unavailable during splice validation, so the code now uses max(local htlcMinimum, 1000 msat) — the value used by Phoenix, the only known consumer — to avoid negative funding fees.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scalaInspect captured patch +4 / −3
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
index 5001c51..bd9dd6c 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
@@ -467,10 +467,11 @@ class Peer(val nodeParams: NodeParams,
case Event(msg: SpliceInit, d: ConnectedData) =>
d.channels.get(FinalChannelId(msg.channelId)) match {
case Some(_) if msg.usesOnTheFlyFunding && !d.fundingFeerateOk(msg.feerate) =>
- log.info("rejecting open_channel2: feerate too low ({} < {})", msg.feerate, d.currentFeerates.fundingFeerate)
+ log.info("rejecting splice_init: feerate too low ({} < {})", msg.feerate, d.currentFeerates.fundingFeerate)
self ! Peer.OutgoingMessage(TxAbort(msg.channelId, FundingFeerateTooLow(msg.channelId, msg.feerate, d.currentFeerates.fundingFeerate).getMessage), d.peerConnection)
case Some(channel) =>
- OnTheFlyFunding.validateSplice(nodeParams.onTheFlyFundingConfig, msg, nodeParams.channelConf.htlcMinimum, pendingOnTheFlyFunding, feeCredit.getOrElse(0 msat)) match {
+ // We don't have access to the remote htlc_minimum here, so we hard-code the value Phoenix uses (1000 msat).
+ OnTheFlyFunding.validateSplice(nodeParams.onTheFlyFundingConfig, msg, nodeParams.channelConf.htlcMinimum.max(1000 msat), pendingOnTheFlyFunding, feeCredit.getOrElse(0 msat)) match {
case reject: OnTheFlyFunding.ValidationResult.Reject =>
log.warning("rejecting on-the-fly splice: {}", reject.cancel.toAscii)
self ! Peer.OutgoingMessage(reject.cancel, d.peerConnection)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala
index 08549f8..ed791b7 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala
@@ -91,7 +91,7 @@ object OnTheFlyFunding {
/** An on-the-fly funding proposal sent to our peer. */
case class Proposal(htlc: WillAddHtlc, upstream: Upstream.Hot, onionSharedSecrets: Seq[Sphinx.SharedSecret]) {
/** Maximum fees that can be collected from this HTLC. */
- def maxFees(htlcMinimum: MilliSatoshi): MilliSatoshi = htlc.amount - htlcMinimum
+ def maxFees(htlcMinimum: MilliSatoshi): MilliSatoshi = (htlc.amount - htlcMinimum).max(0 msat)
/** Create commands to fail all upstream HTLCs. */
def createFailureCommands(failure_opt: Option[FailureReason])(implicit log: LoggingAdapter): Seq[(ByteVector32, CMD_FAIL_HTLC)] = upstream match {
Why this scored 36/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.