From 8bea2ed676c01811882c27ca79acd724f17e5806 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 29 Aug 2023 09:43:36 +0200 Subject: [PATCH] Limit how far we look into the blockchain If we're unable to find the spending tx in the mempool, we start looking for it in the blockchain. Unfortunately, there are cases where we end up in that code path even though the spending tx is not confirmed: - a timeout on the `getTransaction` call - the spending tx gets dropped from our mempool for some reason - bitcoind is malicious or buggy Fetching the whole blockchain isn't really useful: after enough time has passed, we can be pretty sure that a potential attacker would have claimed the transaction's outputs already and we can't punish them. We limit this to the last month of blockchain data, which should be much larger than our `to_delay`. --- .../eclair/blockchain/bitcoind/ZmqWatcher.scala | 2 ++ .../bitcoind/rpc/BitcoinCoreClient.scala | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index 558877411d..6a34649b4a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -399,6 +399,8 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client client.lookForSpendingTx(None, w.txId, w.outputIndex).map { spendingTx => log.warn(s"found the spending tx of ${w.txId}:${w.outputIndex} in the blockchain: txid=${spendingTx.txid}") context.self ! ProcessNewTransaction(spendingTx) + }.recover { + case _ => log.warn(s"could not find the spending tx of ${w.txId}:${w.outputIndex} in the blockchain, funds are at risk") } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index 2df951d9ba..a80f470c4b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -170,8 +170,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall /** * Iterate over blocks to find the transaction that has spent a given output. - * NB: only call this method when you're sure the output has been spent, otherwise this will iterate over the whole - * blockchain history. + * NB: this will iterate over the past month of blockchain history, which is resource-intensive. * * @param blockhash_opt hash of a block *after* the output has been spent. If not provided, we will use the blockchain tip. * @param txid id of the transaction output that has been spent. @@ -179,10 +178,13 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall * @return the transaction spending the given output. */ def lookForSpendingTx(blockhash_opt: Option[ByteVector32], txid: ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = { - lookForSpendingTx(blockhash_opt.map(KotlinUtils.scala2kmp), KotlinUtils.scala2kmp(txid), outputIndex) + // It isn't useful to look at the whole blockchain history: if the transaction was confirmed long ago, an attacker + // will have already claimed all possible outputs and there's nothing we can do about it. + val limit = 4 * 720 + lookForSpendingTx(blockhash_opt.map(KotlinUtils.scala2kmp), txid, outputIndex, limit) } - def lookForSpendingTx(blockhash_opt: Option[fr.acinq.bitcoin.ByteVector32], txid: fr.acinq.bitcoin.ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = + def lookForSpendingTx(blockhash_opt: Option[fr.acinq.bitcoin.ByteVector32], txid: ByteVector32, outputIndex: Int, limit: Int)(implicit ec: ExecutionContext): Future[Transaction] = for { blockhash <- blockhash_opt match { case Some(b) => Future.successful(b) @@ -191,9 +193,10 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall // with a verbosity of 0, getblock returns the raw serialized block block <- rpcClient.invoke("getblock", blockhash, 0).collect { case JString(b) => Block.read(b) } prevblockhash = block.header.hashPreviousBlock.reversed() - res <- block.tx.asScala.find(tx => tx.txIn.asScala.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex)) match { - case None => lookForSpendingTx(Some(prevblockhash), txid, outputIndex) + res <- block.tx.asScala.find(tx => tx.txIn.asScala.exists(i => i.outPoint.txid == KotlinUtils.scala2kmp(txid) && i.outPoint.index == outputIndex)) match { case Some(tx) => Future.successful(KotlinUtils.kmp2scala(tx)) + case None if limit > 0 => lookForSpendingTx(Some(prevblockhash), txid, outputIndex, limit - 1) + case None => Future.failed(new RuntimeException(s"couldn't find tx spending $txid:$outputIndex in the blockchain")) } } yield res