Skip to content

Commit 77023c5

Browse files
committed
WIP
1 parent 20b505d commit 77023c5

File tree

12 files changed

+166
-5
lines changed

12 files changed

+166
-5
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ object CheckBalance {
213213
case (r, d: DATA_NORMAL) => r.modify(_.normal).using(updateMainAndHtlcBalance(d.commitments, knownPreimages))
214214
case (r, d: DATA_SHUTDOWN) => r.modify(_.shutdown).using(updateMainAndHtlcBalance(d.commitments, knownPreimages))
215215
case (r, d: DATA_NEGOTIATING) => r.modify(_.negotiating).using(updateMainBalance(d.commitments.latest.localCommit))
216+
case (r, d: DATA_NEGOTIATING_SIMPLE) => ???
216217
case (r, d: DATA_CLOSING) =>
217218
Closing.isClosingTypeAlreadyKnown(d) match {
218219
case None if d.mutualClosePublished.nonEmpty && d.localCommitPublished.isEmpty && d.remoteCommitPublished.isEmpty && d.nextRemoteCommitPublished.isEmpty && d.revokedCommitPublished.isEmpty =>

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ case object WAIT_FOR_DUAL_FUNDING_READY extends ChannelState
7373
case object NORMAL extends ChannelState
7474
case object SHUTDOWN extends ChannelState
7575
case object NEGOTIATING extends ChannelState
76+
case object NEGOTIATING_SIMPLE extends ChannelState
7677
case object CLOSING extends ChannelState
7778
case object CLOSED extends ChannelState
7879
case object OFFLINE extends ChannelState
@@ -595,6 +596,7 @@ final case class DATA_NEGOTIATING(commitments: Commitments,
595596
require(closingTxProposed.nonEmpty, "there must always be a list for the current negotiation")
596597
require(!commitments.params.localParams.isInitiator || closingTxProposed.forall(_.nonEmpty), "initiator must have at least one closing signature for every negotiation attempt because it initiates the closing")
597598
}
599+
final case class DATA_NEGOTIATING_SIMPLE(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown) extends ChannelDataWithCommitments
598600
final case class DATA_CLOSING(commitments: Commitments,
599601
waitingSince: BlockHeight, // how long since we initiated the closing
600602
finalScriptPubKey: ByteVector, // where to send all on-chain funds

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ case class FeerateTooDifferent (override val channelId: Byte
107107
case class InvalidAnnouncementSignatures (override val channelId: ByteVector32, annSigs: AnnouncementSignatures) extends ChannelException(channelId, s"invalid announcement signatures: $annSigs")
108108
case class InvalidCommitmentSignature (override val channelId: ByteVector32, fundingTxId: ByteVector32, fundingTxIndex: Long, unsignedCommitTx: Transaction) extends ChannelException(channelId, s"invalid commitment signature: fundingTxId=$fundingTxId fundingTxIndex=$fundingTxIndex commitTxId=${unsignedCommitTx.txid} commitTx=$unsignedCommitTx")
109109
case class InvalidHtlcSignature (override val channelId: ByteVector32, txId: ByteVector32) extends ChannelException(channelId, s"invalid htlc signature: txId=$txId")
110+
case class CannotGenerateClosingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "failed to generate closing transaction: all outputs are trimmed")
111+
case class MissingCloseSignature (override val channelId: ByteVector32) extends ChannelException(channelId, "closing_complete is missing a signature for a closing transaction including our output")
110112
case class InvalidCloseSignature (override val channelId: ByteVector32, txId: ByteVector32) extends ChannelException(channelId, s"invalid close signature: txId=$txId")
111113
case class InvalidCloseAmountBelowDust (override val channelId: ByteVector32, txId: ByteVector32) extends ChannelException(channelId, s"invalid closing tx: some outputs are below dust: txId=$txId")
112114
case class CommitSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"commit sig count mismatch: expected=$expected actual=$actual")

eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ object Helpers {
5959
case d: DATA_NORMAL => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
6060
case d: DATA_SHUTDOWN => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
6161
case d: DATA_NEGOTIATING => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
62+
case d: DATA_NEGOTIATING_SIMPLE => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
6263
case d: DATA_CLOSING => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
6364
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
6465
}
@@ -677,6 +678,84 @@ object Helpers {
677678
}
678679
}
679680

681+
/** We are the closer: we sign closing transactions for which we pay the fees. */
682+
def makeSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, feeConf: OnChainFeeConf): Either[ChannelException, (ClosingTxs, ClosingComplete)] = {
683+
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit = true, allowOpReturn = true), "invalid localScriptPubkey")
684+
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit = true, allowOpReturn = true), "invalid remoteScriptPubkey")
685+
val closingFee = {
686+
val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), localScriptPubkey, remoteScriptPubkey)
687+
dummyClosingTxs.preferred_opt match {
688+
case Some(dummyTx) =>
689+
val dummySignedTx = Transactions.addSigs(dummyTx, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig)
690+
SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feeConf.getClosingFeerate(feerates), dummySignedTx.tx.weight()))
691+
case None => return Left(CannotGenerateClosingTx(commitment.channelId))
692+
}
693+
}
694+
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, localScriptPubkey, remoteScriptPubkey)
695+
// The actual fee we're paying will be bigger than the one we previously computed if we omit our output.
696+
val actualFee = closingTxs.preferred_opt match {
697+
case Some(closingTx) => closingTx.fee
698+
case None => return Left(CannotGenerateClosingTx(commitment.channelId))
699+
}
700+
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
701+
val closingComplete = ClosingComplete(commitment.channelId, actualFee, TlvStream(Set(
702+
closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
703+
closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserNoClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
704+
closingTxs.remoteOnly_opt.map(tx => ClosingTlv.NoCloserClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
705+
).flatten))
706+
Right(closingTxs, closingComplete)
707+
}
708+
709+
/**
710+
* We are the closee: we choose one of the closer's transactions and sign it back.
711+
* Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that the
712+
* closing_complete doesn't match the latest state of the closing negotiation (someone changed their script).
713+
*/
714+
def signSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = {
715+
val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees)
716+
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, localScriptPubkey, remoteScriptPubkey)
717+
// If our output isn't dust, they must provide a signature for a transaction that includes it.
718+
// Note that we're the closee, so we look for signatures including the closee output.
719+
(closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
720+
case (Some(_), Some(_)) if closingComplete.closerAndCloseeSig_opt.isEmpty && closingComplete.noCloserCloseeSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
721+
case (Some(_), None) if closingComplete.closerAndCloseeSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
722+
case (None, Some(_)) if closingComplete.noCloserCloseeSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
723+
case _ => ()
724+
}
725+
// We choose the closing signature that matches our preferred closing transaction.
726+
val closingTxsWithSigs = Seq(
727+
closingComplete.closerAndCloseeSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndClosee(localSig)))),
728+
closingComplete.noCloserCloseeSig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.NoCloserClosee(localSig)))),
729+
closingComplete.closerNoCloseeSig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserNoClosee(localSig)))),
730+
).flatten
731+
closingTxsWithSigs.headOption match {
732+
case Some((closingTx, remoteSig, sigToTlv)) =>
733+
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
734+
val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat)
735+
val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig)
736+
Transactions.checkSpendable(signedClosingTx) match {
737+
case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
738+
case Success(_) => Right(signedClosingTx, ClosingSig(commitment.channelId, TlvStream(sigToTlv(localSig))))
739+
}
740+
case None => Left(MissingCloseSignature(commitment.channelId))
741+
}
742+
}
743+
744+
/**
745+
* We are the closer: they sent us their signature so we should now have a fully signed closing transaction.
746+
* Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that the
747+
* closing_complete doesn't match the latest state of the closing negotiation (someone changed their script).
748+
*/
749+
def receiveSimpleClosingSig(closingTxs: ClosingTxs, closingSig: ClosingSig): Either[ChannelException, Transaction] = {
750+
(closingSig.closerAndCloseeSig_opt, closingSig.closerNoCloseeSig_opt, closingSig.noCloserCloseeSig_opt) match {
751+
// TODO: check that we have the corresponding tx, add sigs, validate tx
752+
case (Some(closerAndCloseeSig), _, _) => ???
753+
case (_, Some(closerNoCloseeSig), _) => ???
754+
case (_, _, Some(noCloserCloseeSig)) => ???
755+
}
756+
???
757+
}
758+
680759
/**
681760
* Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk
682761
* that the closing transaction will not be relayed to miners' mempool and will not confirm.

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -718,8 +718,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
718718
}
719719
// are there pending signed changes on either side? we need to have received their last revocation!
720720
if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) {
721-
// there are no pending signed changes, let's go directly to NEGOTIATING
722-
if (d.commitments.params.localParams.isInitiator) {
721+
// there are no pending signed changes, let's directly negotiate a closing transaction
722+
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
723+
// val (closingTx, closingSigned) = Closing.MutualClose.makeSimpleClosingTx()
724+
// TODO: if can use option_simple_close:
725+
// - go to a new light negotiating state?
726+
// - we'll need changes once in CLOSING to handle new signature rounds
727+
???
728+
} else if (d.commitments.params.localParams.isInitiator) {
723729
// we are the channel initiator, need to initiate the negotiation by sending the first closing_signed
724730
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentFeerates, nodeParams.onChainFeeConf, d.closingFeerates)
725731
goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned
@@ -1273,6 +1279,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
12731279
log.debug("received a new sig:\n{}", commitments1.latest.specs2String)
12741280
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
12751281
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
1282+
// TODO: if option_simple_close
12761283
if (d.commitments.params.localParams.isInitiator) {
12771284
// we are the channel initiator, need to initiate the negotiation by sending the first closing_signed
12781285
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeerates, nodeParams.onChainFeeConf, closingFeerates)
@@ -1315,6 +1322,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
13151322
}
13161323
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
13171324
log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String)
1325+
// TODO: if option_simple_close
13181326
if (d.commitments.params.localParams.isInitiator) {
13191327
// we are the channel initiator, need to initiate the negotiation by sending the first closing_signed
13201328
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeerates, nodeParams.onChainFeeConf, closingFeerates)
@@ -1332,6 +1340,15 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
13321340
case Left(cause) => handleLocalError(cause, d, Some(revocation))
13331341
}
13341342

1343+
case Event(shutdown: Shutdown, d: DATA_SHUTDOWN) =>
1344+
if (shutdown.scriptPubKey != d.remoteShutdown.scriptPubKey) {
1345+
log.debug("our peer updated their shutdown script (previous={}, current={})", d.remoteShutdown.scriptPubKey, shutdown.scriptPubKey)
1346+
stay() using d.copy(remoteShutdown = shutdown) sending d.localShutdown storing()
1347+
} else {
1348+
// This is a retransmission of their previous shutdown, we can ignore it.
1349+
stay()
1350+
}
1351+
13351352
case Event(r: RevocationTimeout, d: DATA_SHUTDOWN) => handleRevocationTimeout(r, d)
13361353

13371354
case Event(ProcessCurrentBlockHeight(c), d: DATA_SHUTDOWN) => handleNewBlock(c, d)
@@ -2051,6 +2068,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
20512068
goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) sending d.localShutdown
20522069
}
20532070

2071+
// TODO: if option_simple_close (new negotiating state)
2072+
20542073
// This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send
20552074
// a channel_reestablish when reconnecting a channel that recently got confirmed, and instead send a channel_ready
20562075
// first and then go silent. This is due to a race condition on their side, so we trigger a reconnection, hoping that
@@ -2206,6 +2225,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
22062225
case d: DATA_NORMAL => d.copy(commitments = commitments1)
22072226
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
22082227
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
2228+
case d: DATA_NEGOTIATING_SIMPLE => d.copy(commitments = commitments1)
22092229
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
22102230
case d: DATA_CLOSING => d.copy(commitments = commitments1)
22112231
}
@@ -2233,6 +2253,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
22332253
case d: DATA_NORMAL => d.copy(commitments = commitments1)
22342254
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
22352255
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
2256+
case d: DATA_NEGOTIATING_SIMPLE => d.copy(commitments = commitments1)
22362257
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
22372258
case d: DATA_CLOSING => d // there is a dedicated handler in CLOSING state
22382259
}
@@ -2322,7 +2343,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
23222343
case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("syncing->normal", d2, sendToPeer = d2.channelAnnouncement.isEmpty))
23232344
case (NORMAL, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("normal->offline", d2, sendToPeer = false))
23242345
case (OFFLINE, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("offline->offline", d2, sendToPeer = false))
2325-
case (NORMAL | SYNCING | OFFLINE, SHUTDOWN | NEGOTIATING | CLOSING | CLOSED | ERR_INFORMATION_LEAK | WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, d: DATA_NORMAL, _) => Some(EmitLocalChannelDown(d))
2346+
case (NORMAL | SYNCING | OFFLINE, SHUTDOWN | NEGOTIATING | NEGOTIATING_SIMPLE | CLOSING | CLOSED | ERR_INFORMATION_LEAK | WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, d: DATA_NORMAL, _) => Some(EmitLocalChannelDown(d))
23262347
case _ => None
23272348
}
23282349
emitEvent_opt.foreach {

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ trait CommonHandlers {
106106
case d: DATA_NORMAL if d.localShutdown.isDefined => d.localShutdown.get.scriptPubKey
107107
case d: DATA_SHUTDOWN => d.localShutdown.scriptPubKey
108108
case d: DATA_NEGOTIATING => d.localShutdown.scriptPubKey
109+
case d: DATA_NEGOTIATING_SIMPLE => d.localShutdown.scriptPubKey
109110
case d: DATA_CLOSING => d.finalScriptPubKey
110111
case d =>
111112
d.commitments.params.localParams.upfrontShutdownScript_opt match {

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ trait ErrorHandlers extends CommonHandlers {
200200
val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, commitment, commitTx, nodeParams.currentFeerates, nodeParams.onChainFeeConf, finalScriptPubKey)
201201
val nextData = d match {
202202
case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished))
203+
// TODO: mimick that for DATA_NEGOTIATING_SIMPLE in all 4 cases in this file
203204
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished))
204205
case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished))
205206
}

0 commit comments

Comments
 (0)