Skip to content

Commit ff178e2

Browse files
committed
Use OutPoint type instead of txid + output index (no functional changes)
1 parent da7cd96 commit ff178e2

9 files changed

Lines changed: 44 additions & 42 deletions

File tree

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ trait OnChainChannelFunder {
8181
* Note that if this function returns false, that doesn't mean the output cannot be spent. The output could be unknown
8282
* (not in the blockchain nor in the mempool) but could reappear later and be spendable at that point.
8383
*/
84-
def isTransactionOutputSpendable(txid: TxId, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean]
84+
def isTransactionOutputSpendable(outPoint: OutPoint, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean]
8585

8686
/** Rollback a transaction that we failed to commit: this probably translates to "release locks on utxos". */
8787
def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean]

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ object ZmqWatcher {
103103
def txId: TxId
104104
/** Index of the outpoint to watch. */
105105
def outputIndex: Int
106+
/** outpoint to watch */
107+
def outPoint: OutPoint = OutPoint(txId, outputIndex)
106108
/**
107109
* TxIds of potential spending transactions; most of the time we know the txs, and it allows for optimizations.
108110
* This argument can safely be ignored by watcher implementations.
@@ -435,14 +437,14 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
435437
// This is an external channels: funds are not at risk, so we don't need to scan the blockchain to find the
436438
// spending transaction, it is costly and unnecessary. We simply check whether the output has already been
437439
// spent by a confirmed transaction.
438-
client.isTransactionOutputSpent(w.txId, w.outputIndex).collect {
440+
client.isTransactionOutputSpent(w.outPoint).collect {
439441
case true =>
440442
// The output has been spent, so we trigger the watch without including the spending transaction.
441443
context.self ! TriggerEvent(w.replyTo, w, WatchExternalChannelSpentTriggered(w.shortChannelId, None))
442444
}
443445
case _ =>
444446
// The parent tx was published, we need to make sure this particular output has not been spent.
445-
client.isTransactionOutputSpendable(w.txId, w.outputIndex, includeMempool = true).collect {
447+
client.isTransactionOutputSpendable(w.outPoint, includeMempool = true).collect {
446448
case false =>
447449
// The output has been spent, let's find the spending tx.
448450
// If we know some potential spending txs, we try to fetch them directly.
@@ -456,7 +458,7 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
456458
case None =>
457459
// The hints didn't help us, let's search for the spending transaction in the mempool.
458460
log.info("{}:{} has already been spent, looking for the spending tx in the mempool", w.txId, w.outputIndex)
459-
client.lookForMempoolSpendingTx(w.txId, w.outputIndex).map(Some(_)).recover { case _ => None }.map {
461+
client.lookForMempoolSpendingTx(w.outPoint).map(Some(_)).recover { case _ => None }.map {
460462
case Some(spendingTx) =>
461463
log.info("found tx spending {}:{} in the mempool: txid={}", w.txId, w.outputIndex, spendingTx.txid)
462464
context.self ! ProcessNewTransaction(spendingTx)
@@ -465,7 +467,7 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
465467
// before we set the watch. We have to scan the blockchain to find it, which is expensive
466468
// since bitcoind doesn't provide indexes for this scenario.
467469
log.warn("{}:{} has already been spent, spending tx not in the mempool, looking in the blockchain...", w.txId, w.outputIndex)
468-
client.lookForSpendingTx(None, w.txId, w.outputIndex, nodeParams.channelConf.maxChannelSpentRescanBlocks).map { spendingTx =>
470+
client.lookForSpendingTx(None, w.outPoint, nodeParams.channelConf.maxChannelSpentRescanBlocks).map { spendingTx =>
469471
log.warn("found the spending tx of {}:{} in the blockchain: txid={}", w.txId, w.outputIndex, spendingTx.txid)
470472
context.self ! ProcessNewTransaction(spendingTx)
471473
}.recover {

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,17 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool
116116
* (not in the blockchain nor in the mempool) but could reappear later and be spendable at that point. If you want to
117117
* ensure that an output is not spendable anymore, you should use [[isTransactionOutputSpent]].
118118
*/
119-
def isTransactionOutputSpendable(txid: TxId, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] =
119+
def isTransactionOutputSpendable(outPoint: OutPoint, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] =
120120
for {
121-
json <- rpcClient.invoke("gettxout", txid, outputIndex, includeMempool)
121+
json <- rpcClient.invoke("gettxout", outPoint.txid, outPoint.index, includeMempool)
122122
} yield json != JNull
123123

124124
/**
125125
* Return true if this output has already been spent by a confirmed transaction.
126126
* Note that a reorg may invalidate the result of this function and make a spent output spendable again.
127127
*/
128-
def isTransactionOutputSpent(txid: TxId, outputIndex: Int)(implicit ec: ExecutionContext): Future[Boolean] = {
129-
getTxConfirmations(txid).flatMap {
128+
def isTransactionOutputSpent(outPoint: OutPoint)(implicit ec: ExecutionContext): Future[Boolean] = {
129+
getTxConfirmations(outPoint.txid).flatMap {
130130
case Some(confirmations) if confirmations > 0 =>
131131
// There is an important limitation when using isTransactionOutputSpendable: if it returns false, it can mean a
132132
// few different things:
@@ -137,7 +137,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool
137137
// The only way to make sure that our output has been spent is to verify that it is coming from a confirmed
138138
// transaction and that it has been spent by another confirmed transaction. We want to ignore the mempool to
139139
// only consider spending transactions that have been confirmed.
140-
isTransactionOutputSpendable(txid, outputIndex, includeMempool = false).map(r => !r)
140+
isTransactionOutputSpendable(outPoint, includeMempool = false).map(r => !r)
141141
case _ =>
142142
// If the output itself isn't in the blockchain, it cannot be spent by a confirmed transaction.
143143
Future.successful(false)
@@ -166,18 +166,18 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool
166166
// themselves been double-spent, we will never be able to consider our transaction double-spent. With the
167167
// information we have, these unknown inputs could eventually reappear and the transaction could be broadcast
168168
// again.
169-
Future.sequence(tx.txIn.map(txIn => isTransactionOutputSpent(txIn.outPoint.txid, txIn.outPoint.index.toInt))).map(_.exists(_ == true))
169+
Future.sequence(tx.txIn.map(txIn => isTransactionOutputSpent(txIn.outPoint))).map(_.exists(_ == true))
170170
}
171171
} yield doubleSpent
172172

173173
/** Search for mempool transaction spending a given output. */
174-
def lookForMempoolSpendingTx(txid: TxId, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = {
175-
rpcClient.invoke("gettxspendingprevout", Seq(OutpointArg(txid, outputIndex))).collect {
174+
def lookForMempoolSpendingTx(outPoint: OutPoint)(implicit ec: ExecutionContext): Future[Transaction] = {
175+
rpcClient.invoke("gettxspendingprevout", Seq(OutpointArg(outPoint.txid, outPoint.index))).collect {
176176
case JArray(results) => results.flatMap(result => (result \ "spendingtxid").extractOpt[String].map(TxId.fromValidHex))
177177
}.flatMap { spendingTxIds =>
178178
spendingTxIds.headOption match {
179179
case Some(spendingTxId) => getTransaction(spendingTxId)
180-
case None => Future.failed(new RuntimeException(s"mempool doesn't contain any transaction spending $txid:$outputIndex"))
180+
case None => Future.failed(new RuntimeException(s"mempool doesn't contain any transaction spending $outPoint"))
181181
}
182182
}
183183
}
@@ -188,23 +188,22 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool
188188
* will have already claimed all possible outputs and there's nothing we can do about it.
189189
*
190190
* @param blockHash_opt hash of a block *after* the output has been spent. If not provided, we will use the blockchain tip.
191-
* @param txid id of the transaction output that has been spent.
192-
* @param outputIndex index of the transaction output that has been spent.
191+
* @param outPoint transaction output that has been spent.
193192
* @param limit maximum number of previous blocks to scan.
194193
* @return the transaction spending the given output.
195194
*/
196-
def lookForSpendingTx(blockHash_opt: Option[BlockHash], txid: TxId, outputIndex: Int, limit: Int)(implicit ec: ExecutionContext): Future[Transaction] = {
195+
def lookForSpendingTx(blockHash_opt: Option[BlockHash], outPoint: OutPoint, limit: Int)(implicit ec: ExecutionContext): Future[Transaction] = {
197196
for {
198197
blockId <- blockHash_opt match {
199198
case Some(blockHash) => Future.successful(BlockId(blockHash))
200199
// NB: bitcoind confusingly returns the blockId instead of the blockHash.
201200
case None => rpcClient.invoke("getbestblockhash").collect { case JString(blockId) => BlockId(ByteVector32.fromValidHex(blockId)) }
202201
}
203202
block <- getBlock(blockId)
204-
res <- block.tx.asScala.find(tx => tx.txIn.asScala.exists(i => i.outPoint.txid == KotlinUtils.scala2kmp(txid) && i.outPoint.index == outputIndex)) match {
203+
res <- block.tx.asScala.find(tx => tx.txIn.asScala.exists(i => i.outPoint == KotlinUtils.scala2kmp(outPoint))) match {
205204
case Some(tx) => Future.successful(KotlinUtils.kmp2scala(tx))
206-
case None if limit > 0 => lookForSpendingTx(Some(KotlinUtils.kmp2scala(block.header.hashPreviousBlock)), txid, outputIndex, limit - 1)
207-
case None => Future.failed(new RuntimeException(s"couldn't find tx spending $txid:$outputIndex in the blockchain"))
205+
case None if limit > 0 => lookForSpendingTx(Some(KotlinUtils.kmp2scala(block.header.hashPreviousBlock)), outPoint, limit - 1)
206+
case None => Future.failed(new RuntimeException(s"couldn't find tx spending $outPoint in the blockchain"))
208207
}
209208
} yield res
210209
}
@@ -755,12 +754,13 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool
755754
TxId.fromValidHex(txs(txIndex).extract[String])
756755
}.getOrElse(TxId(ByteVector32.Zeroes)))
757756
tx <- getRawTransaction(txid)
758-
unspent <- isTransactionOutputSpendable(txid, outputIndex, includeMempool = true)
757+
outPoint = OutPoint(txid, outputIndex)
758+
unspent <- isTransactionOutputSpendable(outPoint, includeMempool = true)
759759
fundingTxStatus <- if (unspent) {
760760
Future.successful(UtxoStatus.Unspent)
761761
} else {
762762
// if this returns true, it means that the spending tx is *not* in the blockchain
763-
isTransactionOutputSpendable(txid, outputIndex, includeMempool = false).map(res => UtxoStatus.Spent(spendingTxConfirmed = !res))
763+
isTransactionOutputSpendable(outPoint, includeMempool = false).map(res => UtxoStatus.Spent(spendingTxConfirmed = !res))
764764
}
765765
} yield ValidateResult(c, Right((Transaction.read(tx), fundingTxStatus)))
766766
} recover {

eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
758758
// unconfirmed inputs, because if they are valid but not in our mempool we would incorrectly consider
759759
// them unspendable (unknown). We want to reject unspendable inputs to immediately fail the funding
760760
// attempt, instead of waiting to detect the double-spend later.
761-
wallet.isTransactionOutputSpendable(outpoint.txid, outpoint.index.toInt, includeMempool = true)
761+
wallet.isTransactionOutputSpendable(outpoint, includeMempool = true)
762762
case _ => Future.successful(false)
763763
}
764764
case Success(false) => Future.successful(false)

eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ private class MempoolTxMonitor(nodeParams: NodeParams,
200200
private def checkInputStatus(input: OutPoint): Unit = {
201201
val checkInputTask = for {
202202
parentConfirmations <- bitcoinClient.getTxConfirmations(input.txid)
203-
spendableMempoolExcluded <- bitcoinClient.isTransactionOutputSpendable(input.txid, input.index.toInt, includeMempool = false)
204-
spendableMempoolIncluded <- bitcoinClient.isTransactionOutputSpendable(input.txid, input.index.toInt, includeMempool = true)
203+
spendableMempoolExcluded <- bitcoinClient.isTransactionOutputSpendable(input, includeMempool = false)
204+
spendableMempoolIncluded <- bitcoinClient.isTransactionOutputSpendable(input, includeMempool = true)
205205
} yield computeInputStatus(parentConfirmations, spendableMempoolExcluded, spendableMempoolIncluded)
206206
context.pipeToSelf(checkInputTask) {
207207
case Success(status) => status

eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
9999
context.pipeToSelf(bitcoinClient.getTxConfirmations(fundingOutpoint.txid).flatMap {
100100
case Some(_) =>
101101
// The funding transaction was found, let's see if we can still spend it.
102-
bitcoinClient.isTransactionOutputSpendable(fundingOutpoint.txid, fundingOutpoint.index.toInt, includeMempool = true).flatMap {
102+
bitcoinClient.isTransactionOutputSpendable(fundingOutpoint, includeMempool = true).flatMap {
103103
case true =>
104104
// The funding output is unspent: let's publish our anchor transaction to get our local commit confirmed.
105105
Future.successful(ParentTxOk)
@@ -162,7 +162,7 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
162162
case Some(_) =>
163163
// The funding transaction was found, let's see if we can still spend it. Note that in this case, we only look
164164
// at *confirmed* spending transactions (unlike the local commit case).
165-
bitcoinClient.isTransactionOutputSpendable(fundingOutpoint.txid, fundingOutpoint.index.toInt, includeMempool = false).flatMap {
165+
bitcoinClient.isTransactionOutputSpendable(fundingOutpoint, includeMempool = false).flatMap {
166166
case true =>
167167
// The funding output is unspent, or spent by an *unconfirmed* transaction: let's publish our anchor
168168
// transaction, we may be able to replace our local commit with this (more interesting) remote commit.
@@ -221,7 +221,7 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
221221
case Some(_) =>
222222
// If the HTLC output is already spent by a confirmed transaction, there is no need for RBF: either this is one
223223
// of our transactions (which thus has a high enough feerate), or it was a race with our peer and we lost.
224-
bitcoinClient.isTransactionOutputSpent(input.txid, input.index.toInt).map {
224+
bitcoinClient.isTransactionOutputSpent(input).map {
225225
case true => HtlcOutputAlreadySpent
226226
case false => ParentTxOk
227227
}

eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class DummyOnChainWallet extends OnChainWallet with OnChainAddressCache {
7777

7878
override def getTxConfirmations(txid: TxId)(implicit ec: ExecutionContext): Future[Option[Int]] = Future.failed(new RuntimeException("transaction not found"))
7979

80-
override def isTransactionOutputSpendable(txid: TxId, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true)
80+
override def isTransactionOutputSpendable(outPoint: OutPoint, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true)
8181

8282
override def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = {
8383
rolledback = rolledback + tx
@@ -218,7 +218,7 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnChainAddressCache {
218218

219219
override def getTxConfirmations(txid: TxId)(implicit ec: ExecutionContext): Future[Option[Int]] = Future.successful(None)
220220

221-
override def isTransactionOutputSpendable(txid: TxId, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true)
221+
override def isTransactionOutputSpendable(outPoint: OutPoint, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true)
222222

223223
override def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = {
224224
rolledback = rolledback :+ tx

0 commit comments

Comments
 (0)