Skip to content

Commit

Permalink
Remove please_open_channel
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast committed Jun 4, 2024
1 parent 319dcce commit e307660
Show file tree
Hide file tree
Showing 91 changed files with 895 additions and 842 deletions.
18 changes: 11 additions & 7 deletions src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package fr.acinq.lightning

import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.OutPoint
import fr.acinq.bitcoin.Satoshi
import fr.acinq.lightning.blockchain.electrum.WalletState
import fr.acinq.lightning.channel.InteractiveTxParams
import fr.acinq.lightning.channel.SharedFundingInput
import fr.acinq.lightning.channel.TransactionFees
import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
import fr.acinq.lightning.channel.states.Normal
import fr.acinq.lightning.channel.states.WaitForFundingCreated
import fr.acinq.lightning.db.IncomingPayment
import fr.acinq.lightning.utils.sum
import fr.acinq.lightning.wire.Node
import fr.acinq.lightning.wire.PleaseOpenChannel
import kotlinx.coroutines.CompletableDeferred

sealed interface NodeEvents

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

sealed interface ChannelEvents : NodeEvents {
Expand All @@ -27,6 +32,7 @@ sealed interface ChannelEvents : NodeEvents {
}

sealed interface LiquidityEvents : NodeEvents {
/** Amount of the liquidity event, before fees are paid. */
val amount: MilliSatoshi
val fee: MilliSatoshi
val source: Source
Expand All @@ -42,8 +48,7 @@ sealed interface LiquidityEvents : NodeEvents {
data object ChannelInitializing : Reason()
}
}

data class ApprovalRequested(override val amount: MilliSatoshi, override val fee: MilliSatoshi, override val source: Source, val replyTo: CompletableDeferred<Boolean>) : LiquidityEvents
data class Accepted(override val amount: MilliSatoshi, override val fee: MilliSatoshi, override val source: Source) : LiquidityEvents
}

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

}

/** 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. */
Expand Down
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ data class NodeParams(
maxPaymentAttempts = 5,
zeroConfPeers = emptySet(),
paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(75), CltvExpiryDelta(200)),
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(LiquidityPolicy.Auto(maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)),
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(LiquidityPolicy.Auto(inboundLiquidityTarget = null, maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)),
minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA,
maxFinalCltvExpiryDelta = CltvExpiryDelta(360),
bolt12invoiceExpiry = 60.seconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@ package fr.acinq.lightning.blockchain.electrum

import fr.acinq.bitcoin.OutPoint
import fr.acinq.bitcoin.Transaction
import fr.acinq.bitcoin.TxId
import fr.acinq.lightning.Lightning
import fr.acinq.lightning.SwapInParams
import fr.acinq.lightning.channel.FundingContributions.Companion.stripInputWitnesses
import fr.acinq.lightning.channel.LocalFundingStatus
import fr.acinq.lightning.channel.RbfStatus
import fr.acinq.lightning.channel.SignedSharedTransaction
import fr.acinq.lightning.channel.SpliceStatus
import fr.acinq.lightning.channel.states.*
import fr.acinq.lightning.io.RequestChannelOpen
import fr.acinq.lightning.io.OpenOrSpliceChannel
import fr.acinq.lightning.logging.MDCLogger
import fr.acinq.lightning.utils.sat

internal sealed class SwapInCommand {
data class TrySwapIn(val currentBlockHeight: Int, val wallet: WalletState, val swapInParams: SwapInParams, val trustedTxs: Set<TxId>) : SwapInCommand()
data class TrySwapIn(val currentBlockHeight: Int, val wallet: WalletState, val swapInParams: SwapInParams) : SwapInCommand()
data class UnlockWalletInputs(val inputs: Set<OutPoint>) : SwapInCommand()
}

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

internal fun process(cmd: SwapInCommand): RequestChannelOpen? = when (cmd) {
internal fun process(cmd: SwapInCommand): OpenOrSpliceChannel? = when (cmd) {
is SwapInCommand.TrySwapIn -> {
val availableWallet = cmd.wallet.withoutReservedUtxos(reservedUtxos).withConfirmations(cmd.currentBlockHeight, cmd.swapInParams)
logger.info { "swap-in wallet balance: deeplyConfirmed=${availableWallet.deeplyConfirmed.balance}, weaklyConfirmed=${availableWallet.weaklyConfirmed.balance}, unconfirmed=${availableWallet.unconfirmed.balance}" }
val utxos = buildSet {
// some utxos may be used for swap-in even if they are not confirmed, for example when migrating from the legacy phoenix android app
addAll(availableWallet.all.filter { cmd.trustedTxs.contains(it.outPoint.txid) })
addAll(availableWallet.deeplyConfirmed.filter { Transaction.write(it.previousTx.stripInputWitnesses()).size < 65_000 })
}.toList()
val utxos = availableWallet.deeplyConfirmed.filter { Transaction.write(it.previousTx.stripInputWitnesses()).size < 65_000 }
if (utxos.balance > 0.sat) {
logger.info { "swap-in wallet: requesting channel using ${utxos.size} utxos with balance=${utxos.balance}" }
reservedUtxos = reservedUtxos.union(utxos.map { it.outPoint })
RequestChannelOpen(Lightning.randomBytes32(), utxos)
OpenOrSpliceChannel(utxos)
} else {
null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package fr.acinq.lightning.channel

import fr.acinq.bitcoin.*
import fr.acinq.lightning.ChannelEvents
import fr.acinq.lightning.CltvExpiry
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.NodeEvents
import fr.acinq.lightning.blockchain.Watch
import fr.acinq.lightning.channel.states.PersistedChannelState
import fr.acinq.lightning.db.ChannelClosingType
Expand Down Expand Up @@ -79,7 +79,7 @@ sealed class ChannelAction {
abstract val txId: TxId
abstract val localInputs: Set<OutPoint>
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()
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()
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()
}
/** Payment sent through on-chain operations (channel close or splice-out) */
sealed class StoreOutgoingPayment : Storage() {
Expand Down Expand Up @@ -128,8 +128,8 @@ sealed class ChannelAction {
}
}

data class EmitEvent(val event: ChannelEvents) : ChannelAction()
data class EmitEvent(val event: NodeEvents) : ChannelAction()

object Disconnect : ChannelAction()
data object Disconnect : ChannelAction()
// @formatter:on
}
15 changes: 5 additions & 10 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ sealed class ChannelCommand {
val fundingTxFeerate: FeeratePerKw,
val localParams: LocalParams,
val remoteInit: InitMessage,
val channelFlags: Byte,
val channelFlags: ChannelFlags,
val channelConfig: ChannelConfig,
val channelType: ChannelType.SupportedChannelType,
val channelOrigin: Origin? = null
val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?,
val channelOrigin: Origin?,
) : Init() {
fun temporaryChannelId(keyManager: KeyManager): ByteVector32 = keyManager.channelKeys(localParams.fundingKeyPath).temporaryChannelId
}
Expand Down Expand Up @@ -83,22 +84,16 @@ sealed class ChannelCommand {
sealed class Commitment : ChannelCommand() {
object Sign : Commitment(), ForbiddenDuringSplice
data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice, ForbiddenDuringQuiescence
object CheckHtlcTimeout : Commitment()
data object CheckHtlcTimeout : Commitment()
sealed class Splice : Commitment() {
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() {
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() {
val pushAmount: MilliSatoshi = spliceIn?.pushAmount ?: 0.msat
val spliceOutputs: List<TxOut> = spliceOut?.let { listOf(TxOut(it.amount, it.scriptPubKey)) } ?: emptyList()

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

/**
* @param miningFee on-chain fee that will be paid for the splice transaction.
* @param serviceFee service-fee that will be paid to the remote node for a service they provide with the splice transaction.
*/
data class Fees(val miningFee: Satoshi, val serviceFee: MilliSatoshi)

sealed class Response {
/**
* This response doesn't fully guarantee that the splice will confirm, because our peer may potentially double-spend
Expand Down
47 changes: 33 additions & 14 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import fr.acinq.lightning.channel.Helpers.publishIfNeeded
import fr.acinq.lightning.channel.Helpers.watchConfirmedIfNeeded
import fr.acinq.lightning.channel.Helpers.watchSpentIfNeeded
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.logging.*
import fr.acinq.lightning.logging.LoggingContext
import fr.acinq.lightning.transactions.Scripts
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.*
import fr.acinq.lightning.wire.ClosingSigned
Expand Down Expand Up @@ -350,23 +350,29 @@ data class LocalParams(
val htlcMinimum: MilliSatoshi,
val toSelfDelay: CltvExpiryDelta,
val maxAcceptedHtlcs: Int,
val isInitiator: Boolean,
val isChannelOpener: Boolean,
val payCommitTxFees: Boolean,
val defaultFinalScriptPubKey: ByteVector,
val features: Features
) {
constructor(nodeParams: NodeParams, isInitiator: Boolean): this(
constructor(nodeParams: NodeParams, isChannelOpener: Boolean, payCommitTxFees: Boolean) : this(
nodeId = nodeParams.nodeId,
fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isInitiator), // we make sure that initiator and non-initiator key path end differently
fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isChannelOpener), // we make sure that initiator and non-initiator key path end differently
dustLimit = nodeParams.dustLimit,
maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat,
htlcMinimum = nodeParams.htlcMinimum,
toSelfDelay = nodeParams.toRemoteDelayBlocks, // we choose their delay
maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs,
isInitiator = isInitiator,
isChannelOpener = isChannelOpener,
payCommitTxFees = payCommitTxFees,
defaultFinalScriptPubKey = nodeParams.keyManager.finalOnChainWallet.pubkeyScript(addressIndex = 0), // the default closing address is the same for all channels
features = nodeParams.features.initFeatures()
)

// The node responsible for the commit tx fees is also the node paying the mutual close fees.
// The other node's balance may be empty, which wouldn't allow them to pay the closing fees.
val payClosingFees: Boolean = payCommitTxFees

fun channelKeys(keyManager: KeyManager) = keyManager.channelKeys(fundingKeyPath)
}

Expand All @@ -384,20 +390,33 @@ data class RemoteParams(
val features: Features
)

object ChannelFlags {
const val AnnounceChannel = 0x01.toByte()
const val Empty = 0x00.toByte()
}
/**
* The [nonInitiatorPaysCommitFees] parameter can be 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.
*/
data class ChannelFlags(val announceChannel: Boolean, val nonInitiatorPaysCommitFees: Boolean)

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

/** Reason for creating a new channel or a splice. */
/**
* @param miningFee fee paid to miners for the underlying on-chain transaction.
* @param serviceFee fee paid to our peer for any service provided with the on-chain transaction.
*/
data class TransactionFees(val miningFee: Satoshi, val serviceFee: Satoshi) {
val total: Satoshi = miningFee + serviceFee
}

/** Reason for creating a new channel or splicing into an existing channel. */
// @formatter:off
sealed class Origin {
/** Amount of the origin payment, before fees are paid. */
abstract val amount: MilliSatoshi
abstract val serviceFee: MilliSatoshi
abstract val miningFee: Satoshi
data class PayToOpenOrigin(val paymentHash: ByteVector32, override val serviceFee: MilliSatoshi, override val miningFee: Satoshi, override val amount: MilliSatoshi) : Origin()
data class PleaseOpenChannelOrigin(val requestId: ByteVector32, override val serviceFee: MilliSatoshi, override val miningFee: Satoshi, override val amount: MilliSatoshi) : Origin()
/** Fees applied for the channel funding transaction. */
abstract val fees: TransactionFees

data class OnChainWallet(val inputs: Set<OutPoint>, override val amount: MilliSatoshi, override val fees: TransactionFees) : Origin()
data class OffChainPayment(val paymentPreimage: ByteVector32, override val amount: MilliSatoshi, override val fees: TransactionFees) : Origin() {
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage).byteVector32()
}
}
// @formatter:on
Loading

0 comments on commit e307660

Please sign in to comment.