Skip to content

Commit

Permalink
Add quiescence protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
remyers committed Jun 15, 2023
1 parent fe9f32b commit 780f803
Show file tree
Hide file tree
Showing 17 changed files with 694 additions and 91 deletions.
2 changes: 2 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ eclair {
max-total-pending-channels-private-nodes = 99 // maximum number of pending channels we will accept from all private nodes
channel-opener-whitelist = [] // a list of public keys; we will ignore rate limits on pending channels from these peers
}

quiescence-timeout = 2 minutes // maximum time we will wait for quiescence to complete before disconnecting
}

balance-check-interval = 1 hour
Expand Down
8 changes: 8 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@ object Features {
val mandatory = 154
}

// TODO: @remyers reserve feature bits here: currently reserved here: https://github.com/lightning/bolts/issues/605
// TODO: @remyers option_quiesce implementation, to be replaced once quiescence is spec-ed
case object QuiescePrototype extends Feature with InitFeature {
val rfcName = "option_quiesce_prototype"
val mandatory = 158
}

val knownFeatures: Set[Feature] = Set(
DataLossProtect,
InitialRoutingSync,
Expand All @@ -325,6 +332,7 @@ object Features {
TrampolinePaymentPrototype,
AsyncPaymentPrototype,
SplicePrototype,
QuiescePrototype
)

// Features may depend on other features, as specified in Bolt 9.
Expand Down
3 changes: 2 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,8 @@ object NodeParams extends Logging {
channelOpenerWhitelist = channelOpenerWhitelist,
maxPendingChannelsPerPeer = maxPendingChannelsPerPeer,
maxTotalPendingChannelsPrivateNodes = maxTotalPendingChannelsPrivateNodes,
remoteRbfLimits = Channel.RemoteRbfLimits(config.getInt("channel.funding.remote-rbf-limits.max-attempts"), config.getInt("channel.funding.remote-rbf-limits.attempt-delta-blocks"))
remoteRbfLimits = Channel.RemoteRbfLimits(config.getInt("channel.funding.remote-rbf-limits.max-attempts"), config.getInt("channel.funding.remote-rbf-limits.attempt-delta-blocks")),
quiescenceTimeout = FiniteDuration(config.getDuration("channel.quiescence-timeout").getSeconds, TimeUnit.SECONDS),
),
onChainFeeConf = OnChainFeeConf(
feeTargets = feeTargets,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, UInt64}
import scodec.bits.ByteVector

Expand Down Expand Up @@ -187,13 +187,14 @@ sealed trait HasReplyToCommand extends Command { def replyTo: ActorRef }
sealed trait HasOptionalReplyToCommand extends Command { def replyTo_opt: Option[ActorRef] }

sealed trait ForbiddenCommandDuringSplice extends Command
sealed trait ForbiddenCommandDuringQuiescence extends Command

final case class CMD_ADD_HTLC(replyTo: ActorRef, amount: MilliSatoshi, paymentHash: ByteVector32, cltvExpiry: CltvExpiry, onion: OnionRoutingPacket, nextBlindingKey_opt: Option[PublicKey], origin: Origin.Hot, commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringSplice
sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice { def id: Long }
final case class CMD_ADD_HTLC(replyTo: ActorRef, amount: MilliSatoshi, paymentHash: ByteVector32, cltvExpiry: CltvExpiry, onion: OnionRoutingPacket, nextBlindingKey_opt: Option[PublicKey], origin: Origin.Hot, commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence
sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence { def id: Long }
final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice
final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence
final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice

final case class ClosingFees(preferred: Satoshi, min: Satoshi, max: Satoshi)
Expand All @@ -202,7 +203,7 @@ final case class ClosingFeerates(preferred: FeeratePerKw, min: FeeratePerKw, max
}

sealed trait CloseCommand extends HasReplyToCommand
final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], feerates: Option[ClosingFeerates]) extends CloseCommand with ForbiddenCommandDuringSplice
final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], feerates: Option[ClosingFeerates]) extends CloseCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence
final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand

final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FUNDING_FEE]], targetFeerate: FeeratePerKw, lockTime: Long) extends Command
Expand Down Expand Up @@ -450,8 +451,13 @@ object RbfStatus {
}

sealed trait SpliceStatus
sealed trait QuiescenceNegotiation extends SpliceStatus
object SpliceStatus {
case object NoSplice extends SpliceStatus
case class QuiescenceRequested(splice: CMD_SPLICE) extends QuiescenceNegotiation
case class InitiatorQuiescent(splice: CMD_SPLICE) extends QuiescenceNegotiation
case class ReceivedStfu(stfu: Stfu) extends QuiescenceNegotiation
case object NonInitiatorQuiescent extends QuiescenceNegotiation
case class SpliceRequested(cmd: CMD_SPLICE, init: SpliceInit) extends SpliceStatus
case class SpliceInProgress(cmd_opt: Option[CMD_SPLICE], splice: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends SpliceStatus
case class SpliceWaitingForSigs(signingSession: InteractiveTxSigningSession.WaitingForSigs) extends SpliceStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,5 @@ case class InvalidFailureCode (override val channelId: Byte
case class PleasePublishYourCommitment (override val channelId: ByteVector32) extends ChannelException(channelId, "please publish your local commitment")
case class CommandUnavailableInThisState (override val channelId: ByteVector32, command: String, state: ChannelState) extends ChannelException(channelId, s"cannot execute command=$command in state=$state")
case class ForbiddenDuringSplice (override val channelId: ByteVector32, command: String) extends ChannelException(channelId, s"cannot process $command while splicing")
case class ForbiddenDuringQuiescence (override val channelId: ByteVector32, command: String) extends ChannelException(channelId, s"cannot process $command while quiescent")
// @formatter:on
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import akka.event.LoggingAdapter
import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Script, Transaction}
import fr.acinq.eclair.Features.QuiescePrototype
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, OnChainFeeConf}
import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel.Monitoring.Metrics
Expand Down Expand Up @@ -359,7 +360,7 @@ case class Commitment(fundingTxIndex: Long,
changes.localChanges.all.exists(_.isInstanceOf[UpdateAddHtlc]) ||
changes.remoteChanges.all.exists(_.isInstanceOf[UpdateAddHtlc])

def isIdle(changes: CommitmentChanges): Boolean = hasNoPendingHtlcs && changes.localChanges.all.isEmpty && changes.remoteChanges.all.isEmpty
def isIdle(changes: CommitmentChanges, pendingHtlcsOk: Boolean): Boolean = (pendingHtlcsOk || hasNoPendingHtlcs) && changes.localChanges.all.isEmpty && changes.remoteChanges.all.isEmpty

def timedOutOutgoingHtlcs(currentHeight: BlockHeight): Set[UpdateAddHtlc] = {
def expired(add: UpdateAddHtlc): Boolean = currentHeight >= add.cltvExpiry.blockHeight
Expand Down Expand Up @@ -796,7 +797,7 @@ case class Commitments(params: ChannelParams,

// @formatter:off
// HTLCs and pending changes are the same for all active commitments, so we don't need to loop through all of them.
def isIdle: Boolean = active.head.isIdle(changes)
def isIdle: Boolean = active.head.isIdle(changes, params.remoteParams.initFeatures.hasFeature(QuiescePrototype))
def hasNoPendingHtlcsOrFeeUpdate: Boolean = active.head.hasNoPendingHtlcsOrFeeUpdate(changes)
def hasPendingOrProposedHtlcs: Boolean = active.head.hasPendingOrProposedHtlcs(changes)
def timedOutOutgoingHtlcs(currentHeight: BlockHeight): Set[UpdateAddHtlc] = active.head.timedOutOutgoingHtlcs(currentHeight)
Expand Down
19 changes: 10 additions & 9 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -367,19 +367,19 @@ object Helpers {
fundingTxHash: ByteVector32, fundingTxOutputIndex: Int,
remoteFundingPubKey: PublicKey,
remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] =
makeCommitTxsWithoutHtlcs(keyManager, params,
makeArbitraryCommitTxs(keyManager, params,
fundingAmount = localFundingAmount + remoteFundingAmount,
toLocal = localFundingAmount.toMilliSatoshi - localPushAmount + remotePushAmount,
toRemote = remoteFundingAmount.toMilliSatoshi + localPushAmount - remotePushAmount,
commitTxFeerate,
fundingTxIndex = 0,
fundingTxHash, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remotePerCommitmentPoint = remoteFirstPerCommitmentPoint, commitmentIndex = 0)
fundingTxHash, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remotePerCommitmentPoint = remoteFirstPerCommitmentPoint, commitmentIndex = 0, Set.empty[DirectedHtlc])

/**
* This creates commitment transactions for both sides at an arbitrary `commitmentIndex`. There are no htlcs, only
* local/remote balances are provided.
* This creates commitment transactions for both sides at an arbitrary `commitmentIndex` and with (optional) `htlc`
* outputs.
*/
def makeCommitTxsWithoutHtlcs(keyManager: ChannelKeyManager,
def makeArbitraryCommitTxs(keyManager: ChannelKeyManager,
params: ChannelParams,
fundingAmount: Satoshi,
toLocal: MilliSatoshi, toRemote: MilliSatoshi,
Expand All @@ -388,14 +388,15 @@ object Helpers {
fundingTxHash: ByteVector32, fundingTxOutputIndex: Int,
remoteFundingPubKey: PublicKey,
remotePerCommitmentPoint: PublicKey,
commitmentIndex: Long): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = {
commitmentIndex: Long,
localHtlcs: Set[DirectedHtlc]): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = {
import params._
val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toLocal, toRemote = toRemote)
val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toRemote, toRemote = toLocal)
val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote)
val remoteSpec = CommitmentSpec(localHtlcs.map(_.opposite), commitTxFeerate, toLocal = toRemote, toRemote = toLocal)

if (!localParams.isInitiator) {
// They initiated the channel open, therefore they pay the fee: we need to make sure they can afford it!
// Note that the reserve may not be always be met: we could be using dual funding with a large funding amount on
// Note that the reserve may not always be met: we could be using dual funding with a large funding amount on
// our side and a small funding amount on their side. But we shouldn't care as long as they can pay the fees for
// the commitment transaction.
val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat)
Expand Down
Loading

0 comments on commit 780f803

Please sign in to comment.