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
12 changes: 11 additions & 1 deletion docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@

## Major changes

<insert changes>
### Simplified mutual close

This release includes support for the latest [mutual close protocol](https://github.com/lightning/bolts/pull/1205).
This protocol allows both channel participants to decide exactly how much fees they're willing to pay to close the channel.
Each participant obtains a channel closing transaction where they are paying the fees.

Once closing transactions are broadcast, they can be RBF-ed by calling the `close` RPC again with a higher feerate:

```sh
./eclair-cli close --channelId=<channel_id> --preferredFeerateSatByte=<rbf_feerate>
```

### Peer storage

Expand Down
4 changes: 2 additions & 2 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ eclair {
// node that you trust using override-init-features (see below).
option_zeroconf = disabled
keysend = disabled
option_simple_close=optional
trampoline_payment_prototype = disabled
async_payment_prototype = disabled
on_the_fly_funding = disabled
Expand Down Expand Up @@ -132,8 +133,7 @@ eclair {

to-remote-delay-blocks = 720 // number of blocks that the other node's to-self outputs must be delayed (720 ~ 5 days)
max-to-local-delay-blocks = 2016 // maximum number of blocks that we are ready to accept for our own delayed outputs (2016 ~ 2 weeks)
min-depth-funding-blocks = 6 // minimum number of confirmations for funding transactions
min-depth-closing-blocks = 3 // minimum number of confirmations for closing transactions
min-depth-blocks = 6 // minimum number of confirmations for channel transactions, which we will additionally scale based on the amount at stake
expiry-delta-blocks = 144
max-expiry-delta-blocks = 2016 // we won't forward HTLCs with timeouts greater than this delta
// When we receive the preimage for an HTLC and want to fulfill it but the upstream peer stops responding, we want to
Expand Down
7 changes: 7 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 @@ -305,6 +305,11 @@ object Features {
val mandatory = 54
}

case object SimpleClose extends Feature with InitFeature with NodeFeature {
val rfcName = "option_simple_close"
val mandatory = 60
}

/** This feature bit indicates that the node is a mobile wallet that can be woken up via push notifications. */
case object WakeUpNotificationClient extends Feature with InitFeature {
val rfcName = "wake_up_notification_client"
Expand Down Expand Up @@ -375,6 +380,7 @@ object Features {
PaymentMetadata,
ZeroConf,
KeySend,
SimpleClose,
WakeUpNotificationClient,
TrampolinePaymentPrototype,
AsyncPaymentPrototype,
Expand All @@ -393,6 +399,7 @@ object Features {
RouteBlinding -> (VariableLengthOnion :: Nil),
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil),
SimpleClose -> (ShutdownAnySegwit :: Nil),
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
OnTheFlyFunding -> (SplicePrototype :: Nil),
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
Expand Down
7 changes: 3 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ object NodeParams extends Logging {
"on-chain-fees.target-blocks.safe-utxos-threshold" -> "on-chain-fees.safe-utxos-threshold",
"on-chain-fees.target-blocks" -> "on-chain-fees.confirmation-priority",
// v0.12.0
"channel.mindepth-blocks" -> "channel.min-depth-funding-blocks",
"channel.mindepth-blocks" -> "channel.min-depth-blocks",
"sync-whitelist" -> "router.sync.whitelist",
)
deprecatedKeyPaths.foreach {
Expand Down Expand Up @@ -362,7 +362,7 @@ object NodeParams extends Logging {
require(fulfillSafetyBeforeTimeout * 2 < expiryDelta, "channel.fulfill-safety-before-timeout-blocks must be smaller than channel.expiry-delta-blocks / 2 because it effectively reduces that delta; if you want to increase this value, you may want to increase expiry-delta-blocks as well")
val minFinalExpiryDelta = CltvExpiryDelta(config.getInt("channel.min-final-expiry-delta-blocks"))
require(minFinalExpiryDelta > fulfillSafetyBeforeTimeout, "channel.min-final-expiry-delta-blocks must be strictly greater than channel.fulfill-safety-before-timeout-blocks; otherwise it may lead to undesired channel closure")
require(config.getInt("channel.min-depth-funding-blocks") >= 6, "channel.min-depth-funding-blocks must be at least 6 to ensure that channels are safe from reorgs, otherwise funds can be stolen")
require(config.getInt("channel.min-depth-blocks") >= 6, "channel.min-depth-blocks must be at least 6 to ensure that channels are safe from reorgs, otherwise funds can be stolen")

val nodeAlias = config.getString("node-alias")
require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)")
Expand Down Expand Up @@ -575,8 +575,7 @@ object NodeParams extends Logging {
minFundingPrivateSatoshis = Satoshi(config.getLong("channel.min-private-funding-satoshis")),
toRemoteDelay = offeredCLTV,
maxToLocalDelay = maxToLocalCLTV,
minDepthFunding = config.getInt("channel.min-depth-funding-blocks"),
minDepthClosing = config.getInt("channel.min-depth-closing-blocks"),
minDepth = config.getInt("channel.min-depth-blocks"),
expiryDelta = expiryDelta,
maxExpiryDelta = maxExpiryDelta,
fulfillSafetyBeforeTimeout = fulfillSafetyBeforeTimeout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ object CheckBalance {
case (r, d: DATA_NORMAL) => r.modify(_.normal).using(updateMainAndHtlcBalance(d.commitments, knownPreimages))
case (r, d: DATA_SHUTDOWN) => r.modify(_.shutdown).using(updateMainAndHtlcBalance(d.commitments, knownPreimages))
case (r, d: DATA_NEGOTIATING) => r.modify(_.negotiating).using(updateMainBalance(d.commitments.latest.localCommit))
case (r, d: DATA_NEGOTIATING_SIMPLE) => r.modify(_.negotiating).using(updateMainBalance(d.commitments.latest.localCommit))
case (r, d: DATA_CLOSING) =>
Closing.isClosingTypeAlreadyKnown(d) match {
case None if d.mutualClosePublished.nonEmpty && d.localCommitPublished.isEmpty && d.remoteCommitPublished.isEmpty && d.nextRemoteCommitPublished.isEmpty && d.revokedCommitPublished.isEmpty =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ object ZmqWatcher {
case class WatchFundingSpent(replyTo: ActorRef[WatchFundingSpentTriggered], txId: TxId, outputIndex: Int, hints: Set[TxId]) extends WatchSpent[WatchFundingSpentTriggered]
case class WatchFundingSpentTriggered(spendingTx: Transaction) extends WatchSpentTriggered

case class WatchOutputSpent(replyTo: ActorRef[WatchOutputSpentTriggered], txId: TxId, outputIndex: Int, hints: Set[TxId]) extends WatchSpent[WatchOutputSpentTriggered]
case class WatchOutputSpentTriggered(spendingTx: Transaction) extends WatchSpentTriggered
case class WatchOutputSpent(replyTo: ActorRef[WatchOutputSpentTriggered], txId: TxId, outputIndex: Int, amount: Satoshi, hints: Set[TxId]) extends WatchSpent[WatchOutputSpentTriggered]
case class WatchOutputSpentTriggered(amount: Satoshi, spendingTx: Transaction) extends WatchSpentTriggered

/** Waiting for a wallet transaction to be published guarantees that bitcoind won't double-spend it in the future, unless we explicitly call abandontransaction. */
case class WatchPublished(replyTo: ActorRef[WatchPublishedTriggered], txId: TxId) extends Watch[WatchPublishedTriggered]
Expand Down Expand Up @@ -233,7 +233,7 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
.foreach {
case w: WatchExternalChannelSpent => context.self ! TriggerEvent(w.replyTo, w, WatchExternalChannelSpentTriggered(w.shortChannelId, tx))
case w: WatchFundingSpent => context.self ! TriggerEvent(w.replyTo, w, WatchFundingSpentTriggered(tx))
case w: WatchOutputSpent => context.self ! TriggerEvent(w.replyTo, w, WatchOutputSpentTriggered(tx))
case w: WatchOutputSpent => context.self ! TriggerEvent(w.replyTo, w, WatchOutputSpentTriggered(w.amount, tx))
case _: WatchPublished => // nothing to do
case _: WatchConfirmed[_] => // nothing to do
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ case object WAIT_FOR_DUAL_FUNDING_READY extends ChannelState
case object NORMAL extends ChannelState
case object SHUTDOWN extends ChannelState
case object NEGOTIATING extends ChannelState
case object NEGOTIATING_SIMPLE extends ChannelState
case object CLOSING extends ChannelState
case object CLOSED extends ChannelState
case object OFFLINE extends ChannelState
Expand Down Expand Up @@ -643,6 +644,16 @@ final case class DATA_NEGOTIATING(commitments: Commitments,
require(closingTxProposed.nonEmpty, "there must always be a list for the current negotiation")
require(!commitments.params.localParams.paysClosingFees || closingTxProposed.forall(_.nonEmpty), "initiator must have at least one closing signature for every negotiation attempt because it initiates the closing")
}
final case class DATA_NEGOTIATING_SIMPLE(commitments: Commitments,
lastClosingFeerate: FeeratePerKw,
localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector,
// Closing transactions we created, where we pay the fees (unsigned).
proposedClosingTxs: List[ClosingTxs],
// Closing transactions we published: this contains our local transactions for
// which they sent a signature, and their closing transactions that we signed.
publishedClosingTxs: List[ClosingTx]) extends ChannelDataWithCommitments {
def findClosingTx(tx: Transaction): Option[ClosingTx] = publishedClosingTxs.find(_.tx.txid == tx.txid).orElse(proposedClosingTxs.flatMap(_.all).find(_.tx.txid == tx.txid))
}
final case class DATA_CLOSING(commitments: Commitments,
waitingSince: BlockHeight, // how long since we initiated the closing
finalScriptPubKey: ByteVector, // where to send all on-chain funds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ case class FeerateTooDifferent (override val channelId: Byte
case class InvalidAnnouncementSignatures (override val channelId: ByteVector32, annSigs: AnnouncementSignatures) extends ChannelException(channelId, s"invalid announcement signatures: $annSigs")
case class InvalidCommitmentSignature (override val channelId: ByteVector32, fundingTxId: TxId, fundingTxIndex: Long, unsignedCommitTx: Transaction) extends ChannelException(channelId, s"invalid commitment signature: fundingTxId=$fundingTxId fundingTxIndex=$fundingTxIndex commitTxId=${unsignedCommitTx.txid} commitTx=$unsignedCommitTx")
case class InvalidHtlcSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid htlc signature: txId=$txId")
case class CannotGenerateClosingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "failed to generate closing transaction: all outputs are trimmed")
case class MissingCloseSignature (override val channelId: ByteVector32) extends ChannelException(channelId, "closing_complete is missing a signature for a closing transaction including our output")
case class InvalidCloseSignature (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid close signature: txId=$txId")
case class InvalidCloseeScript (override val channelId: ByteVector32, received: ByteVector, expected: ByteVector) extends ChannelException(channelId, s"invalid closee script used in closing_complete: our latest script is $expected, you're using $received")
case class InvalidCloseAmountBelowDust (override val channelId: ByteVector32, txId: TxId) extends ChannelException(channelId, s"invalid closing tx: some outputs are below dust: txId=$txId")
case class CommitSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"commit sig count mismatch: expected=$expected actual=$actual")
case class HtlcSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"htlc sig count mismatch: expected=$expected actual=$actual")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,11 @@ case class ChannelParams(channelId: ByteVector32,
// README: if we set our bitcoin node to generate taproot addresses and our peer does not support option_shutdown_anysegwit, we will not be able to mutual-close
// channels as the isValidFinalScriptPubkey() check would fail.
val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit)
val allowOpReturn = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.SimpleClose)
val mustUseUpfrontShutdownScript = channelFeatures.hasFeature(Features.UpfrontShutdownScript)
// we only enforce using the pre-generated shutdown script if option_upfront_shutdown_script is set
if (mustUseUpfrontShutdownScript && localParams.upfrontShutdownScript_opt.exists(_ != localScriptPubKey)) Left(InvalidFinalScript(channelId))
else if (!Closing.MutualClose.isValidFinalScriptPubkey(localScriptPubKey, allowAnySegwit)) Left(InvalidFinalScript(channelId))
else if (!Closing.MutualClose.isValidFinalScriptPubkey(localScriptPubKey, allowAnySegwit, allowOpReturn)) Left(InvalidFinalScript(channelId))
else Right(localScriptPubKey)
}

Expand All @@ -128,10 +129,11 @@ case class ChannelParams(channelId: ByteVector32,
def validateRemoteShutdownScript(remoteScriptPubKey: ByteVector): Either[ChannelException, ByteVector] = {
// to check whether shutdown_any_segwit is active we check features in local and remote parameters, which are negotiated each time we connect to our peer.
val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit)
val allowOpReturn = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.SimpleClose)
val mustUseUpfrontShutdownScript = channelFeatures.hasFeature(Features.UpfrontShutdownScript)
// we only enforce using the pre-generated shutdown script if option_upfront_shutdown_script is set
if (mustUseUpfrontShutdownScript && remoteParams.upfrontShutdownScript_opt.exists(_ != remoteScriptPubKey)) Left(InvalidFinalScript(channelId))
else if (!Closing.MutualClose.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit)) Left(InvalidFinalScript(channelId))
else if (!Closing.MutualClose.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit, allowOpReturn)) Left(InvalidFinalScript(channelId))
else Right(remoteScriptPubKey)
}

Expand Down
Loading