Skip to content

Commit

Permalink
Add quiescence negotiation
Browse files Browse the repository at this point in the history
  • Loading branch information
remyers committed Dec 12, 2023
1 parent 1409cc1 commit 4d07bca
Show file tree
Hide file tree
Showing 16 changed files with 924 additions and 114 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, 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
),
dustLimit = 546.sat,
maxRemoteDustLimit = 600.sat,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,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 @@ -61,10 +61,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 @@ -80,7 +81,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 @@ -108,7 +109,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 @@ -121,7 +123,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 @@ -41,7 +41,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 @@ -85,4 +85,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 @@ -897,10 +897,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()
}
Loading

0 comments on commit 4d07bca

Please sign in to comment.