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 Sep 1, 2023
1 parent 0315535 commit d3f8384
Show file tree
Hide file tree
Showing 59 changed files with 397 additions and 386 deletions.
2 changes: 1 addition & 1 deletion 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-SNAPSHOT") // 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
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 @@ -218,9 +218,9 @@ class ElectrumClient(
}
}

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

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +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? {
suspend fun IElectrumClient.getConfirmations(txId: TxId): Int? {
val tx = kotlin.runCatching { getTx(txId) }.getOrNull()
return tx?.let { getConfirmations(tx) }
}
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 @@ -84,7 +84,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 ->
val merkle = client.getMerkle(w.txId, item.blockHeight)
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 @@ -2,6 +2,7 @@ package fr.acinq.lightning.blockchain.electrum

import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Transaction
import fr.acinq.bitcoin.TxId
import fr.acinq.lightning.utils.Connection
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
Expand All @@ -11,9 +12,9 @@ interface IElectrumClient {

suspend fun send(request: ElectrumRequest, replyTo: CompletableDeferred<ElectrumResponse>)

suspend fun getTx(txid: ByteVector32): Transaction
suspend fun getTx(txId: TxId): Transaction

suspend fun getMerkle(txid: ByteVector32, blockHeight: Int, contextOpt: Transaction? = null): GetMerkleResponse
suspend fun getMerkle(txId: TxId, blockHeight: Int, contextOpt: Transaction? = null): GetMerkleResponse

suspend fun getScriptHashHistory(scriptHash: ByteVector32): List<TransactionHistoryItem>

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.channel.LocalFundingStatus
import fr.acinq.lightning.channel.RbfStatus
Expand All @@ -13,7 +13,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 swapInConfirmations: Int, val trustedTxs: Set<ByteVector32>) : SwapInCommand()
data class TrySwapIn(val currentBlockHeight: Int, val wallet: WalletState, val swapInConfirmations: Int, val trustedTxs: Set<TxId>) : SwapInCommand()
data class UnlockWalletInputs(val inputs: Set<OutPoint>) : SwapInCommand()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,24 +72,24 @@ sealed class ChannelAction {
data class RemoveChannel(val data: PersistedChannelState) : Storage()
data class HtlcInfo(val channelId: ByteVector32, val commitmentNumber: Long, val paymentHash: ByteVector32, val cltvExpiry: CltvExpiry)
data class StoreHtlcInfos(val htlcs: List<HtlcInfo>) : Storage()
data class GetHtlcInfos(val revokedCommitTxId: ByteVector32, val commitmentNumber: Long) : Storage()
data class GetHtlcInfos(val revokedCommitTxId: TxId, val commitmentNumber: Long) : Storage()
/** Payment received through on-chain operations (channel creation or splice-in) */
sealed class StoreIncomingPayment : Storage() {
abstract val origin: Origin?
abstract val txId: ByteVector32
abstract val txId: TxId
abstract val localInputs: Set<OutPoint>
data class ViaNewChannel(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: ByteVector32, override val origin: Origin?) : StoreIncomingPayment()
data class ViaSpliceIn(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: ByteVector32, override val origin: Origin.PayToOpenOrigin?) : StoreIncomingPayment()
data class ViaNewChannel(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment()
data class ViaSpliceIn(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set<OutPoint>, override val txId: TxId, override val origin: Origin.PayToOpenOrigin?) : StoreIncomingPayment()
}
/** Payment sent through on-chain operations (channel close or splice-out) */
sealed class StoreOutgoingPayment : Storage() {
abstract val miningFees: Satoshi
abstract val txId: ByteVector32
data class ViaSpliceOut(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: ByteVector32) : StoreOutgoingPayment()
data class ViaSpliceCpfp(override val miningFees: Satoshi, override val txId: ByteVector32) : StoreOutgoingPayment()
data class ViaClose(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: ByteVector32, val isSentToDefaultAddress: Boolean, val closingType: ChannelClosingType) : StoreOutgoingPayment()
abstract val txId: TxId
data class ViaSpliceOut(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId) : StoreOutgoingPayment()
data class ViaSpliceCpfp(override val miningFees: Satoshi, override val txId: TxId) : StoreOutgoingPayment()
data class ViaClose(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId, val isSentToDefaultAddress: Boolean, val closingType: ChannelClosingType) : StoreOutgoingPayment()
}
data class SetLocked(val txId: ByteVector32) : Storage()
data class SetLocked(val txId: TxId) : Storage()
}

data class ProcessIncomingHtlc(val add: UpdateAddHtlc) : ChannelAction()
Expand Down
Loading

0 comments on commit d3f8384

Please sign in to comment.