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