Skip to content
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
5 changes: 5 additions & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ eclair.relay.peer-reputation.enabled = false
eclair.relay.reserved-for-accountable = 0.0
```

### Faster scanning for spending transactions with Bitcoin Core's txospenderindex

If Bitcoin Core's `txospenderindex` (available in Bitcoin Core 31.0 and newer) is available and synced, Eclair will use it to find channel spending transactions, which is much faster and less expensive than scanning blocks.
To enable this index, start Bitcoin Core with `-txospenderindex` or add `txospenderindex=1` to your `bitcoin.conf`.

### Configuration changes

<insert changes>
Expand Down
12 changes: 6 additions & 6 deletions eclair-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-30.2/bitcoin-30.2-x86_64-linux-gnu.tar.gz</bitcoind.url>
<bitcoind.sha256>6aa7bb4feb699c4c6262dd23e4004191f6df7f373b5d5978b5bcdd4bb72f75d8</bitcoind.sha256>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-31.0/bitcoin-31.0-x86_64-linux-gnu.tar.gz</bitcoind.url>
<bitcoind.sha256>d3e4c58a35b1d0a97a457462c94f55501ad167c660c245cb1ffa565641c65074</bitcoind.sha256>
</properties>
</profile>
<profile>
Expand All @@ -99,8 +99,8 @@
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-30.2/bitcoin-30.2-x86_64-apple-darwin.tar.gz</bitcoind.url>
<bitcoind.sha256>99d5cee9b9c37be506396c30837a4b98e320bfea71c474d6120a7e8eb6075c7b</bitcoind.sha256>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-31.0/bitcoin-31.0-x86_64-apple-darwin.tar.gz</bitcoind.url>
<bitcoind.sha256>56824dd705bc2a3b22d42e8aa02ed53498d491ff7c2c8aa96831333871887ead</bitcoind.sha256>
</properties>
</profile>
<profile>
Expand All @@ -111,8 +111,8 @@
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-30.2/bitcoin-30.2-win64.zip</bitcoind.url>
<bitcoind.sha256>0d7e1f16f8823aa26d29b44855ff6dbac11c03d75631a6c1d2ea5fab3a84fdf8</bitcoind.sha256>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-31.0/bitcoin-31.0-win64.zip</bitcoind.url>
<bitcoind.sha256>82fd2c504a0f20a31d4d13bd407783d6fc7bf17622d0ce85228a9b92694e03f0</bitcoind.sha256>
</properties>
</profile>
</profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ trait OnChainChannelFunder {
* Note that if this function returns false, that doesn't mean the output cannot be spent. The output could be unknown
* (not in the blockchain nor in the mempool) but could reappear later and be spendable at that point.
*/
def isTransactionOutputSpendable(txid: TxId, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean]
def isTransactionOutputSpendable(outPoint: OutPoint, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean]

/** Rollback a transaction that we failed to commit: this probably translates to "release locks on utxos". */
def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ object ZmqWatcher {
def txId: TxId
/** Index of the outpoint to watch. */
def outputIndex: Int
/** outpoint to watch */
def outPoint: OutPoint = OutPoint(txId, outputIndex)
/**
* TxIds of potential spending transactions; most of the time we know the txs, and it allows for optimizations.
* This argument can safely be ignored by watcher implementations.
Expand Down Expand Up @@ -427,6 +429,8 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
}
}

private def canUseTxoSpenderIndex: Future[Boolean] = client.getIndexInfo().map(_.get("txospenderindex").exists(_.synced))

private def checkSpent(w: WatchSpent[_ <: WatchSpentTriggered]): Future[Unit] = {
// First let's see if the parent tx was published or not before checking whether it has been spent.
client.getTxConfirmations(w.txId).collect {
Expand All @@ -435,45 +439,60 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
// This is an external channels: funds are not at risk, so we don't need to scan the blockchain to find the
// spending transaction, it is costly and unnecessary. We simply check whether the output has already been
// spent by a confirmed transaction.
client.isTransactionOutputSpent(w.txId, w.outputIndex).collect {
client.isTransactionOutputSpent(w.outPoint).collect {
case true =>
// The output has been spent, so we trigger the watch without including the spending transaction.
context.self ! TriggerEvent(w.replyTo, w, WatchExternalChannelSpentTriggered(w.shortChannelId, None))
}
case _ =>
// The parent tx was published, we need to make sure this particular output has not been spent.
client.isTransactionOutputSpendable(w.txId, w.outputIndex, includeMempool = true).collect {
case false =>
// The output has been spent, let's find the spending tx.
// If we know some potential spending txs, we try to fetch them directly.
Future.sequence(w.hints.map(txid => client.getTransaction(txid).map(Some(_)).recover { case _ => None }))
.map(_.flatten) // filter out errors and hint transactions that can't be found
.map(hintTxs => {
hintTxs.find(tx => tx.txIn.exists(i => i.outPoint.txid == w.txId && i.outPoint.index == w.outputIndex)) match {
case Some(spendingTx) =>
log.info("{}:{} has already been spent by a tx provided in hints: txid={}", w.txId, w.outputIndex, spendingTx.txid)
context.self ! ProcessNewTransaction(spendingTx)
case None =>
// The hints didn't help us, let's search for the spending transaction in the mempool.
log.info("{}:{} has already been spent, looking for the spending tx in the mempool", w.txId, w.outputIndex)
client.lookForMempoolSpendingTx(w.txId, w.outputIndex).map(Some(_)).recover { case _ => None }.map {
case Some(spendingTx) =>
log.info("found tx spending {}:{} in the mempool: txid={}", w.txId, w.outputIndex, spendingTx.txid)
context.self ! ProcessNewTransaction(spendingTx)
case None =>
// The spending transaction isn't in the mempool, so it must be a transaction that confirmed
// before we set the watch. We have to scan the blockchain to find it, which is expensive
// since bitcoind doesn't provide indexes for this scenario.
log.warn("{}:{} has already been spent, spending tx not in the mempool, looking in the blockchain...", w.txId, w.outputIndex)
client.lookForSpendingTx(None, w.txId, w.outputIndex, nodeParams.channelConf.maxChannelSpentRescanBlocks).map { spendingTx =>
log.warn("found the spending tx of {}:{} in the blockchain: txid={}", w.txId, w.outputIndex, spendingTx.txid)
client.isTransactionOutputSpendable(w.outPoint, includeMempool = true).collect {
case false => canUseTxoSpenderIndex.foreach {
case true =>
// The output has been spent, let's find the spending tx in the txospenderindex.
log.info("{} has already been spent, looking for the spending tx", w.outPoint)
client.findSpendingTx(w.outPoint) map {
case Some((spendingTx, _)) =>
log.info("found tx spending {} txid={}", w.outPoint, spendingTx.txid)
context.self ! ProcessNewTransaction(spendingTx)
case None =>
log.warn("could not find the spending tx of {}, funds are at risk", w.outPoint)
} recover {
case error =>
log.warn("error finding the spending tx of {}, funds are at risk", w.outPoint, error)
}
case false =>
// txospenderindex is not available, scan the mempool and the blockchain
// If we know some potential spending txs, we try to fetch them directly.
Future.sequence(w.hints.map(txid => client.getTransaction(txid).map(Some(_)).recover { case _ => None }))
.map(_.flatten) // filter out errors and hint transactions that can't be found
.map(hintTxs => {
hintTxs.find(tx => tx.txIn.exists(i => i.outPoint.txid == w.txId && i.outPoint.index == w.outputIndex)) match {
case Some(spendingTx) =>
log.info("{}:{} has already been spent by a tx provided in hints: txid={}", w.txId, w.outputIndex, spendingTx.txid)
context.self ! ProcessNewTransaction(spendingTx)
case None =>
// The hints didn't help us, let's search for the spending transaction in the mempool.
log.info("{}:{} has already been spent, looking for the spending tx in the mempool", w.txId, w.outputIndex)
client.lookForMempoolSpendingTx(w.outPoint).map(Some(_)).recover { case _ => None }.map {
case Some(spendingTx) =>
log.info("found tx spending {}:{} in the mempool: txid={}", w.txId, w.outputIndex, spendingTx.txid)
context.self ! ProcessNewTransaction(spendingTx)
}.recover {
case _ => log.warn("could not find the spending tx of {}:{} in the blockchain, funds are at risk", w.txId, w.outputIndex)
}
}
}
})
case None =>
// The spending transaction isn't in the mempool, so it must be a transaction that confirmed
// before we set the watch. We have to scan the blockchain to find it, which is expensive
// since bitcoind doesn't provide indexes for this scenario.
log.warn("{}:{} has already been spent, spending tx not in the mempool, looking in the blockchain...", w.txId, w.outputIndex)
client.lookForSpendingTx(None, w.outPoint, nodeParams.channelConf.maxChannelSpentRescanBlocks).map { spendingTx =>
log.warn("found the spending tx of {}:{} in the blockchain: txid={}", w.txId, w.outputIndex, spendingTx.txid)
context.self ! ProcessNewTransaction(spendingTx)
}.recover {
case _ => log.warn("could not find the spending tx of {}:{} in the blockchain, funds are at risk", w.txId, w.outputIndex)
}
}
}
})
}
}
}
}
Expand Down
Loading
Loading