1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 22:46:44 +01:00

Accept closing fee above commit fee (#2662)

When performing a mutual close, we initially rejected fees that were
higher to the commit tx fees. This was removed from the specification
for anchor output channels, and doesn't make a lot of sense for standard
channels either: even at a higher fee, it makes sense to do a mutual
close to avoid waiting for relative delays on our outputs.

Fixes #2646
This commit is contained in:
Bastien Teinturier 2023-05-25 13:13:12 +02:00 committed by GitHub
parent 4713a541b6
commit aaad2e1d61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 27 additions and 31 deletions

View file

@ -64,13 +64,13 @@ object Helpers {
} }
} }
def extractShutdownScript(channelId: ByteVector32, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { private def extractShutdownScript(channelId: ByteVector32, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = {
val canUseUpfrontShutdownScript = Features.canUseFeature(localFeatures, remoteFeatures, Features.UpfrontShutdownScript) val canUseUpfrontShutdownScript = Features.canUseFeature(localFeatures, remoteFeatures, Features.UpfrontShutdownScript)
val canUseAnySegwit = Features.canUseFeature(localFeatures, remoteFeatures, Features.ShutdownAnySegwit) val canUseAnySegwit = Features.canUseFeature(localFeatures, remoteFeatures, Features.ShutdownAnySegwit)
extractShutdownScript(channelId, canUseUpfrontShutdownScript, canUseAnySegwit, upfrontShutdownScript_opt) extractShutdownScript(channelId, canUseUpfrontShutdownScript, canUseAnySegwit, upfrontShutdownScript_opt)
} }
def extractShutdownScript(channelId: ByteVector32, hasOptionUpfrontShutdownScript: Boolean, allowAnySegwit: Boolean, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { private def extractShutdownScript(channelId: ByteVector32, hasOptionUpfrontShutdownScript: Boolean, allowAnySegwit: Boolean, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = {
(hasOptionUpfrontShutdownScript, upfrontShutdownScript_opt) match { (hasOptionUpfrontShutdownScript, upfrontShutdownScript_opt) match {
case (true, None) => Left(MissingUpfrontShutdownScript(channelId)) case (true, None) => Left(MissingUpfrontShutdownScript(channelId))
case (true, Some(script)) if script.isEmpty => Right(None) // but the provided script can be empty case (true, Some(script)) if script.isEmpty => Right(None) // but the provided script can be empty
@ -304,7 +304,7 @@ object Helpers {
* @param remoteFeeratePerKw remote fee rate per kiloweight * @param remoteFeeratePerKw remote fee rate per kiloweight
* @return true if the remote fee rate is too small * @return true if the remote fee rate is too small
*/ */
def isFeeTooSmall(remoteFeeratePerKw: FeeratePerKw): Boolean = { private def isFeeTooSmall(remoteFeeratePerKw: FeeratePerKw): Boolean = {
remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw
} }
@ -605,9 +605,6 @@ object Helpers {
object MutualClose { object MutualClose {
// used only to compute tx weights and estimate fees
lazy val dummyPublicKey: PublicKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey
def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = { def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = {
Try(Script.parse(scriptPubKey)) match { Try(Script.parse(scriptPubKey)) match {
case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true
@ -622,7 +619,7 @@ object Helpers {
def firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { def firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = {
// this is just to estimate the weight, it depends on size of the pubkey scripts // this is just to estimate the weight, it depends on size of the pubkey scripts
val dummyClosingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localParams.isInitiator, Satoshi(0), Satoshi(0), commitment.localCommit.spec) val dummyClosingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localParams.isInitiator, Satoshi(0), Satoshi(0), commitment.localCommit.spec)
val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, commitment.remoteFundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, Transactions.PlaceHolderPubKey, commitment.remoteFundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx)
log.info(s"using feerates=$feerates for initial closing tx") log.info(s"using feerates=$feerates for initial closing tx")
feerates.computeFees(closingWeight) feerates.computeFees(closingWeight)
} }
@ -666,21 +663,15 @@ object Helpers {
} }
def checkClosingSignature(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { def checkClosingSignature(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = {
val lastCommitFeeSatoshi = commitment.commitInput.txOut.amount - commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.map(_.amount).sum val (closingTx, closingSigned) = makeClosingTx(keyManager, commitment, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee))
if (remoteClosingFee > lastCommitFeeSatoshi && commitment.params.commitmentFormat == DefaultCommitmentFormat) { if (checkClosingDustAmounts(closingTx)) {
log.error(s"remote proposed a commit fee higher than the last commitment fee: remote closing fee=${remoteClosingFee.toLong} last commit fees=$lastCommitFeeSatoshi") val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey, commitment.remoteFundingPubKey, closingSigned.signature, remoteClosingSig)
Left(InvalidCloseFee(commitment.channelId, remoteClosingFee)) Transactions.checkSpendable(signedClosingTx) match {
} else { case Success(_) => Right(signedClosingTx, closingSigned)
val (closingTx, closingSigned) = makeClosingTx(keyManager, commitment, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee)) case _ => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
if (checkClosingDustAmounts(closingTx)) {
val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey, commitment.remoteFundingPubKey, closingSigned.signature, remoteClosingSig)
Transactions.checkSpendable(signedClosingTx) match {
case Success(_) => Right(signedClosingTx, closingSigned)
case _ => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid))
}
} else {
Left(InvalidCloseAmountBelowDust(commitment.channelId, closingTx.tx.txid))
} }
} else {
Left(InvalidCloseAmountBelowDust(commitment.channelId, closingTx.tx.txid))
} }
} }
@ -995,7 +986,7 @@ object Helpers {
* *
* This function returns the per-commitment secret in the first case, and None in the other cases. * This function returns the per-commitment secret in the first case, and None in the other cases.
*/ */
def getRemotePerCommitmentSecret(keyManager: ChannelKeyManager, params: ChannelParams, remotePerCommitmentSecrets: ShaChain, commitTx: Transaction)(implicit log: LoggingAdapter): Option[(Long, PrivateKey)] = { def getRemotePerCommitmentSecret(keyManager: ChannelKeyManager, params: ChannelParams, remotePerCommitmentSecrets: ShaChain, commitTx: Transaction): Option[(Long, PrivateKey)] = {
import params._ import params._
// a valid tx will always have at least one input, but this ensures we don't throw in tests // a valid tx will always have at least one input, but this ensures we don't throw in tests
val sequence = commitTx.txIn.headOption.map(_.sequence).getOrElse(0L) val sequence = commitTx.txIn.headOption.map(_.sequence).getOrElse(0L)

View file

@ -26,6 +26,7 @@ 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}
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.transactions.Transactions 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.ClosingSignedTlv.FeeRange
import fr.acinq.eclair.wire.protocol.{ClosingSigned, Error, Shutdown, TlvStream, Warning} import fr.acinq.eclair.wire.protocol.{ClosingSigned, Error, Shutdown, TlvStream, Warning}
import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32} import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32}
@ -415,17 +416,21 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
bob2alice.expectNoMessage(100 millis) bob2alice.expectNoMessage(100 millis)
} }
test("recv ClosingSigned (fee too high)") { f => test("recv ClosingSigned (fee higher than commit tx fee)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._ import f._
bobClose(f) val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest
val commitFee = Transactions.commitTxFeeMsat(commitment.localParams.dustLimit, commitment.localCommit.spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat)
aliceClose(f)
val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned]
val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx assert(aliceCloseSig.feeSatoshis > commitFee.truncateToSatoshi)
alice2bob.forward(bob, aliceCloseSig.copy(feeSatoshis = 99000 sat)) // sig doesn't matter, it is checked later alice2bob.forward(bob, aliceCloseSig)
val error = bob2alice.expectMsgType[Error] val bobCloseSig = bob2alice.expectMsgType[ClosingSigned]
assert(new String(error.data.toArray).startsWith("invalid close fee: fee_satoshis=99000 sat")) assert(bobCloseSig.feeSatoshis == aliceCloseSig.feeSatoshis)
assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) awaitCond(bob.stateName == CLOSING)
bob2blockchain.expectMsgType[PublishTx] val closingTx = bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx
bob2blockchain.expectMsgType[WatchTxConfirmed] assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.txid)
bob2alice.forward(alice, bobCloseSig)
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.txid)
} }
test("recv ClosingSigned (invalid sig)") { f => test("recv ClosingSigned (invalid sig)") { f =>