Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use package relay for anchor force-close #2963

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ This means that instead of re-implementing them, Eclair benefits from the verifi

* Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-enabled_, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node.
* You must configure your Bitcoin node to use `bech32` or `bech32m` (segwit) addresses. If your wallet has "non-segwit UTXOs" (outputs that are neither `p2sh-segwit`, `bech32` or `bech32m`), you must send them to a `bech32` or `bech32m` address before running Eclair.
* Eclair requires Bitcoin Core 27.2 or higher. If you are upgrading an existing wallet, you may need to create a new address and send all your funds to that address.
* Eclair requires Bitcoin Core 28.1 or higher. If you are upgrading an existing wallet, you may need to create a new address and send all your funds to that address.

Run bitcoind with the following minimal `bitcoin.conf`:

Expand Down
13 changes: 12 additions & 1 deletion docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@

## Major changes

<insert changes>
### Update minimal version of Bitcoin Core

With this release, eclair requires using Bitcoin Core 28.1.
Newer versions of Bitcoin Core may be used, but have not been extensively tested.

### Package relay

With Bitcoin Core 28.1, eclair starts relying on the `submitpackage` RPC during channel force-close.
When using anchor outputs, this allows replacing the remote commitment transaction with the local commitment transaction (and a CPFP child).
This allows propagating our local commitment transaction to peers who are also running Bitcoin Core 28.x or newer, even if the commitment feerate is low (package relay).

This removes the need for increasing the commitment feerate based on mempool conditions, which ensures that channels won't be force-closed anymore when nodes disagree on the current feerate.

### API changes

Expand Down
18 changes: 9 additions & 9 deletions eclair-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-27.2/bitcoin-27.2-x86_64-linux-gnu.tar.gz</bitcoind.url>
<bitcoind.md5>c6dcec7ce5c43dafa48fe459911a8049</bitcoind.md5>
<bitcoind.sha1>4342a03bbcc98d81fca2c4fb404f96d5dbae4e10</bitcoind.sha1>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz</bitcoind.url>
<bitcoind.md5>2c915b5ea3a7e6662dd059d720109d7a</bitcoind.md5>
<bitcoind.sha1>e0fd253757e5f8d7d9c9cd73936e92cc6e168558</bitcoind.sha1>
</properties>
</profile>
<profile>
Expand All @@ -101,9 +101,9 @@
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-27.2/bitcoin-27.2-x86_64-apple-darwin.tar.gz</bitcoind.url>
<bitcoind.md5>25857522febc428160bc4eedf46eb6db</bitcoind.md5>
<bitcoind.sha1>574d753359ef2b5c1bc0ef1e028d516da86392af</bitcoind.sha1>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-apple-darwin.tar.gz</bitcoind.url>
<bitcoind.md5>d4ad664051072de807d4a864d58ccd2a</bitcoind.md5>
<bitcoind.sha1>f3e10c839a04929da870fea16829567d26dbecd8</bitcoind.sha1>
</properties>
</profile>
<profile>
Expand All @@ -114,9 +114,9 @@
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-27.2/bitcoin-27.2-win64.zip</bitcoind.url>
<bitcoind.md5>1a05b7880a01c0437e5e0b7e13a02635</bitcoind.md5>
<bitcoind.sha1>84c0b8d1a02d3c024881a180e8a3c670c1e0073a</bitcoind.sha1>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-win64.zip</bitcoind.url>
<bitcoind.md5>29748a873277cbb112b96c3662dcb3a4</bitcoind.md5>
<bitcoind.sha1>a7815c48d8f879f3728b50ad06a5fa1f1e10e0dc</bitcoind.sha1>
</properties>
</profile>
</profiles>
Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ class Setup(val datadir: File,
await(getBitcoinStatus(bitcoinClient), 30 seconds, "bitcoind did not respond after 30 seconds")
}
logger.info(s"bitcoind version=${bitcoinStatus.version}")
assert(bitcoinStatus.version >= 270200, "Eclair requires Bitcoin Core 27.2 or higher")
assert(bitcoinStatus.version >= 280000, "Eclair requires Bitcoin Core 28.x or higher")
bitcoinStatus.unspentAddresses.foreach { address =>
val isSegwit = addressToPublicKeyScript(bitcoinStatus.chainHash, address).map(script => Script.isNativeWitnessScript(script)).getOrElse(false)
assert(isSegwit, s"Your wallet contains non-segwit UTXOs (e.g. address=$address). You must send those UTXOs to a segwit address to use Eclair (check out our README for more details).")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ trait OnChainChannelFunder {
* Fund the provided transaction by adding inputs (and a change output if necessary).
* Callers must verify that the resulting transaction isn't sending funds to unexpected addresses (malicious bitcoin node).
*/
def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse]
def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi], minConfirmations_opt: Option[Int])(implicit ec: ExecutionContext): Future[FundTransactionResponse]

/**
* Sign a PSBT. Result may be partially signed: only inputs known to our bitcoin wallet will be signed. *
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFu
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult}
import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw}
import fr.acinq.eclair.crypto.keymanager.OnChainKeyManager
import fr.acinq.eclair.json.SatoshiSerializer
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire.protocol.ChannelAnnouncement
import fr.acinq.eclair.{BlockHeight, TimestampSecond, TxCoordinates}
Expand Down Expand Up @@ -262,8 +261,8 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag
})
}

def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, inputWeights = externalInputsWeight.map { case (outpoint, weight) => InputWeight(outpoint, weight) }.toSeq), feeBudget_opt = feeBudget_opt)
def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi] = None, minConfirmations_opt: Option[Int] = None)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, inputWeights = externalInputsWeight.map { case (outpoint, weight) => InputWeight(outpoint, weight) }.toSeq, minConfirmations = minConfirmations_opt), feeBudget_opt = feeBudget_opt)
}

private def processPsbt(psbt: Psbt, sign: Boolean = true, sighashType: Option[Int] = None)(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = {
Expand Down Expand Up @@ -483,6 +482,25 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag
getRawTransaction(tx.txid).map(_ => tx.txid).recoverWith { case _ => Future.failed(e) }
}

/**
* Publish a 1-parent-1-child transaction package, which allows replacing a conflicting parent transaction that has
* the same (or a higher) feerate by leveraging CPFP. The child transaction cannot have other unconfirmed parents.
*/
def publishPackage(parentTx: Transaction, childTx: Transaction)(implicit ec: ExecutionContext): Future[TxId] = {
rpcClient.invoke("submitpackage", Seq(parentTx, childTx).map(_.toString())).flatMap(json => {
val JString(msg) = json \ "package_msg"
if (msg == "success") {
// All transactions were accepted into or are already in the mempool.
Future.successful(childTx.txid)
} else {
val childError = (json \ "tx-results" \ childTx.wtxid.toHex \ "error").extractOpt[String]
val parentError = (json \ "tx-results" \ parentTx.wtxid.toHex \ "error").extractOpt[String]
val error = childError.orElse(parentError).getOrElse("unknown failure")
Future.failed(new IllegalArgumentException(error))
}
})
}

override def abandon(txId: TxId)(implicit ec: ExecutionContext): Future[Boolean] = {
rpcClient.invoke("abandontransaction", txId).map(_ => true).recover(_ => false)
}
Expand Down Expand Up @@ -704,10 +722,10 @@ object BitcoinCoreClient {
def apply(outPoint: OutPoint, weight: Long): InputWeight = InputWeight(outPoint.txid.value.toHex, outPoint.index, weight)
}

case class FundTransactionOptions(feeRate: BigDecimal, replaceable: Boolean, lockUnspents: Boolean, changePosition: Option[Int], input_weights: Option[Seq[InputWeight]])
case class FundTransactionOptions(feeRate: BigDecimal, replaceable: Boolean, lockUnspents: Boolean, changePosition: Option[Int], minconf: Option[Int], input_weights: Option[Seq[InputWeight]])

object FundTransactionOptions {
def apply(feerate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, inputWeights: Seq[InputWeight] = Nil): FundTransactionOptions = {
def apply(feerate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, minConfirmations: Option[Int] = None, inputWeights: Seq[InputWeight] = Nil): FundTransactionOptions = {
FundTransactionOptions(
BigDecimal(FeeratePerKB(feerate).toLong).bigDecimal.scaleByPowerOfTen(-8),
replaceable,
Expand All @@ -721,6 +739,7 @@ object BitcoinCoreClient {
// potentially be double-spent.
lockUnspents = true,
changePosition,
minConfirmations,
if (inputWeights.isEmpty) None else Some(inputWeights)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response
case p: SpliceTxRbf => p.feeBudget_opt
case _ => None
}
context.pipeToSelf(wallet.fundTransaction(txNotFunded, fundingParams.targetFeerate, replaceable = true, externalInputsWeight = sharedInputWeight, feeBudget_opt = feeBudget_opt)) {
val minConfirmations_opt = if (fundingParams.requireConfirmedInputs.forLocal) Some(1) else None
context.pipeToSelf(wallet.fundTransaction(txNotFunded, fundingParams.targetFeerate, replaceable = true, externalInputsWeight = sharedInputWeight, feeBudget_opt = feeBudget_opt, minConfirmations_opt)) {
case Failure(t) => WalletFailure(t)
case Success(result) => FundTransactionResult(result.tx, result.changePosition)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ private class FinalTxPublisher(nodeParams: NodeParams,

def publish(): Behavior[Command] = {
val txMonitor = context.spawn(MempoolTxMonitor(nodeParams, bitcoinClient, txPublishContext), "mempool-tx-monitor")
txMonitor ! MempoolTxMonitor.Publish(context.messageAdapter[MempoolTxMonitor.TxResult](WrappedTxResult), cmd.tx, cmd.input, cmd.desc, cmd.fee)
txMonitor ! MempoolTxMonitor.Publish(context.messageAdapter[MempoolTxMonitor.TxResult](WrappedTxResult), cmd.tx, None, cmd.input, cmd.desc, cmd.fee)
Behaviors.receiveMessagePartial {
case WrappedTxResult(txResult) =>
txResult match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object MempoolTxMonitor {

// @formatter:off
sealed trait Command
case class Publish(replyTo: ActorRef[TxResult], tx: Transaction, input: OutPoint, desc: String, fee: Satoshi) extends Command
case class Publish(replyTo: ActorRef[TxResult], tx: Transaction, parentTx_opt: Option[Transaction], input: OutPoint, desc: String, fee: Satoshi) extends Command
private case object PublishOk extends Command
private case class PublishFailed(reason: Throwable) extends Command
private case class InputStatus(spentConfirmed: Boolean, spentUnconfirmed: Boolean) extends Command
Expand Down Expand Up @@ -95,7 +95,10 @@ private class MempoolTxMonitor(nodeParams: NodeParams,
private val log = context.log

def publish(): Behavior[Command] = {
context.pipeToSelf(bitcoinClient.publishTransaction(cmd.tx)) {
context.pipeToSelf(cmd.parentTx_opt match {
case Some(parentTx) => bitcoinClient.publishPackage(parentTx, cmd.tx)
case None => bitcoinClient.publishTransaction(cmd.tx)
}) {
case Success(_) => PublishOk
case Failure(reason) => PublishFailed(reason)
}
Expand Down
Loading
Loading