Skip to content

Commit e307660

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 319dcce commit e307660

File tree

91 files changed

+895
-842
lines changed

Some content is hidden

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

91 files changed

+895
-842
lines changed

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

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

33
import fr.acinq.bitcoin.ByteVector32
4+
import fr.acinq.bitcoin.OutPoint
45
import fr.acinq.bitcoin.Satoshi
6+
import fr.acinq.lightning.blockchain.electrum.WalletState
57
import fr.acinq.lightning.channel.InteractiveTxParams
68
import fr.acinq.lightning.channel.SharedFundingInput
9+
import fr.acinq.lightning.channel.TransactionFees
710
import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
811
import fr.acinq.lightning.channel.states.Normal
912
import fr.acinq.lightning.channel.states.WaitForFundingCreated
1013
import fr.acinq.lightning.db.IncomingPayment
1114
import fr.acinq.lightning.utils.sum
12-
import fr.acinq.lightning.wire.Node
13-
import fr.acinq.lightning.wire.PleaseOpenChannel
1415
import kotlinx.coroutines.CompletableDeferred
1516

1617
sealed interface NodeEvents
1718

1819
sealed interface SwapInEvents : NodeEvents {
19-
data class Requested(val req: PleaseOpenChannel) : SwapInEvents
20-
data class Accepted(val requestId: ByteVector32, val serviceFee: MilliSatoshi, val miningFee: Satoshi) : SwapInEvents
20+
data class Requested(val walletInputs: List<WalletState.Utxo>) : SwapInEvents {
21+
val totalAmount: Satoshi = walletInputs.map { it.amount }.sum()
22+
}
23+
data class Accepted(val inputs: Set<OutPoint>, val amount: Satoshi, val fees: TransactionFees) : SwapInEvents {
24+
val receivedAmount: Satoshi = amount - fees.total
25+
}
2126
}
2227

2328
sealed interface ChannelEvents : NodeEvents {
@@ -27,6 +32,7 @@ sealed interface ChannelEvents : NodeEvents {
2732
}
2833

2934
sealed interface LiquidityEvents : NodeEvents {
35+
/** Amount of the liquidity event, before fees are paid. */
3036
val amount: MilliSatoshi
3137
val fee: MilliSatoshi
3238
val source: Source
@@ -42,8 +48,7 @@ sealed interface LiquidityEvents : NodeEvents {
4248
data object ChannelInitializing : Reason()
4349
}
4450
}
45-
46-
data class ApprovalRequested(override val amount: MilliSatoshi, override val fee: MilliSatoshi, override val source: Source, val replyTo: CompletableDeferred<Boolean>) : LiquidityEvents
51+
data class Accepted(override val amount: MilliSatoshi, override val fee: MilliSatoshi, override val source: Source) : LiquidityEvents
4752
}
4853

4954
/** This is useful on iOS to ask the OS for time to finish some sensitive tasks. */
@@ -56,7 +61,6 @@ sealed interface SensitiveTaskEvents : NodeEvents {
5661
}
5762
data class TaskStarted(val id: TaskIdentifier) : SensitiveTaskEvents
5863
data class TaskEnded(val id: TaskIdentifier) : SensitiveTaskEvents
59-
6064
}
6165

6266
/** 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ data class NodeParams(
229229
maxPaymentAttempts = 5,
230230
zeroConfPeers = emptySet(),
231231
paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(75), CltvExpiryDelta(200)),
232-
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(LiquidityPolicy.Auto(maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)),
232+
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(LiquidityPolicy.Auto(inboundLiquidityTarget = null, maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)),
233233
minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA,
234234
maxFinalCltvExpiryDelta = CltvExpiryDelta(360),
235235
bolt12invoiceExpiry = 60.seconds

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

Lines changed: 5 additions & 12 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.OpenOrSpliceChannel
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,19 +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): OpenOrSpliceChannel? = 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.all.filter { cmd.trustedTxs.contains(it.outPoint.txid) })
43-
addAll(availableWallet.deeplyConfirmed.filter { Transaction.write(it.previousTx.stripInputWitnesses()).size < 65_000 })
44-
}.toList()
37+
val utxos = availableWallet.deeplyConfirmed.filter { Transaction.write(it.previousTx.stripInputWitnesses()).size < 65_000 }
4538
if (utxos.balance > 0.sat) {
4639
logger.info { "swap-in wallet: requesting channel using ${utxos.size} utxos with balance=${utxos.balance}" }
4740
reservedUtxos = reservedUtxos.union(utxos.map { it.outPoint })
48-
RequestChannelOpen(Lightning.randomBytes32(), utxos)
41+
OpenOrSpliceChannel(utxos)
4942
} else {
5043
null
5144
}

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: 5 additions & 10 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
}
@@ -83,22 +84,16 @@ sealed class ChannelCommand {
8384
sealed class Commitment : ChannelCommand() {
8485
object Sign : Commitment(), ForbiddenDuringSplice
8586
data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice, ForbiddenDuringQuiescence
86-
object CheckHtlcTimeout : Commitment()
87+
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

9293
data class SpliceIn(val walletInputs: List<WalletState.Utxo>, val pushAmount: MilliSatoshi = 0.msat)
9394
data class SpliceOut(val amount: Satoshi, val scriptPubKey: ByteVector)
9495
}
9596

96-
/**
97-
* @param miningFee on-chain fee that will be paid for the splice transaction.
98-
* @param serviceFee service-fee that will be paid to the remote node for a service they provide with the splice transaction.
99-
*/
100-
data class Fees(val miningFee: Satoshi, val serviceFee: MilliSatoshi)
101-
10297
sealed class Response {
10398
/**
10499
* This response doesn't fully guarantee that the splice will confirm, because our peer may potentially double-spend

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

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import fr.acinq.lightning.channel.Helpers.publishIfNeeded
99
import fr.acinq.lightning.channel.Helpers.watchConfirmedIfNeeded
1010
import fr.acinq.lightning.channel.Helpers.watchSpentIfNeeded
1111
import fr.acinq.lightning.crypto.KeyManager
12-
import fr.acinq.lightning.logging.*
12+
import fr.acinq.lightning.logging.LoggingContext
1313
import fr.acinq.lightning.transactions.Scripts
1414
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.*
1515
import fr.acinq.lightning.wire.ClosingSigned
@@ -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 payCommitTxFees: 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+
payCommitTxFees = 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 payClosingFees: Boolean = payCommitTxFees
375+
370376
fun channelKeys(keyManager: KeyManager) = keyManager.channelKeys(fundingKeyPath)
371377
}
372378

@@ -384,20 +390,33 @@ 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

394-
/** Reason for creating a new channel or a splice. */
401+
/**
402+
* @param miningFee fee paid to miners for the underlying on-chain transaction.
403+
* @param serviceFee fee paid to our peer for any service provided with the on-chain transaction.
404+
*/
405+
data class TransactionFees(val miningFee: Satoshi, val serviceFee: Satoshi) {
406+
val total: Satoshi = miningFee + serviceFee
407+
}
408+
409+
/** Reason for creating a new channel or splicing into an existing channel. */
395410
// @formatter:off
396411
sealed class Origin {
412+
/** Amount of the origin payment, before fees are paid. */
397413
abstract val amount: MilliSatoshi
398-
abstract val serviceFee: MilliSatoshi
399-
abstract val miningFee: Satoshi
400-
data class PayToOpenOrigin(val paymentHash: ByteVector32, override val serviceFee: MilliSatoshi, override val miningFee: Satoshi, override val amount: MilliSatoshi) : Origin()
401-
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: TransactionFees
416+
417+
data class OnChainWallet(val inputs: Set<OutPoint>, override val amount: MilliSatoshi, override val fees: TransactionFees) : Origin()
418+
data class OffChainPayment(val paymentPreimage: ByteVector32, override val amount: MilliSatoshi, override val fees: TransactionFees) : Origin() {
419+
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage).byteVector32()
420+
}
402421
}
403422
// @formatter:on

0 commit comments

Comments
 (0)