Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a basic mempool.space blockchain backend #657

Merged
merged 9 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest

plugins {
Expand Down Expand Up @@ -89,16 +90,16 @@ kotlin {
api("co.touchlab:kermit:$kermitLoggerVersion")
api(ktor("network"))
api(ktor("network-tls"))
}
}

commonTest {
dependencies {
implementation(ktor("client-core"))
implementation(ktor("client-auth"))
implementation(ktor("client-json"))
implementation(ktor("client-content-negotiation"))
implementation(ktor("serialization-kotlinx-json"))
}
}

commonTest {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation("org.kodein.memory:klio-files:0.12.0")
Expand All @@ -123,14 +124,19 @@ kotlin {
}

if (currentOs.isMacOsX) {
iosTest {
iosMain {
dependencies {
implementation(ktor("client-ios"))
}
}
macosMain {
dependencies {
implementation(ktor("client-darwin"))
}
}
}

linuxTest {
linuxMain {
dependencies {
implementation(ktor("client-curl"))
}
Expand Down Expand Up @@ -306,6 +312,13 @@ tasks
it.filter.excludeTestsMatching("*SwapInWalletTestsCommon")
}

// Those tests do not work with the ios simulator
tasks
.filterIsInstance<KotlinNativeSimulatorTest>()
.map {
it.filter.excludeTestsMatching("*MempoolSpace*Test")
}

// Make NS_FORMAT_ARGUMENT(1) a no-op
// This fixes an issue when building PhoenixCrypto using XCode 13
// More on this: https://youtrack.jetbrains.com/issue/KT-48807#focus=Comments-27-5210791.0-0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
package fr.acinq.lightning.blockchain.electrum
package fr.acinq.lightning.blockchain

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

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

/**
* @return the number of confirmations, zero if the transaction is in the mempool, null if the transaction is not found
*/
suspend fun IElectrumClient.getConfirmations(tx: Transaction): Int? {
return when (val status = connectionStatus.value) {
is ElectrumConnectionStatus.Connected -> {
val currentBlockHeight = status.height
val scriptHash = ElectrumClient.computeScriptHash(tx.txOut.first().publicKeyScript)
val scriptHashHistory = getScriptHashHistory(scriptHash)
val item = scriptHashHistory.find { it.txid == tx.txid }
item?.let { if (item.blockHeight > 0) currentBlockHeight - item.blockHeight + 1 else 0 }
}
else -> null
}
suspend fun getFeerates(): Feerates?
}

data class Feerates(
val minimum: FeeratePerByte,
val slow: FeeratePerByte,
val medium: FeeratePerByte,
val fast: FeeratePerByte,
val fastest: FeeratePerByte
)

/**
* @weight must be the total estimated weight of the splice tx, otherwise the feerate estimation will be wrong
*/
suspend fun IElectrumClient.computeSpliceCpfpFeerate(commitments: Commitments, targetFeerate: FeeratePerKw, spliceWeight: Int, logger: MDCLogger): Pair<FeeratePerKw, Satoshi> {
suspend fun IClient.computeSpliceCpfpFeerate(commitments: Commitments, targetFeerate: FeeratePerKw, spliceWeight: Int, logger: MDCLogger): Pair<FeeratePerKw, Satoshi> {
val (parentsWeight, parentsFees) = commitments.all
.takeWhile { getConfirmations(it.fundingTxId).let { confirmations -> confirmations == null || confirmations == 0 } } // we check for null in case the tx has been evicted
.fold(Pair(0, 0.sat)) { (parentsWeight, parentsFees), commitment ->
Expand All @@ -55,4 +51,4 @@ suspend fun IElectrumClient.computeSpliceCpfpFeerate(commitments: Commitments, t
logger.info { "projectedFeerate=$projectedFeerate projectedFee=$projectedFee" }
logger.info { "actualFeerate=$actualFeerate actualFee=$actualFee" }
return Pair(actualFeerate, actualFee)
}
}
13 changes: 13 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/blockchain/IWatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package fr.acinq.lightning.blockchain

import fr.acinq.bitcoin.Transaction
import fr.acinq.lightning.blockchain.electrum.IElectrumClient
import kotlinx.coroutines.flow.Flow

interface IWatcher {
fun openWatchNotificationsFlow(): Flow<WatchEvent>

suspend fun watch(watch: Watch)

suspend fun publish(tx: Transaction)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import fr.acinq.bitcoin.utils.Either
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.io.TcpSocket
import fr.acinq.lightning.io.send
import fr.acinq.lightning.logging.*
import fr.acinq.lightning.logging.LoggerFactory
import fr.acinq.lightning.logging.debug
import fr.acinq.lightning.logging.info
import fr.acinq.lightning.logging.warning
import fr.acinq.lightning.utils.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package fr.acinq.lightning.blockchain.electrum
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Transaction
import fr.acinq.lightning.blockchain.*
import fr.acinq.lightning.logging.*
import fr.acinq.lightning.logging.LoggerFactory
import fr.acinq.lightning.logging.debug
import fr.acinq.lightning.logging.info
import fr.acinq.lightning.transactions.Scripts
import fr.acinq.lightning.utils.currentTimestampMillis
import kotlinx.coroutines.*
Expand All @@ -15,24 +17,24 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlin.math.max

class ElectrumWatcher(val client: IElectrumClient, val scope: CoroutineScope, loggerFactory: LoggerFactory) : CoroutineScope by scope {
class ElectrumWatcher(val client: IElectrumClient, val scope: CoroutineScope, loggerFactory: LoggerFactory) : IWatcher, CoroutineScope by scope {

private val logger = loggerFactory.newLogger(this::class)
private val mailbox = Channel<WatcherCommand>(Channel.BUFFERED)

private val _notificationsFlow = MutableSharedFlow<WatchEvent>(replay = 0, extraBufferCapacity = 64, onBufferOverflow = BufferOverflow.SUSPEND)
fun openWatchNotificationsFlow(): Flow<WatchEvent> = _notificationsFlow.asSharedFlow()
override fun openWatchNotificationsFlow(): Flow<WatchEvent> = _notificationsFlow.asSharedFlow()

// this is used by a Swift watch-tower module in the Phoenix iOS app to tell when the watcher is up-to-date
// the value that is emitted in the time elapsed (in milliseconds) since the watcher is ready and idle
private val _uptodateFlow = MutableSharedFlow<Long>(replay = 0, extraBufferCapacity = 64, onBufferOverflow = BufferOverflow.SUSPEND)
fun openUpToDateFlow(): Flow<Long> = _uptodateFlow.asSharedFlow()

suspend fun watch(watch: Watch) {
override suspend fun watch(watch: Watch) {
mailbox.send(WatcherCommand.AddWatch(watch))
}

suspend fun publish(tx: Transaction) {
override suspend fun publish(tx: Transaction) {
mailbox.send(WatcherCommand.Publish(tx))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ 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.Feerates
import fr.acinq.lightning.blockchain.IClient
import fr.acinq.lightning.blockchain.fee.FeeratePerByte
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow

/** Note to implementers: methods exposed through this interface must *not* throw exceptions. */
interface IElectrumClient {
interface IElectrumClient : IClient {
val notifications: Flow<ElectrumSubscriptionResponse>
val connectionStatus: StateFlow<ElectrumConnectionStatus>

Expand Down Expand Up @@ -47,4 +50,32 @@ interface IElectrumClient {

/** Subscribe to headers for new blocks found. */
suspend fun startHeaderSubscription(): HeaderSubscriptionResponse

/**
* @return the number of confirmations, zero if the transaction is in the mempool, null if the transaction is not found
*/
suspend fun getConfirmations(tx: Transaction): Int? {
return when (val status = connectionStatus.value) {
is ElectrumConnectionStatus.Connected -> {
val currentBlockHeight = status.height
val scriptHash = ElectrumClient.computeScriptHash(tx.txOut.first().publicKeyScript)
val scriptHashHistory = getScriptHashHistory(scriptHash)
val item = scriptHashHistory.find { it.txid == tx.txid }
item?.let { if (item.blockHeight > 0) currentBlockHeight - item.blockHeight + 1 else 0 }
}
else -> null
}
}

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

override suspend fun getFeerates(): Feerates? {
return Feerates(
minimum = estimateFees(144)?.let { FeeratePerByte(it) } ?: return null,
slow = estimateFees(18)?.let { FeeratePerByte(it) } ?: return null,
medium = estimateFees(6)?.let { FeeratePerByte(it) } ?: return null,
fast = estimateFees(2)?.let { FeeratePerByte(it) } ?: return null,
fastest = estimateFees(1)?.let { FeeratePerByte(it) } ?: return null,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.acinq.lightning.blockchain.fee

import fr.acinq.bitcoin.Satoshi
import fr.acinq.lightning.blockchain.Feerates
import fr.acinq.lightning.utils.sat

interface FeeEstimator {
Expand All @@ -15,7 +16,14 @@ interface FeeEstimator {
* @param claimMainFeerate feerate used to claim our main output when a channel is force-closed (typically configured by the user, based on their preference).
* @param fastFeerate feerate used to claim outputs quickly to avoid loss of funds: this one should not be set by the user (we should look at current on-chain fees).
*/
data class OnChainFeerates(val fundingFeerate: FeeratePerKw, val mutualCloseFeerate: FeeratePerKw, val claimMainFeerate: FeeratePerKw, val fastFeerate: FeeratePerKw)
data class OnChainFeerates(val fundingFeerate: FeeratePerKw, val mutualCloseFeerate: FeeratePerKw, val claimMainFeerate: FeeratePerKw, val fastFeerate: FeeratePerKw) {
constructor(feerates: Feerates) : this(
fundingFeerate = FeeratePerKw(feerates.medium),
mutualCloseFeerate = FeeratePerKw(feerates.medium),
claimMainFeerate = FeeratePerKw(feerates.medium),
fastFeerate = FeeratePerKw(feerates.fast),
)
}

data class FeerateTolerance(val ratioLow: Double, val ratioHigh: Double)

Expand Down
Loading
Loading