Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add quiescence #558

Closed
wants to merge 1 commit into from
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
8 changes: 8 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
}

@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
Expand Down Expand Up @@ -320,6 +327,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.ChannelBackupClient,
Feature.ChannelBackupProvider,
Feature.ExperimentalSplice,
Feature.Quiescence
)

operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray())
Expand Down
1 change: 1 addition & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ data class NodeParams(
Feature.PayToOpenClient to FeatureSupport.Optional,
Feature.ChannelBackupClient to FeatureSupport.Optional,
Feature.ExperimentalSplice to FeatureSupport.Optional,
Feature.Quiescence to FeatureSupport.Mandatory
t-bast marked this conversation as resolved.
Show resolved Hide resolved
),
dustLimit = 546.sat,
maxRemoteDustLimit = 600.sat,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,7 @@ sealed class ChannelAction {
}

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

object Disconnect : ChannelAction()
// @formatter:on
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ sealed class ChannelCommand {
data class WatchReceived(val watch: WatchEvent) : ChannelCommand()

sealed interface ForbiddenDuringSplice
sealed interface ForbiddenDuringQuiescence
sealed class Htlc : ChannelCommand() {
data class Add(val amount: MilliSatoshi, val paymentHash: ByteVector32, val cltvExpiry: CltvExpiry, val onion: OnionRoutingPacket, val paymentId: UUID, val commit: Boolean = false) : Htlc(), ForbiddenDuringSplice
data class Add(val amount: MilliSatoshi, val paymentHash: ByteVector32, val cltvExpiry: CltvExpiry, val onion: OnionRoutingPacket, val paymentId: UUID, val commit: Boolean = false) : Htlc(), ForbiddenDuringSplice, ForbiddenDuringQuiescence

sealed class Settlement : Htlc(), ForbiddenDuringSplice {
sealed class Settlement : Htlc(), ForbiddenDuringSplice, ForbiddenDuringQuiescence {
abstract val id: Long

data class Fulfill(override val id: Long, val r: ByteVector32, val commit: Boolean = false) : Settlement()
Expand All @@ -81,7 +82,7 @@ sealed class ChannelCommand {

sealed class Commitment : ChannelCommand() {
object Sign : Commitment(), ForbiddenDuringSplice
data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice
data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice, ForbiddenDuringQuiescence
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() {
Expand Down Expand Up @@ -116,7 +117,8 @@ sealed class ChannelCommand {
object InsufficientFunds : Failure()
object InvalidSpliceOutPubKeyScript : Failure()
object SpliceAlreadyInProgress : Failure()
object ChannelNotIdle : Failure()
object ConcurrentRemoteSplice : Failure()
object ChannelNotQuiescent : Failure()
data class InvalidLiquidityAds(val reason: ChannelException) : Failure()
data class FundingFailure(val reason: FundingContributionFailure) : Failure()
object CannotStartSession : Failure()
Expand All @@ -130,7 +132,7 @@ sealed class ChannelCommand {
}

sealed class Close : ChannelCommand() {
data class MutualClose(val scriptPubKey: ByteVector?, val feerates: ClosingFeerates?) : Close()
data class MutualClose(val scriptPubKey: ByteVector?, val feerates: ClosingFeerates?) : Close(), ForbiddenDuringSplice, ForbiddenDuringQuiescence
object ForceClose : Close()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ data class InvalidRbfNonInitiator (override val channelId: Byte
data class InvalidRbfAttempt (override val channelId: ByteVector32) : ChannelException(channelId, "invalid rbf attempt")
data class InvalidSpliceAlreadyInProgress (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice attempt: the current splice attempt must be completed or aborted first")
data class InvalidSpliceAbortNotAcked (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice attempt: our previous tx_abort has not been acked")
data class InvalidSpliceChannelNotIdle (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice attempt: channel is not idle")
data class InvalidSpliceNotQuiescent (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice attempt: the channel is not quiescent")
data class NoMoreHtlcsClosingInProgress (override val channelId: ByteVector32) : ChannelException(channelId, "cannot send new htlcs, closing in progress")
data class ClosingAlreadyInProgress (override val channelId: ByteVector32) : ChannelException(channelId, "closing already in progress")
data class CannotCloseWithUnsignedOutgoingHtlcs (override val channelId: ByteVector32) : ChannelException(channelId, "cannot close when there are unsigned outgoing htlc")
Expand Down Expand Up @@ -89,4 +89,5 @@ data class InvalidFailureCode (override val channelId: Byte
data class PleasePublishYourCommitment (override val channelId: ByteVector32) : ChannelException(channelId, "please publish your local commitment")
data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state")
data class ForbiddenDuringSplice (override val channelId: ByteVector32, val command: String?) : ChannelException(channelId, "cannot process $command while splicing")
data class InvalidSpliceRequest (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice request")
// @formatter:on
21 changes: 18 additions & 3 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,6 @@ data class Commitment(
return hasNoPendingHtlcs() && hasNoPendingFeeUpdate
}

fun isIdle(changes: CommitmentChanges): Boolean = hasNoPendingHtlcs() && changes.localChanges.all.isEmpty() && changes.remoteChanges.all.isEmpty()

fun timedOutOutgoingHtlcs(blockHeight: Long): Set<UpdateAddHtlc> {
fun expired(add: UpdateAddHtlc) = blockHeight >= add.cltvExpiry.toLong()

Expand Down Expand Up @@ -596,15 +594,32 @@ data class Commitments(
}

// @formatter:off
fun localIsQuiescent(): Boolean = changes.localChanges.all.isEmpty()
fun remoteIsQuiescent(): Boolean = changes.remoteChanges.all.isEmpty()
fun isQuiescent(): Boolean = localIsQuiescent() && remoteIsQuiescent()
// HTLCs and pending changes are the same for all active commitments, so we don't need to loop through all of them.
fun isIdle(): Boolean = active.first().isIdle(changes)
fun hasNoPendingHtlcsOrFeeUpdate(): Boolean = active.first().hasNoPendingHtlcsOrFeeUpdate(changes)
fun timedOutOutgoingHtlcs(currentHeight: Long): Set<UpdateAddHtlc> = active.first().timedOutOutgoingHtlcs(currentHeight)
fun almostTimedOutIncomingHtlcs(currentHeight: Long, fulfillSafety: CltvExpiryDelta): Set<UpdateAddHtlc> = active.first().almostTimedOutIncomingHtlcs(currentHeight, fulfillSafety, changes)
fun getOutgoingHtlcCrossSigned(htlcId: Long): UpdateAddHtlc? = active.first().getOutgoingHtlcCrossSigned(htlcId)
fun getIncomingHtlcCrossSigned(htlcId: Long): UpdateAddHtlc? = active.first().getIncomingHtlcCrossSigned(htlcId)
// @formatter:on

/**
* Whenever we're not sure the `IncomingPaymentHandler` has received our previous `ChannelAction.ProcessIncomingHtlcs`,
* or when we may have ignored the responses from the `IncomingPaymentHandler` (eg. while quiescent or disconnected),
* we need to reprocess those incoming HTLCs.
*/
fun reprocessIncomingHtlcs(): List<ChannelAction.ProcessIncomingHtlc> {
// We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been forwarded to the payment handler).
// They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when we subsequently sign it.
// That's why we need to look in *their* commitment with direction=OUT.
//
// We also need to filter out htlcs that we already settled and signed (the settlement messages are being retransmitted).
val alreadySettled = changes.localChanges.signed.filterIsInstance<HtlcSettlementMessage>().map { it.id }.toSet()
return latest.remoteCommit.spec.htlcs.outgoings().filter { !alreadySettled.contains(it.id) }.map { ChannelAction.ProcessIncomingHtlc(it) }
}

fun sendAdd(cmd: ChannelCommand.Htlc.Add, paymentId: UUID, blockHeight: Long): Either<ChannelException, Pair<Commitments, UpdateAddHtlc>> {
val maxExpiry = Channel.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(blockHeight)
// we don't want to use too high a refund timeout, because our funds will be locked during that time if the payment is never fulfilled
Expand Down
31 changes: 27 additions & 4 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -900,17 +900,40 @@ sealed class RbfStatus {
object RbfAborted : RbfStatus()
}

/** We're waiting for the channel to be quiescent. */
sealed class QuiescenceNegotiation : SpliceStatus() {
abstract class Initiator : QuiescenceNegotiation() {
abstract val command: ChannelCommand.Commitment.Splice.Request
}
abstract class NonInitiator : QuiescenceNegotiation()
}

/** The channel is quiescent and a splice attempt was initiated. */
sealed class QuiescentSpliceStatus : SpliceStatus()

sealed class SpliceStatus {
object None : SpliceStatus()
data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : SpliceStatus()
/** We stop sending new updates and wait for our updates to be added to the local and remote commitments. */
data class QuiescenceRequested(override val command: ChannelCommand.Commitment.Splice.Request) : QuiescenceNegotiation.Initiator()
/** Our updates have been added to the local and remote commitments, we wait for our peer to do the same. */
data class InitiatorQuiescent(override val command: ChannelCommand.Commitment.Splice.Request) : QuiescenceNegotiation.Initiator()
/** Our peer has asked us to stop sending new updates and wait for our updates to be added to the local and remote commitments. */
data class ReceivedStfu(val stfu: Stfu) : QuiescenceNegotiation.NonInitiator()
/** Our updates have been added to the local and remote commitments, we wait for our peer to use the now quiescent channel. */
object NonInitiatorQuiescent : QuiescentSpliceStatus()
/** We told our peer we want to splice funds in the channel. */
data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : QuiescentSpliceStatus()
/** We both agreed to splice and are building the splice transaction. */
data class InProgress(
val replyTo: CompletableDeferred<ChannelCommand.Commitment.Splice.Response>?,
val spliceSession: InteractiveTxSession,
val localPushAmount: MilliSatoshi,
val remotePushAmount: MilliSatoshi,
val liquidityLease: LiquidityAds.Lease?,
val origins: List<Origin.PayToOpenOrigin>
) : SpliceStatus()
data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List<Origin.PayToOpenOrigin>) : SpliceStatus()
object Aborted : SpliceStatus()
) : QuiescentSpliceStatus()
/** The splice transaction has been negotiated, we're exchanging signatures. */
data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List<Origin.PayToOpenOrigin>) : QuiescentSpliceStatus()
/** The splice attempt was aborted by us, we're waiting for our peer to ack. */
object Aborted : QuiescentSpliceStatus()
}
Loading
Loading