Skip to content

Commit

Permalink
Add quiescence negotiation
Browse files Browse the repository at this point in the history
  • Loading branch information
remyers committed Nov 3, 2023
1 parent 991b45e commit 060391d
Show file tree
Hide file tree
Showing 13 changed files with 892 additions and 100 deletions.
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)
}

}

@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
),
dustLimit = 546.sat,
maxRemoteDustLimit = 600.sat,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,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 @@ -83,7 +84,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 feerate: FeeratePerKw, val origins: List<Origin.PayToOpenOrigin> = emptyList()) : Splice() {
Expand Down Expand Up @@ -111,7 +112,8 @@ sealed class ChannelCommand {
object InsufficientFunds : Failure()
object InvalidSpliceOutPubKeyScript : Failure()
object SpliceAlreadyInProgress : Failure()
object ChannelNotIdle : Failure()
object ConcurrentRemoteSplice : Failure()
object ChannelNotQuiescent : Failure()
data class FundingFailure(val reason: FundingContributionFailure) : Failure()
object CannotStartSession : Failure()
data class InteractiveTxSessionFailed(val reason: InteractiveTxSessionAction.RemoteFailure) : Failure()
Expand All @@ -124,7 +126,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 @@ -39,7 +39,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 @@ -83,4 +83,6 @@ 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 ForbiddenDuringQuiescence (override val channelId: ByteVector32, val command: String?) : ChannelException(channelId, "cannot process $command while quiescent")
data class InvalidSpliceRequest (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice request")
// @formatter:on
10 changes: 9 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ data class Commitment(
return hasNoPendingHtlcs() && hasNoPendingFeeUpdate
}

fun hasPendingOrProposedHtlcs(changes: CommitmentChanges): Boolean {
return !hasNoPendingHtlcs() || changes.localChanges.all.filterIsInstance<UpdateAddHtlc>().isNotEmpty() || changes.remoteChanges.all.filterIsInstance<UpdateAddHtlc>().isNotEmpty()
}

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

fun timedOutOutgoingHtlcs(blockHeight: Long): Set<UpdateAddHtlc> {
Expand Down Expand Up @@ -554,9 +558,13 @@ 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 hasPendingOrProposedHtlcs(): Boolean = active.first().hasPendingOrProposedHtlcs(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)
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 @@ -877,10 +877,33 @@ 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()
data class InProgress(val replyTo: CompletableDeferred<ChannelCommand.Commitment.Splice.Response>?, val spliceSession: InteractiveTxSession, val localPushAmount: MilliSatoshi, val remotePushAmount: MilliSatoshi, val origins: List<Origin.PayToOpenOrigin>) : SpliceStatus()
data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List<Origin.PayToOpenOrigin>) : SpliceStatus()
object Aborted : 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 origins: List<Origin.PayToOpenOrigin>) : 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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,9 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() {
null
}
}

// in Normal during splice we cache htlc settlements and process them when we end quiescence
var settlementStash: List<ChannelCommand.Htlc.Settlement> = emptyList()
}

object Channel {
Expand Down
Loading

0 comments on commit 060391d

Please sign in to comment.