Skip to content

Commit 8c7aaa5

Browse files
Implement dual funding (#368)
In this PR, we change the funding flow to use dual-funding (see spec here: lightning/bolts#851) We completely remove the previous funding flow, as dual funding is a super-set of it. A few notes: - we implicitly move existing channels to use a 1% reserve (for the side that does have a reserve): this is ok since this is already the setting used by Phoenix - we support restoring unconfirmed channels that used the single-funded flow: there won't be any backwards compat issue for existing users - we change the zero-conf behavior to directly go to the WaitingForFundingLocked state without using a hacky depth-0 watch - we currently don't detect when an unconfirmed channel will never confirm because the funding candidates have been double-spent: such channels will stay stuck in the WaitingForFundingConfirmed state
1 parent e797ea4 commit 8c7aaa5

File tree

71 files changed

+3692
-3136
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+3692
-3136
lines changed

src/commonMain/kotlin/fr/acinq/lightning/Features.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ sealed class Feature {
112112
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
113113
}
114114

115+
@Serializable
116+
object DualFunding : Feature() {
117+
override val rfcName get() = "option_dual_fund"
118+
override val mandatory get() = 28
119+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
120+
}
121+
115122
@Serializable
116123
object ChannelType : Feature() {
117124
override val rfcName get() = "option_channel_type"
@@ -290,6 +297,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
290297
Feature.Wumbo,
291298
Feature.AnchorOutputs,
292299
Feature.ShutdownAnySegwit,
300+
Feature.DualFunding,
293301
Feature.ChannelType,
294302
Feature.PaymentMetadata,
295303
Feature.TrampolinePayment,

src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ data class WalletParams(
5656
* @param minDepthBlocks minimum depth of a transaction before we consider it safely confirmed.
5757
* @param feeBase base fee used in our channel_update: since our channels are private and we don't relay payments, this will be basically ignored.
5858
* @param feeProportionalMillionth proportional fee used in our channel_update: since our channels are private and we don't relay payments, this will be basically ignored.
59-
* @param reserveToFundingRatio size of the channel reserve we required from our peer.
60-
* @param maxReserveToFundingRatio maximum size of the channel reserve our peer can require from us.
6159
* @param revocationTimeoutSeconds delay after which we disconnect from our peer if they don't send us a revocation after a new commitment is signed.
6260
* @param authTimeoutSeconds timeout for the connection authentication phase.
6361
* @param initTimeoutSeconds timeout for the connection initialization phase.
@@ -95,8 +93,6 @@ data class NodeParams(
9593
val minDepthBlocks: Int,
9694
val feeBase: MilliSatoshi,
9795
val feeProportionalMillionth: Int,
98-
val reserveToFundingRatio: Double,
99-
val maxReserveToFundingRatio: Double,
10096
val revocationTimeoutSeconds: Long,
10197
val authTimeoutSeconds: Long,
10298
val initTimeoutSeconds: Long,
@@ -122,6 +118,7 @@ data class NodeParams(
122118
require(features.hasFeature(Feature.VariableLengthOnion, FeatureSupport.Mandatory)) { "${Feature.VariableLengthOnion.rfcName} should be mandatory" }
123119
require(features.hasFeature(Feature.PaymentSecret, FeatureSupport.Mandatory)) { "${Feature.PaymentSecret.rfcName} should be mandatory" }
124120
require(features.hasFeature(Feature.ChannelType, FeatureSupport.Mandatory)) { "${Feature.ChannelType.rfcName} should be mandatory" }
121+
require(features.hasFeature(Feature.DualFunding, FeatureSupport.Mandatory)) { "${Feature.DualFunding.rfcName} should be mandatory" }
125122
Features.validateFeatureGraph(features)
126123
}
127124
}

src/commonMain/kotlin/fr/acinq/lightning/blockchain/WatcherTypes.kt

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
package fr.acinq.lightning.blockchain
22

33
import fr.acinq.bitcoin.*
4-
import fr.acinq.lightning.ShortChannelId
54
import fr.acinq.lightning.utils.Try
65
import fr.acinq.lightning.utils.runTrying
7-
import kotlinx.coroutines.CompletableDeferred
86

97
sealed class BitcoinEvent
10-
object BITCOIN_FUNDING_PUBLISH_FAILED : BitcoinEvent()
118
object BITCOIN_FUNDING_DEPTHOK : BitcoinEvent()
129
object BITCOIN_FUNDING_DEEPLYBURIED : BitcoinEvent()
13-
object BITCOIN_FUNDING_LOST : BitcoinEvent()
14-
object BITCOIN_FUNDING_TIMEOUT : BitcoinEvent()
1510
object BITCOIN_FUNDING_SPENT : BitcoinEvent()
1611
object BITCOIN_OUTPUT_SPENT : BitcoinEvent()
1712
data class BITCOIN_TX_CONFIRMED(val tx: Transaction) : BitcoinEvent()
18-
data class BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(val shortChannelId: ShortChannelId) : BitcoinEvent()
1913
data class BITCOIN_PARENT_TX_CONFIRMED(val childTx: Transaction) : BitcoinEvent()
2014

2115
/**
@@ -45,6 +39,10 @@ data class WatchConfirmed(
4539
event
4640
)
4741

42+
init {
43+
require(minDepth > 0) { "minimum depth must be greater than 0 when watching transaction confirmation" }
44+
}
45+
4846
companion object {
4947
fun extractPublicKeyScript(witness: ScriptWitness): ByteVector {
5048
val result = runTrying {
@@ -75,8 +73,6 @@ data class WatchSpent(
7573
)
7674
}
7775

78-
data class WatchLost(override val channelId: ByteVector32, val txId: ByteVector32, val minDepth: Long, override val event: BitcoinEvent) : Watch()
79-
8076
/**
8177
* generic "watch" event
8278
*/
@@ -87,8 +83,6 @@ sealed class WatchEvent {
8783

8884
data class WatchEventConfirmed(override val channelId: ByteVector32, override val event: BitcoinEvent, val blockHeight: Int, val txIndex: Int, val tx: Transaction) : WatchEvent()
8985
data class WatchEventSpent(override val channelId: ByteVector32, override val event: BitcoinEvent, val tx: Transaction) : WatchEvent()
90-
data class WatchEventSpentBasic(override val channelId: ByteVector32, override val event: BitcoinEvent) : WatchEvent()
91-
data class WatchEventLost(override val channelId: ByteVector32, override val event: BitcoinEvent) : WatchEvent()
9286

9387
data class PublishAsap(val tx: Transaction)
9488
data class GetTxWithMeta(val channelId: ByteVector32, val txid: ByteVector32)

src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import fr.acinq.lightning.utils.Connection
55
import fr.acinq.lightning.utils.sat
66
import kotlinx.coroutines.CoroutineScope
77
import kotlinx.coroutines.channels.Channel
8-
import kotlinx.coroutines.flow.*
8+
import kotlinx.coroutines.flow.MutableStateFlow
9+
import kotlinx.coroutines.flow.asStateFlow
10+
import kotlinx.coroutines.flow.consumeAsFlow
11+
import kotlinx.coroutines.flow.filterIsInstance
912
import kotlinx.coroutines.launch
1013
import org.kodein.log.LoggerFactory
1114
import org.kodein.log.newLogger
@@ -30,38 +33,39 @@ data class WalletState(val addresses: Map<String, List<UnspentItem>>, val privat
3033
private val outPoint2Address = addresses.entries.flatMap { entry -> entry.value.map { it.outPoint to entry.key } }.toMap()
3134

3235
/** Sign the inputs owned by this wallet (only works for P2WPKH scripts) */
33-
fun sign(tx: Transaction): Transaction {
34-
return tx.txIn.foldIndexed(tx) { index, wipTx, txIn ->
35-
val witness = outPoint2Address[txIn.outPoint]?.let { address ->
36-
addresses[address]?.find { it.outPoint == txIn.outPoint }?.let { utxo ->
37-
privateKeys[address]?.let { privateKey ->
38-
// mind this: the pubkey script used for signing is not the prevout pubscript (which is just a push
39-
// of the pubkey hash), but the actual script that is evaluated by the script engine, in this case a PAY2PKH script
40-
val publicKey = privateKey.publicKey()
41-
val pubKeyScript = Script.pay2pkh(publicKey)
42-
val sig = Transaction.signInput(
43-
tx,
44-
index,
45-
pubKeyScript,
46-
SigHash.SIGHASH_ALL,
47-
utxo.value.sat,
48-
SigVersion.SIGVERSION_WITNESS_V0,
49-
privateKey
50-
)
51-
Script.witnessPay2wpkh(publicKey, sig.byteVector())
52-
}
36+
fun sign(tx: Transaction): Transaction = tx.txIn.foldIndexed(tx) { index, wipTx, _ -> signInput(wipTx, index).first }
37+
38+
fun signInput(tx: Transaction, index: Int): Pair<Transaction, ScriptWitness?> {
39+
val txIn = tx.txIn[index]
40+
val witness = outPoint2Address[txIn.outPoint]?.let { address ->
41+
addresses[address]?.find { it.outPoint == txIn.outPoint }?.let { utxo ->
42+
privateKeys[address]?.let { privateKey ->
43+
// mind this: the pubkey script used for signing is not the prevout pubscript (which is just a push
44+
// of the pubkey hash), but the actual script that is evaluated by the script engine, in this case a PAY2PKH script
45+
val publicKey = privateKey.publicKey()
46+
val pubKeyScript = Script.pay2pkh(publicKey)
47+
val sig = Transaction.signInput(
48+
tx,
49+
index,
50+
pubKeyScript,
51+
SigHash.SIGHASH_ALL,
52+
utxo.value.sat,
53+
SigVersion.SIGVERSION_WITNESS_V0,
54+
privateKey
55+
)
56+
Script.witnessPay2wpkh(publicKey, sig.byteVector())
5357
}
5458
}
55-
when (witness) {
56-
is ScriptWitness ->
57-
// update the signature for the corresponding input
58-
wipTx.updateWitness(index, witness)
59-
else ->
60-
// we don't know how to sign this input
61-
wipTx
62-
}
59+
}
60+
return when (witness) {
61+
is ScriptWitness -> Pair(tx.updateWitness(index, witness), witness)
62+
else -> Pair(tx, null)
6363
}
6464
}
65+
66+
companion object {
67+
val empty: WalletState = WalletState(mapOf(), mapOf(), mapOf())
68+
}
6569
}
6670

6771
private sealed interface WalletCommand {
@@ -72,7 +76,6 @@ private sealed interface WalletCommand {
7276
}
7377
}
7478

75-
7679
/**
7780
* A very simple wallet that only watches one address and publishes its utxos.
7881
*/

src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumWatcher.kt

Lines changed: 19 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ import fr.acinq.bitcoin.BlockHeader
44
import fr.acinq.bitcoin.ByteVector32
55
import fr.acinq.bitcoin.Transaction
66
import fr.acinq.bitcoin.TxIn
7-
import fr.acinq.bitcoin.crypto.Pack
87
import fr.acinq.lightning.blockchain.*
98
import fr.acinq.lightning.blockchain.electrum.ElectrumClient.Companion.computeScriptHash
10-
import fr.acinq.lightning.blockchain.electrum.ElectrumWatcher.Companion.makeDummyShortChannelId
119
import fr.acinq.lightning.blockchain.electrum.ElectrumWatcher.Companion.registerToScriptHash
1210
import fr.acinq.lightning.transactions.Scripts
1311
import fr.acinq.lightning.utils.Connection
@@ -21,7 +19,6 @@ import kotlinx.coroutines.flow.*
2119
import org.kodein.log.Logger
2220
import org.kodein.log.LoggerFactory
2321
import org.kodein.log.newLogger
24-
import kotlin.math.absoluteValue
2522
import kotlin.math.max
2623

2724
sealed class WatcherEvent
@@ -85,7 +82,7 @@ private data class WatcherDisconnected(
8582
is HeaderSubscriptionResponse -> {
8683
newState {
8784
actions = buildList {
88-
watches.mapNotNull { registerToScriptHash(it, logger) }.forEach { add(it) }
85+
watches.map { registerToScriptHash(it, logger) }.forEach { add(it) }
8986
publishQueue.forEach { add(PublishAsapAction(it.tx)) }
9087
getTxQueue.forEach { add(AskForTransaction(it.txid, it.channelId)) }
9188
}
@@ -194,9 +191,7 @@ internal data class WatcherRunning(
194191
.filter { it.txId == outPoint.txid && it.outputIndex == outPoint.index.toInt() }
195192
.map { w ->
196193
logger.info { "output ${w.txId}:${w.outputIndex} spent by transaction ${tx.txid}" }
197-
NotifyWatch(
198-
WatchEventSpent(w.channelId, w.event, tx)
199-
)
194+
NotifyWatch(WatchEventSpent(w.channelId, w.event, tx))
200195
}
201196
}
202197

@@ -207,37 +202,20 @@ internal data class WatcherRunning(
207202
watches.filterIsInstance<WatchConfirmed>()
208203
.filter { it.txId == tx.txid }
209204
.forEach { w ->
210-
if (w.event is BITCOIN_FUNDING_DEPTHOK && w.minDepth == 0L) {
211-
// special case for mempool watches (min depth = 0)
212-
val (dummyHeight, dummyTxIndex) = makeDummyShortChannelId(w.txId)
213-
notifyWatchConfirmedList.add(
214-
NotifyWatch(
215-
watchEvent = WatchEventConfirmed(
216-
w.channelId,
217-
BITCOIN_FUNDING_DEPTHOK,
218-
dummyHeight,
219-
dummyTxIndex,
220-
tx
221-
),
222-
broadcastNotification = w.channelNotification
223-
)
224-
)
225-
watchConfirmedTriggered.add(w)
226-
} else if (w.minDepth > 0L && item.blockHeight > 0) {
227-
val txHeight = item.blockHeight
228-
val confirmations = height - txHeight + 1
229-
logger.info { "txid=${w.txId} was confirmed at height=$txHeight and now has confirmations=$confirmations (currentHeight=$height)" }
205+
if (item.blockHeight > 0) {
206+
val confirmations = height - item.blockHeight + 1
207+
logger.info { "txid=${w.txId} was confirmed at blockHeight=${item.blockHeight} and now has confirmations=$confirmations (currentHeight=$height)" }
230208
if (confirmations >= w.minDepth) {
231209
// we need to get the tx position in the block
232-
getMerkleList.add(AskForMerkle(w.txId, txHeight, tx))
210+
getMerkleList.add(AskForMerkle(w.txId, item.blockHeight, tx))
233211
}
234212
}
235213
}
236214

237215
newState {
238216
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
239217
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
240-
state = copy(watches = watches - watchConfirmedTriggered)
218+
state = copy(watches = watches - watchConfirmedTriggered.toSet())
241219
actions = notifyWatchSpentList + notifyWatchConfirmedList + getMerkleList
242220
}
243221
}
@@ -279,7 +257,7 @@ internal data class WatcherRunning(
279257
} ?: emptyList()
280258

281259
newState {
282-
state = copy(watches = watches - triggered)
260+
state = copy(watches = watches - triggered.toSet())
283261
actions = notifyWatchConfirmedList
284262
}
285263
}
@@ -364,27 +342,22 @@ internal data class WatcherRunning(
364342
}
365343

366344
private fun setupWatch(watch: Watch, logger: Logger) = when (watch) {
367-
is WatchLost -> returnState() // ignore WatchLost for now
368345
in watches -> returnState()
369346
else -> {
370347
val action = registerToScriptHash(watch, logger)
371348
newState {
372349
state = copy(
373350
watches = watches + watch,
374-
scriptHashSubscriptions = action?.let {
375-
scriptHashSubscriptions + it.scriptHash
376-
} ?: scriptHashSubscriptions
351+
scriptHashSubscriptions = scriptHashSubscriptions + action.scriptHash
377352
)
378-
actions = action?.let {
379-
if (scriptHashSubscriptions.contains(it.scriptHash)) {
380-
// We've already registered to subscriptions from this scriptHash.
381-
// Both WatchSpent & WatchConfirmed can be translated into the exact same
382-
// electrum request, so we filter the duplicates here.
383-
actions
384-
} else {
385-
actions + it
386-
}
387-
} ?: actions
353+
actions = if (scriptHashSubscriptions.contains(action.scriptHash)) {
354+
// We've already registered to subscriptions from this scriptHash.
355+
// Both WatchSpent & WatchConfirmed can be translated into the exact same
356+
// electrum request, so we filter the duplicates here.
357+
actions
358+
} else {
359+
actions + action
360+
}
388361
}
389362
}
390363
}
@@ -532,7 +505,7 @@ class ElectrumWatcher(
532505
timerJob = null
533506
}
534507

535-
private suspend fun checkIsUpToDate(): Unit {
508+
private suspend fun checkIsUpToDate() {
536509

537510
// Get a list of timestamps from the client for all outgoing requests.
538511
// This returns an array of `RequestResponseTimestamp` instances, which includes:
@@ -609,7 +582,7 @@ class ElectrumWatcher(
609582

610583
companion object {
611584

612-
internal fun registerToScriptHash(watch: Watch, logger: Logger): RegisterToScriptHashNotification? = when (watch) {
585+
internal fun registerToScriptHash(watch: Watch, logger: Logger): RegisterToScriptHashNotification = when (watch) {
613586
is WatchSpent -> {
614587
val (_, txid, outputIndex, publicKeyScript, _) = watch
615588
val scriptHash = computeScriptHash(publicKeyScript)
@@ -622,20 +595,6 @@ class ElectrumWatcher(
622595
logger.info { "added watch-confirmed on txid=$txid scriptHash=$scriptHash" }
623596
RegisterToScriptHashNotification(scriptHash)
624597
}
625-
else -> null
626-
}
627-
628-
internal fun makeDummyShortChannelId(txid: ByteVector32): Pair<Int, Int> {
629-
// we use a height of 0
630-
// - to make sure that the tx will be marked as "confirmed"
631-
// - to easily identify scids linked to 0-conf channels
632-
//
633-
// this gives us a probability of collisions of 0.1% for 5 0-conf channels and 1% for 20
634-
// collisions mean that users may temporarily see incorrect numbers for their 0-conf channels (until they've been confirmed)
635-
// if this ever becomes a problem we could just extract some bits for our dummy height instead of just returning 0
636-
val height = 0
637-
val txIndex = Pack.int32BE(txid.slice(0, 16).toByteArray()).absoluteValue
638-
return height to txIndex
639598
}
640599
}
641600
}

src/commonMain/kotlin/fr/acinq/lightning/blockchain/fee/FeeEstimator.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ data class OnChainFeeConf(val closeOnOfflineMismatch: Boolean, val updateFeeMinD
2525
/** Fee rate in satoshi-per-bytes. */
2626
data class FeeratePerByte(val feerate: Satoshi) {
2727
constructor(feeratePerKw: FeeratePerKw) : this(FeeratePerKB(feeratePerKw).feerate / 1000)
28+
29+
override fun toString(): String = "$feerate/byte"
2830
}
2931

3032
/** Fee rate in satoshi-per-kilo-bytes (1 kB = 1000 bytes). */
@@ -36,6 +38,8 @@ data class FeeratePerKB(val feerate: Satoshi) : Comparable<FeeratePerKB> {
3638
fun max(other: FeeratePerKB): FeeratePerKB = if (this > other) this else other
3739
fun min(other: FeeratePerKB): FeeratePerKB = if (this < other) this else other
3840
fun toLong(): Long = feerate.toLong()
41+
42+
override fun toString(): String = "$feerate/kB"
3943
}
4044

4145
@Serializable
@@ -53,6 +57,8 @@ data class FeeratePerKw(@Contextual val feerate: Satoshi) : Comparable<FeeratePe
5357
operator fun div(l: Long): FeeratePerKw = FeeratePerKw(feerate / l)
5458
fun toLong(): Long = feerate.toLong()
5559

60+
override fun toString(): String = "$feerate/kw"
61+
5662
companion object {
5763
/**
5864
* Minimum relay fee rate in satoshi per kilo-vbyte (taken from Bitcoin Core).

0 commit comments

Comments
 (0)