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 14, 2024
1 parent c70401e commit 7c3d782
Show file tree
Hide file tree
Showing 92 changed files with 890 additions and 819 deletions.
19 changes: 11 additions & 8 deletions src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
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.ChannelManagementFees
import fr.acinq.lightning.channel.InteractiveTxParams
import fr.acinq.lightning.channel.SharedFundingInput
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: ChannelManagementFees) : SwapInEvents {
val receivedAmount: Satoshi = amount - fees.total
}
}

sealed interface ChannelEvents : NodeEvents {
Expand All @@ -27,6 +31,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 +47,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 +60,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
5 changes: 4 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import fr.acinq.lightning.payment.LiquidityPolicy
import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.sat
import fr.acinq.lightning.utils.toMilliSatoshi
import fr.acinq.lightning.wire.LiquidityAds
import fr.acinq.lightning.wire.OfferTypes
import io.ktor.utils.io.charsets.*
import io.ktor.utils.io.core.*
Expand Down Expand Up @@ -69,12 +70,14 @@ object DefaultSwapInParams {
* @param trampolineFees ordered list of trampoline fees to try when making an outgoing payment.
* @param invoiceDefaultRoutingFees default routing fees set in invoices when we don't have any channel.
* @param swapInParams parameters for swap-in transactions.
* @param leaseRate rate at which our peer sells their liquidity.
*/
data class WalletParams(
val trampolineNode: NodeUri,
val trampolineFees: List<TrampolineFees>,
val invoiceDefaultRoutingFees: InvoiceDefaultRoutingFees,
val swapInParams: SwapInParams,
val leaseRate: LiquidityAds.LeaseRate,
)

/**
Expand Down Expand Up @@ -229,7 +232,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.AddWalletInputsToChannel
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): AddWalletInputsToChannel? = 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)
AddWalletInputsToChannel(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
}
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 @@ -85,7 +86,7 @@ sealed class ChannelCommand {
data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice, ForbiddenDuringQuiescence
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()

Expand Down
37 changes: 24 additions & 13 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt
Original file line number Diff line number Diff line change
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 paysCommitTxFees: 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,
paysCommitTxFees = 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 paysClosingFees: Boolean = paysCommitTxFees

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

Expand All @@ -384,10 +390,11 @@ 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)

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

/** Reason for creating a new channel or a splice. */
/** 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: ChannelManagementFees

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

0 comments on commit 7c3d782

Please sign in to comment.