Skip to content

Commit a223c78

Browse files
committed
WIP
1 parent c715b9b commit a223c78

File tree

12 files changed

+322
-30
lines changed

12 files changed

+322
-30
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) => r.modify(_.negotiating).using(updateMainBalance(d.commitments.latest.localCommit))
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: 10 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
@@ -602,6 +603,15 @@ final case class DATA_NEGOTIATING(commitments: Commitments,
602603
require(closingTxProposed.nonEmpty, "there must always be a list for the current negotiation")
603604
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")
604605
}
606+
final case class DATA_NEGOTIATING_SIMPLE(commitments: Commitments,
607+
localShutdown: Shutdown, remoteShutdown: Shutdown,
608+
// Closing transactions we created, where we pay the fees (unsigned).
609+
proposedClosingTxs: List[ClosingTxs],
610+
// Closing transactions we published: this contains our local transactions for
611+
// which they sent a signature, and their closing transactions that we signed.
612+
publishedClosingTxs: List[ClosingTx]) extends ChannelDataWithCommitments {
613+
def findClosingTx(tx: Transaction): Option[ClosingTx] = publishedClosingTxs.find(_.tx.txid == tx.txid).orElse(proposedClosingTxs.flatMap(_.all).find(_.tx.txid == tx.txid))
614+
}
605615
final case class DATA_CLOSING(commitments: Commitments,
606616
waitingSince: BlockHeight, // how long since we initiated the closing
607617
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: 88 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
}
@@ -688,6 +689,93 @@ object Helpers {
688689
}
689690
}
690691

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

0 commit comments

Comments
 (0)