Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object Quiescence : Feature() {
override val rfcName get() = "option_quiescence"
override val mandatory get() = 34
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object ChannelType : Feature() {
override val rfcName get() = "option_channel_type"
Expand Down Expand Up @@ -185,15 +192,15 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

/** This feature bit should be activated when a node accepts on-the-fly channel creation. */
/** DEPRECATED: this feature bit was used for the legacy pay-to-open protocol. */
@Serializable
object PayToOpenClient : Feature() {
override val rfcName get() = "pay_to_open_client"
override val mandatory get() = 136
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
}

/** This feature bit should be activated when a node supports opening channels on-the-fly when liquidity is missing to receive a payment. */
/** DEPRECATED: this feature bit was used for the legacy pay-to-open protocol. */
@Serializable
object PayToOpenProvider : Feature() {
override val rfcName get() = "pay_to_open_provider"
Expand Down Expand Up @@ -249,13 +256,30 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
}

/** This feature bit should be activated when a node accepts on-the-fly funding using the [MaybeAddHtlc] message. */
@Serializable
object Quiescence : Feature() {
override val rfcName get() = "option_quiescence"
override val mandatory get() = 34
object OnTheFlyFundingClient : Feature() {
override val rfcName get() = "on_the_fly_funding_client"
override val mandatory get() = 156
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
}

/** This feature bit should be activated when a node supports on-the-fly funding when liquidity is missing to receive a payment. */
@Serializable
object OnTheFlyFundingProvider : Feature() {
override val rfcName get() = "on_the_fly_funding_provider"
override val mandatory get() = 158
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

/** This feature bit should be activated when a node accepts exchanging payment preimages for a fee credit using the [AddFeeCredit] message. */
@Serializable
object OnTheFlyFundingFeeCredit : Feature() {
override val rfcName: String = "on_the_fly_fee_credit"
override val mandatory get() = 160
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
}

}

@Serializable
Expand Down Expand Up @@ -321,6 +345,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.AnchorOutputs,
Feature.ShutdownAnySegwit,
Feature.DualFunding,
Feature.Quiescence,
Feature.ChannelType,
Feature.PaymentMetadata,
Feature.TrampolinePayment,
Expand All @@ -336,7 +361,9 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.ChannelBackupClient,
Feature.ChannelBackupProvider,
Feature.ExperimentalSplice,
Feature.Quiescence
Feature.OnTheFlyFundingClient,
Feature.OnTheFlyFundingProvider,
Feature.OnTheFlyFundingFeeCredit,
)

operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray())
Expand Down
23 changes: 14 additions & 9 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.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.serviceFee - fees.miningFee
}
}

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 @@ -39,11 +44,12 @@ sealed interface LiquidityEvents : NodeEvents {
data class OverAbsoluteFee(val maxAbsoluteFee: Satoshi) : TooExpensive()
data class OverRelativeFee(val maxRelativeFeeBasisPoints: Int) : TooExpensive()
}
data object ChannelInitializing : Reason()
data object ChannelFundingInProgress : Reason()
data class MissingOffChainAmountTooLow(val missingOffChainAmount: MilliSatoshi) : Reason()
data class ChannelFundingCancelled(val paymentHash: ByteVector32) : 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 +62,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
6 changes: 3 additions & 3 deletions src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -182,16 +182,16 @@ data class NodeParams(
Feature.StaticRemoteKey to FeatureSupport.Mandatory,
Feature.AnchorOutputs to FeatureSupport.Optional, // can't set Mandatory because peers prefers AnchorOutputsZeroFeeHtlcTx
Feature.DualFunding to FeatureSupport.Mandatory,
Feature.Quiescence to FeatureSupport.Mandatory,
Feature.ShutdownAnySegwit to FeatureSupport.Mandatory,
Feature.ChannelType to FeatureSupport.Mandatory,
Feature.PaymentMetadata to FeatureSupport.Optional,
Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional,
Feature.ZeroReserveChannels to FeatureSupport.Optional,
Feature.WakeUpNotificationClient to FeatureSupport.Optional,
Feature.PayToOpenClient to FeatureSupport.Optional,
Feature.ChannelBackupClient to FeatureSupport.Optional,
Feature.ExperimentalSplice to FeatureSupport.Optional,
Feature.Quiescence to FeatureSupport.Mandatory
Feature.OnTheFlyFundingClient to FeatureSupport.Optional,
),
dustLimit = 546.sat,
maxRemoteDustLimit = 600.sat,
Expand Down Expand Up @@ -219,6 +219,6 @@ 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))
)
}
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
14 changes: 10 additions & 4 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt
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 @@ -78,8 +78,14 @@ sealed class ChannelAction {
abstract val origin: Origin?
abstract val txId: TxId
abstract val localInputs: Set<OutPoint>
/** @param amount amount received after deducing service and mining fees. */
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()
/** @param amount amount received after deducing service and mining fees. */
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()
data class Cancelled(override val origin: Origin.OffChainPayment) : StoreIncomingPayment() {
override val localInputs: Set<OutPoint> = setOf()
override val txId: TxId = TxId(ByteVector32.Zeroes)
}
}
/** Payment sent through on-chain operations (channel close or splice-out) */
sealed class StoreOutgoingPayment : Storage() {
Expand Down Expand Up @@ -128,8 +134,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
}
18 changes: 7 additions & 11 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 All @@ -47,7 +48,8 @@ sealed class ChannelCommand {
val walletInputs: List<WalletState.Utxo>,
val localParams: LocalParams,
val channelConfig: ChannelConfig,
val remoteInit: InitMessage
val remoteInit: InitMessage,
val leaseRate: LiquidityAds.LeaseRate?,
) : Init()

data class Restore(val state: PersistedChannelState) : Init()
Expand Down Expand Up @@ -83,22 +85,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
Loading