mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-13 11:35:47 +01:00
Implement the option_simple_close
protocol
We introduce a new `NEGOTIATING_SIMPLE` state where we exchange the `closing_complete` and `closing_sig` messages, and allow RBF-ing previous transactions and updating our closing script. We stay in that state until one of the transactions confirms, or a force close is detected. This is important to ensure we're able to correctly reconnect and negotiate RBF candidates. We keep this separate from the previous `NEGOTIATING` state to make it easier to remove support for the older mutual close protocols once we're confident the network has been upgraded.
This commit is contained in:
parent
8a9e63703f
commit
67aa29ff7e
21 changed files with 935 additions and 82 deletions
|
@ -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/1096).
|
||||
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
|
||||
|
||||
|
|
|
@ -213,6 +213,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 =>
|
||||
|
|
|
@ -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
|
||||
|
@ -653,6 +654,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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -59,6 +59,7 @@ object Helpers {
|
|||
case d: DATA_NORMAL => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
|
||||
case d: DATA_SHUTDOWN => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
|
||||
case d: DATA_NEGOTIATING => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
|
||||
case d: DATA_NEGOTIATING_SIMPLE => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
|
||||
case d: DATA_CLOSING => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
|
||||
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.modify(_.commitments.params).using(_.updateFeatures(localInit, remoteInit))
|
||||
}
|
||||
|
@ -709,6 +710,96 @@ object Helpers {
|
|||
}
|
||||
}
|
||||
|
||||
/** We are the closer: we sign closing transactions for which we pay the fees. */
|
||||
def makeSimpleClosingTx(currentBlockHeight: BlockHeight, keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = {
|
||||
// We must convert the feerate to a fee: we must build dummy transactions to compute their weight.
|
||||
val closingFee = {
|
||||
val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
|
||||
dummyClosingTxs.preferred_opt match {
|
||||
case Some(dummyTx) =>
|
||||
val dummySignedTx = Transactions.addSigs(dummyTx, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig)
|
||||
SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.tx.weight()))
|
||||
case None => return Left(CannotGenerateClosingTx(commitment.channelId))
|
||||
}
|
||||
}
|
||||
// Now that we know the fee we're ready to pay, we can create our closing transactions.
|
||||
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey)
|
||||
// The actual fee we're paying will be bigger than the one we previously computed if we omit our output.
|
||||
val actualFee = closingTxs.preferred_opt match {
|
||||
case Some(closingTx) if closingTx.fee > 0.sat => closingTx.fee
|
||||
case _ => return Left(CannotGenerateClosingTx(commitment.channelId))
|
||||
}
|
||||
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
|
||||
val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, actualFee, currentBlockHeight.toLong, TlvStream(Set(
|
||||
closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
|
||||
closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
|
||||
closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))),
|
||||
).flatten[ClosingTlv]))
|
||||
Right(closingTxs, closingComplete)
|
||||
}
|
||||
|
||||
/**
|
||||
* We are the closee: we choose one of the closer's transactions and sign it back.
|
||||
*
|
||||
* Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that they
|
||||
* are not using our latest script (race condition between our closing_complete and theirs).
|
||||
*/
|
||||
def signSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = {
|
||||
val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees)
|
||||
val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey)
|
||||
// If our output isn't dust, they must provide a signature for a transaction that includes it.
|
||||
// Note that we're the closee, so we look for signatures including the closee output.
|
||||
(closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
|
||||
case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
|
||||
case (Some(_), None) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
|
||||
case (None, Some(_)) if closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
|
||||
case _ => ()
|
||||
}
|
||||
// We choose the closing signature that matches our preferred closing transaction.
|
||||
val closingTxsWithSigs = Seq(
|
||||
closingComplete.closerAndCloseeOutputsSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndCloseeOutputs(localSig)))),
|
||||
closingComplete.closeeOutputOnlySig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloseeOutputOnly(localSig)))),
|
||||
closingComplete.closerOutputOnlySig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserOutputOnly(localSig)))),
|
||||
).flatten
|
||||
closingTxsWithSigs.headOption match {
|
||||
case Some((closingTx, remoteSig, sigToTlv)) =>
|
||||
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
|
||||
val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat)
|
||||
val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig)
|
||||
Transactions.checkSpendable(signedClosingTx) match {
|
||||
case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
|
||||
case Success(_) => Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig))))
|
||||
}
|
||||
case None => Left(MissingCloseSignature(commitment.channelId))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We are the closer: they sent us their signature so we should now have a fully signed closing transaction.
|
||||
*
|
||||
* Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that we
|
||||
* sent another closing_complete before receiving their closing_sig, which is now obsolete: we ignore it and wait
|
||||
* for their next closing_sig that will match our latest closing_complete.
|
||||
*/
|
||||
def receiveSimpleClosingSig(keyManager: ChannelKeyManager, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig): Either[ChannelException, ClosingTx] = {
|
||||
val closingTxsWithSig = Seq(
|
||||
closingSig.closerAndCloseeOutputsSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))),
|
||||
closingSig.closerOutputOnlySig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))),
|
||||
closingSig.closeeOutputOnlySig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))),
|
||||
).flatten
|
||||
closingTxsWithSig.headOption match {
|
||||
case Some((closingTx, remoteSig)) =>
|
||||
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex)
|
||||
val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat)
|
||||
val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig)
|
||||
Transactions.checkSpendable(signedClosingTx) match {
|
||||
case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
|
||||
case Success(_) => Right(signedClosingTx)
|
||||
}
|
||||
case None => Left(MissingCloseSignature(commitment.channelId))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk
|
||||
* that the closing transaction will not be relayed to miners' mempool and will not confirm.
|
||||
|
|
|
@ -734,10 +734,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
}
|
||||
// are there pending signed changes on either side? we need to have received their last revocation!
|
||||
if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) {
|
||||
// there are no pending signed changes, let's go directly to NEGOTIATING
|
||||
if (d.commitments.params.localParams.paysClosingFees) {
|
||||
// there are no pending signed changes, let's directly negotiate a closing transaction
|
||||
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
|
||||
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, sendList)
|
||||
} else if (d.commitments.params.localParams.paysClosingFees) {
|
||||
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
|
||||
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, d.closingFeerates)
|
||||
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, d.closingFeerates)
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned
|
||||
} else {
|
||||
// we are not the channel initiator, will wait for their closing_signed
|
||||
|
@ -1513,9 +1515,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
log.debug("received a new sig:\n{}", commitments1.latest.specs2String)
|
||||
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
|
||||
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
|
||||
if (d.commitments.params.localParams.paysClosingFees) {
|
||||
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
|
||||
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, revocation :: Nil)
|
||||
} else if (d.commitments.params.localParams.paysClosingFees) {
|
||||
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
|
||||
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates)
|
||||
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates)
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil
|
||||
} else {
|
||||
// we are not the channel initiator, will wait for their closing_signed
|
||||
|
@ -1555,9 +1559,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
}
|
||||
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
|
||||
log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String)
|
||||
if (d.commitments.params.localParams.paysClosingFees) {
|
||||
if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) {
|
||||
startSimpleClose(d.commitments, localShutdown, remoteShutdown, d.closingFeerates, Nil)
|
||||
} else if (d.commitments.params.localParams.paysClosingFees) {
|
||||
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
|
||||
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates)
|
||||
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates)
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending closingSigned
|
||||
} else {
|
||||
// we are not the channel initiator, will wait for their closing_signed
|
||||
|
@ -1572,6 +1578,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
case Left(cause) => handleLocalError(cause, d, Some(revocation))
|
||||
}
|
||||
|
||||
case Event(shutdown: Shutdown, d: DATA_SHUTDOWN) =>
|
||||
if (shutdown.scriptPubKey != d.remoteShutdown.scriptPubKey) {
|
||||
log.debug("our peer updated their shutdown script (previous={}, current={})", d.remoteShutdown.scriptPubKey, shutdown.scriptPubKey)
|
||||
}
|
||||
stay() using d.copy(remoteShutdown = shutdown) storing()
|
||||
|
||||
case Event(r: RevocationTimeout, d: DATA_SHUTDOWN) => handleRevocationTimeout(r, d)
|
||||
|
||||
case Event(ProcessCurrentBlockHeight(c), d: DATA_SHUTDOWN) => handleNewBlock(c, d)
|
||||
|
@ -1579,17 +1591,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
case Event(c: CurrentFeerates.BitcoinCore, d: DATA_SHUTDOWN) => handleCurrentFeerate(c, d)
|
||||
|
||||
case Event(c: CMD_CLOSE, d: DATA_SHUTDOWN) =>
|
||||
c.feerates match {
|
||||
case Some(feerates) if c.feerates != d.closingFeerates =>
|
||||
if (c.scriptPubKey.nonEmpty && !c.scriptPubKey.contains(d.localShutdown.scriptPubKey)) {
|
||||
log.warning("cannot update closing script when closing is already in progress")
|
||||
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
|
||||
} else {
|
||||
log.info("updating our closing feerates: {}", feerates)
|
||||
handleCommandSuccess(c, d.copy(closingFeerates = c.feerates)) storing()
|
||||
}
|
||||
case _ =>
|
||||
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
|
||||
val useSimpleClose = Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)
|
||||
val localShutdown_opt = c.scriptPubKey match {
|
||||
case Some(scriptPubKey) if scriptPubKey != d.localShutdown.scriptPubKey && useSimpleClose => Some(Shutdown(d.channelId, scriptPubKey))
|
||||
case _ => None
|
||||
}
|
||||
if (c.scriptPubKey.exists(_ != d.localShutdown.scriptPubKey) && !useSimpleClose) {
|
||||
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
|
||||
} else if (localShutdown_opt.nonEmpty || c.feerates.nonEmpty) {
|
||||
val d1 = d.copy(localShutdown = localShutdown_opt.getOrElse(d.localShutdown), closingFeerates = c.feerates.orElse(d.closingFeerates))
|
||||
handleCommandSuccess(c, d1) storing() sending localShutdown_opt.toSeq
|
||||
} else {
|
||||
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
|
||||
}
|
||||
|
||||
case Event(e: Error, d: DATA_SHUTDOWN) => handleRemoteError(e, d)
|
||||
|
@ -1597,17 +1610,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
})
|
||||
|
||||
when(NEGOTIATING)(handleExceptions {
|
||||
// Upon reconnection, nodes must re-transmit their shutdown message, so we may receive it now.
|
||||
case Event(remoteShutdown: Shutdown, d: DATA_NEGOTIATING) =>
|
||||
if (remoteShutdown != d.remoteShutdown) {
|
||||
// This is a spec violation: it will likely lead to a disagreement when exchanging closing_signed and a force-close.
|
||||
log.warning("received unexpected shutdown={} (previous={})", remoteShutdown, d.remoteShutdown)
|
||||
if (remoteShutdown.scriptPubKey != d.remoteShutdown.scriptPubKey) {
|
||||
// This may lead to a signature mismatch if our peer changed their script without using option_simple_close.
|
||||
log.warning("received shutdown changing remote script, this may lead to a signature mismatch: previous={}, current={}", d.remoteShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
|
||||
stay() using d.copy(remoteShutdown = remoteShutdown) storing()
|
||||
} else {
|
||||
stay()
|
||||
}
|
||||
stay()
|
||||
|
||||
case Event(c: ClosingSigned, d: DATA_NEGOTIATING) =>
|
||||
val (remoteClosingFee, remoteSig) = (c.feeSatoshis, c.signature)
|
||||
Closing.MutualClose.checkClosingSignature(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match {
|
||||
MutualClose.checkClosingSignature(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match {
|
||||
case Right((signedClosingTx, closingSignedRemoteFees)) =>
|
||||
val lastLocalClosingSigned_opt = d.closingTxProposed.last.lastOption
|
||||
if (lastLocalClosingSigned_opt.exists(_.localClosingSigned.feeSatoshis == remoteClosingFee)) {
|
||||
|
@ -1630,7 +1644,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.params.localParams.paysClosingFees =>
|
||||
// if we are not paying the closing fees and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation
|
||||
// we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation
|
||||
val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf)
|
||||
val localClosingFees = MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf)
|
||||
if (maxFee < localClosingFees.min) {
|
||||
log.warning("their highest closing fee is below our minimum fee: {} < {}", maxFee, localClosingFees.min)
|
||||
stay() sending Warning(d.channelId, s"closing fee range must not be below ${localClosingFees.min}")
|
||||
|
@ -1645,7 +1659,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
log.info("accepting their closing fee={}", remoteClosingFee)
|
||||
handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees
|
||||
} else {
|
||||
val (closingTx, closingSigned) = Closing.MutualClose.makeClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, ClosingFees(closingFee, minFee, maxFee))
|
||||
val (closingTx, closingSigned) = MutualClose.makeClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, ClosingFees(closingFee, minFee, maxFee))
|
||||
log.info("proposing closing fee={} in their fee range (min={} max={})", closingSigned.feeSatoshis, minFee, maxFee)
|
||||
val closingTxProposed1 = (d.closingTxProposed: @unchecked) match {
|
||||
case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned))
|
||||
|
@ -1657,9 +1671,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
val lastLocalClosingFee_opt = lastLocalClosingSigned_opt.map(_.localClosingSigned.feeSatoshis)
|
||||
val (closingTx, closingSigned) = {
|
||||
// if we are not the channel initiator and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee
|
||||
val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf)
|
||||
val nextPreferredFee = Closing.MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee)
|
||||
Closing.MutualClose.makeClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee))
|
||||
val localClosingFees = MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf)
|
||||
val nextPreferredFee = MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee)
|
||||
MutualClose.makeClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee))
|
||||
}
|
||||
val closingTxProposed1 = (d.closingTxProposed: @unchecked) match {
|
||||
case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned))
|
||||
|
@ -1688,7 +1702,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
|
||||
} else {
|
||||
log.info("updating our closing feerates: {}", feerates)
|
||||
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, Some(feerates))
|
||||
val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, Some(feerates))
|
||||
val closingTxProposed1 = d.closingTxProposed match {
|
||||
case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned))
|
||||
case previousNegotiations => previousNegotiations :+ List(ClosingTxProposed(closingTx, closingSigned))
|
||||
|
@ -1703,6 +1717,68 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
|
||||
})
|
||||
|
||||
when(NEGOTIATING_SIMPLE)(handleExceptions {
|
||||
case Event(shutdown: Shutdown, d: DATA_NEGOTIATING_SIMPLE) =>
|
||||
if (shutdown.scriptPubKey != d.remoteScriptPubKey) {
|
||||
// This may lead to a signature mismatch: peers must use closing_complete to update their closing script.
|
||||
log.warning("received shutdown changing remote script, this may lead to a signature mismatch: previous={}, current={}", d.remoteScriptPubKey, shutdown.scriptPubKey)
|
||||
stay() using d.copy(remoteScriptPubKey = shutdown.scriptPubKey) storing()
|
||||
} else {
|
||||
stay()
|
||||
}
|
||||
|
||||
case Event(c: CMD_CLOSE, d: DATA_NEGOTIATING_SIMPLE) =>
|
||||
val localScript = c.scriptPubKey.getOrElse(d.localScriptPubKey)
|
||||
val closingFeerate = c.feerates.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates))
|
||||
if (closingFeerate < d.lastClosingFeerate) {
|
||||
val err = InvalidRbfFeerate(d.channelId, closingFeerate, d.lastClosingFeerate * 1.2)
|
||||
handleCommandError(err, c)
|
||||
} else {
|
||||
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, d.remoteScriptPubKey, closingFeerate) match {
|
||||
case Left(f) => handleCommandError(f, c)
|
||||
case Right((closingTxs, closingComplete)) =>
|
||||
log.debug("signing local mutual close transactions: {}", closingTxs)
|
||||
handleCommandSuccess(c, d.copy(lastClosingFeerate = closingFeerate, localScriptPubKey = localScript, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs)) storing() sending closingComplete
|
||||
}
|
||||
}
|
||||
|
||||
case Event(closingComplete: ClosingComplete, d: DATA_NEGOTIATING_SIMPLE) =>
|
||||
// Note that if there is a failure here and we don't send our closing_sig, they may eventually disconnect.
|
||||
// On reconnection, we will retransmit shutdown with our latest scripts, so future signing attempts should work.
|
||||
if (closingComplete.closeeScriptPubKey != d.localScriptPubKey) {
|
||||
log.warning("their closing_complete is not using our latest script: this may happen if we changed our script while they were sending closing_complete")
|
||||
// No need to persist their latest script, they will re-sent it on reconnection.
|
||||
stay() using d.copy(remoteScriptPubKey = closingComplete.closerScriptPubKey) sending Warning(d.channelId, InvalidCloseeScript(d.channelId, closingComplete.closeeScriptPubKey, d.localScriptPubKey).getMessage)
|
||||
} else {
|
||||
MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, closingComplete.closeeScriptPubKey, closingComplete.closerScriptPubKey, closingComplete) match {
|
||||
case Left(f) =>
|
||||
log.warning("invalid closing_complete: {}", f.getMessage)
|
||||
stay() sending Warning(d.channelId, f.getMessage)
|
||||
case Right((signedClosingTx, closingSig)) =>
|
||||
log.debug("signing remote mutual close transaction: {}", signedClosingTx.tx)
|
||||
val d1 = d.copy(remoteScriptPubKey = closingComplete.closerScriptPubKey, publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx)
|
||||
stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = false) sending closingSig
|
||||
}
|
||||
}
|
||||
|
||||
case Event(closingSig: ClosingSig, d: DATA_NEGOTIATING_SIMPLE) =>
|
||||
// Note that if we sent two closing_complete in a row, without waiting for their closing_sig for the first one,
|
||||
// this will fail because we only care about our latest closing_complete. This is fine, we should receive their
|
||||
// closing_sig for the last closing_complete afterwards.
|
||||
MutualClose.receiveSimpleClosingSig(keyManager, d.commitments.latest, d.proposedClosingTxs.last, closingSig) match {
|
||||
case Left(f) =>
|
||||
log.warning("invalid closing_sig: {}", f.getMessage)
|
||||
stay() sending Warning(d.channelId, f.getMessage)
|
||||
case Right(signedClosingTx) =>
|
||||
log.debug("received signatures for local mutual close transaction: {}", signedClosingTx.tx)
|
||||
val d1 = d.copy(publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx)
|
||||
stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = true)
|
||||
}
|
||||
|
||||
case Event(e: Error, d: DATA_NEGOTIATING_SIMPLE) => handleRemoteError(e, d)
|
||||
|
||||
})
|
||||
|
||||
when(CLOSING)(handleExceptions {
|
||||
case Event(c: HtlcSettlementCommand, d: DATA_CLOSING) =>
|
||||
(c match {
|
||||
|
@ -2352,6 +2428,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) sending d.localShutdown
|
||||
}
|
||||
|
||||
case Event(_: ChannelReestablish, d: DATA_NEGOTIATING_SIMPLE) =>
|
||||
// We retransmit our shutdown: we may have updated our script and they may not have received it.
|
||||
val localShutdown = Shutdown(d.channelId, d.localScriptPubKey)
|
||||
goto(NEGOTIATING_SIMPLE) using d sending localShutdown
|
||||
|
||||
// This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send
|
||||
// a channel_reestablish when reconnecting a channel that recently got confirmed, and instead send a channel_ready
|
||||
// first and then go silent. This is due to a race condition on their side, so we trigger a reconnection, hoping that
|
||||
|
@ -2507,6 +2588,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
case d: DATA_NORMAL => d.copy(commitments = commitments1)
|
||||
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
|
||||
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
|
||||
case d: DATA_NEGOTIATING_SIMPLE => d.copy(commitments = commitments1)
|
||||
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
|
||||
case d: DATA_CLOSING => d.copy(commitments = commitments1)
|
||||
}
|
||||
|
@ -2534,6 +2616,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
case d: DATA_NORMAL => d.copy(commitments = commitments1)
|
||||
case d: DATA_SHUTDOWN => d.copy(commitments = commitments1)
|
||||
case d: DATA_NEGOTIATING => d.copy(commitments = commitments1)
|
||||
case d: DATA_NEGOTIATING_SIMPLE => d.copy(commitments = commitments1)
|
||||
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = commitments1)
|
||||
case d: DATA_CLOSING => d // there is a dedicated handler in CLOSING state
|
||||
}
|
||||
|
@ -2550,6 +2633,23 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
// if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that
|
||||
handleMutualClose(d.bestUnpublishedClosingTx_opt.get, Left(d))
|
||||
|
||||
case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING_SIMPLE) if d.findClosingTx(tx).nonEmpty =>
|
||||
if (!d.publishedClosingTxs.exists(_.tx.txid == tx.txid)) {
|
||||
// They published one of our closing transactions without sending us their signature (or we ignored them because
|
||||
// of a race with our closing_complete). We need to publish it ourselves to record the fees and watch for confirmation.
|
||||
val closingTx = d.findClosingTx(tx).get.copy(tx = tx)
|
||||
stay() using d.copy(publishedClosingTxs = d.publishedClosingTxs :+ closingTx) storing() calling doPublish(closingTx, localPaysClosingFees = true)
|
||||
} else {
|
||||
// This is one of the transactions we published.
|
||||
stay()
|
||||
}
|
||||
|
||||
case Event(WatchTxConfirmedTriggered(_, _, tx), d: DATA_NEGOTIATING_SIMPLE) if d.findClosingTx(tx).nonEmpty =>
|
||||
val closingType = MutualClose(d.findClosingTx(tx).get)
|
||||
log.info("channel closed (type={})", EventType.Closed(closingType).label)
|
||||
context.system.eventStream.publish(ChannelClosed(self, d.channelId, closingType, d.commitments))
|
||||
goto(CLOSED) using d storing()
|
||||
|
||||
case Event(WatchFundingSpentTriggered(tx), d: ChannelDataWithCommitments) =>
|
||||
if (d.commitments.all.map(_.fundingTxId).contains(tx.txid)) {
|
||||
// if the spending tx is itself a funding tx, this is a splice and there is nothing to do
|
||||
|
@ -2623,7 +2723,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
|
|||
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))
|
||||
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))
|
||||
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))
|
||||
case (NORMAL | SYNCING | OFFLINE, SHUTDOWN | NEGOTIATING | CLOSING | CLOSED | ERR_INFORMATION_LEAK | WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, d: DATA_NORMAL, _) => Some(EmitLocalChannelDown(d))
|
||||
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))
|
||||
case _ => None
|
||||
}
|
||||
emitEvent_opt.foreach {
|
||||
|
|
|
@ -16,13 +16,14 @@
|
|||
|
||||
package fr.acinq.eclair.channel.fsm
|
||||
|
||||
import akka.actor.{ActorRef, FSM, Status}
|
||||
import akka.actor.FSM
|
||||
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Script}
|
||||
import fr.acinq.eclair.Features
|
||||
import fr.acinq.eclair.channel.Helpers.Closing.MutualClose
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.db.PendingCommandsDb
|
||||
import fr.acinq.eclair.io.Peer
|
||||
import fr.acinq.eclair.wire.protocol.{HtlcSettlementMessage, LightningMessage, UpdateMessage}
|
||||
import fr.acinq.eclair.wire.protocol.{HtlcSettlementMessage, LightningMessage, Shutdown, UpdateMessage}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent.duration.DurationInt
|
||||
|
@ -106,6 +107,7 @@ trait CommonHandlers {
|
|||
case d: DATA_NORMAL if d.localShutdown.isDefined => d.localShutdown.get.scriptPubKey
|
||||
case d: DATA_SHUTDOWN => d.localShutdown.scriptPubKey
|
||||
case d: DATA_NEGOTIATING => d.localShutdown.scriptPubKey
|
||||
case d: DATA_NEGOTIATING_SIMPLE => d.localScriptPubKey
|
||||
case d: DATA_CLOSING => d.finalScriptPubKey
|
||||
case d =>
|
||||
d.commitments.params.localParams.upfrontShutdownScript_opt match {
|
||||
|
@ -130,4 +132,20 @@ trait CommonHandlers {
|
|||
finalScriptPubKey
|
||||
}
|
||||
|
||||
def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates], toSend: List[LightningMessage]) = {
|
||||
val localScript = localShutdown.scriptPubKey
|
||||
val remoteScript = remoteShutdown.scriptPubKey
|
||||
val closingFeerate = closingFeerates.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates))
|
||||
MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate) match {
|
||||
case Left(f) =>
|
||||
log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage)
|
||||
val d = DATA_NEGOTIATING_SIMPLE(commitments, closingFeerate, localScript, remoteScript, Nil, Nil)
|
||||
goto(NEGOTIATING_SIMPLE) using d storing() sending toSend
|
||||
case Right((closingTxs, closingComplete)) =>
|
||||
log.debug("signing local mutual close transactions: {}", closingTxs)
|
||||
val d = DATA_NEGOTIATING_SIMPLE(commitments, closingFeerate, localScript, remoteScript, closingTxs :: Nil, Nil)
|
||||
goto(NEGOTIATING_SIMPLE) using d storing() sending toSend :+ closingComplete
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -87,6 +87,10 @@ trait ErrorHandlers extends CommonHandlers {
|
|||
log.info(s"we have a valid closing tx, publishing it instead of our commitment: closingTxId=${bestUnpublishedClosingTx.tx.txid}")
|
||||
// if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that
|
||||
handleMutualClose(bestUnpublishedClosingTx, Left(negotiating))
|
||||
case negotiating: DATA_NEGOTIATING_SIMPLE if negotiating.publishedClosingTxs.nonEmpty =>
|
||||
// We have published at least one mutual close transaction, it's better to use it instead of our local commit.
|
||||
val closing = DATA_CLOSING(negotiating.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = negotiating.localScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs)
|
||||
goto(CLOSING) using closing storing()
|
||||
case dd: ChannelDataWithCommitments =>
|
||||
// We publish our commitment even if we have nothing at stake: it's a nice thing to do because it lets our peer
|
||||
// get their funds back without delays.
|
||||
|
@ -133,6 +137,10 @@ trait ErrorHandlers extends CommonHandlers {
|
|||
case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) =>
|
||||
// if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that
|
||||
handleMutualClose(bestUnpublishedClosingTx, Left(negotiating))
|
||||
case negotiating: DATA_NEGOTIATING_SIMPLE if negotiating.publishedClosingTxs.nonEmpty =>
|
||||
// We have published at least one mutual close transaction, it's better to use it instead of our local commit.
|
||||
val closing = DATA_CLOSING(negotiating.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = negotiating.localScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs)
|
||||
goto(CLOSING) using closing storing()
|
||||
// NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that)
|
||||
case hasCommitments: ChannelDataWithCommitments =>
|
||||
if (e.toAscii == "internal error") {
|
||||
|
@ -211,6 +219,7 @@ trait ErrorHandlers extends CommonHandlers {
|
|||
val nextData = d match {
|
||||
case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished))
|
||||
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished))
|
||||
case negotiating: DATA_NEGOTIATING_SIMPLE => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs, localCommitPublished = Some(localCommitPublished))
|
||||
case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished))
|
||||
}
|
||||
goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, commitment)
|
||||
|
@ -257,6 +266,7 @@ trait ErrorHandlers extends CommonHandlers {
|
|||
val nextData = d match {
|
||||
case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished))
|
||||
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished))
|
||||
case negotiating: DATA_NEGOTIATING_SIMPLE => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs, remoteCommitPublished = Some(remoteCommitPublished))
|
||||
case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished))
|
||||
}
|
||||
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, commitments)
|
||||
|
@ -275,6 +285,7 @@ trait ErrorHandlers extends CommonHandlers {
|
|||
val nextData = d match {
|
||||
case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished))
|
||||
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished))
|
||||
case negotiating: DATA_NEGOTIATING_SIMPLE => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs, remoteCommitPublished = Some(remoteCommitPublished))
|
||||
// NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined
|
||||
case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished))
|
||||
}
|
||||
|
@ -314,6 +325,7 @@ trait ErrorHandlers extends CommonHandlers {
|
|||
val nextData = d match {
|
||||
case closing: DATA_CLOSING => closing.copy(revokedCommitPublished = closing.revokedCommitPublished :+ revokedCommitPublished)
|
||||
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), revokedCommitPublished = revokedCommitPublished :: Nil)
|
||||
case negotiating: DATA_NEGOTIATING_SIMPLE => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs, revokedCommitPublished = revokedCommitPublished :: Nil)
|
||||
// NB: if there is a revoked commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined
|
||||
case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil)
|
||||
}
|
||||
|
|
|
@ -274,6 +274,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any],
|
|||
case _: DATA_NORMAL => false
|
||||
case _: DATA_SHUTDOWN => true
|
||||
case _: DATA_NEGOTIATING => true
|
||||
case _: DATA_NEGOTIATING_SIMPLE => true
|
||||
case _: DATA_CLOSING => true
|
||||
case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true
|
||||
}
|
||||
|
|
|
@ -198,6 +198,7 @@ object PeerReadyNotifier {
|
|||
case channel.NORMAL => true
|
||||
case channel.SHUTDOWN => true
|
||||
case channel.NEGOTIATING => true
|
||||
case channel.NEGOTIATING_SIMPLE => true
|
||||
case channel.CLOSING => true
|
||||
case channel.CLOSED => true
|
||||
case channel.WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true
|
||||
|
|
|
@ -643,6 +643,7 @@ object CustomTypeHints {
|
|||
classOf[DATA_NORMAL],
|
||||
classOf[DATA_SHUTDOWN],
|
||||
classOf[DATA_NEGOTIATING],
|
||||
classOf[DATA_NEGOTIATING_SIMPLE],
|
||||
classOf[DATA_CLOSING],
|
||||
classOf[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT]
|
||||
), typeHintFieldName = "type")
|
||||
|
|
|
@ -835,6 +835,77 @@ object Transactions {
|
|||
ClosingTx(commitTxInput, tx, toLocalOutput)
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
/** We always create multiple versions of each closing transaction, where fees are either paid by us or by our peer. */
|
||||
sealed trait SimpleClosingTxFee
|
||||
object SimpleClosingTxFee {
|
||||
case class PaidByUs(fee: Satoshi) extends SimpleClosingTxFee
|
||||
case class PaidByThem(fee: Satoshi) extends SimpleClosingTxFee
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
/** Each closing attempt can result in multiple potential closing transactions, depending on which outputs are included. */
|
||||
case class ClosingTxs(localAndRemote_opt: Option[ClosingTx], localOnly_opt: Option[ClosingTx], remoteOnly_opt: Option[ClosingTx]) {
|
||||
/** Preferred closing transaction for this closing attempt. */
|
||||
val preferred_opt: Option[ClosingTx] = localAndRemote_opt.orElse(localOnly_opt).orElse(remoteOnly_opt)
|
||||
val all: Seq[ClosingTx] = Seq(localAndRemote_opt, localOnly_opt, remoteOnly_opt).flatten
|
||||
|
||||
override def toString: String = s"localAndRemote=${localAndRemote_opt.map(_.tx.toString()).getOrElse("n/a")}, localOnly=${localOnly_opt.map(_.tx.toString()).getOrElse("n/a")}, remoteOnly=${remoteOnly_opt.map(_.tx.toString()).getOrElse("n/a")}"
|
||||
}
|
||||
|
||||
def makeSimpleClosingTxs(input: InputInfo, spec: CommitmentSpec, fee: SimpleClosingTxFee, lockTime: Long, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector): ClosingTxs = {
|
||||
require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs")
|
||||
|
||||
val txNoOutput = Transaction(2, Seq(TxIn(input.outPoint, ByteVector.empty, sequence = 0xFFFFFFFDL)), Nil, lockTime)
|
||||
|
||||
// We compute the remaining balance for each side after paying the closing fees.
|
||||
// This lets us decide whether outputs can be included in the closing transaction or not.
|
||||
val (toLocalAmount, toRemoteAmount) = fee match {
|
||||
case SimpleClosingTxFee.PaidByUs(fee) => (spec.toLocal.truncateToSatoshi - fee, spec.toRemote.truncateToSatoshi)
|
||||
case SimpleClosingTxFee.PaidByThem(fee) => (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - fee)
|
||||
}
|
||||
|
||||
// An OP_RETURN script may be provided, but only when burning all of the peer's balance to fees.
|
||||
val toLocalOutput_opt = if (toLocalAmount >= dustLimit(localScriptPubKey)) {
|
||||
val amount = if (isOpReturn(localScriptPubKey)) 0.sat else toLocalAmount
|
||||
Some(TxOut(amount, localScriptPubKey))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
val toRemoteOutput_opt = if (toRemoteAmount >= dustLimit(remoteScriptPubKey)) {
|
||||
val amount = if (isOpReturn(remoteScriptPubKey)) 0.sat else toRemoteAmount
|
||||
Some(TxOut(amount, remoteScriptPubKey))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
// We may create multiple closing transactions based on which outputs may be included.
|
||||
(toLocalOutput_opt, toRemoteOutput_opt) match {
|
||||
case (Some(toLocalOutput), Some(toRemoteOutput)) =>
|
||||
val txLocalAndRemote = LexicographicalOrdering.sort(txNoOutput.copy(txOut = Seq(toLocalOutput, toRemoteOutput)))
|
||||
val toLocalOutputInfo = findPubKeyScriptIndex(txLocalAndRemote, localScriptPubKey).map(index => OutputInfo(index, toLocalOutput.amount, localScriptPubKey)).toOption
|
||||
ClosingTxs(
|
||||
localAndRemote_opt = Some(ClosingTx(input, txLocalAndRemote, toLocalOutputInfo)),
|
||||
// We also provide a version of the transaction without the remote output, which they may want to omit if not economical to spend.
|
||||
localOnly_opt = Some(ClosingTx(input, txNoOutput.copy(txOut = Seq(toLocalOutput)), Some(OutputInfo(0, toLocalOutput.amount, localScriptPubKey)))),
|
||||
remoteOnly_opt = None
|
||||
)
|
||||
case (Some(toLocalOutput), None) =>
|
||||
ClosingTxs(
|
||||
localAndRemote_opt = None,
|
||||
localOnly_opt = Some(ClosingTx(input, txNoOutput.copy(txOut = Seq(toLocalOutput)), Some(OutputInfo(0, toLocalOutput.amount, localScriptPubKey)))),
|
||||
remoteOnly_opt = None
|
||||
)
|
||||
case (None, Some(toRemoteOutput)) =>
|
||||
ClosingTxs(
|
||||
localAndRemote_opt = None,
|
||||
localOnly_opt = None,
|
||||
remoteOnly_opt = Some(ClosingTx(input, txNoOutput.copy(txOut = Seq(toRemoteOutput)), None))
|
||||
)
|
||||
case (None, None) => ClosingTxs(None, None, None)
|
||||
}
|
||||
}
|
||||
|
||||
def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteVector): Either[TxGenerationSkipped, Int] = {
|
||||
val outputIndex = tx.txOut.indexWhere(_.publicKeyScript == pubkeyScript)
|
||||
if (outputIndex >= 0) {
|
||||
|
|
|
@ -681,7 +681,7 @@ private[channel] object ChannelCodecs4 {
|
|||
("remotePushAmount" | millisatoshi) ::
|
||||
("status" | interactiveTxWaitingForSigsCodec) ::
|
||||
("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED]
|
||||
|
||||
|
||||
val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_02_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = (
|
||||
("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) ::
|
||||
("localPushAmount" | millisatoshi) ::
|
||||
|
@ -754,6 +754,19 @@ private[channel] object ChannelCodecs4 {
|
|||
("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) ::
|
||||
("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING]
|
||||
|
||||
private val closingTxsCodec: Codec[ClosingTxs] = (
|
||||
("localAndRemote_opt" | optional(bool8, closingTxCodec)) ::
|
||||
("localOnly_opt" | optional(bool8, closingTxCodec)) ::
|
||||
("remoteOnly_opt" | optional(bool8, closingTxCodec))).as[ClosingTxs]
|
||||
|
||||
val DATA_NEGOTIATING_SIMPLE_14_Codec: Codec[DATA_NEGOTIATING_SIMPLE] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("lastClosingFeerate" | feeratePerKw) ::
|
||||
("localScriptPubKey" | varsizebinarydata) ::
|
||||
("remoteScriptPubKey" | varsizebinarydata) ::
|
||||
("proposedClosingTxs" | listOfN(uint16, closingTxsCodec)) ::
|
||||
("publishedClosingTxs" | listOfN(uint16, closingTxCodec))).as[DATA_NEGOTIATING_SIMPLE]
|
||||
|
||||
val DATA_CLOSING_07_Codec: Codec[DATA_CLOSING] = (
|
||||
("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) ::
|
||||
("waitingSince" | blockHeight) ::
|
||||
|
@ -789,6 +802,7 @@ private[channel] object ChannelCodecs4 {
|
|||
|
||||
// Order matters!
|
||||
val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16)
|
||||
.typecase(0x14, Codecs.DATA_NEGOTIATING_SIMPLE_14_Codec)
|
||||
.typecase(0x13, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_13_Codec)
|
||||
.typecase(0x12, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_12_Codec)
|
||||
.typecase(0x11, Codecs.DATA_CLOSING_11_Codec)
|
||||
|
|
|
@ -79,11 +79,12 @@ object TestDatabases {
|
|||
case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = freeze2(d.commitments))
|
||||
case d: DATA_NORMAL => d.copy(commitments = freeze2(d.commitments))
|
||||
.modify(_.spliceStatus).using {
|
||||
case s: SpliceStatus.SpliceWaitingForSigs => s
|
||||
case _ => SpliceStatus.NoSplice
|
||||
}
|
||||
case s: SpliceStatus.SpliceWaitingForSigs => s
|
||||
case _ => SpliceStatus.NoSplice
|
||||
}
|
||||
case d: DATA_CLOSING => d.copy(commitments = freeze2(d.commitments))
|
||||
case d: DATA_NEGOTIATING => d.copy(commitments = freeze2(d.commitments))
|
||||
case d: DATA_NEGOTIATING_SIMPLE => d.copy(commitments = freeze2(d.commitments))
|
||||
case d: DATA_SHUTDOWN => d.copy(commitments = freeze2(d.commitments))
|
||||
}
|
||||
|
||||
|
@ -132,6 +133,7 @@ object TestDatabases {
|
|||
}
|
||||
|
||||
object TestPgDatabases {
|
||||
|
||||
import _root_.io.zonky.test.db.postgres.embedded.EmbeddedPostgres
|
||||
|
||||
/** single instance */
|
||||
|
|
|
@ -31,11 +31,12 @@ import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainWallet, OnchainPub
|
|||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.channel.fsm.Channel
|
||||
import fr.acinq.eclair.channel.publish.TxPublisher
|
||||
import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx
|
||||
import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx}
|
||||
import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory
|
||||
import fr.acinq.eclair.payment.send.SpontaneousRecipient
|
||||
import fr.acinq.eclair.payment.{Invoice, OutgoingPaymentPacket}
|
||||
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route}
|
||||
import fr.acinq.eclair.testutils.PimpTestProbe.convert
|
||||
import fr.acinq.eclair.transactions.Transactions
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
import fr.acinq.eclair.wire.protocol._
|
||||
|
@ -93,8 +94,10 @@ object ChannelStateTestsTags {
|
|||
val RejectRbfAttempts = "reject_rbf_attempts"
|
||||
/** If set, the non-initiator will require a 1-block delay between RBF attempts. */
|
||||
val DelayRbfAttempts = "delay_rbf_attempts"
|
||||
/** If set, channels will adapt their max HTLC amount to the available balance */
|
||||
val AdaptMaxHtlcAmount = "adapt-max-htlc-amount"
|
||||
/** If set, channels will adapt their max HTLC amount to the available balance. */
|
||||
val AdaptMaxHtlcAmount = "adapt_max_htlc_amount"
|
||||
/** If set, closing will use option_simple_close. */
|
||||
val SimpleClose = "option_simple_close"
|
||||
}
|
||||
|
||||
trait ChannelStateTestsBase extends Assertions with Eventually {
|
||||
|
@ -190,6 +193,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
|
|||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional))
|
||||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional))
|
||||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional))
|
||||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional))
|
||||
.initFeatures()
|
||||
val bobInitFeatures = Bob.nodeParams.features
|
||||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableWumbo))(_.removed(Features.Wumbo))
|
||||
|
@ -202,6 +206,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
|
|||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional))
|
||||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional))
|
||||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional))
|
||||
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional))
|
||||
.initFeatures()
|
||||
|
||||
val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, announceChannel = channelFlags.announceChannel)
|
||||
|
@ -515,23 +520,41 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
|
|||
s2r.forward(r)
|
||||
r2s.expectMsgType[Shutdown]
|
||||
r2s.forward(s)
|
||||
// agreeing on a closing fee
|
||||
var sCloseFee, rCloseFee = 0.sat
|
||||
do {
|
||||
sCloseFee = s2r.expectMsgType[ClosingSigned].feeSatoshis
|
||||
if (s.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures.hasFeature(Features.SimpleClose)) {
|
||||
s2r.expectMsgType[ClosingComplete]
|
||||
s2r.forward(r)
|
||||
rCloseFee = r2s.expectMsgType[ClosingSigned].feeSatoshis
|
||||
r2s.expectMsgType[ClosingComplete]
|
||||
r2s.forward(s)
|
||||
} while (sCloseFee != rCloseFee)
|
||||
s2blockchain.expectMsgType[TxPublisher.PublishTx]
|
||||
s2blockchain.expectMsgType[WatchTxConfirmed]
|
||||
r2blockchain.expectMsgType[TxPublisher.PublishTx]
|
||||
r2blockchain.expectMsgType[WatchTxConfirmed]
|
||||
eventually {
|
||||
assert(s.stateName == CLOSING)
|
||||
assert(r.stateName == CLOSING)
|
||||
r2s.expectMsgType[ClosingSig]
|
||||
r2s.forward(s)
|
||||
val sTx = r2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
r2blockchain.expectWatchTxConfirmed(sTx.txid)
|
||||
s2r.expectMsgType[ClosingSig]
|
||||
s2r.forward(r)
|
||||
val rTx = s2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
s2blockchain.expectWatchTxConfirmed(rTx.txid)
|
||||
assert(s2blockchain.expectMsgType[PublishFinalTx].tx.txid == sTx.txid)
|
||||
s2blockchain.expectWatchTxConfirmed(sTx.txid)
|
||||
assert(r2blockchain.expectMsgType[PublishFinalTx].tx.txid == rTx.txid)
|
||||
r2blockchain.expectWatchTxConfirmed(rTx.txid)
|
||||
} else {
|
||||
// agreeing on a closing fee
|
||||
var sCloseFee, rCloseFee = 0.sat
|
||||
do {
|
||||
sCloseFee = s2r.expectMsgType[ClosingSigned].feeSatoshis
|
||||
s2r.forward(r)
|
||||
rCloseFee = r2s.expectMsgType[ClosingSigned].feeSatoshis
|
||||
r2s.forward(s)
|
||||
} while (sCloseFee != rCloseFee)
|
||||
s2blockchain.expectMsgType[TxPublisher.PublishTx]
|
||||
s2blockchain.expectMsgType[WatchTxConfirmed]
|
||||
r2blockchain.expectMsgType[TxPublisher.PublishTx]
|
||||
r2blockchain.expectMsgType[WatchTxConfirmed]
|
||||
eventually {
|
||||
assert(s.stateName == CLOSING)
|
||||
assert(r.stateName == CLOSING)
|
||||
}
|
||||
}
|
||||
// both nodes are now in CLOSING state with a mutual close tx pending for confirmation
|
||||
}
|
||||
|
||||
def localClose(s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe): LocalCommitPublished = {
|
||||
|
|
|
@ -20,7 +20,7 @@ import akka.testkit.TestProbe
|
|||
import com.softwaremill.quicklens.ModifyPimp
|
||||
import fr.acinq.bitcoin.ScriptFlags
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, SatoshiLong, Transaction}
|
||||
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, SatoshiLong, Script, Transaction}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw}
|
||||
import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates}
|
||||
|
@ -33,7 +33,7 @@ import fr.acinq.eclair.payment.relay.Relayer._
|
|||
import fr.acinq.eclair.payment.send.SpontaneousRecipient
|
||||
import fr.acinq.eclair.transactions.Transactions.ClaimLocalAnchorOutputTx
|
||||
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
|
||||
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32}
|
||||
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
import scodec.bits.ByteVector
|
||||
|
@ -911,6 +911,25 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
|
|||
assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates.contains(closingFeerates2))
|
||||
}
|
||||
|
||||
test("recv CMD_CLOSE with updated script") { f =>
|
||||
import f._
|
||||
val sender = TestProbe()
|
||||
val script = Script.write(Script.pay2wpkh(randomKey().publicKey))
|
||||
alice ! CMD_CLOSE(sender.ref, Some(script), None)
|
||||
sender.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]]
|
||||
}
|
||||
|
||||
test("recv CMD_CLOSE with updated script (option_simple_close)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
val sender = TestProbe()
|
||||
val script = Script.write(Script.pay2wpkh(randomKey().publicKey))
|
||||
alice ! CMD_CLOSE(sender.ref, Some(script), None)
|
||||
sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
|
||||
assert(alice2bob.expectMsgType[Shutdown].scriptPubKey == script)
|
||||
alice2bob.forward(bob)
|
||||
awaitCond(bob.stateData.asInstanceOf[DATA_SHUTDOWN].remoteShutdown.scriptPubKey == script)
|
||||
}
|
||||
|
||||
test("recv CMD_FORCECLOSE") { f =>
|
||||
import f._
|
||||
|
||||
|
|
|
@ -17,20 +17,22 @@
|
|||
package fr.acinq.eclair.channel.states.g
|
||||
|
||||
import akka.testkit.TestProbe
|
||||
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong, Transaction}
|
||||
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong, Script, Transaction}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw}
|
||||
import fr.acinq.eclair.channel.Helpers.Closing
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.channel.fsm.Channel
|
||||
import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx}
|
||||
import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx, SetChannelId}
|
||||
import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM
|
||||
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
|
||||
import fr.acinq.eclair.testutils.PimpTestProbe._
|
||||
import fr.acinq.eclair.transactions.Transactions
|
||||
import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
|
||||
import fr.acinq.eclair.wire.protocol.ClosingSignedTlv.FeeRange
|
||||
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, Error, Shutdown, TlvStream, Warning}
|
||||
import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32}
|
||||
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingComplete, ClosingSig, ClosingSigned, ClosingTlv, Error, Shutdown, TlvStream, Warning}
|
||||
import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
|
||||
import org.scalatest.Inside.inside
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
|
||||
|
@ -63,11 +65,15 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
alice2bob.forward(bob, aliceShutdown)
|
||||
val bobShutdown = bob2alice.expectMsgType[Shutdown]
|
||||
bob2alice.forward(alice, bobShutdown)
|
||||
awaitCond(alice.stateName == NEGOTIATING)
|
||||
assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == aliceShutdown.scriptPubKey))
|
||||
|
||||
awaitCond(bob.stateName == NEGOTIATING)
|
||||
assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == bobShutdown.scriptPubKey))
|
||||
if (alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures.hasFeature(Features.SimpleClose)) {
|
||||
awaitCond(alice.stateName == NEGOTIATING_SIMPLE)
|
||||
awaitCond(bob.stateName == NEGOTIATING_SIMPLE)
|
||||
} else {
|
||||
awaitCond(alice.stateName == NEGOTIATING)
|
||||
assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == aliceShutdown.scriptPubKey))
|
||||
awaitCond(bob.stateName == NEGOTIATING)
|
||||
assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == bobShutdown.scriptPubKey))
|
||||
}
|
||||
}
|
||||
|
||||
def bobClose(f: FixtureParam, feerates: Option[ClosingFeerates] = None): Unit = {
|
||||
|
@ -79,11 +85,15 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
bob2alice.forward(alice, bobShutdown)
|
||||
val aliceShutdown = alice2bob.expectMsgType[Shutdown]
|
||||
alice2bob.forward(bob, aliceShutdown)
|
||||
awaitCond(alice.stateName == NEGOTIATING)
|
||||
assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == aliceShutdown.scriptPubKey))
|
||||
|
||||
awaitCond(bob.stateName == NEGOTIATING)
|
||||
assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == bobShutdown.scriptPubKey))
|
||||
if (bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures.hasFeature(Features.SimpleClose)) {
|
||||
awaitCond(alice.stateName == NEGOTIATING_SIMPLE)
|
||||
awaitCond(bob.stateName == NEGOTIATING_SIMPLE)
|
||||
} else {
|
||||
awaitCond(alice.stateName == NEGOTIATING)
|
||||
assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == aliceShutdown.scriptPubKey))
|
||||
awaitCond(bob.stateName == NEGOTIATING)
|
||||
assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.params.localParams.upfrontShutdownScript_opt.forall(_ == bobShutdown.scriptPubKey))
|
||||
}
|
||||
}
|
||||
|
||||
def buildFeerates(feerate: FeeratePerKw, minFeerate: FeeratePerKw = FeeratePerKw(250 sat)): FeeratesPerKw =
|
||||
|
@ -473,6 +483,211 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
bob2blockchain.expectMsgType[WatchTxConfirmed]
|
||||
}
|
||||
|
||||
test("recv ClosingComplete (both outputs)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
aliceClose(f)
|
||||
val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete]
|
||||
assert(aliceClosingComplete.fees > 0.sat)
|
||||
assert(aliceClosingComplete.closerAndCloseeOutputsSig_opt.nonEmpty)
|
||||
assert(aliceClosingComplete.closerOutputOnlySig_opt.nonEmpty)
|
||||
assert(aliceClosingComplete.closeeOutputOnlySig_opt.isEmpty)
|
||||
val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete]
|
||||
assert(bobClosingComplete.fees > 0.sat)
|
||||
assert(bobClosingComplete.closerAndCloseeOutputsSig_opt.nonEmpty)
|
||||
assert(bobClosingComplete.closerOutputOnlySig_opt.nonEmpty)
|
||||
assert(bobClosingComplete.closeeOutputOnlySig_opt.isEmpty)
|
||||
|
||||
alice2bob.forward(bob, aliceClosingComplete)
|
||||
val bobClosingSig = bob2alice.expectMsgType[ClosingSig]
|
||||
assert(bobClosingSig.fees == aliceClosingComplete.fees)
|
||||
assert(bobClosingSig.lockTime == aliceClosingComplete.lockTime)
|
||||
bob2alice.forward(alice, bobClosingSig)
|
||||
val aliceTx = alice2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(aliceTx.desc == "closing")
|
||||
assert(aliceTx.fee > 0.sat)
|
||||
alice2blockchain.expectWatchTxConfirmed(aliceTx.tx.txid)
|
||||
inside(bob2blockchain.expectMsgType[PublishFinalTx]) { p =>
|
||||
assert(p.tx.txid == aliceTx.tx.txid)
|
||||
assert(p.fee == 0.sat)
|
||||
}
|
||||
bob2blockchain.expectWatchTxConfirmed(aliceTx.tx.txid)
|
||||
assert(alice.stateName == NEGOTIATING_SIMPLE)
|
||||
|
||||
bob2alice.forward(alice, bobClosingComplete)
|
||||
val aliceClosingSig = alice2bob.expectMsgType[ClosingSig]
|
||||
assert(aliceClosingSig.fees == bobClosingComplete.fees)
|
||||
assert(aliceClosingSig.lockTime == bobClosingComplete.lockTime)
|
||||
alice2bob.forward(bob, aliceClosingSig)
|
||||
val bobTx = bob2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(bobTx.desc == "closing")
|
||||
assert(bobTx.fee > 0.sat)
|
||||
bob2blockchain.expectWatchTxConfirmed(bobTx.tx.txid)
|
||||
inside(alice2blockchain.expectMsgType[PublishFinalTx]) { p =>
|
||||
assert(p.tx.txid == bobTx.tx.txid)
|
||||
assert(p.fee == 0.sat)
|
||||
}
|
||||
assert(aliceTx.tx.txid != bobTx.tx.txid)
|
||||
alice2blockchain.expectWatchTxConfirmed(bobTx.tx.txid)
|
||||
assert(bob.stateName == NEGOTIATING_SIMPLE)
|
||||
}
|
||||
|
||||
test("recv ClosingComplete (single output)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f =>
|
||||
import f._
|
||||
aliceClose(f)
|
||||
val closingComplete = alice2bob.expectMsgType[ClosingComplete]
|
||||
assert(closingComplete.closerAndCloseeOutputsSig_opt.isEmpty)
|
||||
assert(closingComplete.closerOutputOnlySig_opt.nonEmpty)
|
||||
assert(closingComplete.closeeOutputOnlySig_opt.isEmpty)
|
||||
// Bob has nothing at stake.
|
||||
bob2alice.expectNoMessage(100 millis)
|
||||
|
||||
alice2bob.forward(bob, closingComplete)
|
||||
bob2alice.expectMsgType[ClosingSig]
|
||||
bob2alice.forward(alice)
|
||||
val closingTx = alice2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.tx.txid)
|
||||
alice2blockchain.expectWatchTxConfirmed(closingTx.tx.txid)
|
||||
bob2blockchain.expectWatchTxConfirmed(closingTx.tx.txid)
|
||||
assert(alice.stateName == NEGOTIATING_SIMPLE)
|
||||
assert(bob.stateName == NEGOTIATING_SIMPLE)
|
||||
}
|
||||
|
||||
test("recv ClosingComplete (single output, trimmed)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f =>
|
||||
import f._
|
||||
val (r, htlc) = addHtlc(250_000 msat, alice, bob, alice2bob, bob2alice)
|
||||
crossSign(alice, bob, alice2bob, bob2alice)
|
||||
fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob)
|
||||
crossSign(bob, alice, bob2alice, alice2bob)
|
||||
|
||||
aliceClose(f)
|
||||
val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete]
|
||||
assert(aliceClosingComplete.closerAndCloseeOutputsSig_opt.isEmpty)
|
||||
assert(aliceClosingComplete.closerOutputOnlySig_opt.nonEmpty)
|
||||
assert(aliceClosingComplete.closeeOutputOnlySig_opt.isEmpty)
|
||||
val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete]
|
||||
assert(bobClosingComplete.closerAndCloseeOutputsSig_opt.isEmpty)
|
||||
assert(bobClosingComplete.closerOutputOnlySig_opt.isEmpty)
|
||||
assert(bobClosingComplete.closeeOutputOnlySig_opt.nonEmpty)
|
||||
|
||||
bob2alice.forward(alice, bobClosingComplete)
|
||||
val aliceClosingSig = alice2bob.expectMsgType[ClosingSig]
|
||||
alice2bob.forward(bob, aliceClosingSig)
|
||||
val bobTx = bob2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobTx.tx.txid)
|
||||
bob2blockchain.expectWatchTxConfirmed(bobTx.tx.txid)
|
||||
alice2blockchain.expectWatchTxConfirmed(bobTx.tx.txid)
|
||||
assert(alice.stateName == NEGOTIATING_SIMPLE)
|
||||
assert(bob.stateName == NEGOTIATING_SIMPLE)
|
||||
}
|
||||
|
||||
test("recv ClosingComplete (missing closee output)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
aliceClose(f)
|
||||
val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete]
|
||||
val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete]
|
||||
alice2bob.forward(bob, aliceClosingComplete.copy(tlvStream = TlvStream(ClosingTlv.CloserOutputOnly(aliceClosingComplete.closerOutputOnlySig_opt.get))))
|
||||
// Bob expects to receive a signature for a closing transaction containing his output, so he ignores Alice's
|
||||
// closing_complete instead of sending back his closing_sig.
|
||||
bob2alice.expectMsgType[Warning]
|
||||
bob2alice.expectNoMessage(100 millis)
|
||||
bob2alice.forward(alice, bobClosingComplete)
|
||||
val aliceClosingSig = alice2bob.expectMsgType[ClosingSig]
|
||||
alice2bob.forward(bob, aliceClosingSig.copy(tlvStream = TlvStream(ClosingTlv.CloseeOutputOnly(aliceClosingSig.closerAndCloseeOutputsSig_opt.get))))
|
||||
bob2alice.expectMsgType[Warning]
|
||||
bob2alice.expectNoMessage(100 millis)
|
||||
bob2blockchain.expectNoMessage(100 millis)
|
||||
}
|
||||
|
||||
test("recv ClosingComplete (with concurrent script update)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
aliceClose(f)
|
||||
alice2bob.expectMsgType[ClosingComplete]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[ClosingComplete]
|
||||
bob2alice.forward(alice)
|
||||
val aliceTx1 = bob2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTx1.tx.txid)
|
||||
val bobTx1 = alice2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobTx1.tx.txid)
|
||||
alice2bob.expectMsgType[ClosingSig]
|
||||
alice2bob.forward(bob)
|
||||
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobTx1.tx.txid)
|
||||
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == bobTx1.tx.txid)
|
||||
bob2alice.expectMsgType[ClosingSig]
|
||||
bob2alice.forward(alice)
|
||||
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTx1.tx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTx1.tx.txid)
|
||||
val aliceScript1 = alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].localScriptPubKey
|
||||
val bobScript1 = bob.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].localScriptPubKey
|
||||
|
||||
// Alice sends another closing_complete, updating her script and the fees.
|
||||
val probe = TestProbe()
|
||||
val aliceScript2 = Script.write(Script.pay2wpkh(randomKey().publicKey))
|
||||
val aliceFeerate2 = alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].lastClosingFeerate * 1.25
|
||||
alice ! CMD_CLOSE(probe.ref, Some(aliceScript2), Some(ClosingFeerates(aliceFeerate2, aliceFeerate2, aliceFeerate2)))
|
||||
probe.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
|
||||
inside(alice2bob.expectMsgType[ClosingComplete]) { msg =>
|
||||
assert(msg.fees > aliceTx1.fee)
|
||||
assert(msg.closerScriptPubKey == aliceScript2)
|
||||
assert(msg.closeeScriptPubKey == bobScript1)
|
||||
}
|
||||
// Bob also sends closing_complete concurrently, updating his script and the fees.
|
||||
val bobScript2 = Script.write(Script.pay2wpkh(randomKey().publicKey))
|
||||
val bobFeerate2 = bob.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].lastClosingFeerate * 1.25
|
||||
bob ! CMD_CLOSE(probe.ref, Some(bobScript2), Some(ClosingFeerates(bobFeerate2, bobFeerate2, bobFeerate2)))
|
||||
probe.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
|
||||
inside(bob2alice.expectMsgType[ClosingComplete]) { msg =>
|
||||
assert(msg.fees > bobTx1.fee)
|
||||
assert(msg.closerScriptPubKey == bobScript2)
|
||||
assert(msg.closeeScriptPubKey == aliceScript1)
|
||||
}
|
||||
// Those messages are ignored because they don't match the latest version of each participant's scripts.
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.forward(alice)
|
||||
alice2bob.expectMsgType[Warning]
|
||||
alice2bob.expectNoMessage(100 millis)
|
||||
bob2alice.expectMsgType[Warning]
|
||||
bob2alice.expectNoMessage(100 millis)
|
||||
|
||||
// Alice retries with a higher fee, now that she received Bob's latest script.
|
||||
val aliceFeerate3 = aliceFeerate2 * 1.25
|
||||
alice ! CMD_CLOSE(probe.ref, Some(aliceScript2), Some(ClosingFeerates(aliceFeerate3, aliceFeerate3, aliceFeerate3)))
|
||||
probe.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
|
||||
inside(alice2bob.expectMsgType[ClosingComplete]) { msg =>
|
||||
assert(msg.closerScriptPubKey == aliceScript2)
|
||||
assert(msg.closeeScriptPubKey == bobScript2)
|
||||
}
|
||||
alice2bob.forward(bob)
|
||||
val bobClosingSig3 = bob2alice.expectMsgType[ClosingSig]
|
||||
assert(bobClosingSig3.closerScriptPubKey == aliceScript2)
|
||||
assert(bobClosingSig3.closeeScriptPubKey == bobScript2)
|
||||
// Before receiving Bob's closing_sig, Alice updates her script again.
|
||||
val aliceFeerate4 = aliceFeerate3 * 1.25
|
||||
val aliceScript4 = Script.write(Script.pay2wpkh(randomKey().publicKey))
|
||||
alice ! CMD_CLOSE(probe.ref, Some(aliceScript4), Some(ClosingFeerates(aliceFeerate4, aliceFeerate4, aliceFeerate4)))
|
||||
probe.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
|
||||
inside(alice2bob.expectMsgType[ClosingComplete]) { msg =>
|
||||
assert(msg.closerScriptPubKey == aliceScript4)
|
||||
assert(msg.closeeScriptPubKey == bobScript2)
|
||||
}
|
||||
alice2bob.forward(bob)
|
||||
val bobClosingSig4 = bob2alice.expectMsgType[ClosingSig]
|
||||
assert(bobClosingSig4.closerScriptPubKey == aliceScript4)
|
||||
assert(bobClosingSig4.closeeScriptPubKey == bobScript2)
|
||||
|
||||
// The first closing_sig is ignored because it's not using Alice's latest script.
|
||||
bob2alice.forward(alice, bobClosingSig3)
|
||||
alice2bob.expectMsgType[Warning]
|
||||
alice2blockchain.expectNoMessage(100 millis)
|
||||
// The second closing_sig lets Alice broadcast a new version of her closing transaction.
|
||||
bob2alice.forward(alice, bobClosingSig4)
|
||||
val aliceTx4 = alice2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(aliceTx4.fee > aliceTx1.fee)
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTx4.tx.txid)
|
||||
alice2blockchain.expectNoMessage(100 millis)
|
||||
alice2bob.expectNoMessage(100 millis)
|
||||
}
|
||||
|
||||
test("recv WatchFundingSpentTriggered (counterparty's mutual close)") { f =>
|
||||
import f._
|
||||
aliceClose(f)
|
||||
|
@ -533,6 +748,92 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(bob.stateName == CLOSING)
|
||||
}
|
||||
|
||||
test("recv WatchFundingSpentTriggered (signed closing tx)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
bobClose(f)
|
||||
// Alice and Bob publish a first closing tx.
|
||||
val aliceClosingComplete1 = alice2bob.expectMsgType[ClosingComplete]
|
||||
alice2bob.forward(bob, aliceClosingComplete1)
|
||||
val bobClosingComplete1 = bob2alice.expectMsgType[ClosingComplete]
|
||||
bob2alice.forward(alice, bobClosingComplete1)
|
||||
val aliceClosingSig1 = alice2bob.expectMsgType[ClosingSig]
|
||||
val bobTx1 = alice2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
alice2blockchain.expectWatchTxConfirmed(bobTx1.txid)
|
||||
val bobClosingSig1 = bob2alice.expectMsgType[ClosingSig]
|
||||
val aliceTx1 = bob2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
bob2blockchain.expectWatchTxConfirmed(aliceTx1.txid)
|
||||
alice2bob.forward(bob, aliceClosingSig1)
|
||||
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobTx1.txid)
|
||||
bob2blockchain.expectWatchTxConfirmed(bobTx1.txid)
|
||||
bob2alice.forward(alice, bobClosingSig1)
|
||||
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTx1.txid)
|
||||
alice2blockchain.expectWatchTxConfirmed(aliceTx1.txid)
|
||||
|
||||
// Alice updates her closing script.
|
||||
alice ! CMD_CLOSE(TestProbe().ref, Some(Script.write(Script.pay2wpkh(randomKey().publicKey))), None)
|
||||
alice2bob.expectMsgType[ClosingComplete]
|
||||
alice2bob.forward(bob)
|
||||
val bobClosingSig = bob2alice.expectMsgType[ClosingSig]
|
||||
bob2alice.forward(alice, bobClosingSig)
|
||||
val aliceTx2 = alice2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
alice2blockchain.expectWatchTxConfirmed(aliceTx2.txid)
|
||||
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTx2.txid)
|
||||
bob2blockchain.expectWatchTxConfirmed(aliceTx2.txid)
|
||||
|
||||
// They first receive a watch event for the older transaction, then the new one.
|
||||
alice ! WatchFundingSpentTriggered(aliceTx1)
|
||||
alice ! WatchFundingSpentTriggered(bobTx1)
|
||||
alice ! WatchFundingSpentTriggered(aliceTx2)
|
||||
alice2blockchain.expectNoMessage(100 millis)
|
||||
assert(alice.stateName == NEGOTIATING_SIMPLE)
|
||||
bob ! WatchFundingSpentTriggered(aliceTx1)
|
||||
bob ! WatchFundingSpentTriggered(bobTx1)
|
||||
bob ! WatchFundingSpentTriggered(aliceTx2)
|
||||
bob2blockchain.expectNoMessage(100 millis)
|
||||
assert(bob.stateName == NEGOTIATING_SIMPLE)
|
||||
}
|
||||
|
||||
test("recv WatchFundingSpentTriggered (unsigned closing tx)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
bobClose(f)
|
||||
val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete]
|
||||
alice2bob.forward(bob, aliceClosingComplete)
|
||||
val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete]
|
||||
bob2alice.forward(alice, bobClosingComplete)
|
||||
alice2bob.expectMsgType[ClosingSig]
|
||||
val bobTx = alice2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
alice2blockchain.expectWatchTxConfirmed(bobTx.txid)
|
||||
bob2alice.expectMsgType[ClosingSig]
|
||||
val aliceTx = bob2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
bob2blockchain.expectWatchTxConfirmed(aliceTx.txid)
|
||||
|
||||
alice ! WatchFundingSpentTriggered(aliceTx)
|
||||
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceTx.txid)
|
||||
alice2blockchain.expectWatchTxConfirmed(aliceTx.txid)
|
||||
alice2blockchain.expectNoMessage(100 millis)
|
||||
|
||||
bob ! WatchFundingSpentTriggered(bobTx)
|
||||
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobTx.txid)
|
||||
bob2blockchain.expectWatchTxConfirmed(bobTx.txid)
|
||||
bob2blockchain.expectNoMessage(100 millis)
|
||||
}
|
||||
|
||||
test("recv WatchFundingSpentTriggered (unrecognized commit)") { f =>
|
||||
import f._
|
||||
bobClose(f)
|
||||
alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0))
|
||||
alice2blockchain.expectNoMessage(100 millis)
|
||||
assert(alice.stateName == NEGOTIATING)
|
||||
}
|
||||
|
||||
test("recv WatchFundingSpentTriggered (unrecognized commit, option_simple_close)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
bobClose(f)
|
||||
alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0))
|
||||
alice2blockchain.expectNoMessage(100 millis)
|
||||
assert(alice.stateName == NEGOTIATING_SIMPLE)
|
||||
}
|
||||
|
||||
test("recv CMD_CLOSE") { f =>
|
||||
import f._
|
||||
bobClose(f)
|
||||
|
@ -573,12 +874,57 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
awaitCond(bob.stateName == CLOSING)
|
||||
}
|
||||
|
||||
test("recv WatchFundingSpentTriggered (unrecognized commit)") { f =>
|
||||
test("recv CMD_CLOSE with RBF feerate too low", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
bobClose(f)
|
||||
alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0))
|
||||
alice2blockchain.expectNoMessage(100 millis)
|
||||
assert(alice.stateName == NEGOTIATING)
|
||||
|
||||
alice.setFeerates(buildFeerates(FeeratePerKw(500 sat)))
|
||||
aliceClose(f)
|
||||
alice2bob.expectMsgType[ClosingComplete]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[ClosingComplete] // ignored
|
||||
val bobClosingSig = bob2alice.expectMsgType[ClosingSig]
|
||||
bob2alice.forward(alice, bobClosingSig)
|
||||
|
||||
val probe = TestProbe()
|
||||
alice ! CMD_CLOSE(probe.ref, None, Some(ClosingFeerates(FeeratePerKw(450 sat), FeeratePerKw(450 sat), FeeratePerKw(450 sat))))
|
||||
probe.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidRbfFeerate]]
|
||||
alice ! CMD_CLOSE(probe.ref, None, Some(ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(500 sat), FeeratePerKw(500 sat))))
|
||||
probe.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
|
||||
}
|
||||
|
||||
test("receive INPUT_RESTORED", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
aliceClose(f)
|
||||
alice2bob.expectMsgType[ClosingComplete]
|
||||
alice2bob.forward(bob)
|
||||
val aliceTx = bob2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTx.txid)
|
||||
bob2alice.expectMsgType[ClosingComplete]
|
||||
bob2alice.forward(alice)
|
||||
val bobTx = alice2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobTx.txid)
|
||||
alice2bob.expectMsgType[ClosingSig]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[ClosingSig] // Alice doesn't receive Bob's closing_sig
|
||||
val aliceData = alice.underlyingActor.nodeParams.db.channels.getChannel(channelId(alice)).get
|
||||
|
||||
// Alice restarts before receiving Bob's closing_sig: she cannot publish her own closing transaction, but will
|
||||
// detect it when receiving it in her mempool (or in the blockchain).
|
||||
alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing)
|
||||
alice ! INPUT_RESTORED(aliceData)
|
||||
alice2blockchain.expectMsgType[SetChannelId]
|
||||
alice2blockchain.expectMsgType[WatchFundingSpent]
|
||||
awaitCond(alice.stateName == OFFLINE)
|
||||
|
||||
// Alice's transaction (published by Bob) confirms.
|
||||
alice ! WatchFundingSpentTriggered(aliceTx)
|
||||
inside(alice2blockchain.expectMsgType[PublishFinalTx]) { p =>
|
||||
assert(p.tx.txid == aliceTx.txid)
|
||||
assert(p.fee > 0.sat)
|
||||
}
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceTx.txid)
|
||||
alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, aliceTx)
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
||||
test("recv Error") { f =>
|
||||
|
@ -593,4 +939,28 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid)
|
||||
}
|
||||
|
||||
test("recv Error (option_simple_close)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
aliceClose(f)
|
||||
val closingComplete = alice2bob.expectMsgType[ClosingComplete]
|
||||
alice2bob.forward(bob, closingComplete)
|
||||
bob2alice.expectMsgType[ClosingComplete]
|
||||
val closingSig = bob2alice.expectMsgType[ClosingSig]
|
||||
bob2alice.forward(alice, closingSig)
|
||||
val closingTx = alice2blockchain.expectMsgType[PublishFinalTx].tx
|
||||
alice2blockchain.expectWatchTxConfirmed(closingTx.txid)
|
||||
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.txid)
|
||||
bob2blockchain.expectWatchTxConfirmed(closingTx.txid)
|
||||
|
||||
alice ! Error(ByteVector32.Zeroes, "oops")
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.nonEmpty)
|
||||
alice2blockchain.expectNoMessage(100 millis) // we have a mutual close transaction, so we don't publish the commit tx
|
||||
|
||||
bob ! Error(ByteVector32.Zeroes, "oops")
|
||||
awaitCond(bob.stateName == CLOSING)
|
||||
assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.nonEmpty)
|
||||
bob2blockchain.expectNoMessage(100 millis) // we have a mutual close transaction, so we don't publish the commit tx
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -335,6 +335,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
|||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
||||
test("recv WatchTxConfirmedTriggered (mutual close, option_simple_close)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
val mutualCloseTx = alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].publishedClosingTxs.last
|
||||
|
||||
alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mutualCloseTx.tx)
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
|
||||
bob ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mutualCloseTx.tx)
|
||||
awaitCond(bob.stateName == CLOSED)
|
||||
}
|
||||
|
||||
test("recv WatchFundingSpentTriggered (local commit)") { f =>
|
||||
import f._
|
||||
// an error occurs and alice publishes her commit tx
|
||||
|
@ -859,6 +871,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
|||
assert(!listener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled)
|
||||
}
|
||||
|
||||
test("recv WatchFundingSpentTriggered (remote commit, option_simple_close)", Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
// Bob publishes his last current commit tx, the one it had when entering NEGOTIATING state.
|
||||
val bobCommitTx = bobCommitTxs.last.commitTx.tx
|
||||
val closingState = remoteClose(bobCommitTx, alice, alice2blockchain)
|
||||
assert(closingState.claimHtlcTxs.isEmpty)
|
||||
val txPublished = txListener.expectMsgType[TransactionPublished]
|
||||
assert(txPublished.tx == bobCommitTx)
|
||||
assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit
|
||||
}
|
||||
|
||||
test("recv CMD_BUMP_FORCE_CLOSE_FEE (remote commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
|
||||
import f._
|
||||
|
||||
|
@ -904,10 +928,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
|||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
||||
test("recv WatchTxConfirmedTriggered (remote commit, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f =>
|
||||
test("recv WatchTxConfirmedTriggered (remote commit, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.SimpleClose)) { f =>
|
||||
import f._
|
||||
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures == ChannelFeatures(Features.StaticRemoteKey))
|
||||
assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].commitments.params.channelFeatures == ChannelFeatures(Features.StaticRemoteKey))
|
||||
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
|
||||
val bobCommitTx = bobCommitTxs.last.commitTx.tx
|
||||
assert(bobCommitTx.txOut.size == 2) // two main outputs
|
||||
|
|
|
@ -580,7 +580,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec {
|
|||
fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_CLOSE(sender.ref, None, None))
|
||||
sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
|
||||
// we then wait for C and F to negotiate the closing fee
|
||||
awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds)
|
||||
awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NEGOTIATING_SIMPLE, max = 60 seconds)
|
||||
// and close the channel
|
||||
val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
|
|
|
@ -10,7 +10,7 @@ import com.typesafe.config.ConfigFactory
|
|||
import fr.acinq.bitcoin.scalacompat.ByteVector32
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
|
||||
import fr.acinq.eclair.blockchain.CurrentBlockHeight
|
||||
import fr.acinq.eclair.channel.NEGOTIATING
|
||||
import fr.acinq.eclair.channel.{NEGOTIATING, NEGOTIATING_SIMPLE}
|
||||
import fr.acinq.eclair.io.Switchboard.GetPeerInfo
|
||||
import fr.acinq.eclair.io.{Peer, PeerConnected, PeerReadyManager, Switchboard}
|
||||
import fr.acinq.eclair.payment.relay.AsyncPaymentTriggerer._
|
||||
|
@ -166,7 +166,7 @@ class AsyncPaymentTriggererSpec extends ScalaTestWithActorTestKit(ConfigFactory.
|
|||
system.eventStream ! EventStream.Publish(PeerConnected(peer.ref.toClassic, remoteNodeId, null))
|
||||
val request2 = switchboard.expectMessageType[Switchboard.GetPeerInfo]
|
||||
request2.replyTo ! Peer.PeerInfo(peer.ref.toClassic, remoteNodeId, Peer.CONNECTED, None, None, Set(TestProbe().ref.toClassic))
|
||||
peer.expectMessageType[Peer.GetPeerChannels].replyTo ! Peer.PeerChannels(remoteNodeId, Seq(Peer.ChannelInfo(null, NEGOTIATING, null)))
|
||||
peer.expectMessageType[Peer.GetPeerChannels].replyTo ! Peer.PeerChannels(remoteNodeId, Seq(Peer.ChannelInfo(null, NEGOTIATING_SIMPLE, null)))
|
||||
probe.expectNoMessage(100 millis)
|
||||
probe2.expectMessage(AsyncPaymentTriggered)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ package fr.acinq.eclair.transactions
|
|||
import fr.acinq.bitcoin.SigHash._
|
||||
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, ripemd160, sha256}
|
||||
import fr.acinq.bitcoin.scalacompat.Script.{pay2wpkh, pay2wsh, write}
|
||||
import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi}
|
||||
import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OP_PUSHDATA, OP_RETURN, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi}
|
||||
import fr.acinq.eclair.TestUtils.randomTxId
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
|
||||
|
@ -828,6 +828,56 @@ class TransactionsSpec extends AnyFunSuite with Logging {
|
|||
val toRemoteIndex = (toLocal.index + 1) % 2
|
||||
assert(closingTx.tx.txOut(toRemoteIndex.toInt).amount == 250_000.sat)
|
||||
}
|
||||
{
|
||||
// Different amounts, both outputs untrimmed, local is closer (option_simple_close):
|
||||
val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 250_000_000 msat)
|
||||
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(5_000 sat), 0, localPubKeyScript, remotePubKeyScript)
|
||||
assert(closingTxs.localAndRemote_opt.nonEmpty)
|
||||
assert(closingTxs.localOnly_opt.nonEmpty)
|
||||
assert(closingTxs.remoteOnly_opt.isEmpty)
|
||||
val localAndRemote = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutput).get
|
||||
assert(localAndRemote.publicKeyScript == localPubKeyScript)
|
||||
assert(localAndRemote.amount == 145_000.sat)
|
||||
val localOnly = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get
|
||||
assert(localOnly.publicKeyScript == localPubKeyScript)
|
||||
assert(localOnly.amount == 145_000.sat)
|
||||
}
|
||||
{
|
||||
// Remote is using OP_RETURN (option_simple_close): we set their output amount to 0 sat.
|
||||
val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 1_500_000 msat)
|
||||
val remotePubKeyScript = Script.write(OP_RETURN :: OP_PUSHDATA(hex"deadbeef") :: Nil)
|
||||
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(5_000 sat), 0, localPubKeyScript, remotePubKeyScript)
|
||||
assert(closingTxs.localAndRemote_opt.nonEmpty)
|
||||
assert(closingTxs.localOnly_opt.nonEmpty)
|
||||
assert(closingTxs.remoteOnly_opt.isEmpty)
|
||||
val localAndRemote = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutput).get
|
||||
assert(localAndRemote.publicKeyScript == localPubKeyScript)
|
||||
assert(localAndRemote.amount == 145_000.sat)
|
||||
val remoteOutput = closingTxs.localAndRemote_opt.get.tx.txOut((localAndRemote.index.toInt + 1) % 2)
|
||||
assert(remoteOutput.amount == 0.sat)
|
||||
assert(remoteOutput.publicKeyScript == remotePubKeyScript)
|
||||
val localOnly = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get
|
||||
assert(localOnly.publicKeyScript == localPubKeyScript)
|
||||
assert(localOnly.amount == 145_000.sat)
|
||||
}
|
||||
{
|
||||
// Remote is using OP_RETURN (option_simple_close) and paying the fees: we set their output amount to 0 sat.
|
||||
val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 10_000_000 msat)
|
||||
val remotePubKeyScript = Script.write(OP_RETURN :: OP_PUSHDATA(hex"deadbeef") :: Nil)
|
||||
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByThem(5_000 sat), 0, localPubKeyScript, remotePubKeyScript)
|
||||
assert(closingTxs.localAndRemote_opt.nonEmpty)
|
||||
assert(closingTxs.localOnly_opt.nonEmpty)
|
||||
assert(closingTxs.remoteOnly_opt.isEmpty)
|
||||
val localAndRemote = closingTxs.localAndRemote_opt.flatMap(_.toLocalOutput).get
|
||||
assert(localAndRemote.publicKeyScript == localPubKeyScript)
|
||||
assert(localAndRemote.amount == 150_000.sat)
|
||||
val remoteOutput = closingTxs.localAndRemote_opt.get.tx.txOut((localAndRemote.index.toInt + 1) % 2)
|
||||
assert(remoteOutput.amount == 0.sat)
|
||||
assert(remoteOutput.publicKeyScript == remotePubKeyScript)
|
||||
val localOnly = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get
|
||||
assert(localOnly.publicKeyScript == localPubKeyScript)
|
||||
assert(localOnly.amount == 150_000.sat)
|
||||
}
|
||||
{
|
||||
// Same amounts, both outputs untrimmed, local is fundee:
|
||||
val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 150_000_000 msat)
|
||||
|
@ -851,6 +901,29 @@ class TransactionsSpec extends AnyFunSuite with Logging {
|
|||
assert(toLocal.amount == 150_000.sat)
|
||||
assert(toLocal.index == 0)
|
||||
}
|
||||
{
|
||||
// Their output is trimmed (option_simple_close):
|
||||
val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 1_000_000 msat)
|
||||
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByThem(800 sat), 0, localPubKeyScript, remotePubKeyScript)
|
||||
assert(closingTxs.all.size == 1)
|
||||
assert(closingTxs.localOnly_opt.nonEmpty)
|
||||
val toLocal = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get
|
||||
assert(toLocal.publicKeyScript == localPubKeyScript)
|
||||
assert(toLocal.amount == 150_000.sat)
|
||||
assert(toLocal.index == 0)
|
||||
}
|
||||
{
|
||||
// Their OP_RETURN output is trimmed (option_simple_close):
|
||||
val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 1_000_000 msat)
|
||||
val remotePubKeyScript = Script.write(OP_RETURN :: OP_PUSHDATA(hex"deadbeef") :: Nil)
|
||||
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByThem(1_001 sat), 0, localPubKeyScript, remotePubKeyScript)
|
||||
assert(closingTxs.all.size == 1)
|
||||
assert(closingTxs.localOnly_opt.nonEmpty)
|
||||
val toLocal = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get
|
||||
assert(toLocal.publicKeyScript == localPubKeyScript)
|
||||
assert(toLocal.amount == 150_000.sat)
|
||||
assert(toLocal.index == 0)
|
||||
}
|
||||
{
|
||||
// Our output is trimmed:
|
||||
val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 150_000_000 msat)
|
||||
|
@ -858,6 +931,14 @@ class TransactionsSpec extends AnyFunSuite with Logging {
|
|||
assert(closingTx.tx.txOut.length == 1)
|
||||
assert(closingTx.toLocalOutput.isEmpty)
|
||||
}
|
||||
{
|
||||
// Our output is trimmed (option_simple_close):
|
||||
val spec = CommitmentSpec(Set.empty, feeratePerKw, 1_000_000 msat, 150_000_000 msat)
|
||||
val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(800 sat), 0, localPubKeyScript, remotePubKeyScript)
|
||||
assert(closingTxs.all.size == 1)
|
||||
assert(closingTxs.remoteOnly_opt.nonEmpty)
|
||||
assert(closingTxs.remoteOnly_opt.flatMap(_.toLocalOutput).isEmpty)
|
||||
}
|
||||
{
|
||||
// Both outputs are trimmed:
|
||||
val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 10_000 msat)
|
||||
|
|
Loading…
Add table
Reference in a new issue