Skip to content

Commit

Permalink
Use TxId and TxHash objects
Browse files Browse the repository at this point in the history
This helps disambiguate when each is used. Even though confusingly,
Electrum calls its field `tx_hash` while actually returning the `txid`.
  • Loading branch information
t-bast committed Nov 23, 2023
1 parent f1a09ce commit 4235964
Show file tree
Hide file tree
Showing 59 changed files with 404 additions and 386 deletions.
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ kotlin {

val commonMain by sourceSets.getting {
dependencies {
api("fr.acinq.bitcoin:bitcoin-kmp:0.13.0") // when upgrading, keep secp256k1-kmp-jni-jvm in sync below
api("fr.acinq.bitcoin:bitcoin-kmp:0.14.0") // when upgrading, keep secp256k1-kmp-jni-jvm in sync below
api("org.kodein.log:canard:0.18.0")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
api("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
Expand Down Expand Up @@ -63,7 +63,7 @@ kotlin {
api(ktor("client-okhttp"))
api(ktor("network"))
api(ktor("network-tls"))
implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:0.10.1")
implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:0.11.0")
implementation("org.slf4j:slf4j-api:1.7.36")
api("org.xerial:sqlite-jdbc:3.32.3.2")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ sealed class Watch {
// we need a public key script to use electrum apis
data class WatchConfirmed(
override val channelId: ByteVector32,
val txId: ByteVector32,
val txId: TxId,
val publicKeyScript: ByteVector,
val minDepth: Long,
override val event: BitcoinEvent,
Expand Down Expand Up @@ -59,7 +59,7 @@ data class WatchConfirmed(

data class WatchSpent(
override val channelId: ByteVector32,
val txId: ByteVector32,
val txId: TxId,
val outputIndex: Int,
val publicKeyScript: ByteVector,
override val event: BitcoinEvent
Expand All @@ -83,7 +83,3 @@ sealed class WatchEvent {

data class WatchEventConfirmed(override val channelId: ByteVector32, override val event: BitcoinEvent, val blockHeight: Int, val txIndex: Int, val tx: Transaction) : WatchEvent()
data class WatchEventSpent(override val channelId: ByteVector32, override val event: BitcoinEvent, val tx: Transaction) : WatchEvent()

data class PublishAsap(val tx: Transaction)
data class GetTxWithMeta(val channelId: ByteVector32, val txid: ByteVector32)
data class GetTxWithMetaResponse(val txid: ByteVector32, val tx_opt: Transaction?, val lastBlockTimestamp: Long)
Original file line number Diff line number Diff line change
Expand Up @@ -270,19 +270,19 @@ class ElectrumClient(
}
}

override suspend fun getTx(txid: ByteVector32): Transaction? = rpcCall<GetTransactionResponse>(GetTransaction(txid)).right?.tx
override suspend fun getTx(txId: TxId): Transaction? = rpcCall<GetTransactionResponse>(GetTransaction(txId)).right?.tx

override suspend fun getHeader(blockHeight: Int): BlockHeader? = rpcCall<GetHeaderResponse>(GetHeader(blockHeight)).right?.header

override suspend fun getHeaders(startHeight: Int, count: Int): List<BlockHeader> = rpcCall<GetHeadersResponse>(GetHeaders(startHeight, count)).right?.headers ?: listOf()

override suspend fun getMerkle(txid: ByteVector32, blockHeight: Int, contextOpt: Transaction?): GetMerkleResponse? = rpcCall<GetMerkleResponse>(GetMerkle(txid, blockHeight, contextOpt)).right
override suspend fun getMerkle(txId: TxId, blockHeight: Int, contextOpt: Transaction?): GetMerkleResponse? = rpcCall<GetMerkleResponse>(GetMerkle(txId, blockHeight, contextOpt)).right

override suspend fun getScriptHashHistory(scriptHash: ByteVector32): List<TransactionHistoryItem> = rpcCall<GetScriptHashHistoryResponse>(GetScriptHashHistory(scriptHash)).right?.history ?: listOf()

override suspend fun getScriptHashUnspents(scriptHash: ByteVector32): List<UnspentItem> = rpcCall<ScriptHashListUnspentResponse>(ScriptHashListUnspent(scriptHash)).right?.unspents ?: listOf()

override suspend fun broadcastTransaction(tx: Transaction): ByteVector32 = rpcCall<BroadcastTransactionResponse>(BroadcastTransaction(tx)).right?.tx?.txid ?: tx.txid
override suspend fun broadcastTransaction(tx: Transaction): TxId = rpcCall<BroadcastTransactionResponse>(BroadcastTransaction(tx)).right?.tx?.txid ?: tx.txid

override suspend fun estimateFees(confirmations: Int): FeeratePerKw? = rpcCall<EstimateFeeResponse>(EstimateFees(confirmations)).right?.feerate

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package fr.acinq.lightning.blockchain.electrum

import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Satoshi
import fr.acinq.bitcoin.Transaction
import fr.acinq.bitcoin.TxId
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.channel.Commitments
import fr.acinq.lightning.channel.LocalFundingStatus
import fr.acinq.lightning.transactions.Transactions
import fr.acinq.lightning.utils.MDCLogger
import fr.acinq.lightning.utils.sat

suspend fun IElectrumClient.getConfirmations(txId: ByteVector32): Int? = getTx(txId)?.let { tx -> getConfirmations(tx) }
suspend fun IElectrumClient.getConfirmations(txId: TxId): Int? = getTx(txId)?.let { tx -> getConfirmations(tx) }

/**
* @return the number of confirmations, zero if the transaction is in the mempool, null if the transaction is not found
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ data class GetScriptHashHistory(val scriptHash: ByteVector32) : ElectrumRequest(
override val method: String = "blockchain.scripthash.get_history"
}

data class TransactionHistoryItem(val blockHeight: Int, val txid: ByteVector32)
data class TransactionHistoryItem(val blockHeight: Int, val txid: TxId)
data class GetScriptHashHistoryResponse(val scriptHash: ByteVector32, val history: List<TransactionHistoryItem>) : ElectrumResponse

data class ScriptHashListUnspent(val scriptHash: ByteVector32) : ElectrumRequest(scriptHash) {
override val method: String = "blockchain.scripthash.listunspent"
}

data class UnspentItem(val txid: ByteVector32, val outputIndex: Int, val value: Long, val blockHeight: Long) {
val outPoint by lazy { OutPoint(txid.reversed(), outputIndex.toLong()) }
data class UnspentItem(val txid: TxId, val outputIndex: Int, val value: Long, val blockHeight: Long) {
val outPoint by lazy { OutPoint(txid, outputIndex.toLong()) }
}

data class ScriptHashListUnspentResponse(val scriptHash: ByteVector32, val unspents: List<UnspentItem>) : ElectrumResponse
Expand All @@ -110,9 +110,9 @@ data class GetTransactionIdFromPosition(val blockHeight: Int, val txIndex: Int,
override val method: String = "blockchain.transaction.id_from_pos"
}

data class GetTransactionIdFromPositionResponse(val txid: ByteVector32, val blockHeight: Int, val txIndex: Int, val merkle: List<ByteVector32> = emptyList()) : ElectrumResponse
data class GetTransactionIdFromPositionResponse(val txid: TxId, val blockHeight: Int, val txIndex: Int, val merkleProof: List<ByteVector32> = emptyList()) : ElectrumResponse

data class GetTransaction(val txid: ByteVector32, val contextOpt: Any? = null) : ElectrumRequest(txid) {
data class GetTransaction(val txid: TxId, val contextOpt: Any? = null) : ElectrumRequest(txid) {
override val method: String = "blockchain.transaction.get"
}

Expand All @@ -132,11 +132,11 @@ data class GetHeadersResponse(val start_height: Int, val headers: List<BlockHead
override fun toString(): String = "GetHeadersResponse($start_height, ${headers.size}, ${headers.first()}, ${headers.last()}, $max)"
}

data class GetMerkle(val txid: ByteVector32, val blockHeight: Int, val contextOpt: Transaction? = null) : ElectrumRequest(txid, blockHeight) {
data class GetMerkle(val txid: TxId, val blockHeight: Int, val contextOpt: Transaction? = null) : ElectrumRequest(txid, blockHeight) {
override val method: String = "blockchain.transaction.get_merkle"
}

data class GetMerkleResponse(val txid: ByteVector32, val merkle: List<ByteVector32>, val block_height: Int, val pos: Int, val contextOpt: Transaction? = null) : ElectrumResponse {
data class GetMerkleResponse(val txid: TxId, val merkleProof: List<ByteVector32>, val blockHeight: Int, val pos: Int, val contextOpt: Transaction? = null) : ElectrumResponse {
val root: ByteVector32 by lazy {
tailrec fun loop(pos: Int, hashes: List<ByteVector32>): ByteVector32 {
return if (hashes.size == 1) hashes[0]
Expand All @@ -146,8 +146,7 @@ data class GetMerkleResponse(val txid: ByteVector32, val merkle: List<ByteVector
}
}

@Suppress("UNCHECKED_CAST")
loop(pos, listOf(txid.reversed()) + merkle.map { it.reversed() })
loop(pos, listOf(txid.value.reversed()) + merkleProof.map { it.reversed() })
}
}

Expand Down Expand Up @@ -264,34 +263,37 @@ internal fun parseJsonResponse(request: ElectrumRequest, rpcResponse: JsonRPCRes
val jsonArray = rpcResponse.result.jsonArray
val items = jsonArray.map {
val height = it.jsonObject.getValue("height").jsonPrimitive.int
val txHash = it.jsonObject.getValue("tx_hash").jsonPrimitive.content
TransactionHistoryItem(height, ByteVector32.fromValidHex(txHash))
// Electrum calls this field tx_hash but actually returns the tx_id.
val txId = TxId(it.jsonObject.getValue("tx_hash").jsonPrimitive.content)
TransactionHistoryItem(height, txId)
}
GetScriptHashHistoryResponse(request.scriptHash, items)
}

is ScriptHashListUnspent -> {
val jsonArray = rpcResponse.result.jsonArray
val items = jsonArray.map {
val txHash = it.jsonObject.getValue("tx_hash").jsonPrimitive.content
// Electrum calls this field tx_hash but actually returns the tx_id.
val txId = TxId(it.jsonObject.getValue("tx_hash").jsonPrimitive.content)
val txPos = it.jsonObject.getValue("tx_pos").jsonPrimitive.int
val value = it.jsonObject.getValue("value").jsonPrimitive.long
val height = it.jsonObject.getValue("height").jsonPrimitive.long
UnspentItem(ByteVector32.fromValidHex(txHash), txPos, value, height)
UnspentItem(txId, txPos, value, height)
}
ScriptHashListUnspentResponse(request.scriptHash, items)
}

is GetTransactionIdFromPosition -> {
val (txHash, leaves) = if (rpcResponse.result is JsonPrimitive) {
rpcResponse.result.content to emptyList()
val (txId, merkleProof) = if (rpcResponse.result is JsonPrimitive) {
Pair(TxId(rpcResponse.result.content), emptyList())
} else {
val jsonObject = rpcResponse.result.jsonObject
jsonObject.getValue("tx_hash").jsonPrimitive.content to
jsonObject.getValue("merkle").jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) }
// Electrum calls this field tx_hash but actually returns the tx_id.
val txId = TxId(jsonObject.getValue("tx_hash").jsonPrimitive.content)
val merkleProof = jsonObject.getValue("merkle").jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) }
Pair(txId, merkleProof)
}

GetTransactionIdFromPositionResponse(ByteVector32.fromValidHex(txHash), request.blockHeight, request.txIndex, leaves)
GetTransactionIdFromPositionResponse(txId, request.blockHeight, request.txIndex, merkleProof)
}

is GetTransaction -> {
Expand All @@ -312,15 +314,14 @@ internal fun parseJsonResponse(request: ElectrumRequest, rpcResponse: JsonRPCRes
// if we got here, it means that the server's response does not contain an error and message should be our
// transaction id. However, it seems that at least on testnet some servers still use an older version of the
// Electrum protocol and return an error message in the result field
val result = runTrying<ByteVector32> {
ByteVector32.fromValidHex(message)
val txId = runTrying {
TxId(message)
}
when (result) {
when (txId) {
is Try.Success -> {
if (result.result == request.tx.txid) BroadcastTransactionResponse(request.tx)
else BroadcastTransactionResponse(request.tx, JsonRPCError(1, "response txid $result does not match request txid ${request.tx.txid}"))
if (txId.result == request.tx.txid) BroadcastTransactionResponse(request.tx)
else BroadcastTransactionResponse(request.tx, JsonRPCError(1, "response txid $txId does not match request txid ${request.tx.txid}"))
}

is Try.Failure -> {
BroadcastTransactionResponse(request.tx, JsonRPCError(1, message))
}
Expand Down Expand Up @@ -356,10 +357,10 @@ internal fun parseJsonResponse(request: ElectrumRequest, rpcResponse: JsonRPCRes

is GetMerkle -> {
val jsonObject = rpcResponse.result.jsonObject
val leaves = jsonObject.getValue("merkle").jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) }
val merkleProof = jsonObject.getValue("merkle").jsonArray.map { ByteVector32.fromValidHex(it.jsonPrimitive.content) }
val blockHeight = jsonObject.getValue("block_height").jsonPrimitive.int
val pos = jsonObject.getValue("pos").jsonPrimitive.int
GetMerkleResponse(request.txid, leaves, blockHeight, pos, request.contextOpt)
GetMerkleResponse(request.txid, merkleProof, blockHeight, pos, request.contextOpt)
}

is EstimateFees -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import org.kodein.log.Logger
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger

data class WalletState(val addresses: Map<String, List<UnspentItem>>, val parentTxs: Map<ByteVector32, Transaction>) {
data class WalletState(val addresses: Map<String, List<UnspentItem>>, val parentTxs: Map<TxId, Transaction>) {
/** Electrum sends parent txs separately from utxo outpoints, this boolean indicates when the wallet is consistent */
val consistent: Boolean = addresses.flatMap { it.value }.all { parentTxs.containsKey(it.txid) }
val utxos: List<Utxo> = addresses
Expand Down Expand Up @@ -96,7 +96,7 @@ private sealed interface WalletCommand {
* A very simple wallet that only watches one address and publishes its utxos.
*/
class ElectrumMiniWallet(
val chainHash: ByteVector32,
val chainHash: BlockHash,
private val client: IElectrumClient,
private val scope: CoroutineScope,
loggerFactory: LoggerFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ class ElectrumWatcher(val client: IElectrumClient, val scope: CoroutineScope, lo
.filter { state.height - item.blockHeight + 1 >= it.minDepth }
triggered.forEach { w ->
client.getMerkle(w.txId, item.blockHeight)?.let { merkle ->
val confirmations = state.height - merkle.block_height + 1
logger.info { "txid=${w.txId} had confirmations=$confirmations in block=${merkle.block_height} pos=${merkle.pos}" }
_notificationsFlow.emit(WatchEventConfirmed(w.channelId, w.event, merkle.block_height, merkle.pos, txMap[w.txId]!!))
val confirmations = state.height - merkle.blockHeight + 1
logger.info { "txid=${w.txId} had confirmations=$confirmations in block=${merkle.blockHeight} pos=${merkle.pos}" }
_notificationsFlow.emit(WatchEventConfirmed(w.channelId, w.event, merkle.blockHeight, merkle.pos, txMap[w.txId]!!))

// check whether we have transactions to publish
when (val event = w.event) {
Expand All @@ -103,7 +103,7 @@ class ElectrumWatcher(val client: IElectrumClient, val scope: CoroutineScope, lo
logger.info { "parent tx of txid=${tx.txid} has been confirmed" }
val cltvTimeout = Scripts.cltvTimeout(tx)
val csvTimeout = Scripts.csvTimeout(tx)
val absTimeout = max(merkle.block_height + csvTimeout, cltvTimeout)
val absTimeout = max(merkle.blockHeight + csvTimeout, cltvTimeout)
state = if (absTimeout > state.height) {
logger.info { "delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=${state.height})" }
val block2tx = state.block2tx + (absTimeout to state.block2tx.getOrElse(absTimeout) { setOf() } + tx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fr.acinq.lightning.blockchain.electrum
import fr.acinq.bitcoin.BlockHeader
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Transaction
import fr.acinq.bitcoin.TxId
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -13,7 +14,7 @@ interface IElectrumClient {
val connectionStatus: StateFlow<ElectrumConnectionStatus>

/** Return the transaction matching the txId provided, if it can be found. */
suspend fun getTx(txid: ByteVector32): Transaction?
suspend fun getTx(txId: TxId): Transaction?

/** Return the block header at the given height, if it exists. */
suspend fun getHeader(blockHeight: Int): BlockHeader?
Expand All @@ -22,7 +23,7 @@ interface IElectrumClient {
suspend fun getHeaders(startHeight: Int, count: Int): List<BlockHeader>

/** Return a merkle proof for the given transaction, if it can be found. */
suspend fun getMerkle(txid: ByteVector32, blockHeight: Int, contextOpt: Transaction? = null): GetMerkleResponse?
suspend fun getMerkle(txId: TxId, blockHeight: Int, contextOpt: Transaction? = null): GetMerkleResponse?

/** Return the transaction history for a given script, or an empty list if the script is unknown. */
suspend fun getScriptHashHistory(scriptHash: ByteVector32): List<TransactionHistoryItem>
Expand All @@ -34,7 +35,7 @@ interface IElectrumClient {
* Try broadcasting a transaction: we cannot know whether the remote server really broadcast the transaction,
* so we always consider it to be a success. The client should regularly retry transactions that don't confirm.
*/
suspend fun broadcastTransaction(tx: Transaction): ByteVector32
suspend fun broadcastTransaction(tx: Transaction): TxId

/** Estimate the feerate required for a transaction to be confirmed in the next [confirmations] blocks. */
suspend fun estimateFees(confirmations: Int): FeeratePerKw?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package fr.acinq.lightning.blockchain.electrum

import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.OutPoint
import fr.acinq.bitcoin.TxId
import fr.acinq.lightning.Lightning
import fr.acinq.lightning.SwapInParams
import fr.acinq.lightning.channel.LocalFundingStatus
Expand All @@ -14,7 +14,7 @@ import fr.acinq.lightning.utils.MDCLogger
import fr.acinq.lightning.utils.sat

internal sealed class SwapInCommand {
data class TrySwapIn(val currentBlockHeight: Int, val wallet: WalletState, val swapInParams: SwapInParams, val trustedTxs: Set<ByteVector32>) : SwapInCommand()
data class TrySwapIn(val currentBlockHeight: Int, val wallet: WalletState, val swapInParams: SwapInParams, val trustedTxs: Set<TxId>) : SwapInCommand()
data class UnlockWalletInputs(val inputs: Set<OutPoint>) : SwapInCommand()
}

Expand Down
Loading

0 comments on commit 4235964

Please sign in to comment.