Skip to content

Commit

Permalink
Set anchor output feerates when force-closing (#1702)
Browse files Browse the repository at this point in the history
When using anchor outputs, the commitment feerate is kept low (<10 sat/byte).
When we need to force-close a channel, we must ensure the commit tx and htlc
txs confirm before a given deadline, so we need to increase their feerates.

This is currently done only once, at broadcast time.
We use CPFP for the commit tx and RBF for the htlc txs.
If publishing fails because we don't have enough utxos available, it will
be retried after the next block is confirmed.

Note that it's still not recommended to activate anchor outputs.
More work needs to be done on this fee bumping logic and utxos management.
  • Loading branch information
t-bast committed Feb 24, 2021
1 parent bf2a35f commit 5d662fc
Show file tree
Hide file tree
Showing 25 changed files with 1,619 additions and 485 deletions.
Expand Up @@ -18,8 +18,10 @@ package fr.acinq.eclair.blockchain

import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Script, ScriptWitness, Transaction}
import fr.acinq.bitcoin.{ByteVector32, Satoshi, Script, ScriptWitness, Transaction}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.BitcoinEvent
import fr.acinq.eclair.transactions.Transactions.TransactionSigningKit
import fr.acinq.eclair.wire.ChannelAnnouncement
import scodec.bits.ByteVector

Expand Down Expand Up @@ -136,8 +138,16 @@ final case class WatchEventSpentBasic(event: BitcoinEvent) extends WatchEvent
// TODO: not implemented yet.
final case class WatchEventLost(event: BitcoinEvent) extends WatchEvent

/** Publish the provided tx as soon as possible depending on locktime and csv */
final case class PublishAsap(tx: Transaction)
sealed trait PublishStrategy
object PublishStrategy {
case object JustPublish extends PublishStrategy
case class SetFeerate(currentFeerate: FeeratePerKw, targetFeerate: FeeratePerKw, dustLimit: Satoshi, signingKit: TransactionSigningKit) extends PublishStrategy {
override def toString = s"SetFeerate(target=$targetFeerate)"
}
}

/** Publish the provided tx as soon as possible depending on lock time, csv and publishing strategy. */
final case class PublishAsap(tx: Transaction, strategy: PublishStrategy)

sealed trait UtxoStatus
object UtxoStatus {
Expand Down

Large diffs are not rendered by default.

Expand Up @@ -111,12 +111,20 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
}
}

def signTransaction(tx: Transaction, previousTxs: Seq[PreviousTx])(implicit ec: ExecutionContext): Future[SignTransactionResponse] = {
def signTransaction(tx: Transaction, previousTxs: Seq[PreviousTx], allowIncomplete: Boolean = false)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = {
rpcClient.invoke("signrawtransactionwithwallet", tx.toString(), previousTxs).map(json => {
val JString(hex) = json \ "hex"
val JBool(complete) = json \ "complete"
if (!complete) {
val message = (json \ "errors" \\ classOf[JString]).mkString(",")
// TODO: remove allowIncomplete once https://github.com/bitcoin/bitcoin/issues/21151 is fixed
if (!complete && !allowIncomplete) {
val JArray(errors) = json \ "errors"
val message = errors.map(error => {
val JString(txid) = error \ "txid"
val JInt(vout) = error \ "vout"
val JString(scriptSig) = error \ "scriptSig"
val JString(message) = error \ "error"
s"txid=$txid vout=$vout scriptSig=$scriptSig error=$message"
}).mkString(", ")
throw JsonRPCError(Error(-1, message))
}
SignTransactionResponse(Transaction.read(hex), complete)
Expand Down Expand Up @@ -212,7 +220,7 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
val JDecimal(descendantFees) = json \ "fees" \ "descendant"
val JBool(replaceable) = json \ "bip125-replaceable"
// NB: bitcoind counts the transaction itself as its own ancestor and descendant, which is confusing: we fix that by decrementing these counters.
MempoolTx(vsize.toLong, weight.toLong, replaceable, toSatoshi(fees), ancestorCount.toInt - 1, toSatoshi(ancestorFees), descendantCount.toInt - 1, toSatoshi(descendantFees))
MempoolTx(txid, vsize.toLong, weight.toLong, replaceable, toSatoshi(fees), ancestorCount.toInt - 1, toSatoshi(ancestorFees), descendantCount.toInt - 1, toSatoshi(descendantFees))
})
}

Expand Down Expand Up @@ -276,6 +284,7 @@ object ExtendedBitcoinClient {
/**
* Information about a transaction currently in the mempool.
*
* @param txid transaction id.
* @param vsize virtual transaction size as defined in BIP 141.
* @param weight transaction weight as defined in BIP 141.
* @param replaceable Whether this transaction could be replaced with RBF (BIP125).
Expand All @@ -285,7 +294,7 @@ object ExtendedBitcoinClient {
* @param descendantCount number of unconfirmed child transactions.
* @param descendantFees transactions fees for the package consisting of this transaction and its unconfirmed children (without its unconfirmed parents).
*/
case class MempoolTx(vsize: Long, weight: Long, replaceable: Boolean, fees: Satoshi, ancestorCount: Int, ancestorFees: Satoshi, descendantCount: Int, descendantFees: Satoshi)
case class MempoolTx(txid: ByteVector32, vsize: Long, weight: Long, replaceable: Boolean, fees: Satoshi, ancestorCount: Int, ancestorFees: Satoshi, descendantCount: Int, descendantFees: Satoshi)

def toSatoshi(btcAmount: BigDecimal): Satoshi = Satoshi(btcAmount.bigDecimal.scaleByPowerOfTen(8).longValue)

Expand Down
Expand Up @@ -170,7 +170,7 @@ class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor wi

case ElectrumClient.ServerError(ElectrumClient.GetTransaction(txid, Some(origin: ActorRef)), _) => origin ! GetTxWithMetaResponse(txid, None, tip.time)

case PublishAsap(tx) =>
case PublishAsap(tx, _) =>
val blockCount = this.blockCount.get()
val cltvTimeout = Scripts.cltvTimeout(tx)
val csvTimeouts = Scripts.csvTimeouts(tx)
Expand All @@ -180,7 +180,7 @@ class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor wi
csvTimeouts.foreach { case (parentTxId, csvTimeout) =>
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parentTxId=$parentTxId tx={}", tx)
val parentPublicKeyScript = WatchConfirmed.extractPublicKeyScript(tx.txIn.find(_.outPoint.txid == parentTxId).get.witness)
self ! WatchConfirmed(self, parentTxId, parentPublicKeyScript, minDepth = csvTimeout, BITCOIN_PARENT_TX_CONFIRMED(tx))
self ! WatchConfirmed(self, parentTxId, parentPublicKeyScript, minDepth = csvTimeout, BITCOIN_PARENT_TX_CONFIRMED(PublishAsap(tx, PublishStrategy.JustPublish)))
}
} else if (cltvTimeout > blockCount) {
log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)")
Expand All @@ -191,7 +191,7 @@ class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor wi
context become running(height, tip, watches, scriptHashStatus, block2tx, sent :+ tx)
}

case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), _, _, _) =>
case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(PublishAsap(tx, _)), _, _, _) =>
log.info(s"parent tx of txid=${tx.txid} has been confirmed")
val blockCount = this.blockCount.get()
val cltvTimeout = Scripts.cltvTimeout(tx)
Expand All @@ -214,8 +214,8 @@ class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor wi

case ElectrumClient.ElectrumDisconnected =>
// we remember watches and keep track of tx that have not yet been published
// we also re-send the txes that we previously sent but hadn't yet received the confirmation
context become disconnected(watches, sent.map(PublishAsap), block2tx, Queue.empty)
// we also re-send the txs that we previously sent but hadn't yet received the confirmation
context become disconnected(watches, sent.map(tx => PublishAsap(tx, PublishStrategy.JustPublish)), block2tx, Queue.empty)
}

def publish(tx: Transaction): Unit = {
Expand Down

0 comments on commit 5d662fc

Please sign in to comment.