Skip to content

Commit 79836a9

Browse files
committed
Remove please_open_channel
It is usually the wallet that decides that it needs a channel, but we want the LSP to pay the commit fees to allow the wallet user to empty its wallet over lightning. We previously used a `please_open_channel` message that was sent by the wallet to the LSP, but it doesn't work well with liquidity ads. We remove that message and instead send `open_channel` from the wallet but with a custom channel flag that tells the LSP that they should be paying the commit fees. This only works if the LSP adds funds on their side of the channel, so we couple that with liquidity ads to request funds from the LSP. We also add a `recommended_feerates` message from the LSP which lets the wallet know the on-chain feerates that the LSP will accept for on-chain funding operations, since those feerates are set in the `open_channel` message that is now sent by the wallet.
1 parent fb0ae19 commit 79836a9

File tree

93 files changed

+945
-818
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+945
-818
lines changed

src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package fr.acinq.lightning
22

33
import fr.acinq.bitcoin.ByteVector32
44
import fr.acinq.bitcoin.PublicKey
5+
import fr.acinq.bitcoin.OutPoint
56
import fr.acinq.bitcoin.Satoshi
7+
import fr.acinq.lightning.blockchain.electrum.WalletState
8+
import fr.acinq.lightning.channel.ChannelManagementFees
69
import fr.acinq.lightning.channel.InteractiveTxParams
710
import fr.acinq.lightning.channel.SharedFundingInput
811
import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
@@ -11,16 +14,18 @@ import fr.acinq.lightning.channel.states.WaitForFundingCreated
1114
import fr.acinq.lightning.db.IncomingPayment
1215
import fr.acinq.lightning.utils.sum
1316
import fr.acinq.lightning.wire.Init
14-
import fr.acinq.lightning.wire.PleaseOpenChannel
15-
import kotlinx.coroutines.CompletableDeferred
1617

1718
sealed interface NodeEvents
1819

1920
data class PeerConnected(val remoteNodeId: PublicKey, val theirInit: Init) : NodeEvents
2021

2122
sealed interface SwapInEvents : NodeEvents {
22-
data class Requested(val req: PleaseOpenChannel) : SwapInEvents
23-
data class Accepted(val requestId: ByteVector32, val serviceFee: MilliSatoshi, val miningFee: Satoshi) : SwapInEvents
23+
data class Requested(val walletInputs: List<WalletState.Utxo>) : SwapInEvents {
24+
val totalAmount: Satoshi = walletInputs.map { it.amount }.sum()
25+
}
26+
data class Accepted(val inputs: Set<OutPoint>, val amount: Satoshi, val fees: ChannelManagementFees) : SwapInEvents {
27+
val receivedAmount: Satoshi = amount - fees.total
28+
}
2429
}
2530

2631
sealed interface ChannelEvents : NodeEvents {
@@ -30,6 +35,7 @@ sealed interface ChannelEvents : NodeEvents {
3035
}
3136

3237
sealed interface LiquidityEvents : NodeEvents {
38+
/** Amount of the liquidity event, before fees are paid. */
3339
val amount: MilliSatoshi
3440
val fee: MilliSatoshi
3541
val source: Source
@@ -45,8 +51,7 @@ sealed interface LiquidityEvents : NodeEvents {
4551
data object ChannelInitializing : Reason()
4652
}
4753
}
48-
49-
data class ApprovalRequested(override val amount: MilliSatoshi, override val fee: MilliSatoshi, override val source: Source, val replyTo: CompletableDeferred<Boolean>) : LiquidityEvents
54+
data class Accepted(override val amount: MilliSatoshi, override val fee: MilliSatoshi, override val source: Source) : LiquidityEvents
5055
}
5156

5257
/** This is useful on iOS to ask the OS for time to finish some sensitive tasks. */
@@ -59,7 +64,6 @@ sealed interface SensitiveTaskEvents : NodeEvents {
5964
}
6065
data class TaskStarted(val id: TaskIdentifier) : SensitiveTaskEvents
6166
data class TaskEnded(val id: TaskIdentifier) : SensitiveTaskEvents
62-
6367
}
6468

6569
/** This will be emitted in a corner case where the user restores a wallet on an older version of the app, which is unable to read the channel data. */

src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import fr.acinq.lightning.payment.LiquidityPolicy
1212
import fr.acinq.lightning.utils.msat
1313
import fr.acinq.lightning.utils.sat
1414
import fr.acinq.lightning.utils.toMilliSatoshi
15+
import fr.acinq.lightning.wire.LiquidityAds
1516
import fr.acinq.lightning.wire.OfferTypes
1617
import io.ktor.utils.io.charsets.*
1718
import io.ktor.utils.io.core.*
@@ -69,12 +70,14 @@ object DefaultSwapInParams {
6970
* @param trampolineFees ordered list of trampoline fees to try when making an outgoing payment.
7071
* @param invoiceDefaultRoutingFees default routing fees set in invoices when we don't have any channel.
7172
* @param swapInParams parameters for swap-in transactions.
73+
* @param leaseRate rate at which our peer sells their liquidity.
7274
*/
7375
data class WalletParams(
7476
val trampolineNode: NodeUri,
7577
val trampolineFees: List<TrampolineFees>,
7678
val invoiceDefaultRoutingFees: InvoiceDefaultRoutingFees,
7779
val swapInParams: SwapInParams,
80+
val leaseRate: LiquidityAds.LeaseRate,
7881
)
7982

8083
/**
@@ -229,7 +232,7 @@ data class NodeParams(
229232
maxPaymentAttempts = 5,
230233
zeroConfPeers = emptySet(),
231234
paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(75), CltvExpiryDelta(200)),
232-
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(LiquidityPolicy.Auto(maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)),
235+
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(LiquidityPolicy.Auto(inboundLiquidityTarget = null, maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)),
233236
minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA,
234237
maxFinalCltvExpiryDelta = CltvExpiryDelta(360),
235238
bolt12invoiceExpiry = 60.seconds,

src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,18 @@ package fr.acinq.lightning.blockchain.electrum
22

33
import fr.acinq.bitcoin.OutPoint
44
import fr.acinq.bitcoin.Transaction
5-
import fr.acinq.bitcoin.TxId
6-
import fr.acinq.lightning.Lightning
75
import fr.acinq.lightning.SwapInParams
86
import fr.acinq.lightning.channel.FundingContributions.Companion.stripInputWitnesses
97
import fr.acinq.lightning.channel.LocalFundingStatus
108
import fr.acinq.lightning.channel.RbfStatus
11-
import fr.acinq.lightning.channel.SignedSharedTransaction
129
import fr.acinq.lightning.channel.SpliceStatus
1310
import fr.acinq.lightning.channel.states.*
14-
import fr.acinq.lightning.io.RequestChannelOpen
11+
import fr.acinq.lightning.io.AddWalletInputsToChannel
1512
import fr.acinq.lightning.logging.MDCLogger
1613
import fr.acinq.lightning.utils.sat
1714

1815
internal sealed class SwapInCommand {
19-
data class TrySwapIn(val currentBlockHeight: Int, val wallet: WalletState, val swapInParams: SwapInParams, val trustedTxs: Set<TxId>) : SwapInCommand()
16+
data class TrySwapIn(val currentBlockHeight: Int, val wallet: WalletState, val swapInParams: SwapInParams) : SwapInCommand()
2017
data class UnlockWalletInputs(val inputs: Set<OutPoint>) : SwapInCommand()
2118
}
2219

@@ -33,20 +30,15 @@ internal sealed class SwapInCommand {
3330
class SwapInManager(private var reservedUtxos: Set<OutPoint>, private val logger: MDCLogger) {
3431
constructor(bootChannels: List<PersistedChannelState>, logger: MDCLogger) : this(reservedWalletInputs(bootChannels), logger)
3532

36-
internal fun process(cmd: SwapInCommand): RequestChannelOpen? = when (cmd) {
33+
internal fun process(cmd: SwapInCommand): AddWalletInputsToChannel? = when (cmd) {
3734
is SwapInCommand.TrySwapIn -> {
3835
val availableWallet = cmd.wallet.withoutReservedUtxos(reservedUtxos).withConfirmations(cmd.currentBlockHeight, cmd.swapInParams)
3936
logger.info { "swap-in wallet balance: deeplyConfirmed=${availableWallet.deeplyConfirmed.balance}, weaklyConfirmed=${availableWallet.weaklyConfirmed.balance}, unconfirmed=${availableWallet.unconfirmed.balance}" }
40-
val utxos = buildSet {
41-
// some utxos may be used for swap-in even if they are not confirmed, for example when migrating from the legacy phoenix android app
42-
addAll(availableWallet.unconfirmed.filter { cmd.trustedTxs.contains(it.outPoint.txid) })
43-
addAll(availableWallet.weaklyConfirmed.filter { cmd.trustedTxs.contains(it.outPoint.txid) })
44-
addAll(availableWallet.deeplyConfirmed.filter { Transaction.write(it.previousTx.stripInputWitnesses()).size < 65_000 })
45-
}.toList()
37+
val utxos = availableWallet.deeplyConfirmed.filter { Transaction.write(it.previousTx.stripInputWitnesses()).size < 65_000 }
4638
if (utxos.balance > 0.sat) {
4739
logger.info { "swap-in wallet: requesting channel using ${utxos.size} utxos with balance=${utxos.balance}" }
4840
reservedUtxos = reservedUtxos.union(utxos.map { it.outPoint })
49-
RequestChannelOpen(Lightning.randomBytes32(), utxos)
41+
AddWalletInputsToChannel(utxos)
5042
} else {
5143
null
5244
}

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package fr.acinq.lightning.channel
22

33
import fr.acinq.bitcoin.*
4-
import fr.acinq.lightning.ChannelEvents
54
import fr.acinq.lightning.CltvExpiry
65
import fr.acinq.lightning.MilliSatoshi
6+
import fr.acinq.lightning.NodeEvents
77
import fr.acinq.lightning.blockchain.Watch
88
import fr.acinq.lightning.channel.states.PersistedChannelState
99
import fr.acinq.lightning.db.ChannelClosingType
@@ -79,7 +79,7 @@ sealed class ChannelAction {
7979
abstract val txId: TxId
8080
abstract val localInputs: Set<OutPoint>
8181
data class ViaNewChannel(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment()
82-
data class ViaSpliceIn(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: TxId, override val origin: Origin.PayToOpenOrigin?) : StoreIncomingPayment()
82+
data class ViaSpliceIn(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment()
8383
}
8484
/** Payment sent through on-chain operations (channel close or splice-out) */
8585
sealed class StoreOutgoingPayment : Storage() {
@@ -128,8 +128,8 @@ sealed class ChannelAction {
128128
}
129129
}
130130

131-
data class EmitEvent(val event: ChannelEvents) : ChannelAction()
131+
data class EmitEvent(val event: NodeEvents) : ChannelAction()
132132

133-
object Disconnect : ChannelAction()
133+
data object Disconnect : ChannelAction()
134134
// @formatter:on
135135
}

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ sealed class ChannelCommand {
3232
val fundingTxFeerate: FeeratePerKw,
3333
val localParams: LocalParams,
3434
val remoteInit: InitMessage,
35-
val channelFlags: Byte,
35+
val channelFlags: ChannelFlags,
3636
val channelConfig: ChannelConfig,
3737
val channelType: ChannelType.SupportedChannelType,
38-
val channelOrigin: Origin? = null
38+
val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?,
39+
val channelOrigin: Origin?,
3940
) : Init() {
4041
fun temporaryChannelId(keyManager: KeyManager): ByteVector32 = keyManager.channelKeys(localParams.fundingKeyPath).temporaryChannelId
4142
}
@@ -85,7 +86,7 @@ sealed class ChannelCommand {
8586
data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice, ForbiddenDuringQuiescence
8687
data object CheckHtlcTimeout : Commitment()
8788
sealed class Splice : Commitment() {
88-
data class Request(val replyTo: CompletableDeferred<Response>, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, val feerate: FeeratePerKw, val origins: List<Origin.PayToOpenOrigin> = emptyList()) : Splice() {
89+
data class Request(val replyTo: CompletableDeferred<Response>, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, val feerate: FeeratePerKw, val origins: List<Origin>) : Splice() {
8990
val pushAmount: MilliSatoshi = spliceIn?.pushAmount ?: 0.msat
9091
val spliceOutputs: List<TxOut> = spliceOut?.let { listOf(TxOut(it.amount, it.scriptPubKey)) } ?: emptyList()
9192

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -350,23 +350,29 @@ data class LocalParams(
350350
val htlcMinimum: MilliSatoshi,
351351
val toSelfDelay: CltvExpiryDelta,
352352
val maxAcceptedHtlcs: Int,
353-
val isInitiator: Boolean,
353+
val isChannelOpener: Boolean,
354+
val paysCommitTxFees: Boolean,
354355
val defaultFinalScriptPubKey: ByteVector,
355356
val features: Features
356357
) {
357-
constructor(nodeParams: NodeParams, isInitiator: Boolean) : this(
358+
constructor(nodeParams: NodeParams, isChannelOpener: Boolean, payCommitTxFees: Boolean) : this(
358359
nodeId = nodeParams.nodeId,
359-
fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isInitiator), // we make sure that initiator and non-initiator key path end differently
360+
fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isChannelOpener), // we make sure that initiator and non-initiator key path end differently
360361
dustLimit = nodeParams.dustLimit,
361362
maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat,
362363
htlcMinimum = nodeParams.htlcMinimum,
363364
toSelfDelay = nodeParams.toRemoteDelayBlocks, // we choose their delay
364365
maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs,
365-
isInitiator = isInitiator,
366+
isChannelOpener = isChannelOpener,
367+
paysCommitTxFees = payCommitTxFees,
366368
defaultFinalScriptPubKey = nodeParams.keyManager.finalOnChainWallet.pubkeyScript(addressIndex = 0), // the default closing address is the same for all channels
367369
features = nodeParams.features.initFeatures()
368370
)
369371

372+
// The node responsible for the commit tx fees is also the node paying the mutual close fees.
373+
// The other node's balance may be empty, which wouldn't allow them to pay the closing fees.
374+
val paysClosingFees: Boolean = paysCommitTxFees
375+
370376
fun channelKeys(keyManager: KeyManager) = keyManager.channelKeys(fundingKeyPath)
371377
}
372378

@@ -384,10 +390,11 @@ data class RemoteParams(
384390
val features: Features
385391
)
386392

387-
object ChannelFlags {
388-
const val AnnounceChannel = 0x01.toByte()
389-
const val Empty = 0x00.toByte()
390-
}
393+
/**
394+
* The [nonInitiatorPaysCommitFees] parameter can be set to true when the sender wants the receiver to pay the commitment transaction fees.
395+
* This is not part of the BOLTs and won't be needed anymore once commitment transactions don't pay any on-chain fees.
396+
*/
397+
data class ChannelFlags(val announceChannel: Boolean, val nonInitiatorPaysCommitFees: Boolean)
391398

392399
data class ClosingTxProposed(val unsignedTx: ClosingTx, val localClosingSigned: ClosingSigned)
393400

@@ -399,13 +406,17 @@ data class ChannelManagementFees(val miningFee: Satoshi, val serviceFee: Satoshi
399406
val total: Satoshi = miningFee + serviceFee
400407
}
401408

402-
/** Reason for creating a new channel or a splice. */
409+
/** Reason for creating a new channel or splicing into an existing channel. */
403410
// @formatter:off
404411
sealed class Origin {
412+
/** Amount of the origin payment, before fees are paid. */
405413
abstract val amount: MilliSatoshi
406-
abstract val serviceFee: MilliSatoshi
407-
abstract val miningFee: Satoshi
408-
data class PayToOpenOrigin(val paymentHash: ByteVector32, override val serviceFee: MilliSatoshi, override val miningFee: Satoshi, override val amount: MilliSatoshi) : Origin()
409-
data class PleaseOpenChannelOrigin(val requestId: ByteVector32, override val serviceFee: MilliSatoshi, override val miningFee: Satoshi, override val amount: MilliSatoshi) : Origin()
414+
/** Fees applied for the channel funding transaction. */
415+
abstract val fees: ChannelManagementFees
416+
417+
data class OnChainWallet(val inputs: Set<OutPoint>, override val amount: MilliSatoshi, override val fees: ChannelManagementFees) : Origin()
418+
data class OffChainPayment(val paymentPreimage: ByteVector32, override val amount: MilliSatoshi, override val fees: ChannelManagementFees) : Origin() {
419+
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage).byteVector32()
420+
}
410421
}
411422
// @formatter:on

0 commit comments

Comments
 (0)