Skip to content
Merged
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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,22 @@ If you want to use a different wallet from the default one, you must set `eclair
Eclair will return BTC from closed channels to the wallet configured.
Any BTC found in the wallet can be used to fund the channels you choose to open.

We also recommend tweaking the following parameters in `bitcoin.conf`:

```conf
# This parameter ensures that your wallet will not create chains of unconfirmed
# transactions that would be rejected by other nodes.
walletrejectlongchains=1
# The following parameters set the maximum length of chains of unconfirmed
# transactions to 20 instead of the default value of 25.
limitancestorcount=20
limitdescendantcount=20
```

Setting these parameters lets you unblock long chains of unconfirmed channel funding transactions by using child-pays-for-parent (CPFP) to make them confirm.

With the default `bitcoind` parameters, if your node created a chain of 25 unconfirmed funding transactions with a low-feerate, you wouldn't be able to use CPFP to raise their fees because your CPFP transaction would likely be rejected by the rest of the network.

### Java Environment Variables

Some advanced parameters can be changed with java environment variables. Most users won't need this and can skip this section.
Expand Down Expand Up @@ -278,9 +294,14 @@ so you can easily run your Bitcoin node on both mainnet and testnet. For example
```conf
server=1
txindex=1

addresstype=bech32
changetype=bech32

walletrejectlongchains=1
limitancestorcount=20
limitdescendantcount=20

[main]
rpcuser=<your-mainnet-rpc-user-here>
rpcpassword=<your-mainnet-rpc-password-here>
Expand Down
2 changes: 1 addition & 1 deletion contrib/eclair-cli.bash-completion
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ _eclair-cli()
*)
# works fine, but is too slow at the moment.
# allopts=$($eclaircli help 2>&1 | awk '$1 ~ /^"/ { sub(/,/, ""); print $1}' | sed 's/[":]//g')
allopts="getinfo connect open close forceclose updaterelayfee peers channels channel allnodes allchannels allupdates findroute findroutetonode findroutebetweennodes parseinvoice payinvoice sendtonode getsentinfo createinvoice getinvoice listinvoices listpendinginvoices listreceivedpayments getreceivedinfo audit networkfees channelstats"
allopts="getinfo connect open cpfpbumpfees close forceclose updaterelayfee peers channels channel allnodes allchannels allupdates findroute findroutetonode findroutebetweennodes parseinvoice payinvoice sendtonode getsentinfo createinvoice getinvoice listinvoices listpendinginvoices listreceivedpayments getreceivedinfo audit networkfees channelstats"

if ! [[ " $allopts " =~ " $prev " ]]; then # prevent double arguments
if [[ -z "$cur" || "$cur" =~ ^[a-z] ]]; then
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ All this data is signed and encrypted so that it can not be read or forged by th
- `channel-opened` websocket event was updated to contain the final `channel_id` and be published when a channel is ready to process payments (#2567)
- `getsentinfo` can now be used with `--offer` to list payments sent to a specific offer.
- `listreceivedpayments` lists payments received by your node (#2607)
- `cpfpbumpfees` can be used to unblock chains of unconfirmed transactions by creating a child transaction that pays a high fee (#1783)

### Miscellaneous improvements and bug fixes

Expand Down
1 change: 1 addition & 0 deletions eclair-core/eclair-cli
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ and COMMAND is one of the available commands:
=== OnChain ===
- getnewaddress
- sendonchain
- cpfpbumpfees
- onchainbalance
- onchaintransactions
- globalbalance
Expand Down
11 changes: 10 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import akka.pattern._
import akka.util.Timeout
import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, Script, addressToPublicKeyScript}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Satoshi, Script, addressToPublicKeyScript}
import fr.acinq.eclair.ApiTypes.ChannelNotFound
import fr.acinq.eclair.balance.CheckBalance.GlobalBalance
import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener}
Expand Down Expand Up @@ -129,6 +129,8 @@ trait Eclair {

def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32]

def cpfpBumpFees(targetFeeratePerByte: FeeratePerByte, outpoints: Set[OutPoint]): Future[ByteVector32]

def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse]

def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse]
Expand Down Expand Up @@ -335,6 +337,13 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
}
}

override def cpfpBumpFees(targetFeeratePerByte: FeeratePerByte, outpoints: Set[OutPoint]): Future[ByteVector32] = {
appKit.wallet match {
case w: BitcoinCoreClient => w.cpfp(outpoints, FeeratePerKw(targetFeeratePerByte)).map(_.txid)
case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend"))
}
}

override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] =
findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, extraEdges, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,81 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
}
}

/**
* Create a child-pays-for-parent transaction to increase the effective feerate of a set of unconfirmed transactions.
* These unconfirmed transactions must:
* - be in our mempool (evicted transactions cannot be used)
* - have an output that can be spent by our bitcoin wallet (provided in the outpoints set)
* - the total amount of the set of outpoints must be high enough to pay the target feerate
*
* @param outpoints outpoints that should be spent by the CPFP transaction.
* @param targetFeerate feerate to apply to the package of unconfirmed transactions.
*/
def cpfp(outpoints: Set[OutPoint], targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[Transaction] = {
getMempoolPackage(outpoints.map(_.txid)).transformWith {
case Failure(ex) => Future.failed(new IllegalArgumentException("unable to analyze mempool package: some transactions could not be found in your mempool", ex))
case Success(mempoolPackage) =>
getTxOutputs(outpoints).transformWith {
case Failure(ex) => Future.failed(new IllegalArgumentException("some outpoints are invalid or cannot be resolved", ex))
case Success(txOutputs) =>
getP2wpkhPubkeyHashForChange().transformWith {
case Failure(ex) => Future.failed(new IllegalArgumentException("change address generation failed", ex))
case Success(changePubkeyHash) =>
val amountIn = txOutputs.values.map(_.amount).sum
// We build a transaction spending all the inputs provided to a single change output. Our inputs are
// using either p2wpkh or p2tr: p2tr inputs are slightly smaller, but we don't bother doing an exact
// calculation and always use the weight of p2wpkh inputs for simplicity.
val p2wpkhInputWeight = 272
val txWeight = p2wpkhInputWeight * outpoints.size + Transaction(2, Nil, Seq(TxOut(amountIn, Script.pay2wpkh(changePubkeyHash))), 0).weight()
val totalWeight = mempoolPackage.values.map(_.weight).sum + txWeight
val targetFees = Transactions.weight2fee(targetFeerate, totalWeight.toInt)
val currentFees = mempoolPackage.values.map(_.fees).sum
val missingFees = targetFees - currentFees
if (missingFees <= 0.sat) {
Future.failed(new IllegalArgumentException("package feerate is already higher than the target feerate"))
} else if (amountIn <= missingFees + 660.sat) {
Future.failed(new IllegalArgumentException("input amount is not sufficient to cover the target feerate"))
} else {
val unsignedTx = Transaction(2, outpoints.toSeq.map(o => TxIn(o, Seq.empty, 0)), Seq(TxOut(amountIn - missingFees, Script.pay2wpkh(changePubkeyHash))), 0)
signTransaction(unsignedTx, Nil).transformWith {
case Failure(ex) => Future.failed(new IllegalArgumentException("tx signing failed: some inputs don't belong to our wallet", ex))
case Success(signedTx) => publishTransaction(signedTx.tx).map(_ => signedTx.tx)
}
}
}
}
}
}

/** Recursively fetch unconfirmed parents and return the complete unconfirmed ancestors tree. */
def getMempoolPackage(leaves: Set[ByteVector32])(implicit ec: ExecutionContext): Future[Map[ByteVector32, MempoolTx]] = getMempoolPackage(leaves, Map.empty)

private def getMempoolPackage(leaves: Set[ByteVector32], current: Map[ByteVector32, MempoolTx])(implicit ec: ExecutionContext): Future[Map[ByteVector32, MempoolTx]] = {
Comment thread
sstone marked this conversation as resolved.
Future.sequence(leaves.map(txid => getMempoolTx(txid))).flatMap(txs => {
val current2 = current.concat(txs.map(tx => tx.txid -> tx))
val remainingParents = txs.flatMap(_.unconfirmedParents) -- current2.keySet
if (remainingParents.isEmpty) {
Future.successful(current2)
} else {
getMempoolPackage(remainingParents, current2)
}
})
}

/** Fetch transaction output details for the given outpoints. */
private def getTxOutputs(outpoints: Set[OutPoint])(implicit ec: ExecutionContext): Future[Map[OutPoint, TxOut]] = {
Comment thread
sstone marked this conversation as resolved.
Future.sequence(outpoints.map(_.txid).map(txid => getTransaction(txid))).flatMap(txs => {
val txOuts = outpoints.flatMap(o => txs.find(tx => tx.txid == o.txid && o.index < tx.txOut.length) match {
case Some(tx) => Some(o -> tx.txOut(o.index.toInt))
case None => None
}).toMap
outpoints.find(o => !txOuts.contains(o)) match {
case Some(o) => Future.failed(new IllegalArgumentException(s"invalid outpoint $o"))
case None => Future.successful(txOuts)
}
})
}

//------------------------- SIGNING -------------------------//

def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = signTransaction(tx, Nil)
Expand Down Expand Up @@ -381,7 +456,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
/**
* @return the public key hash of a bech32 raw change address.
*/
def getChangeAddress()(implicit ec: ExecutionContext): Future[ByteVector] = {
def getP2wpkhPubkeyHashForChange()(implicit ec: ExecutionContext): Future[ByteVector] = {
rpcClient.invoke("getrawchangeaddress", "bech32").collect {
case JString(changeAddress) =>
val pubkeyHash = ByteVector.view(Bech32.decodeWitnessAddress(changeAddress).getThird)
Expand Down Expand Up @@ -422,8 +497,9 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall
val JDecimal(ancestorFees) = json \ "fees" \ "ancestor"
val JDecimal(descendantFees) = json \ "fees" \ "descendant"
val JBool(replaceable) = json \ "bip125-replaceable"
val unconfirmedParents = (json \ "depends").extract[List[String]].map(ByteVector32.fromValidHex).toSet
// NB: bitcoind counts the transaction itself as its own ancestor and descendant, which is confusing: we fix that by decrementing these counters.
MempoolTx(txid, 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), unconfirmedParents)
})
}

Expand Down Expand Up @@ -528,17 +604,18 @@ object BitcoinCoreClient {
/**
* 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).
* @param fees transaction fees.
* @param ancestorCount number of unconfirmed parent transactions.
* @param ancestorFees transactions fees for the package consisting of this transaction and its unconfirmed parents.
* @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).
* @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).
* @param fees transaction fees.
* @param ancestorCount number of unconfirmed parent transactions.
* @param ancestorFees transactions fees for the package consisting of this transaction and its unconfirmed parents.
* @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).
* @param unconfirmedParents unconfirmed transactions used as inputs for this transaction.
*/
case class MempoolTx(txid: ByteVector32, 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, unconfirmedParents: Set[ByteVector32])

case class WalletTx(address: String, amount: Satoshi, fees: Satoshi, blockHash: ByteVector32, confirmations: Long, txid: ByteVector32, timestamp: Long)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams,
(anchorTx.updateTx(fundedTx), fundTxResponse.amountIn)
})
case None =>
bitcoinClient.getChangeAddress().map(pubkeyHash => {
bitcoinClient.getP2wpkhPubkeyHashForChange().map(pubkeyHash => {
val fundedTx = fundTxResponse.tx.copy(txOut = Seq(TxOut(dustLimit, Script.pay2wpkh(pubkeyHash))))
(anchorTx.updateTx(fundedTx), fundTxResponse.amountIn)
})
Expand Down
Loading