Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement on-the-fly funding based on splicing and liquidity ads #2861

Merged
merged 9 commits into from
Sep 25, 2024
8 changes: 8 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ eclair {
keysend = disabled
trampoline_payment_prototype = disabled
async_payment_prototype = disabled
on_the_fly_funding = disabled
}
// The following section lets you customize features for specific nodes.
// The overrides will be applied on top of the default features settings.
Expand Down Expand Up @@ -340,6 +341,13 @@ eclair {
]
}

// On-the-fly funding leverages liquidity ads to fund channels with wallet peers based on their payment patterns.
on-the-fly-funding {
// If our peer doesn't respond to our funding proposal, we must fail the corresponding upstream HTLCs.
// Since MPP may be used, we should use a timeout greater than the MPP timeout.
proposal-timeout = 90 seconds
}

peer-connection {
auth-timeout = 15 seconds // will disconnect if connection authentication doesn't happen within that timeframe
init-timeout = 15 seconds // will disconnect if initialization doesn't happen within that timeframe
Expand Down
13 changes: 12 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,15 @@ object Features {
val mandatory = 154
}

/**
* Activate this feature to provide on-the-fly funding to remote nodes, as specified in bLIP 36: https://github.com/lightning/blips/blob/master/blip-0036.md.
* TODO: add NodeFeature once bLIP is merged.
*/
case object OnTheFlyFunding extends Feature with InitFeature {
val rfcName = "on_the_fly_funding"
val mandatory = 560
}

val knownFeatures: Set[Feature] = Set(
DataLossProtect,
InitialRoutingSync,
Expand All @@ -349,6 +358,7 @@ object Features {
TrampolinePaymentPrototype,
AsyncPaymentPrototype,
SplicePrototype,
OnTheFlyFunding
)

// Features may depend on other features, as specified in Bolt 9.
Expand All @@ -361,7 +371,8 @@ object Features {
RouteBlinding -> (VariableLengthOnion :: Nil),
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil),
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil)
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
OnTheFlyFunding -> (SplicePrototype :: Nil)
)

case class FeatureException(message: String) extends IllegalArgumentException(message)
Expand Down
14 changes: 11 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import fr.acinq.eclair.db._
import fr.acinq.eclair.io.MessageRelay.{RelayAll, RelayChannelsOnly, RelayPolicy}
import fr.acinq.eclair.io.{PeerConnection, PeerReadyNotifier}
import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig
import fr.acinq.eclair.payment.relay.OnTheFlyFunding
import fr.acinq.eclair.payment.relay.Relayer.{AsyncPaymentsParams, RelayFees, RelayParams}
import fr.acinq.eclair.router.Announcements.AddressException
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios}
Expand Down Expand Up @@ -90,7 +91,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
purgeInvoicesInterval: Option[FiniteDuration],
revokedHtlcInfoCleanerConfig: RevokedHtlcInfoCleaner.Config,
willFundRates_opt: Option[LiquidityAds.WillFundRates],
peerWakeUpConfig: PeerReadyNotifier.WakeUpConfig) {
peerWakeUpConfig: PeerReadyNotifier.WakeUpConfig,
onTheFlyFundingConfig: OnTheFlyFunding.Config) {
val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey

val nodeId: PublicKey = nodeKeyManager.nodeId
Expand Down Expand Up @@ -504,7 +506,10 @@ object NodeParams extends Logging {

val willFundRates_opt = {
val supportedPaymentTypes = Map(
LiquidityAds.PaymentType.FromChannelBalance.rfcName -> LiquidityAds.PaymentType.FromChannelBalance
LiquidityAds.PaymentType.FromChannelBalance.rfcName -> LiquidityAds.PaymentType.FromChannelBalance,
LiquidityAds.PaymentType.FromChannelBalanceForFutureHtlc.rfcName -> LiquidityAds.PaymentType.FromChannelBalanceForFutureHtlc,
LiquidityAds.PaymentType.FromFutureHtlc.rfcName -> LiquidityAds.PaymentType.FromFutureHtlc,
LiquidityAds.PaymentType.FromFutureHtlcWithPreimage.rfcName -> LiquidityAds.PaymentType.FromFutureHtlcWithPreimage,
)
val paymentTypes: Set[LiquidityAds.PaymentType] = config.getStringList("liquidity-ads.payment-types").asScala.map(s => {
supportedPaymentTypes.get(s) match {
Expand Down Expand Up @@ -668,7 +673,10 @@ object NodeParams extends Logging {
willFundRates_opt = willFundRates_opt,
peerWakeUpConfig = PeerReadyNotifier.WakeUpConfig(
enabled = config.getBoolean("peer-wake-up.enabled"),
timeout = FiniteDuration(config.getDuration("peer-wake-up.timeout").getSeconds, TimeUnit.SECONDS)
timeout = FiniteDuration(config.getDuration("peer-wake-up.timeout").getSeconds, TimeUnit.SECONDS),
),
onTheFlyFundingConfig = OnTheFlyFunding.Config(
proposalTimeout = FiniteDuration(config.getDuration("on-the-fly-funding.proposal-timeout").getSeconds, TimeUnit.SECONDS),
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ case class INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId: ByteVector32,
fundingContribution_opt: Option[LiquidityAds.AddFunding],
dualFunded: Boolean,
pushAmount_opt: Option[MilliSatoshi],
requireConfirmedInputs: Boolean,
localParams: LocalParams,
remote: ActorRef,
remoteInit: Init,
Expand Down Expand Up @@ -146,7 +147,7 @@ object Upstream {
val expiryIn: CltvExpiry = add.cltvExpiry
}
/** Our node is forwarding a payment based on a set of HTLCs from potentially multiple upstream channels. */
case class Trampoline(received: Seq[Channel]) extends Hot {
case class Trampoline(received: List[Channel]) extends Hot {
override val amountIn: MilliSatoshi = received.map(_.add.amountMsat).sum
// We must use the lowest expiry of the incoming HTLC set.
val expiryIn: CltvExpiry = received.map(_.add.cltvExpiry).min
Expand All @@ -165,6 +166,10 @@ object Upstream {

/** Our node is forwarding a single incoming HTLC. */
case class Channel(originChannelId: ByteVector32, originHtlcId: Long, amountIn: MilliSatoshi) extends Cold
object Channel {
def apply(add: UpdateAddHtlc): Channel = Channel(add.channelId, add.id, add.amountMsat)
}

/** Our node is forwarding a payment based on a set of HTLCs from potentially multiple upstream channels. */
case class Trampoline(originHtlcs: List[Channel]) extends Cold { override val amountIn: MilliSatoshi = originHtlcs.map(_.amountIn).sum }
}
Expand Down Expand Up @@ -197,7 +202,17 @@ sealed trait HasOptionalReplyToCommand extends Command { def replyTo_opt: Option
sealed trait ForbiddenCommandDuringSplice extends Command
sealed trait ForbiddenCommandDuringQuiescence extends Command

final case class CMD_ADD_HTLC(replyTo: ActorRef, amount: MilliSatoshi, paymentHash: ByteVector32, cltvExpiry: CltvExpiry, onion: OnionRoutingPacket, nextBlindingKey_opt: Option[PublicKey], confidence: Double, origin: Origin.Hot, commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence
final case class CMD_ADD_HTLC(replyTo: ActorRef,
amount: MilliSatoshi,
paymentHash: ByteVector32,
cltvExpiry: CltvExpiry,
onion: OnionRoutingPacket,
nextBlindingKey_opt: Option[PublicKey],
confidence: Double,
fundingFee_opt: Option[LiquidityAds.FundingFee],
origin: Origin.Hot,
commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence

sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence { def id: Long }
final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
Expand Down Expand Up @@ -666,8 +681,16 @@ case class RemoteParams(nodeId: PublicKey,
initFeatures: Features[InitFeature],
upfrontShutdownScript_opt: Option[ByteVector])

case class ChannelFlags(announceChannel: Boolean) {
override def toString: String = s"ChannelFlags(announceChannel=$announceChannel)"
/**
* The [[nonInitiatorPaysCommitFees]] parameter is set to true when the sender wants the receiver to pay the commitment transaction fees.
* This is not part of the BOLTs and won't be needed anymore once commitment transactions don't pay any on-chain fees.
*/
case class ChannelFlags(nonInitiatorPaysCommitFees: Boolean, announceChannel: Boolean) {
override def toString: String = s"ChannelFlags(announceChannel=$announceChannel, nonInitiatorPaysCommitFees=$nonInitiatorPaysCommitFees)"
}

object ChannelFlags {
def apply(announceChannel: Boolean): ChannelFlags = ChannelFlags(nonInitiatorPaysCommitFees = false, announceChannel = announceChannel)
}

/** Information about what triggered the opening of the channel */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ case class ChannelAborted(channel: ActorRef, remoteNodeId: PublicKey, channelId:
/** This event will be sent once a channel has been successfully opened and is ready to process payments. */
case class ChannelOpened(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32) extends ChannelEvent

/** This event is sent once channel_ready or splice_locked have been exchanged. */
case class ChannelReadyForPayments(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, fundingTxIndex: Long) extends ChannelEvent

case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: Commitments) extends ChannelEvent {
/**
* We always include the local alias because we must always be able to route based on it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ case class Commitments(params: ChannelParams,
return Left(HtlcValueTooSmall(params.channelId, minimum = htlcMinimum, actual = cmd.amount))
}

val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextBlindingKey_opt, cmd.confidence)
val add = UpdateAddHtlc(channelId, changes.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextBlindingKey_opt, cmd.confidence, cmd.fundingFee_opt)
// we increment the local htlc index and add an entry to the origins map
val changes1 = changes.addLocalProposal(add).copy(localNextHtlcId = changes.localNextHtlcId + 1)
val originChannels1 = originChannels + (add.id -> cmd.origin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType
import fr.acinq.eclair.db.PendingCommandsDb
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.io.Peer.LiquidityPurchaseSigned
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain}
import fr.acinq.eclair.router.Announcements
Expand Down Expand Up @@ -1095,10 +1096,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
log.info("ignoring outgoing interactive-tx message {} from previous session", msg.getClass.getSimpleName)
stay()
}
case InteractiveTxBuilder.Succeeded(signingSession, commitSig) =>
case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt) =>
log.info(s"splice tx created with fundingTxIndex=${signingSession.fundingTxIndex} fundingTxId=${signingSession.fundingTx.txId}")
cmd_opt.foreach(cmd => cmd.replyTo ! RES_SPLICE(fundingTxIndex = signingSession.fundingTxIndex, signingSession.fundingTx.txId, signingSession.fundingParams.fundingAmount, signingSession.localCommit.fold(_.spec, _.spec).toLocal))
remoteCommitSig_opt.foreach(self ! _)
liquidityPurchase_opt.collect {
case purchase if !signingSession.fundingParams.isInitiator => peer ! LiquidityPurchaseSigned(d.channelId, signingSession.fundingTx.txId, signingSession.fundingTxIndex, d.commitments.params.remoteParams.htlcMinimum, purchase)
}
val d1 = d.copy(spliceStatus = SpliceStatus.SpliceWaitingForSigs(signingSession))
stay() using d1 storing() sending commitSig
case f: InteractiveTxBuilder.Failed =>
Expand Down Expand Up @@ -2139,6 +2143,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
}
}

// We tell the peer that the channel is ready to process payments that may be queued.
if (!shutdownInProgress) {
val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min
peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex)
}

goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1) sending sendQueue
}

Expand Down Expand Up @@ -2710,6 +2720,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
if (oldCommitments.availableBalanceForSend != newCommitments.availableBalanceForSend || oldCommitments.availableBalanceForReceive != newCommitments.availableBalanceForReceive) {
context.system.eventStream.publish(AvailableBalanceChanged(self, newCommitments.channelId, shortIds, newCommitments))
}
if (oldCommitments.active.size != newCommitments.active.size) {
// Some commitments have been deactivated, which means our available balance changed, which may allow forwarding
// payments that couldn't be forwarded before.
val fundingTxIndex = newCommitments.active.map(_.fundingTxIndex).min
peer ! ChannelReadyForPayments(self, remoteNodeId, newCommitments.channelId, fundingTxIndex)
}
}

private def maybeUpdateMaxHtlcAmount(currentMaxHtlcAmount: MilliSatoshi, newCommitments: Commitments): Unit = {
Expand Down
Loading
Loading