Skip to content
Merged
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
13 changes: 11 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

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

}

@Serializable
Expand Down Expand Up @@ -345,7 +352,8 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.ChannelBackupClient,
Feature.ChannelBackupProvider,
Feature.ExperimentalSplice,
Feature.OnTheFlyFunding
Feature.OnTheFlyFunding,
Feature.FundingFeeCredit
)

operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray())
Expand Down Expand Up @@ -378,7 +386,8 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
Feature.TrampolinePayment to listOf(Feature.PaymentSecret),
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret),
Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice)
Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice),
Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding)
)

class FeatureException(message: String) : IllegalArgumentException(message)
Expand Down
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ sealed interface LiquidityEvents : NodeEvents {
}
data object ChannelFundingInProgress : Reason()
data object NoMatchingFundingRate : Reason()
data class MissingOffChainAmountTooLow(val missingOffChainAmount: MilliSatoshi) : Reason()
data class MissingOffChainAmountTooLow(val missingOffChainAmount: MilliSatoshi, val currentFeeCredit: MilliSatoshi) : Reason()
data class TooManyParts(val parts: Int) : Reason()
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,15 @@ data class NodeParams(
maxPaymentAttempts = 5,
zeroConfPeers = emptySet(),
paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(75), CltvExpiryDelta(200)),
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(LiquidityPolicy.Auto(inboundLiquidityTarget = null, 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,
maxAllowedFeeCredit = 0.msat
)
),
minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA,
maxFinalCltvExpiryDelta = CltvExpiryDelta(360),
bolt12invoiceExpiry = 60.seconds,
Expand Down
12 changes: 10 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,15 @@ 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.RequestFunding?, val feerate: FeeratePerKw, val origins: List<Origin>) : Splice() {
data class Request(
val replyTo: CompletableDeferred<Response>,
val spliceIn: SpliceIn?,
val spliceOut: SpliceOut?,
val requestRemoteFunding: LiquidityAds.RequestFunding?,
val currentFeeCredit: MilliSatoshi,
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 All @@ -110,7 +118,7 @@ sealed class ChannelCommand {
) : Response()

sealed class Failure : Response() {
data object InsufficientFunds : Failure()
data class InsufficientFunds(val balanceAfterFees: MilliSatoshi, val liquidityFees: MilliSatoshi, val currentFeeCredit: MilliSatoshi) : Failure()
data object InvalidSpliceOutPubKeyScript : Failure()
data object SpliceAlreadyInProgress : Failure()
data object ConcurrentRemoteSplice : Failure()
Expand Down
62 changes: 44 additions & 18 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ data class InteractiveTxParams(
val fundingTxIndex = (sharedInput as? SharedFundingInput.Multisig2of2)?.let { it.fundingTxIndex + 1 } ?: 0
return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey)
}

fun liquidityFees(purchase: LiquidityAds.Purchase?): MilliSatoshi = purchase?.let { l ->
val fees = when (l) {
is LiquidityAds.Purchase.Standard -> l.fees.total.toMilliSatoshi()
is LiquidityAds.Purchase.WithFeeCredit -> l.fees.total.toMilliSatoshi() - l.feeCreditUsed
}
when (l.paymentDetails) {
is LiquidityAds.PaymentDetails.FromChannelBalance -> if (isInitiator) fees else -fees
is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> if (isInitiator) fees else -fees
// Fees will be paid later, from relayed HTLCs.
is LiquidityAds.PaymentDetails.FromFutureHtlc -> 0.msat
is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> 0.msat
}
} ?: 0.msat
}

sealed class InteractiveTxInput {
Expand Down Expand Up @@ -209,7 +223,12 @@ sealed class InteractiveTxOutput {
*/
data class Remote(override val serialId: Long, override val amount: Satoshi, override val pubkeyScript: ByteVector) : InteractiveTxOutput(), Incoming

/** The shared output can be added by us or by our peer, depending on who initiated the protocol. */
/**
* The shared output can be added by us or by our peer, depending on who initiated the protocol.
*
* @param localAmount amount contributed by us, before applying push_amount and (optional) liquidity fees: this is different from the channel balance.
* @param remoteAmount amount contributed by our peer, before applying push_amount and (optional) liquidity fees: this is different from the channel balance.
*/
data class Shared(override val serialId: Long, override val pubkeyScript: ByteVector, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi, val htlcAmount: MilliSatoshi) : InteractiveTxOutput(), Incoming, Outgoing {
// Note that the truncation is a no-op: the sum of balances in a channel must be a satoshi amount.
override val amount: Satoshi = (localAmount + remoteAmount + htlcAmount).truncateToSatoshi()
Expand Down Expand Up @@ -246,8 +265,17 @@ data class FundingContributions(val inputs: List<InteractiveTxInput.Outgoing>, v
/**
* @param walletInputs 2-of-2 swap-in wallet inputs.
*/
fun create(channelKeys: KeyManager.ChannelKeys, swapInKeys: KeyManager.SwapInOnChainKeys, params: InteractiveTxParams, walletInputs: List<WalletState.Utxo>): Either<FundingContributionFailure, FundingContributions> =
create(channelKeys, swapInKeys, params, null, walletInputs, listOf())
fun create(
channelKeys: KeyManager.ChannelKeys,
swapInKeys: KeyManager.SwapInOnChainKeys,
params: InteractiveTxParams,
walletInputs: List<WalletState.Utxo>,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
liquidityPurchase: LiquidityAds.Purchase?
): Either<FundingContributionFailure, FundingContributions> {
return create(channelKeys, swapInKeys, params, null, walletInputs, listOf(), localPushAmount, remotePushAmount, liquidityPurchase)
}

/**
* @param sharedUtxo previous input shared between the two participants (e.g. previous funding output when splicing) and our corresponding balance.
Expand All @@ -262,6 +290,9 @@ data class FundingContributions(val inputs: List<InteractiveTxInput.Outgoing>, v
sharedUtxo: Pair<SharedFundingInput, SharedFundingInputBalances>?,
walletInputs: List<WalletState.Utxo>,
localOutputs: List<TxOut>,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
liquidityPurchase: LiquidityAds.Purchase?,
changePubKey: PublicKey? = null
): Either<FundingContributionFailure, FundingContributions> {
walletInputs.forEach { utxo ->
Expand All @@ -277,14 +308,18 @@ data class FundingContributions(val inputs: List<InteractiveTxInput.Outgoing>, v
return Either.Left(FundingContributionFailure.NotEnoughFunding(params.localContribution, localOutputs.map { it.amount }.sum(), totalAmountIn))
}

val nextLocalBalance = (sharedUtxo?.second?.toLocal ?: 0.msat) + params.localContribution.toMilliSatoshi()
val nextRemoteBalance = (sharedUtxo?.second?.toRemote ?: 0.msat) + params.remoteContribution.toMilliSatoshi()
if (nextLocalBalance < 0.msat || nextRemoteBalance < 0.msat) {
return Either.Left(FundingContributionFailure.InvalidFundingBalances(params.fundingAmount, nextLocalBalance, nextRemoteBalance))
val liquidityFees = params.liquidityFees(liquidityPurchase)
val nextLocalBalanceBeforePush = (sharedUtxo?.second?.toLocal ?: 0.msat) + params.localContribution.toMilliSatoshi()
val nextLocalBalanceAfterPush = (sharedUtxo?.second?.toLocal ?: 0.msat) + params.localContribution.toMilliSatoshi() - localPushAmount + remotePushAmount - liquidityFees
val nextRemoteBalanceBeforePush = (sharedUtxo?.second?.toRemote ?: 0.msat) + params.remoteContribution.toMilliSatoshi()
val nextRemoteBalanceAfterPush = (sharedUtxo?.second?.toRemote ?: 0.msat) + params.remoteContribution.toMilliSatoshi() + localPushAmount - remotePushAmount + liquidityFees
if (nextLocalBalanceAfterPush < 0.msat || nextRemoteBalanceAfterPush < 0.msat) {
return Either.Left(FundingContributionFailure.InvalidFundingBalances(params.fundingAmount, nextLocalBalanceAfterPush, nextRemoteBalanceAfterPush))
}

val fundingPubkeyScript = params.fundingPubkeyScript(channelKeys)
val sharedOutput = listOf(InteractiveTxOutput.Shared(0, fundingPubkeyScript, nextLocalBalance, nextRemoteBalance, sharedUtxo?.second?.toHtlcs ?: 0.msat))
// We use local and remote balances before amounts are pushed to allow computing the local and remote mining fees.
val sharedOutput = listOf(InteractiveTxOutput.Shared(0, fundingPubkeyScript, nextLocalBalanceBeforePush, nextRemoteBalanceBeforePush, sharedUtxo?.second?.toHtlcs ?: 0.msat))
val nonChangeOutputs = localOutputs.map { o -> InteractiveTxOutput.Local.NonChange(0, o.amount, o.publicKeyScript) }
val changeOutput = when (changePubKey) {
null -> listOf()
Expand Down Expand Up @@ -1068,16 +1103,7 @@ data class InteractiveTxSigningSession(
val channelKeys = channelParams.localParams.channelKeys(keyManager)
val unsignedTx = sharedTx.buildUnsignedTx()
val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) }
val liquidityFees = liquidityPurchase?.let { l ->
val fees = l.fees.total.toMilliSatoshi()
when (l.paymentDetails) {
is LiquidityAds.PaymentDetails.FromChannelBalance -> if (fundingParams.isInitiator) fees else -fees
is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> if (fundingParams.isInitiator) fees else -fees
// Fees will be paid later, from relayed HTLCs.
is LiquidityAds.PaymentDetails.FromFutureHtlc -> 0.msat
is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> 0.msat
}
} ?: 0.msat
val liquidityFees = fundingParams.liquidityFees(liquidityPurchase)
return Helpers.Funding.makeCommitTxs(
channelKeys,
channelParams.channelId,
Expand Down
Loading