From 6604d900f01f09fa0f2bf952f0b33cf5c02b880c Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 29 Nov 2023 11:51:26 +0100 Subject: [PATCH 01/15] Add liquidity ads codecs Use temporary tag 1337 until we have a final agreement on the spec. --- .../acinq/lightning/channel/states/Normal.kt | 4 +- .../fr/acinq/lightning/wire/ChannelTlv.kt | 40 ++++++++++++ .../acinq/lightning/wire/LightningMessages.kt | 16 +++-- .../fr/acinq/lightning/wire/LiquidityAds.kt | 63 +++++++++++++++++++ .../wire/LightningCodecsTestsCommon.kt | 8 ++- 5 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index a0e145cef..031a2739a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -123,7 +123,8 @@ data class Normal( lockTime = currentBlockHeight.toLong(), feerate = cmd.feerate, fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), - pushAmount = cmd.pushAmount + pushAmount = cmd.pushAmount, + requestFunds = null, ) logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution} local.push=${spliceInit.pushAmount}" } Pair(this@Normal.copy(spliceStatus = SpliceStatus.Requested(cmd, spliceInit)), listOf(ChannelAction.Message.Send(spliceInit))) @@ -356,6 +357,7 @@ data class Normal( fundingContribution = 0.sat, // only remote contributes to the splice pushAmount = 0.msat, fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), + willFund = null, ) val fundingParams = InteractiveTxParams( channelId = channelId, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt index 02f3e3be6..89814a562 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt @@ -66,6 +66,46 @@ sealed class ChannelTlv : Tlv { override fun read(input: Input): RequireConfirmedInputsTlv = this } + /** Request inbound liquidity from our peer. */ + data class RequestFunds(val amount: Satoshi, val leaseExpiry: Int, val leaseDuration: Int) : ChannelTlv() { + override val tag: Long get() = RequestFunds.tag + + override fun write(out: Output) { + LightningCodecs.writeU64(amount.toLong(), out) + LightningCodecs.writeU32(leaseExpiry, out) + LightningCodecs.writeU32(leaseDuration, out) + } + + companion object : TlvValueReader { + const val tag: Long = 1337 + + override fun read(input: Input): RequestFunds = RequestFunds( + amount = LightningCodecs.u64(input).sat, + leaseExpiry = LightningCodecs.u32(input), + leaseDuration = LightningCodecs.u32(input), + ) + } + } + + /** Liquidity rates applied to an incoming [[RequestFunds]]. */ + data class WillFund(val sig: ByteVector64, val leaseRates: LiquidityAds.LeaseRates) : ChannelTlv() { + override val tag: Long get() = WillFund.tag + + override fun write(out: Output) { + LightningCodecs.writeBytes(sig, out) + leaseRates.write(out) + } + + companion object : TlvValueReader { + const val tag: Long = 1337 + + override fun read(input: Input): WillFund = WillFund( + sig = LightningCodecs.bytes(input, 64).toByteVector64(), + leaseRates = LiquidityAds.LeaseRates.read(input), + ) + } + } + data class OriginTlv(val origin: Origin) : ChannelTlv() { override val tag: Long get() = OriginTlv.tag diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index bb21ebefd..e0b9994f3 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -635,6 +635,7 @@ data class OpenDualFundedChannel( ) : ChannelMessage, HasTemporaryChannelId, HasChainHash { val channelType: ChannelType? get() = tlvStream.get()?.channelType val pushAmount: MilliSatoshi get() = tlvStream.get()?.amount ?: 0.msat + val requestFunds: ChannelTlv.RequestFunds? get() = tlvStream.get() val origin: Origin? get() = tlvStream.get()?.origin override val type: Long get() = OpenDualFundedChannel.type @@ -670,6 +671,7 @@ data class OpenDualFundedChannel( ChannelTlv.UpfrontShutdownScriptTlv.tag to ChannelTlv.UpfrontShutdownScriptTlv.Companion as TlvValueReader, ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader, ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, + ChannelTlv.RequestFunds.tag to ChannelTlv.RequestFunds as TlvValueReader, ChannelTlv.OriginTlv.tag to ChannelTlv.OriginTlv.Companion as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, ) @@ -718,6 +720,7 @@ data class AcceptDualFundedChannel( val tlvStream: TlvStream = TlvStream.empty() ) : ChannelMessage, HasTemporaryChannelId { val channelType: ChannelType? get() = tlvStream.get()?.channelType + val willFund: ChannelTlv.WillFund? get() = tlvStream.get() val pushAmount: MilliSatoshi get() = tlvStream.get()?.amount ?: 0.msat override val type: Long get() = AcceptDualFundedChannel.type @@ -749,6 +752,7 @@ data class AcceptDualFundedChannel( ChannelTlv.UpfrontShutdownScriptTlv.tag to ChannelTlv.UpfrontShutdownScriptTlv.Companion as TlvValueReader, ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader, ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, + ChannelTlv.WillFund.tag to ChannelTlv.WillFund as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, ) @@ -863,16 +867,17 @@ data class SpliceInit( ) : ChannelMessage, HasChannelId { override val type: Long get() = SpliceInit.type val requireConfirmedInputs: Boolean = tlvStream.get()?.let { true } ?: false + val requestFunds: ChannelTlv.RequestFunds? get() = tlvStream.get() val pushAmount: MilliSatoshi = tlvStream.get()?.amount ?: 0.msat val origins: List = tlvStream.get()?.origins?.filterIsInstance() ?: emptyList() - constructor(channelId: ByteVector32, fundingContribution: Satoshi, pushAmount: MilliSatoshi, feerate: FeeratePerKw, lockTime: Long, fundingPubkey: PublicKey) : this( + constructor(channelId: ByteVector32, fundingContribution: Satoshi, pushAmount: MilliSatoshi, feerate: FeeratePerKw, lockTime: Long, fundingPubkey: PublicKey, requestFunds: ChannelTlv.RequestFunds?) : this( channelId, fundingContribution, feerate, lockTime, fundingPubkey, - TlvStream(ChannelTlv.PushAmountTlv(pushAmount)) + TlvStream(setOfNotNull(if (pushAmount > 0.msat) ChannelTlv.PushAmountTlv(pushAmount) else null, requestFunds)) ) override fun write(out: Output) { @@ -890,6 +895,7 @@ data class SpliceInit( @Suppress("UNCHECKED_CAST") private val readers = mapOf( ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, + ChannelTlv.RequestFunds.tag to ChannelTlv.RequestFunds as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, ChannelTlv.OriginsTlv.tag to ChannelTlv.OriginsTlv.Companion as TlvValueReader ) @@ -913,13 +919,14 @@ data class SpliceAck( ) : ChannelMessage, HasChannelId { override val type: Long get() = SpliceAck.type val requireConfirmedInputs: Boolean = tlvStream.get()?.let { true } ?: false + val willFund: ChannelTlv.WillFund? get() = tlvStream.get() val pushAmount: MilliSatoshi = tlvStream.get()?.amount ?: 0.msat - constructor(channelId: ByteVector32, fundingContribution: Satoshi, pushAmount: MilliSatoshi, fundingPubkey: PublicKey) : this( + constructor(channelId: ByteVector32, fundingContribution: Satoshi, pushAmount: MilliSatoshi, fundingPubkey: PublicKey, willFund: ChannelTlv.WillFund?) : this( channelId, fundingContribution, fundingPubkey, - TlvStream(ChannelTlv.PushAmountTlv(pushAmount)) + TlvStream(setOfNotNull(if (pushAmount > 0.msat) ChannelTlv.PushAmountTlv(pushAmount) else null, willFund)) ) override fun write(out: Output) { @@ -935,6 +942,7 @@ data class SpliceAck( @Suppress("UNCHECKED_CAST") private val readers = mapOf( ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, + ChannelTlv.WillFund.tag to ChannelTlv.WillFund as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, ) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt new file mode 100644 index 000000000..3e9ee2fcd --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt @@ -0,0 +1,63 @@ +package fr.acinq.lightning.wire + +import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.io.Input +import fr.acinq.bitcoin.io.Output +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.transactions.Transactions +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat + +/** + * Liquidity ads create a decentralized market for channel liquidity. + * Nodes advertise fee rates for their available liquidity using the gossip protocol. + * Other nodes can then pay the advertised rate to get inbound liquidity allocated towards them. + */ +object LiquidityAds { + + /** + * Liquidity is leased using the following rates: + * + * - the buyer pays [[leaseFeeBase]] regardless of the amount contributed by the seller + * - the buyer pays [[leaseFeeProportional]] (expressed in basis points) of the amount contributed by the seller + * - the buyer refunds the on-chain fees for up to [[fundingWeight]] of the utxos contributed by the seller + * + * The seller promises that their relay fees towards the buyer will never exceed [[maxRelayFeeBase]] and [[maxRelayFeeProportional]]. + * This cannot be enforced, but if the buyer notices the seller cheating, they should blacklist them and can prove + * that they misbehaved. + */ + data class LeaseRates(val fundingWeight: Int, val leaseFeeProportional: Int, val maxRelayFeeProportional: Int, val leaseFeeBase: Satoshi, val maxRelayFeeBase: MilliSatoshi) { + val maxRelayFeeProportionalMillionths: Long = maxRelayFeeProportional.toLong() * 100 + + /** + * Fees paid by the liquidity buyer: the resulting amount must be added to the seller's output in the corresponding + * commitment transaction. + */ + fun fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): Satoshi { + val onChainFees = Transactions.weight2fee(feerate, fundingWeight) + // If the seller adds more liquidity than requested, the buyer doesn't pay for that extra liquidity. + val proportionalFee = requestedAmount.min(contributedAmount) * leaseFeeProportional / 10_000 + return leaseFeeBase + proportionalFee + onChainFees + } + + fun write(out: Output) { + LightningCodecs.writeU16(fundingWeight, out) + LightningCodecs.writeU16(leaseFeeProportional, out) + LightningCodecs.writeU16(maxRelayFeeProportional, out) + LightningCodecs.writeU32(leaseFeeBase.sat.toInt(), out) + LightningCodecs.writeTU32(maxRelayFeeBase.msat.toInt(), out) + } + + companion object { + fun read(input: Input): LeaseRates = LeaseRates( + fundingWeight = LightningCodecs.u16(input), + leaseFeeProportional = LightningCodecs.u16(input), + maxRelayFeeProportional = LightningCodecs.u16(input), + leaseFeeBase = LightningCodecs.u32(input).sat, + maxRelayFeeBase = LightningCodecs.tu32(input).msat, + ) + } + } + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 5ab126227..1a60d9889 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -284,6 +284,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs))) to (defaultEncoded + ByteVector("0103101000")), defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.PushAmountTlv(25_000.msat))) to (defaultEncoded + ByteVector("0103101000 fe470000070261a8")), defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequireConfirmedInputsTlv)) to (defaultEncoded + ByteVector("0103101000 0200")), + defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequestFunds(50_000.sat, 500_000, 2500))) to (defaultEncoded + ByteVector("0103101000 fd053910000000000000c3500007a120000009c4")), defaultOpen.copy(tlvStream = TlvStream(setOf(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs)), setOf(GenericTlv(321, ByteVector("2a2a")), GenericTlv(325, ByteVector("02"))))) to (defaultEncoded + ByteVector("0103101000 fd0141022a2a fd01450102")), defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.OriginTlv(Origin.PayToOpenOrigin(ByteVector32.fromValidHex("187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281"), 1_000_000.msat, 1234.sat, 200_000_000.msat)))) to (defaultEncoded + ByteVector("fe47000005 3a 0001 187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281 00000000000004d2 00000000000f4240 000000000bebc200")), defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.OriginTlv(Origin.PleaseOpenChannelOrigin(ByteVector32("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25"), 1_234_567.msat, 321.sat, 1_111_000.msat)))) to (defaultEncoded + ByteVector("fe47000005 3a 0004 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 0000000000000141 000000000012d687 000000000010f3d8")), @@ -307,6 +308,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { defaultAccept to defaultEncoded, defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.UnsupportedChannelType(Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory))))) to (defaultEncoded + ByteVector("01021000")), defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector("01abcdef")), ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs))) to (defaultEncoded + ByteVector("000401abcdef 0103101000")), + defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.WillFund(ByteVector64.Zeroes, LiquidityAds.LeaseRates(750, 150, 100, 250.sat, 5.msat)))) to (defaultEncoded + ByteVector("0103101000 fd05394b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee00960064000000fa05")), defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.PushAmountTlv(1729.msat))) to (defaultEncoded + ByteVector("0103101000 fe470000070206c1")), defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequireConfirmedInputsTlv)) to (defaultEncoded + ByteVector("0103101000 0200")), defaultAccept.copy(tlvStream = TlvStream(setOf(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs)), setOf(GenericTlv(113, ByteVector("deadbeef"))))) to (defaultEncoded + ByteVector("0103101000 7104deadbeef")), @@ -432,13 +434,15 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val testCases = listOf( // @formatter:off SpliceInit(channelId, 100_000.sat, FeeratePerKw(2500.sat), 100, fundingPubkey) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), - SpliceInit(channelId, 150_000.sat, 25_000_000.msat, FeeratePerKw(2500.sat), 100, fundingPubkey) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000249f0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000704017d7840"), + SpliceInit(channelId, 150_000.sat, 25_000_000.msat, FeeratePerKw(2500.sat), 100, fundingPubkey, null) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000249f0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000704017d7840"), SpliceInit(channelId, 0.sat, FeeratePerKw(500.sat), 0, fundingPubkey) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500.sat), 0, fundingPubkey) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + SpliceInit(channelId, 100_000.sat, 0.msat, FeeratePerKw(2500.sat), 100, fundingPubkey, ChannelTlv.RequestFunds(100_000.sat, 850_000, 4000)) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd05391000000000000186a0000cf85000000fa0"), SpliceAck(channelId, 25_000.sat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), - SpliceAck(channelId, 40_000.sat, 10_000_000.msat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680"), + SpliceAck(channelId, 40_000.sat, 10_000_000.msat, fundingPubkey, null) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680"), SpliceAck(channelId, 0.sat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), SpliceAck(channelId, (-25_000).sat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff9e58 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + SpliceAck(channelId, 25_000.sat, 0.msat, fundingPubkey, ChannelTlv.WillFund(ByteVector64.Zeroes, LiquidityAds.LeaseRates(750, 150, 100, 250.sat, 0.msat))) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd05394a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee00960064000000fa"), SpliceLocked(channelId, fundingTxId) to ByteVector("908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566"), // @formatter:on ) From 4e60fb8d99bffb89434e7acd3dabba5cb415f0d7 Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 29 Nov 2023 14:59:04 +0100 Subject: [PATCH 02/15] Add liquidity ads proof The seller signs a commitment to the lease parameters. It provides the buyer with a way to prove if the seller later cheats. --- .../lightning/channel/ChannelException.kt | 3 + .../fr/acinq/lightning/wire/LiquidityAds.kt | 64 ++++++++++++++++++- .../wire/LightningCodecsTestsCommon.kt | 35 +++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt index 970ce5967..d8282d576 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt @@ -25,6 +25,9 @@ data class MissingChannelType (override val channelId: Byte data class DustLimitTooSmall (override val channelId: ByteVector32, val dustLimit: Satoshi, val min: Satoshi) : ChannelException(channelId, "dustLimit=$dustLimit is too small (min=$min)") data class DustLimitTooLarge (override val channelId: ByteVector32, val dustLimit: Satoshi, val max: Satoshi) : ChannelException(channelId, "dustLimit=$dustLimit is too large (max=$max)") data class ToSelfDelayTooHigh (override val channelId: ByteVector32, val toSelfDelay: CltvExpiryDelta, val max: CltvExpiryDelta) : ChannelException(channelId, "unreasonable to_self_delay=$toSelfDelay (max=$max)") +data class MissingLiquidityAds (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads field is missing") +data class InvalidLiquidityAdsSig (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads signature is invalid") +data class LiquidityRatesRejected (override val channelId: ByteVector32) : ChannelException(channelId, "rejecting liquidity ads proposed rates") data class ChannelFundingError (override val channelId: ByteVector32) : ChannelException(channelId, "channel funding error") data class RbfAttemptAborted (override val channelId: ByteVector32) : ChannelException(channelId, "rbf attempt aborted") data class SpliceAborted (override val channelId: ByteVector32) : ChannelException(channelId, "splice aborted") diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt index 3e9ee2fcd..520c6be4f 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt @@ -1,11 +1,17 @@ package fr.acinq.lightning.wire -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.ChannelException +import fr.acinq.lightning.channel.InvalidLiquidityAdsSig +import fr.acinq.lightning.channel.LiquidityRatesRejected +import fr.acinq.lightning.channel.MissingLiquidityAds import fr.acinq.lightning.transactions.Transactions +import fr.acinq.lightning.utils.Either import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat @@ -60,4 +66,60 @@ object LiquidityAds { } } + /** Request inbound liquidity from a remote peer that supports liquidity ads. */ + data class RequestRemoteFunding(val fundingAmount: Satoshi, val maxFee: Satoshi, val leaseStart: Int, val leaseDuration: Int) { + private val leaseExpiry: Int = leaseStart + leaseDuration + val requestFunds: ChannelTlv.RequestFunds = ChannelTlv.RequestFunds(fundingAmount, leaseExpiry, leaseDuration) + + fun validateLeaseRates(remoteNodeId: PublicKey, channelId: ByteVector32, remoteFundingPubKey: PublicKey, remoteFundingAmount: Satoshi, fundingFeerate: FeeratePerKw, willFund: ChannelTlv.WillFund?): Either { + return when (willFund) { + // If the remote peer doesn't want to provide inbound liquidity, we immediately fail the attempt. + // The user should retry this funding attempt without requesting inbound liquidity. + null -> Either.Left(MissingLiquidityAds(channelId)) + else -> { + val witness = LeaseWitness(remoteFundingPubKey, leaseExpiry, leaseDuration, willFund.leaseRates.maxRelayFeeProportional, willFund.leaseRates.maxRelayFeeBase) + val fees = willFund.leaseRates.fees(fundingFeerate, fundingAmount, remoteFundingAmount) + return if (!LeaseWitness.verify(remoteNodeId, willFund.sig, witness)) { + Either.Left(InvalidLiquidityAdsSig(channelId)) + } else if (remoteFundingAmount <= 0.sat) { + Either.Left(LiquidityRatesRejected(channelId)) + } else if (maxFee < fees) { + Either.Left(LiquidityRatesRejected(channelId)) + } else { + val leaseAmount = fundingAmount.min(remoteFundingAmount) + Either.Right(Lease(leaseAmount, fees, willFund.sig, witness)) + } + } + } + } + } + + /** + * Once a liquidity ads has been paid, we should keep track of the lease, and check that our peer doesn't raise their + * routing fees above the values they signed up for. + */ + data class Lease(val amount: Satoshi, val fees: Satoshi, val sellerSig: ByteVector64, val witness: LeaseWitness) { + val expiry: Int = witness.leaseEnd + } + + /** The seller signs the lease parameters: if they cheat, the buyer can use that signature to prove they cheated. */ + data class LeaseWitness(val fundingPubKey: PublicKey, val leaseEnd: Int, val leaseDuration: Int, val maxRelayFeeProportional: Int, val maxRelayFeeBase: MilliSatoshi) { + fun sign(nodeKey: PrivateKey): ByteVector64 = Crypto.sign(Crypto.sha256(encode()), nodeKey) + + fun encode(): ByteArray { + val out = ByteArrayOutput() + LightningCodecs.writeBytes("option_will_fund".encodeToByteArray(), out) + LightningCodecs.writeBytes(fundingPubKey.value, out) + LightningCodecs.writeU32(leaseEnd, out) + LightningCodecs.writeU32(leaseDuration, out) + LightningCodecs.writeU16(maxRelayFeeProportional, out) + LightningCodecs.writeU32(maxRelayFeeBase.msat.toInt(), out) + return out.toByteArray() + } + + companion object { + fun verify(nodeId: PublicKey, sig: ByteVector64, witness: LeaseWitness): Boolean = Crypto.verifySignature(Crypto.sha256(witness.encode()), sig, nodeId) + } + } + } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 1a60d9889..fe20e0ac7 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -8,9 +8,9 @@ import fr.acinq.lightning.Lightning.randomBytes import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.Lightning.randomKey +import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.ChannelType -import fr.acinq.lightning.channel.Origin +import fr.acinq.lightning.channel.* import fr.acinq.lightning.crypto.assertArrayEquals import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat @@ -770,4 +770,35 @@ class LightningCodecsTestsCommon : LightningTestSuite() { assertArrayEquals(it.second, encoded) } } + + @Test + fun `validate liquidity ads lease`() { + // The following lease has been signed by eclair. + val channelId = randomBytes32() + val remoteNodeId = PublicKey.fromHex("023d1d3fc041ca0417e60abffb1d44acf3db3bc1bfcab89031df4920f4ac68b91e") + val remoteFundingPubKey = PublicKey.fromHex("03fda99086f3426ccc6f7bcb5a163e1f93fdfd23e2770d462138ddf9f8db779933") + val remoteWillFund = ChannelTlv.WillFund( + sig = ByteVector64("293f412e6a2b2b3eeab7e67134dc0458e9f068f7e1bc9c0460ed0cdb285096eb592d7881f0dd0777b8176a9f8e232af9d3f21c8925617a3e33bca4d41f30a2fe"), + leaseRates = LiquidityAds.LeaseRates(500, 100, 250, 10.sat, 2000.msat), + ) + assertEquals(remoteWillFund.leaseRates.fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 500_000.sat), 5635.sat) + assertEquals(remoteWillFund.leaseRates.fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 600_000.sat), 5635.sat) + assertEquals(remoteWillFund.leaseRates.fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 400_000.sat), 4635.sat) + + data class TestCase(val remoteFundingAmount: Satoshi, val feerate: FeeratePerKw, val willFund: ChannelTlv.WillFund?, val failure: ChannelException?) + + val testCases = listOf( + TestCase(500_000.sat, FeeratePerKw(FeeratePerByte(5.sat)), remoteWillFund, failure = null), + TestCase(500_000.sat, FeeratePerKw(FeeratePerByte(5.sat)), willFund = null, failure = MissingLiquidityAds(channelId)), + TestCase(500_000.sat, FeeratePerKw(FeeratePerByte(5.sat)), remoteWillFund.copy(sig = randomBytes64()), failure = InvalidLiquidityAdsSig(channelId)), + TestCase(0.sat, FeeratePerKw(FeeratePerByte(5.sat)), remoteWillFund, failure = LiquidityRatesRejected(channelId)), + TestCase(800_000.sat, FeeratePerKw(FeeratePerByte(20.sat)), remoteWillFund, failure = LiquidityRatesRejected(channelId)), // exceeds maximum fee + ) + testCases.forEach { + val request = LiquidityAds.RequestRemoteFunding(it.remoteFundingAmount, 10_000.sat, leaseStart = 819_000, leaseDuration = 1000) + val result = request.validateLeaseRates(remoteNodeId, channelId, remoteFundingPubKey, it.remoteFundingAmount, it.feerate, it.willFund) + assertEquals(result.left, it.failure) + } + + } } \ No newline at end of file From 4e0411b22ea9aac1491231244cbe474b5e19756c Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 29 Nov 2023 17:31:35 +0100 Subject: [PATCH 03/15] Purchase liquidity using a splice We allow purchasing inbound liquidity by initiating an empty splice, similar to what we do for CPFP. If the remote doesn't fund, or its rates are too expensive, we immediately abort the splice attempt. --- .../acinq/lightning/channel/ChannelCommand.kt | 7 +- .../acinq/lightning/channel/InteractiveTx.kt | 15 +- .../acinq/lightning/channel/states/Normal.kt | 128 ++++++++++-------- .../channel/states/WaitForFundingConfirmed.kt | 1 + .../channel/states/WaitForFundingCreated.kt | 1 + .../kotlin/fr/acinq/lightning/io/Peer.kt | 21 +++ .../fr/acinq/lightning/wire/LiquidityAds.kt | 30 +++- .../channel/states/SpliceTestsCommon.kt | 44 ++++++ 8 files changed, 187 insertions(+), 60 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt index 5791eb484..9578ec3af 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt @@ -13,6 +13,7 @@ import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.msat import fr.acinq.lightning.wire.FailureMessage import fr.acinq.lightning.wire.LightningMessage +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.lightning.wire.OnionRoutingPacket import kotlinx.coroutines.CompletableDeferred import fr.acinq.lightning.wire.Init as InitMessage @@ -83,7 +84,7 @@ sealed class ChannelCommand { data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice object CheckHtlcTimeout : Commitment() sealed class Splice : Commitment() { - data class Request(val replyTo: CompletableDeferred, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val feerate: FeeratePerKw, val origins: List = emptyList()) : Splice() { + data class Request(val replyTo: CompletableDeferred, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, val feerate: FeeratePerKw, val origins: List = emptyList()) : Splice() { val pushAmount: MilliSatoshi = spliceIn?.pushAmount ?: 0.msat val spliceOutputs: List = spliceOut?.let { listOf(TxOut(it.amount, it.scriptPubKey)) } ?: emptyList() @@ -101,7 +102,8 @@ sealed class ChannelCommand { val fundingTxIndex: Long, val fundingTxId: TxId, val capacity: Satoshi, - val balance: MilliSatoshi + val balance: MilliSatoshi, + val liquidityPurchased: LiquidityAds.Lease?, ) : Response() sealed class Failure : Response() { @@ -109,6 +111,7 @@ sealed class ChannelCommand { object InvalidSpliceOutPubKeyScript : Failure() object SpliceAlreadyInProgress : Failure() object ChannelNotIdle : Failure() + data class InvalidLiquidityAds(val reason: ChannelException) : Failure() data class FundingFailure(val reason: FundingContributionFailure) : Failure() object CannotStartSession : Failure() data class InteractiveTxSessionFailed(val reason: InteractiveTxSessionAction.RemoteFailure) : Failure() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index f68fa92a7..233ec3142 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -826,6 +826,7 @@ data class InteractiveTxSigningSession( sharedTx: SharedTransaction, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, + liquidityPurchased: LiquidityAds.Lease?, localCommitmentIndex: Long, remoteCommitmentIndex: Long, commitTxFeerate: FeeratePerKw, @@ -834,13 +835,14 @@ data class InteractiveTxSigningSession( val channelKeys = channelParams.localParams.channelKeys(keyManager) val unsignedTx = sharedTx.buildUnsignedTx() val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) } + val liquidityFees = liquidityPurchased?.fees?.toMilliSatoshi() ?: 0.msat return Helpers.Funding.makeCommitTxsWithoutHtlcs( channelKeys, channelParams.channelId, channelParams.localParams, channelParams.remoteParams, fundingAmount = sharedTx.sharedOutput.amount, - toLocal = sharedTx.sharedOutput.localAmount - localPushAmount + remotePushAmount, - toRemote = sharedTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount, + toLocal = sharedTx.sharedOutput.localAmount - localPushAmount + remotePushAmount - liquidityFees, + toRemote = sharedTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount + liquidityFees, localCommitmentIndex = localCommitmentIndex, remoteCommitmentIndex = remoteCommitmentIndex, commitTxFeerate, @@ -900,7 +902,14 @@ sealed class RbfStatus { sealed class SpliceStatus { object None : SpliceStatus() data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : SpliceStatus() - data class InProgress(val replyTo: CompletableDeferred?, val spliceSession: InteractiveTxSession, val localPushAmount: MilliSatoshi, val remotePushAmount: MilliSatoshi, val origins: List) : SpliceStatus() + data class InProgress( + val replyTo: CompletableDeferred?, + val spliceSession: InteractiveTxSession, + val localPushAmount: MilliSatoshi, + val remotePushAmount: MilliSatoshi, + val liquidityPurchased: LiquidityAds.Lease?, + val origins: List + ) : SpliceStatus() data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List) : SpliceStatus() object Aborted : SpliceStatus() } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 031a2739a..4681f0015 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -124,9 +124,9 @@ data class Normal( feerate = cmd.feerate, fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), pushAmount = cmd.pushAmount, - requestFunds = null, + requestFunds = cmd.requestRemoteFunding?.requestFunds, ) - logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution} local.push=${spliceInit.pushAmount}" } + logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution} local.push=${spliceInit.pushAmount} requesting ${cmd.requestRemoteFunding?.fundingAmount ?: 0.sat} from our peer" } Pair(this@Normal.copy(spliceStatus = SpliceStatus.Requested(cmd, spliceInit)), listOf(ChannelAction.Message.Send(spliceInit))) } } else { @@ -380,7 +380,7 @@ data class Normal( fundingContributions = FundingContributions(emptyList(), emptyList()), // as non-initiator we don't contribute to this splice for now previousTxs = emptyList() ) - val nextState = this@Normal.copy(spliceStatus = SpliceStatus.InProgress(replyTo = null, session, localPushAmount = 0.msat, remotePushAmount = cmd.message.pushAmount, origins = cmd.message.origins)) + val nextState = this@Normal.copy(spliceStatus = SpliceStatus.InProgress(replyTo = null, session, localPushAmount = 0.msat, remotePushAmount = cmd.message.pushAmount, liquidityPurchased = null, origins = cmd.message.origins)) Pair(nextState, listOf(ChannelAction.Message.Send(spliceAck))) } else { logger.info { "rejecting splice attempt: channel is not idle" } @@ -398,62 +398,80 @@ data class Normal( is SpliceAck -> when (spliceStatus) { is SpliceStatus.Requested -> { logger.info { "our peer accepted our splice request and will contribute ${cmd.message.fundingContribution} to the funding transaction" } - val parentCommitment = commitments.active.first() - val sharedInput = SharedFundingInput.Multisig2of2(parentCommitment) - val fundingParams = InteractiveTxParams( - channelId = channelId, - isInitiator = true, - localContribution = spliceStatus.spliceInit.fundingContribution, - remoteContribution = cmd.message.fundingContribution, - sharedInput = sharedInput, - remoteFundingPubkey = cmd.message.fundingPubkey, - localOutputs = spliceStatus.command.spliceOutputs, - lockTime = spliceStatus.spliceInit.lockTime, - dustLimit = commitments.params.localParams.dustLimit.max(commitments.params.remoteParams.dustLimit), - targetFeerate = spliceStatus.spliceInit.feerate - ) - when (val fundingContributions = FundingContributions.create( - channelKeys = channelKeys(), - swapInKeys = keyManager.swapInOnChainWallet, - params = fundingParams, - sharedUtxo = Pair(sharedInput, SharedFundingInputBalances(toLocal = parentCommitment.localCommit.spec.toLocal, toRemote = parentCommitment.localCommit.spec.toRemote)), - walletInputs = spliceStatus.command.spliceIn?.walletInputs ?: emptyList(), - localOutputs = spliceStatus.command.spliceOutputs, - changePubKey = null // we don't want a change output: we're spending every funds available + when (val liquidityPurchased = LiquidityAds.validateLeaseRates( + remoteNodeId, + channelId, + cmd.message.fundingPubkey, + cmd.message.fundingContribution, + spliceStatus.spliceInit.feerate, + cmd.message.willFund, + spliceStatus.command.requestRemoteFunding )) { is Either.Left -> { - logger.error { "could not create splice contributions: ${fundingContributions.value}" } - spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure(fundingContributions.value)) - Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message)))) + logger.error { "rejecting liquidity proposal: ${liquidityPurchased.value.message}" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds(liquidityPurchased.value)) + Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, liquidityPurchased.value.message)))) } is Either.Right -> { - // The splice initiator always sends the first interactive-tx message. - val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession( - channelKeys(), - keyManager.swapInOnChainWallet, - fundingParams, - previousLocalBalance = parentCommitment.localCommit.spec.toLocal, - previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, - fundingContributions.value, previousTxs = emptyList() - ).send() - when (interactiveTxAction) { - is InteractiveTxSessionAction.SendMessage -> { - val nextState = this@Normal.copy( - spliceStatus = SpliceStatus.InProgress( - replyTo = spliceStatus.command.replyTo, - interactiveTxSession, - localPushAmount = spliceStatus.spliceInit.pushAmount, - remotePushAmount = cmd.message.pushAmount, - origins = spliceStatus.spliceInit.origins - ) - ) - Pair(nextState, listOf(ChannelAction.Message.Send(interactiveTxAction.msg))) - } - else -> { - logger.error { "could not start interactive-tx session: $interactiveTxAction" } - spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession) + val parentCommitment = commitments.active.first() + val sharedInput = SharedFundingInput.Multisig2of2(parentCommitment) + val fundingParams = InteractiveTxParams( + channelId = channelId, + isInitiator = true, + localContribution = spliceStatus.spliceInit.fundingContribution, + remoteContribution = cmd.message.fundingContribution, + sharedInput = sharedInput, + remoteFundingPubkey = cmd.message.fundingPubkey, + localOutputs = spliceStatus.command.spliceOutputs, + lockTime = spliceStatus.spliceInit.lockTime, + dustLimit = commitments.params.localParams.dustLimit.max(commitments.params.remoteParams.dustLimit), + targetFeerate = spliceStatus.spliceInit.feerate + ) + when (val fundingContributions = FundingContributions.create( + channelKeys = channelKeys(), + swapInKeys = keyManager.swapInOnChainWallet, + params = fundingParams, + sharedUtxo = Pair(sharedInput, SharedFundingInputBalances(toLocal = parentCommitment.localCommit.spec.toLocal, toRemote = parentCommitment.localCommit.spec.toRemote)), + walletInputs = spliceStatus.command.spliceIn?.walletInputs ?: emptyList(), + localOutputs = spliceStatus.command.spliceOutputs, + changePubKey = null // we don't want a change output: we're spending every funds available + )) { + is Either.Left -> { + logger.error { "could not create splice contributions: ${fundingContributions.value}" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure(fundingContributions.value)) Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message)))) } + is Either.Right -> { + // The splice initiator always sends the first interactive-tx message. + val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession( + channelKeys(), + keyManager.swapInOnChainWallet, + fundingParams, + previousLocalBalance = parentCommitment.localCommit.spec.toLocal, + previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, + fundingContributions.value, previousTxs = emptyList() + ).send() + when (interactiveTxAction) { + is InteractiveTxSessionAction.SendMessage -> { + val nextState = this@Normal.copy( + spliceStatus = SpliceStatus.InProgress( + replyTo = spliceStatus.command.replyTo, + interactiveTxSession, + localPushAmount = spliceStatus.spliceInit.pushAmount, + remotePushAmount = cmd.message.pushAmount, + liquidityPurchased = liquidityPurchased.value, + origins = spliceStatus.spliceInit.origins + ) + ) + Pair(nextState, listOf(ChannelAction.Message.Send(interactiveTxAction.msg))) + } + else -> { + logger.error { "could not start interactive-tx session: $interactiveTxAction" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession) + Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message)))) + } + } + } } } } @@ -478,6 +496,7 @@ data class Normal( interactiveTxAction.sharedTx, localPushAmount = spliceStatus.localPushAmount, remotePushAmount = spliceStatus.remotePushAmount, + liquidityPurchased = spliceStatus.liquidityPurchased, localCommitmentIndex = parentCommitment.localCommit.index, remoteCommitmentIndex = parentCommitment.remoteCommit.index, parentCommitment.localCommit.spec.feerate, @@ -501,7 +520,8 @@ data class Normal( fundingTxIndex = session.fundingTxIndex, fundingTxId = session.fundingTx.txId, capacity = session.fundingParams.fundingAmount, - balance = session.localCommit.fold({ it.spec }, { it.spec }).toLocal + balance = session.localCommit.fold({ it.spec }, { it.spec }).toLocal, + liquidityPurchased = spliceStatus.liquidityPurchased, ) ) val nextState = this@Normal.copy(spliceStatus = SpliceStatus.WaitingForSigs(session, spliceStatus.origins)) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt index e4b6e9c1d..6eee49c62 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -176,6 +176,7 @@ data class WaitForFundingConfirmed( interactiveTxAction.sharedTx, localPushAmount, remotePushAmount, + liquidityPurchased = null, localCommitmentIndex = replacedCommitment.localCommit.index, remoteCommitmentIndex = replacedCommitment.remoteCommit.index, replacedCommitment.localCommit.spec.feerate, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt index 209848c21..5147083fb 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -62,6 +62,7 @@ data class WaitForFundingCreated( interactiveTxAction.sharedTx, localPushAmount, remotePushAmount, + liquidityPurchased = null, localCommitmentIndex = 0, remoteCommitmentIndex = 0, commitTxFeerate, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index c5a8ad8de..e7910ec25 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -549,6 +549,7 @@ class Peer( replyTo = CompletableDeferred(), spliceIn = null, spliceOut = ChannelCommand.Commitment.Splice.Request.SpliceOut(amount, scriptPubKey), + requestRemoteFunding = null, feerate = feerate ) send(WrappedChannelCommand(channel.channelId, spliceCommand)) @@ -566,6 +567,25 @@ class Peer( // no additional inputs or outputs, the splice is only meant to bump fees spliceIn = null, spliceOut = null, + requestRemoteFunding = null, + feerate = feerate + ) + send(WrappedChannelCommand(channel.channelId, spliceCommand)) + spliceCommand.replyTo.await() + } + } + + suspend fun purchaseInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw, maxFee: Satoshi, leaseDuration: Int): ChannelCommand.Commitment.Splice.Response? { + return channels.values + .filterIsInstance() + .firstOrNull() + ?.let { channel -> + val leaseStart = currentTipFlow.filterNotNull().first().first + val spliceCommand = ChannelCommand.Commitment.Splice.Request( + replyTo = CompletableDeferred(), + spliceIn = null, + spliceOut = null, + requestRemoteFunding = LiquidityAds.RequestRemoteFunding(amount, maxFee, leaseStart, leaseDuration), feerate = feerate ) send(WrappedChannelCommand(channel.channelId, spliceCommand)) @@ -1051,6 +1071,7 @@ class Peer( replyTo = CompletableDeferred(), spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(cmd.walletInputs), spliceOut = null, + requestRemoteFunding = null, feerate = feerate ) // If the splice fails, we immediately unlock the utxos to reuse them in the next attempt. diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt index 520c6be4f..2c37c34c5 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt @@ -47,6 +47,12 @@ object LiquidityAds { return leaseFeeBase + proportionalFee + onChainFees } + fun signLease(nodeKey: PrivateKey, localFundingPubKey: PublicKey, requestFunds: ChannelTlv.RequestFunds): ChannelTlv.WillFund { + val witness = LeaseWitness(localFundingPubKey, requestFunds.leaseExpiry, requestFunds.leaseDuration, maxRelayFeeProportional, maxRelayFeeBase) + val sig = witness.sign(nodeKey) + return ChannelTlv.WillFund(sig, this) + } + fun write(out: Output) { LightningCodecs.writeU16(fundingWeight, out) LightningCodecs.writeU16(leaseFeeProportional, out) @@ -71,7 +77,14 @@ object LiquidityAds { private val leaseExpiry: Int = leaseStart + leaseDuration val requestFunds: ChannelTlv.RequestFunds = ChannelTlv.RequestFunds(fundingAmount, leaseExpiry, leaseDuration) - fun validateLeaseRates(remoteNodeId: PublicKey, channelId: ByteVector32, remoteFundingPubKey: PublicKey, remoteFundingAmount: Satoshi, fundingFeerate: FeeratePerKw, willFund: ChannelTlv.WillFund?): Either { + fun validateLeaseRates( + remoteNodeId: PublicKey, + channelId: ByteVector32, + remoteFundingPubKey: PublicKey, + remoteFundingAmount: Satoshi, + fundingFeerate: FeeratePerKw, + willFund: ChannelTlv.WillFund? + ): Either { return when (willFund) { // If the remote peer doesn't want to provide inbound liquidity, we immediately fail the attempt. // The user should retry this funding attempt without requesting inbound liquidity. @@ -94,6 +107,21 @@ object LiquidityAds { } } + fun validateLeaseRates( + remoteNodeId: PublicKey, + channelId: ByteVector32, + remoteFundingPubKey: PublicKey, + remoteFundingAmount: Satoshi, + fundingFeerate: FeeratePerKw, + willFund: ChannelTlv.WillFund?, + request: RequestRemoteFunding? + ): Either { + return when (request) { + null -> Either.Right(null) + else -> request.validateLeaseRates(remoteNodeId, channelId, remoteFundingPubKey, remoteFundingAmount, fundingFeerate, willFund) + } + } + /** * Once a liquidity ads has been paid, we should keep track of the lease, and check that our peer doesn't raise their * routing fees above the values they signed up for. diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index 0cda32304..13c825a19 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -107,6 +107,47 @@ class SpliceTestsCommon : LightningTestSuite() { spliceCpfp(alice, bob) } + @Test + fun `splice to purchase inbound liquidity`() { + val (alice, bob) = reachNormal() + val liquidityRequest = LiquidityAds.RequestRemoteFunding(200_000.sat, 10_000.sat, alice.currentBlockHeight, 2016) + val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) + val (alice1, actionsAlice1) = alice.process(cmd) + val spliceInit = actionsAlice1.findOutgoingMessage() + assertEquals(spliceInit.requestFunds, liquidityRequest.requestFunds) + // Alice's contribution is negative: she needs to pay on-chain fees for the splice. + assertTrue(spliceInit.fundingContribution < 0.sat) + // We haven't implemented the seller side, so we mock it. + val bobFundingKey = randomKey() + run { + val bobLiquidityRates = LiquidityAds.LeaseRates(250, 250 /* 2.5% */, 200, 10.sat, 100.msat) + val willFund = bobLiquidityRates.signLease(bob.staticParams.nodeParams.nodePrivateKey, bobFundingKey.publicKey(), spliceInit.requestFunds!!) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, bobFundingKey.publicKey(), willFund) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) + assertIs(alice2.state) + assertIs(alice2.state.spliceStatus) + actionsAlice2.hasOutgoingMessage() + } + run { + // Fees exceed Alice's maximum fee. + val bobLiquidityRates = LiquidityAds.LeaseRates(250, 500 /* 5% */, 200, 10.sat, 100.msat) + val willFund = bobLiquidityRates.signLease(bob.staticParams.nodeParams.nodePrivateKey, bobFundingKey.publicKey(), spliceInit.requestFunds!!) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, bobFundingKey.publicKey(), willFund) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) + assertIs(alice2.state) + assertIs(alice2.state.spliceStatus) + actionsAlice2.hasOutgoingMessage() + } + run { + // Bob doesn't fund the splice. + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, bobFundingKey.publicKey(), willFund = null) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) + assertIs(alice2.state) + assertIs(alice2.state.spliceStatus) + actionsAlice2.hasOutgoingMessage() + } + } + @Test fun `reject splice_init`() { val cmd = createSpliceOutRequest(25_000.sat) @@ -985,6 +1026,7 @@ class SpliceTestsCommon : LightningTestSuite() { replyTo = CompletableDeferred(), spliceIn = null, spliceOut = ChannelCommand.Commitment.Splice.Request.SpliceOut(amount, Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector()), + requestRemoteFunding = null, feerate = FeeratePerKw(253.sat) ) @@ -1030,6 +1072,7 @@ class SpliceTestsCommon : LightningTestSuite() { replyTo = CompletableDeferred(), spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(createWalletWithFunds(alice.staticParams.nodeParams.keyManager, amounts)), spliceOut = null, + requestRemoteFunding = null, feerate = FeeratePerKw(253.sat) ) @@ -1069,6 +1112,7 @@ class SpliceTestsCommon : LightningTestSuite() { replyTo = CompletableDeferred(), spliceIn = null, spliceOut = null, + requestRemoteFunding = null, feerate = FeeratePerKw(253.sat) ) From c38b77a488399a3124ce651932634e934a5a9a70 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 7 Dec 2023 14:53:29 +0100 Subject: [PATCH 04/15] Configure different rates based on lease duration Those rates are sent in `node_announcement` and `init`. We select the rate in the remote `init` message for a lease duration of `0` (no strict lease enforcement through CLTV in the commitment transaction). This matches the proposal in https://github.com/lightning/bolts/pull/878#issuecomment-1836543060 --- .../lightning/channel/ChannelException.kt | 3 +- .../fr/acinq/lightning/channel/Helpers.kt | 4 + .../acinq/lightning/channel/InteractiveTx.kt | 4 +- .../acinq/lightning/channel/states/Normal.kt | 17 +++- .../kotlin/fr/acinq/lightning/io/Peer.kt | 13 ++- .../fr/acinq/lightning/wire/ChannelTlv.kt | 22 +++-- .../kotlin/fr/acinq/lightning/wire/InitTlv.kt | 19 ++++ .../acinq/lightning/wire/LightningMessages.kt | 34 ++++--- .../fr/acinq/lightning/wire/LiquidityAds.kt | 99 ++++++++++--------- .../channel/states/SpliceTestsCommon.kt | 23 +++-- .../wire/LightningCodecsTestsCommon.kt | 53 ++++++---- 11 files changed, 184 insertions(+), 107 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt index d8282d576..75ae81213 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt @@ -27,7 +27,8 @@ data class DustLimitTooLarge (override val channelId: Byte data class ToSelfDelayTooHigh (override val channelId: ByteVector32, val toSelfDelay: CltvExpiryDelta, val max: CltvExpiryDelta) : ChannelException(channelId, "unreasonable to_self_delay=$toSelfDelay (max=$max)") data class MissingLiquidityAds (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads field is missing") data class InvalidLiquidityAdsSig (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads signature is invalid") -data class LiquidityRatesRejected (override val channelId: ByteVector32) : ChannelException(channelId, "rejecting liquidity ads proposed rates") +data class InvalidLiquidityAdsAmount (override val channelId: ByteVector32, val proposed: Satoshi, val min: Satoshi) : ChannelException(channelId, "liquidity ads funding amount is too low (expected at least $min, got $proposed)") +data class InvalidLiquidityRates (override val channelId: ByteVector32) : ChannelException(channelId, "rejecting liquidity ads proposed rates") data class ChannelFundingError (override val channelId: ByteVector32) : ChannelException(channelId, "channel funding error") data class RbfAttemptAborted (override val channelId: ByteVector32) : ChannelException(channelId, "rbf attempt aborted") data class SpliceAborted (override val channelId: ByteVector32) : ChannelException(channelId, "splice aborted") diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt index 73c70f532..253510421 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt @@ -257,6 +257,10 @@ object Helpers { } } + fun makeFundingPubKeyScript(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey): ByteVector { + return write(pay2wsh(multiSig2of2(localFundingPubkey, remoteFundingPubkey))).toByteVector() + } + fun makeFundingInputInfo( fundingTxId: TxId, fundingTxOutputIndex: Int, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 233ec3142..ef6f9411b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -86,7 +86,7 @@ data class InteractiveTxParams( fun fundingPubkeyScript(channelKeys: KeyManager.ChannelKeys): ByteVector { val fundingTxIndex = (sharedInput as? SharedFundingInput.Multisig2of2)?.let { it.fundingTxIndex + 1 } ?: 0 - return Script.write(Script.pay2wsh(Scripts.multiSig2of2(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey))).toByteVector() + return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey) } } @@ -835,7 +835,7 @@ data class InteractiveTxSigningSession( val channelKeys = channelParams.localParams.channelKeys(keyManager) val unsignedTx = sharedTx.buildUnsignedTx() val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) } - val liquidityFees = liquidityPurchased?.fees?.toMilliSatoshi() ?: 0.msat + val liquidityFees = liquidityPurchased?.fees?.total?.toMilliSatoshi() ?: 0.msat return Helpers.Funding.makeCommitTxsWithoutHtlcs( channelKeys, channelParams.channelId, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 4681f0015..a5f12f73f 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -380,7 +380,16 @@ data class Normal( fundingContributions = FundingContributions(emptyList(), emptyList()), // as non-initiator we don't contribute to this splice for now previousTxs = emptyList() ) - val nextState = this@Normal.copy(spliceStatus = SpliceStatus.InProgress(replyTo = null, session, localPushAmount = 0.msat, remotePushAmount = cmd.message.pushAmount, liquidityPurchased = null, origins = cmd.message.origins)) + val nextState = this@Normal.copy( + spliceStatus = SpliceStatus.InProgress( + replyTo = null, + session, + localPushAmount = 0.msat, + remotePushAmount = cmd.message.pushAmount, + liquidityPurchased = null, + origins = cmd.message.origins + ) + ) Pair(nextState, listOf(ChannelAction.Message.Send(spliceAck))) } else { logger.info { "rejecting splice attempt: channel is not idle" } @@ -398,14 +407,14 @@ data class Normal( is SpliceAck -> when (spliceStatus) { is SpliceStatus.Requested -> { logger.info { "our peer accepted our splice request and will contribute ${cmd.message.fundingContribution} to the funding transaction" } - when (val liquidityPurchased = LiquidityAds.validateLeaseRates( + when (val liquidityPurchased = LiquidityAds.validateLease( + spliceStatus.command.requestRemoteFunding, remoteNodeId, channelId, - cmd.message.fundingPubkey, + Helpers.Funding.makeFundingPubKeyScript(spliceStatus.spliceInit.fundingPubkey, cmd.message.fundingPubkey), cmd.message.fundingContribution, spliceStatus.spliceInit.feerate, cmd.message.willFund, - spliceStatus.command.requestRemoteFunding )) { is Either.Left -> { logger.error { "rejecting liquidity proposal: ${liquidityPurchased.value.message}" } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index e7910ec25..9b826724e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -168,6 +168,7 @@ class Peer( val currentTipFlow = MutableStateFlow?>(null) val onChainFeeratesFlow = MutableStateFlow(null) val swapInFeeratesFlow = MutableStateFlow(null) + val liquidityRatesFlow = MutableStateFlow(null) private val _channelLogger = nodeParams.loggerFactory.newLogger(ChannelState::class) private suspend fun ChannelState.process(cmd: ChannelCommand): Pair> { @@ -575,17 +576,23 @@ class Peer( } } - suspend fun purchaseInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw, maxFee: Satoshi, leaseDuration: Int): ChannelCommand.Commitment.Splice.Response? { + suspend fun estimateFeeForInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw): LiquidityAds.LeaseFees { + val leaseRate = liquidityRatesFlow.filterNotNull().first { it.leaseDuration == 0 } + return leaseRate.fees(feerate, amount, amount) + } + + suspend fun requestInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw): ChannelCommand.Commitment.Splice.Response? { return channels.values .filterIsInstance() .firstOrNull() ?.let { channel -> val leaseStart = currentTipFlow.filterNotNull().first().first + val leaseRate = liquidityRatesFlow.filterNotNull().first { it.leaseDuration == 0 } val spliceCommand = ChannelCommand.Commitment.Splice.Request( replyTo = CompletableDeferred(), spliceIn = null, spliceOut = null, - requestRemoteFunding = LiquidityAds.RequestRemoteFunding(amount, maxFee, leaseStart, leaseDuration), + requestRemoteFunding = LiquidityAds.RequestRemoteFunding(amount, leaseStart, leaseRate), feerate = feerate ) send(WrappedChannelCommand(channel.channelId, spliceCommand)) @@ -845,10 +852,10 @@ class Peer( logger.error(error) { "feature validation error" } // TODO: disconnect peer } - else -> { theirInit = msg _connectionState.value = Connection.ESTABLISHED + msg.liquidityRates.forEach { liquidityRatesFlow.emit(it) } _channels = _channels.mapValues { entry -> val (state1, actions) = entry.value.process(ChannelCommand.Connected(ourInit, theirInit!!)) processActions(entry.key, peerConnection, actions) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt index 89814a562..bd1aa3e0d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt @@ -67,13 +67,13 @@ sealed class ChannelTlv : Tlv { } /** Request inbound liquidity from our peer. */ - data class RequestFunds(val amount: Satoshi, val leaseExpiry: Int, val leaseDuration: Int) : ChannelTlv() { + data class RequestFunds(val amount: Satoshi, val leaseDuration: Int, val leaseExpiry: Int) : ChannelTlv() { override val tag: Long get() = RequestFunds.tag override fun write(out: Output) { LightningCodecs.writeU64(amount.toLong(), out) + LightningCodecs.writeU16(leaseDuration, out) LightningCodecs.writeU32(leaseExpiry, out) - LightningCodecs.writeU32(leaseDuration, out) } companion object : TlvValueReader { @@ -81,19 +81,25 @@ sealed class ChannelTlv : Tlv { override fun read(input: Input): RequestFunds = RequestFunds( amount = LightningCodecs.u64(input).sat, + leaseDuration = LightningCodecs.u16(input), leaseExpiry = LightningCodecs.u32(input), - leaseDuration = LightningCodecs.u32(input), ) } } /** Liquidity rates applied to an incoming [[RequestFunds]]. */ - data class WillFund(val sig: ByteVector64, val leaseRates: LiquidityAds.LeaseRates) : ChannelTlv() { + data class WillFund(val sig: ByteVector64, val fundingWeight: Int, val leaseFeeProportional: Int, val leaseFeeBase: Satoshi, val maxRelayFeeProportional: Int, val maxRelayFeeBase: MilliSatoshi) : ChannelTlv() { override val tag: Long get() = WillFund.tag + fun leaseRate(leaseDuration: Int): LiquidityAds.LeaseRate = LiquidityAds.LeaseRate(leaseDuration, fundingWeight, leaseFeeProportional, leaseFeeBase, maxRelayFeeProportional, maxRelayFeeBase) + override fun write(out: Output) { LightningCodecs.writeBytes(sig, out) - leaseRates.write(out) + LightningCodecs.writeU16(fundingWeight, out) + LightningCodecs.writeU16(leaseFeeProportional, out) + LightningCodecs.writeU32(leaseFeeBase.sat.toInt(), out) + LightningCodecs.writeU16(maxRelayFeeProportional, out) + LightningCodecs.writeU32(maxRelayFeeBase.msat.toInt(), out) } companion object : TlvValueReader { @@ -101,7 +107,11 @@ sealed class ChannelTlv : Tlv { override fun read(input: Input): WillFund = WillFund( sig = LightningCodecs.bytes(input, 64).toByteVector64(), - leaseRates = LiquidityAds.LeaseRates.read(input), + fundingWeight = LightningCodecs.u16(input), + leaseFeeProportional = LightningCodecs.u16(input), + leaseFeeBase = LightningCodecs.u32(input).sat, + maxRelayFeeProportional = LightningCodecs.u16(input), + maxRelayFeeBase = LightningCodecs.u32(input).msat, ) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InitTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InitTlv.kt index 5ea9b822e..a4ae87672 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InitTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InitTlv.kt @@ -31,6 +31,25 @@ sealed class InitTlv : Tlv { } } + /** Rates at which we sell inbound liquidity to remote peers. */ + data class LiquidityAdsRates(val leaseRates: List) : InitTlv() { + override val tag: Long get() = LiquidityAdsRates.tag + + override fun write(out: Output) { + leaseRates.forEach { it.write(out) } + } + + companion object : TlvValueReader { + const val tag: Long = 1337 + + override fun read(input: Input): LiquidityAdsRates { + val count = input.availableBytes / 16 + val rates = (0 until count).map { LiquidityAds.LeaseRate.read(input) } + return LiquidityAdsRates(rates) + } + } + } + data class PhoenixAndroidLegacyNodeId(val legacyNodeId: PublicKey, val signature: ByteVector64) : InitTlv() { override val tag: Long get() = PhoenixAndroidLegacyNodeId.tag diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index e0b9994f3..b3d969b63 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -180,8 +180,17 @@ interface ChannelMessage data class Init(val features: Features, val tlvs: TlvStream = TlvStream.empty()) : SetupMessage { val networks = tlvs.get()?.chainHashes ?: listOf() + val liquidityRates = tlvs.get()?.leaseRates ?: listOf() - constructor(features: Features, chainHashs: List) : this(features, TlvStream(InitTlv.Networks(chainHashs))) + constructor(features: Features, chainHashs: List, liquidityRates: List) : this( + features, + TlvStream( + setOfNotNull( + if (chainHashs.isNotEmpty()) InitTlv.Networks(chainHashs) else null, + if (liquidityRates.isNotEmpty()) InitTlv.LiquidityAdsRates(liquidityRates) else null, + ) + ) + ) override val type: Long get() = Init.type @@ -191,18 +200,19 @@ data class Init(val features: Features, val tlvs: TlvStream = TlvStream LightningCodecs.writeU16(it.size, out) LightningCodecs.writeBytes(it, out) } - val tlvReaders = HashMap>() - @Suppress("UNCHECKED_CAST") - tlvReaders[InitTlv.Networks.tag] = InitTlv.Networks.Companion as TlvValueReader - @Suppress("UNCHECKED_CAST") - tlvReaders[InitTlv.PhoenixAndroidLegacyNodeId.tag] = InitTlv.PhoenixAndroidLegacyNodeId.Companion as TlvValueReader - val serializer = TlvStreamSerializer(false, tlvReaders) - serializer.write(tlvs, out) + TlvStreamSerializer(false, readers).write(tlvs, out) } companion object : LightningMessageReader { const val type: Long = 16 + @Suppress("UNCHECKED_CAST") + val readers = mapOf( + InitTlv.Networks.tag to InitTlv.Networks.Companion as TlvValueReader, + InitTlv.LiquidityAdsRates.tag to InitTlv.LiquidityAdsRates.Companion as TlvValueReader, + InitTlv.PhoenixAndroidLegacyNodeId.tag to InitTlv.PhoenixAndroidLegacyNodeId.Companion as TlvValueReader, + ) + override fun read(input: Input): Init { val gflen = LightningCodecs.u16(input) val globalFeatures = LightningCodecs.bytes(input, gflen) @@ -211,13 +221,7 @@ data class Init(val features: Features, val tlvs: TlvStream = TlvStream val len = max(gflen, lflen) // merge features together val features = Features(ByteVector(globalFeatures.leftPaddedCopyOf(len).or(localFeatures.leftPaddedCopyOf(len)))) - val tlvReaders = HashMap>() - @Suppress("UNCHECKED_CAST") - tlvReaders[InitTlv.Networks.tag] = InitTlv.Networks.Companion as TlvValueReader - @Suppress("UNCHECKED_CAST") - tlvReaders[InitTlv.PhoenixAndroidLegacyNodeId.tag] = InitTlv.PhoenixAndroidLegacyNodeId.Companion as TlvValueReader - val serializer = TlvStreamSerializer(false, tlvReaders) - val tlvs = serializer.read(input) + val tlvs = TlvStreamSerializer(false, readers).read(input) return Init(features, tlvs) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt index 2c37c34c5..d8ea946a1 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt @@ -6,10 +6,7 @@ import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.ChannelException -import fr.acinq.lightning.channel.InvalidLiquidityAdsSig -import fr.acinq.lightning.channel.LiquidityRatesRejected -import fr.acinq.lightning.channel.MissingLiquidityAds +import fr.acinq.lightning.channel.* import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.Either import fr.acinq.lightning.utils.msat @@ -22,65 +19,74 @@ import fr.acinq.lightning.utils.sat */ object LiquidityAds { + /** + * @param miningFee fee paid to miners for the underlying on-chain transaction. + * @param serviceFee fee paid to the liquidity provider for the inbound liquidity. + */ + data class LeaseFees(val miningFee: Satoshi, val serviceFee: Satoshi) { + val total: Satoshi = miningFee + serviceFee + } + /** * Liquidity is leased using the following rates: * - * - the buyer pays [[leaseFeeBase]] regardless of the amount contributed by the seller - * - the buyer pays [[leaseFeeProportional]] (expressed in basis points) of the amount contributed by the seller - * - the buyer refunds the on-chain fees for up to [[fundingWeight]] of the utxos contributed by the seller + * - the buyer pays [leaseFeeBase] regardless of the amount contributed by the seller + * - the buyer pays [leaseFeeProportional] (expressed in basis points) of the amount contributed by the seller + * - the seller will have to add inputs/outputs to the transaction and pay on-chain fees for them, but the buyer + * refunds on-chain fees for [fundingWeight] vbytes * - * The seller promises that their relay fees towards the buyer will never exceed [[maxRelayFeeBase]] and [[maxRelayFeeProportional]]. + * The seller promises that their relay fees towards the buyer will never exceed [maxRelayFeeBase] and [maxRelayFeeProportional]. * This cannot be enforced, but if the buyer notices the seller cheating, they should blacklist them and can prove - * that they misbehaved. + * that they misbehaved using the seller's signature of the [LeaseWitness]. */ - data class LeaseRates(val fundingWeight: Int, val leaseFeeProportional: Int, val maxRelayFeeProportional: Int, val leaseFeeBase: Satoshi, val maxRelayFeeBase: MilliSatoshi) { - val maxRelayFeeProportionalMillionths: Long = maxRelayFeeProportional.toLong() * 100 - + data class LeaseRate(val leaseDuration: Int, val fundingWeight: Int, val leaseFeeProportional: Int, val leaseFeeBase: Satoshi, val maxRelayFeeProportional: Int, val maxRelayFeeBase: MilliSatoshi) { /** * Fees paid by the liquidity buyer: the resulting amount must be added to the seller's output in the corresponding * commitment transaction. */ - fun fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): Satoshi { + fun fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): LeaseFees { val onChainFees = Transactions.weight2fee(feerate, fundingWeight) // If the seller adds more liquidity than requested, the buyer doesn't pay for that extra liquidity. val proportionalFee = requestedAmount.min(contributedAmount) * leaseFeeProportional / 10_000 - return leaseFeeBase + proportionalFee + onChainFees + return LeaseFees(onChainFees, leaseFeeBase + proportionalFee) } - fun signLease(nodeKey: PrivateKey, localFundingPubKey: PublicKey, requestFunds: ChannelTlv.RequestFunds): ChannelTlv.WillFund { - val witness = LeaseWitness(localFundingPubKey, requestFunds.leaseExpiry, requestFunds.leaseDuration, maxRelayFeeProportional, maxRelayFeeBase) + fun signLease(nodeKey: PrivateKey, fundingScript: ByteVector, requestFunds: ChannelTlv.RequestFunds): ChannelTlv.WillFund { + val witness = LeaseWitness(fundingScript, requestFunds.leaseDuration, requestFunds.leaseExpiry, maxRelayFeeProportional, maxRelayFeeBase) val sig = witness.sign(nodeKey) - return ChannelTlv.WillFund(sig, this) + return ChannelTlv.WillFund(sig, fundingWeight, leaseFeeProportional, leaseFeeBase, maxRelayFeeProportional, maxRelayFeeBase) } fun write(out: Output) { + LightningCodecs.writeU16(leaseDuration, out) LightningCodecs.writeU16(fundingWeight, out) LightningCodecs.writeU16(leaseFeeProportional, out) - LightningCodecs.writeU16(maxRelayFeeProportional, out) LightningCodecs.writeU32(leaseFeeBase.sat.toInt(), out) - LightningCodecs.writeTU32(maxRelayFeeBase.msat.toInt(), out) + LightningCodecs.writeU16(maxRelayFeeProportional, out) + LightningCodecs.writeU32(maxRelayFeeBase.msat.toInt(), out) } companion object { - fun read(input: Input): LeaseRates = LeaseRates( + fun read(input: Input): LeaseRate = LeaseRate( + leaseDuration = LightningCodecs.u16(input), fundingWeight = LightningCodecs.u16(input), leaseFeeProportional = LightningCodecs.u16(input), - maxRelayFeeProportional = LightningCodecs.u16(input), leaseFeeBase = LightningCodecs.u32(input).sat, - maxRelayFeeBase = LightningCodecs.tu32(input).msat, + maxRelayFeeProportional = LightningCodecs.u16(input), + maxRelayFeeBase = LightningCodecs.u32(input).msat, ) } } /** Request inbound liquidity from a remote peer that supports liquidity ads. */ - data class RequestRemoteFunding(val fundingAmount: Satoshi, val maxFee: Satoshi, val leaseStart: Int, val leaseDuration: Int) { - private val leaseExpiry: Int = leaseStart + leaseDuration - val requestFunds: ChannelTlv.RequestFunds = ChannelTlv.RequestFunds(fundingAmount, leaseExpiry, leaseDuration) + data class RequestRemoteFunding(val fundingAmount: Satoshi, val leaseStart: Int, val rate: LeaseRate) { + private val leaseExpiry: Int = leaseStart + rate.leaseDuration + val requestFunds: ChannelTlv.RequestFunds = ChannelTlv.RequestFunds(fundingAmount, rate.leaseDuration, leaseExpiry) - fun validateLeaseRates( + fun validateLease( remoteNodeId: PublicKey, channelId: ByteVector32, - remoteFundingPubKey: PublicKey, + fundingScript: ByteVector, remoteFundingAmount: Satoshi, fundingFeerate: FeeratePerKw, willFund: ChannelTlv.WillFund? @@ -90,35 +96,35 @@ object LiquidityAds { // The user should retry this funding attempt without requesting inbound liquidity. null -> Either.Left(MissingLiquidityAds(channelId)) else -> { - val witness = LeaseWitness(remoteFundingPubKey, leaseExpiry, leaseDuration, willFund.leaseRates.maxRelayFeeProportional, willFund.leaseRates.maxRelayFeeBase) - val fees = willFund.leaseRates.fees(fundingFeerate, fundingAmount, remoteFundingAmount) - return if (!LeaseWitness.verify(remoteNodeId, willFund.sig, witness)) { + val witness = LeaseWitness(fundingScript, rate.leaseDuration, leaseExpiry, willFund.maxRelayFeeProportional, willFund.maxRelayFeeBase) + return if (!witness.verify(remoteNodeId, willFund.sig)) { Either.Left(InvalidLiquidityAdsSig(channelId)) - } else if (remoteFundingAmount <= 0.sat) { - Either.Left(LiquidityRatesRejected(channelId)) - } else if (maxFee < fees) { - Either.Left(LiquidityRatesRejected(channelId)) + } else if (remoteFundingAmount < fundingAmount) { + Either.Left(InvalidLiquidityAdsAmount(channelId, remoteFundingAmount, fundingAmount)) + } else if (willFund.leaseRate(rate.leaseDuration) != rate) { + Either.Left(InvalidLiquidityRates(channelId)) } else { val leaseAmount = fundingAmount.min(remoteFundingAmount) - Either.Right(Lease(leaseAmount, fees, willFund.sig, witness)) + val leaseFees = rate.fees(fundingFeerate, fundingAmount, remoteFundingAmount) + Either.Right(Lease(leaseAmount, leaseFees, willFund.sig, witness)) } } } } } - fun validateLeaseRates( + fun validateLease( + request: RequestRemoteFunding?, remoteNodeId: PublicKey, channelId: ByteVector32, - remoteFundingPubKey: PublicKey, + fundingScript: ByteVector, remoteFundingAmount: Satoshi, fundingFeerate: FeeratePerKw, willFund: ChannelTlv.WillFund?, - request: RequestRemoteFunding? ): Either { return when (request) { null -> Either.Right(null) - else -> request.validateLeaseRates(remoteNodeId, channelId, remoteFundingPubKey, remoteFundingAmount, fundingFeerate, willFund) + else -> request.validateLease(remoteNodeId, channelId, fundingScript, remoteFundingAmount, fundingFeerate, willFund) } } @@ -126,28 +132,27 @@ object LiquidityAds { * Once a liquidity ads has been paid, we should keep track of the lease, and check that our peer doesn't raise their * routing fees above the values they signed up for. */ - data class Lease(val amount: Satoshi, val fees: Satoshi, val sellerSig: ByteVector64, val witness: LeaseWitness) { + data class Lease(val amount: Satoshi, val fees: LeaseFees, val sellerSig: ByteVector64, val witness: LeaseWitness) { val expiry: Int = witness.leaseEnd } /** The seller signs the lease parameters: if they cheat, the buyer can use that signature to prove they cheated. */ - data class LeaseWitness(val fundingPubKey: PublicKey, val leaseEnd: Int, val leaseDuration: Int, val maxRelayFeeProportional: Int, val maxRelayFeeBase: MilliSatoshi) { + data class LeaseWitness(val fundingScript: ByteVector, val leaseDuration: Int, val leaseEnd: Int, val maxRelayFeeProportional: Int, val maxRelayFeeBase: MilliSatoshi) { fun sign(nodeKey: PrivateKey): ByteVector64 = Crypto.sign(Crypto.sha256(encode()), nodeKey) + fun verify(nodeId: PublicKey, sig: ByteVector64): Boolean = Crypto.verifySignature(Crypto.sha256(encode()), sig, nodeId) + fun encode(): ByteArray { val out = ByteArrayOutput() LightningCodecs.writeBytes("option_will_fund".encodeToByteArray(), out) - LightningCodecs.writeBytes(fundingPubKey.value, out) + LightningCodecs.writeU16(fundingScript.size(), out) + LightningCodecs.writeBytes(fundingScript, out) + LightningCodecs.writeU16(leaseDuration, out) LightningCodecs.writeU32(leaseEnd, out) - LightningCodecs.writeU32(leaseDuration, out) LightningCodecs.writeU16(maxRelayFeeProportional, out) LightningCodecs.writeU32(maxRelayFeeBase.msat.toInt(), out) return out.toByteArray() } - - companion object { - fun verify(nodeId: PublicKey, sig: ByteVector64, witness: LeaseWitness): Boolean = Crypto.verifySignature(Crypto.sha256(witness.encode()), sig, nodeId) - } } } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index 13c825a19..c412938b4 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -110,7 +110,8 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice to purchase inbound liquidity`() { val (alice, bob) = reachNormal() - val liquidityRequest = LiquidityAds.RequestRemoteFunding(200_000.sat, 10_000.sat, alice.currentBlockHeight, 2016) + val leaseRate = LiquidityAds.LeaseRate(0, 250, 250 /* 2.5% */, 10.sat, 200, 100.msat) + val liquidityRequest = LiquidityAds.RequestRemoteFunding(200_000.sat, alice.currentBlockHeight, leaseRate) val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) val (alice1, actionsAlice1) = alice.process(cmd) val spliceInit = actionsAlice1.findOutgoingMessage() @@ -118,21 +119,23 @@ class SpliceTestsCommon : LightningTestSuite() { // Alice's contribution is negative: she needs to pay on-chain fees for the splice. assertTrue(spliceInit.fundingContribution < 0.sat) // We haven't implemented the seller side, so we mock it. - val bobFundingKey = randomKey() + val (_, actionsBob2) = bob.process(ChannelCommand.MessageReceived(spliceInit)) + val defaultSpliceAck = actionsBob2.findOutgoingMessage() + assertNull(defaultSpliceAck.willFund) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey) run { - val bobLiquidityRates = LiquidityAds.LeaseRates(250, 250 /* 2.5% */, 200, 10.sat, 100.msat) - val willFund = bobLiquidityRates.signLease(bob.staticParams.nodeParams.nodePrivateKey, bobFundingKey.publicKey(), spliceInit.requestFunds!!) - val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, bobFundingKey.publicKey(), willFund) + val willFund = leaseRate.signLease(bob.staticParams.nodeParams.nodePrivateKey, fundingScript, spliceInit.requestFunds!!) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, defaultSpliceAck.fundingPubkey, willFund) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) assertIs(alice2.state) assertIs(alice2.state.spliceStatus) actionsAlice2.hasOutgoingMessage() } run { - // Fees exceed Alice's maximum fee. - val bobLiquidityRates = LiquidityAds.LeaseRates(250, 500 /* 5% */, 200, 10.sat, 100.msat) - val willFund = bobLiquidityRates.signLease(bob.staticParams.nodeParams.nodePrivateKey, bobFundingKey.publicKey(), spliceInit.requestFunds!!) - val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, bobFundingKey.publicKey(), willFund) + // Bob proposes different fees from what Alice expects. + val bobLiquidityRates = leaseRate.copy(leaseFeeProportional = 500 /* 5% */) + val willFund = bobLiquidityRates.signLease(bob.staticParams.nodeParams.nodePrivateKey, fundingScript, spliceInit.requestFunds!!) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, defaultSpliceAck.fundingPubkey, willFund) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) assertIs(alice2.state) assertIs(alice2.state.spliceStatus) @@ -140,7 +143,7 @@ class SpliceTestsCommon : LightningTestSuite() { } run { // Bob doesn't fund the splice. - val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, bobFundingKey.publicKey(), willFund = null) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, defaultSpliceAck.fundingPubkey, willFund = null) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) assertIs(alice2.state) assertIs(alice2.state.spliceStatus) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index fe20e0ac7..50fe736e3 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -212,16 +212,28 @@ class LightningCodecsTestsCommon : LightningTestSuite() { ), // unknown odd records TestCase(ByteVector("0000 0002088a 03012a04022aa2"), decoded = null), // unknown even records TestCase(ByteVector("0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101"), decoded = null), // invalid tlv stream - TestCase(ByteVector("0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101"), Init(Features(ByteVector("088a")), listOf(chainHash1))), // single network + TestCase(ByteVector("0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101"), Init(Features(ByteVector("088a")), listOf(chainHash1), listOf())), // single network TestCase( ByteVector("0000 0002088a 014001010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202"), - Init(Features(ByteVector("088a")), listOf(chainHash1, chainHash2)) + Init(Features(ByteVector("088a")), listOf(chainHash1, chainHash2), listOf()) ), // multiple networks TestCase( - ByteVector("0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101010103012a"), + ByteVector("0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 03012a"), Init(Features(ByteVector("088a")), tlvs = TlvStream(records = setOf(InitTlv.Networks(listOf(chainHash1))), unknown = setOf(GenericTlv(3, ByteVector("2a"))))) ), // network and unknown odd records - TestCase(ByteVector("0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101010102012a"), decoded = null), // network and unknown even records + TestCase(ByteVector("0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 02012a"), decoded = null), // network and unknown even records + TestCase( + ByteVector("0000 0002088a fd05391007d001f4003200000000025800000000"), + Init(Features(ByteVector("088a")), chainHashs = listOf(), liquidityRates = listOf(LiquidityAds.LeaseRate(2000, 500, 50, 0.sat, 600, 0.msat))), + ), // one liquidity ads + TestCase( + ByteVector("0000 0002088a fd05392003f0019000c8000061a80064000186a00fc001f401f4000027100096000249f0"), + Init( + Features(ByteVector("088a")), + chainHashs = listOf(), + liquidityRates = listOf(LiquidityAds.LeaseRate(1008, 400, 200, 25_000.sat, 100, 100_000.msat), LiquidityAds.LeaseRate(4032, 500, 500, 10_000.sat, 150, 150_000.msat)) + ), + ), // two liquidity ads ) for (testCase in testCases) { @@ -284,7 +296,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs))) to (defaultEncoded + ByteVector("0103101000")), defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.PushAmountTlv(25_000.msat))) to (defaultEncoded + ByteVector("0103101000 fe470000070261a8")), defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequireConfirmedInputsTlv)) to (defaultEncoded + ByteVector("0103101000 0200")), - defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequestFunds(50_000.sat, 500_000, 2500))) to (defaultEncoded + ByteVector("0103101000 fd053910000000000000c3500007a120000009c4")), + defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequestFunds(50_000.sat, 2500, 500_000))) to (defaultEncoded + ByteVector("0103101000 fd05390e000000000000c35009c40007a120")), defaultOpen.copy(tlvStream = TlvStream(setOf(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs)), setOf(GenericTlv(321, ByteVector("2a2a")), GenericTlv(325, ByteVector("02"))))) to (defaultEncoded + ByteVector("0103101000 fd0141022a2a fd01450102")), defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.OriginTlv(Origin.PayToOpenOrigin(ByteVector32.fromValidHex("187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281"), 1_000_000.msat, 1234.sat, 200_000_000.msat)))) to (defaultEncoded + ByteVector("fe47000005 3a 0001 187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281 00000000000004d2 00000000000f4240 000000000bebc200")), defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.OriginTlv(Origin.PleaseOpenChannelOrigin(ByteVector32("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25"), 1_234_567.msat, 321.sat, 1_111_000.msat)))) to (defaultEncoded + ByteVector("fe47000005 3a 0004 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 0000000000000141 000000000012d687 000000000010f3d8")), @@ -308,7 +320,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { defaultAccept to defaultEncoded, defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.UnsupportedChannelType(Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory))))) to (defaultEncoded + ByteVector("01021000")), defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector("01abcdef")), ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs))) to (defaultEncoded + ByteVector("000401abcdef 0103101000")), - defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.WillFund(ByteVector64.Zeroes, LiquidityAds.LeaseRates(750, 150, 100, 250.sat, 5.msat)))) to (defaultEncoded + ByteVector("0103101000 fd05394b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee00960064000000fa05")), + defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.WillFund(ByteVector64.Zeroes, 750, 150, 250.sat, 100, 5.msat))) to (defaultEncoded + ByteVector("0103101000 fd05394e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee0096000000fa006400000005")), defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.PushAmountTlv(1729.msat))) to (defaultEncoded + ByteVector("0103101000 fe470000070206c1")), defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequireConfirmedInputsTlv)) to (defaultEncoded + ByteVector("0103101000 0200")), defaultAccept.copy(tlvStream = TlvStream(setOf(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs)), setOf(GenericTlv(113, ByteVector("deadbeef"))))) to (defaultEncoded + ByteVector("0103101000 7104deadbeef")), @@ -437,12 +449,12 @@ class LightningCodecsTestsCommon : LightningTestSuite() { SpliceInit(channelId, 150_000.sat, 25_000_000.msat, FeeratePerKw(2500.sat), 100, fundingPubkey, null) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000249f0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000704017d7840"), SpliceInit(channelId, 0.sat, FeeratePerKw(500.sat), 0, fundingPubkey) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500.sat), 0, fundingPubkey) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), - SpliceInit(channelId, 100_000.sat, 0.msat, FeeratePerKw(2500.sat), 100, fundingPubkey, ChannelTlv.RequestFunds(100_000.sat, 850_000, 4000)) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd05391000000000000186a0000cf85000000fa0"), + SpliceInit(channelId, 100_000.sat, 0.msat, FeeratePerKw(2500.sat), 100, fundingPubkey, ChannelTlv.RequestFunds(100_000.sat, 4000, 850_000)) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd05390e00000000000186a00fa0000cf850"), SpliceAck(channelId, 25_000.sat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), SpliceAck(channelId, 40_000.sat, 10_000_000.msat, fundingPubkey, null) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680"), SpliceAck(channelId, 0.sat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), SpliceAck(channelId, (-25_000).sat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff9e58 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), - SpliceAck(channelId, 25_000.sat, 0.msat, fundingPubkey, ChannelTlv.WillFund(ByteVector64.Zeroes, LiquidityAds.LeaseRates(750, 150, 100, 250.sat, 0.msat))) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd05394a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee00960064000000fa"), + SpliceAck(channelId, 25_000.sat, 0.msat, fundingPubkey, ChannelTlv.WillFund(ByteVector64.Zeroes, 750, 150, 250.sat, 100, 0.msat)) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd05394e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee0096000000fa006400000000"), SpliceLocked(channelId, fundingTxId) to ByteVector("908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566"), // @formatter:on ) @@ -775,15 +787,19 @@ class LightningCodecsTestsCommon : LightningTestSuite() { fun `validate liquidity ads lease`() { // The following lease has been signed by eclair. val channelId = randomBytes32() - val remoteNodeId = PublicKey.fromHex("023d1d3fc041ca0417e60abffb1d44acf3db3bc1bfcab89031df4920f4ac68b91e") - val remoteFundingPubKey = PublicKey.fromHex("03fda99086f3426ccc6f7bcb5a163e1f93fdfd23e2770d462138ddf9f8db779933") + val remoteNodeId = PublicKey.fromHex("024dd1d24f950df788c124fe855d5a48c632d5fb6e59cf95f7ea6bee2ad47e5bc8") + val fundingScript = ByteVector.fromHex("00202395c9c52c02ca069f1d56a3c6124bf8b152a617328c76e6b31f83ace370c2ff") val remoteWillFund = ChannelTlv.WillFund( - sig = ByteVector64("293f412e6a2b2b3eeab7e67134dc0458e9f068f7e1bc9c0460ed0cdb285096eb592d7881f0dd0777b8176a9f8e232af9d3f21c8925617a3e33bca4d41f30a2fe"), - leaseRates = LiquidityAds.LeaseRates(500, 100, 250, 10.sat, 2000.msat), + sig = ByteVector64("a1b9850389d21b49e074f183e6e1e2d0416e47b4c031843f4cf6f02f68e44ebd5f6ad1baee0b49098c517ac1f04fee6c58335e64ed45f5b0e4ce4b8546cbba09"), + fundingWeight = 500, + leaseFeeProportional = 100, + leaseFeeBase = 10.sat, + maxRelayFeeProportional = 250, + maxRelayFeeBase = 2000.msat, ) - assertEquals(remoteWillFund.leaseRates.fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 500_000.sat), 5635.sat) - assertEquals(remoteWillFund.leaseRates.fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 600_000.sat), 5635.sat) - assertEquals(remoteWillFund.leaseRates.fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 400_000.sat), 4635.sat) + assertEquals(remoteWillFund.leaseRate(0).fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 500_000.sat).total, 5635.sat) + assertEquals(remoteWillFund.leaseRate(0).fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 600_000.sat).total, 5635.sat) + assertEquals(remoteWillFund.leaseRate(0).fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 400_000.sat).total, 4635.sat) data class TestCase(val remoteFundingAmount: Satoshi, val feerate: FeeratePerKw, val willFund: ChannelTlv.WillFund?, val failure: ChannelException?) @@ -791,12 +807,11 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TestCase(500_000.sat, FeeratePerKw(FeeratePerByte(5.sat)), remoteWillFund, failure = null), TestCase(500_000.sat, FeeratePerKw(FeeratePerByte(5.sat)), willFund = null, failure = MissingLiquidityAds(channelId)), TestCase(500_000.sat, FeeratePerKw(FeeratePerByte(5.sat)), remoteWillFund.copy(sig = randomBytes64()), failure = InvalidLiquidityAdsSig(channelId)), - TestCase(0.sat, FeeratePerKw(FeeratePerByte(5.sat)), remoteWillFund, failure = LiquidityRatesRejected(channelId)), - TestCase(800_000.sat, FeeratePerKw(FeeratePerByte(20.sat)), remoteWillFund, failure = LiquidityRatesRejected(channelId)), // exceeds maximum fee + TestCase(0.sat, FeeratePerKw(FeeratePerByte(5.sat)), remoteWillFund, failure = InvalidLiquidityAdsAmount(channelId, 0.sat, 500_000.sat)), ) testCases.forEach { - val request = LiquidityAds.RequestRemoteFunding(it.remoteFundingAmount, 10_000.sat, leaseStart = 819_000, leaseDuration = 1000) - val result = request.validateLeaseRates(remoteNodeId, channelId, remoteFundingPubKey, it.remoteFundingAmount, it.feerate, it.willFund) + val request = LiquidityAds.RequestRemoteFunding(500_000.sat, leaseStart = 820_000, rate = remoteWillFund.leaseRate(leaseDuration = 0)) + val result = request.validateLease(remoteNodeId, channelId, fundingScript, it.remoteFundingAmount, it.feerate, it.willFund) assertEquals(result.left, it.failure) } From bb36627b33c8570368d78cd7004177bf3bd4119d Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 8 Dec 2023 14:09:03 +0100 Subject: [PATCH 05/15] Store liquidity purchase as outgoing payment We store liquidity purchases as on-chain outgoing payments, like what we do for splice-outs and splice-cpfp. --- .../acinq/lightning/channel/ChannelAction.kt | 3 + .../acinq/lightning/channel/InteractiveTx.kt | 5 +- .../acinq/lightning/channel/states/Normal.kt | 18 +++-- .../fr/acinq/lightning/db/PaymentsDb.kt | 16 ++++ .../kotlin/fr/acinq/lightning/io/Peer.kt | 10 +++ .../serialization/v4/Deserialization.kt | 78 ++++++++++++------- .../serialization/v4/Serialization.kt | 74 +++++++++++++----- .../StateSerializationTestsCommon.kt | 31 +++++++- 8 files changed, 179 insertions(+), 56 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index 518971e47..07e0dfccb 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -87,6 +87,9 @@ sealed class ChannelAction { 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 ViaInboundLiquidityRequest(override val txId: TxId, val lease: LiquidityAds.Lease) : StoreOutgoingPayment() { + override val miningFees: Satoshi = lease.fees.miningFee + } 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: TxId) : Storage() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index ef6f9411b..a8c6f3a13 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -751,8 +751,9 @@ data class InteractiveTxSigningSession( val fundingParams: InteractiveTxParams, val fundingTxIndex: Long, val fundingTx: PartiallySignedSharedTransaction, + val liquidityPurchased: LiquidityAds.Lease?, val localCommit: Either, - val remoteCommit: RemoteCommit + val remoteCommit: RemoteCommit, ) { // Example flow: @@ -871,7 +872,7 @@ data class InteractiveTxSigningSession( val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf()) val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint) val signedFundingTx = sharedTx.sign(keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) - Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) + Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, liquidityPurchased, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index a5f12f73f..8e1dab3b5 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -193,7 +193,7 @@ data class Normal( logger.info { "waiting for tx_sigs" } Pair(this@Normal.copy(spliceStatus = spliceStatus.copy(session = signingSession1)), listOf()) } - is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, cmd.message.channelData) + is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityPurchased, cmd.message.channelData) } } ignoreRetransmittedCommitSig(cmd.message) -> { @@ -565,7 +565,7 @@ data class Normal( } is Either.Right -> { val action: InteractiveTxSigningSessionAction.SendTxSigs = res.value - sendSpliceTxSigs(spliceStatus.origins, action, cmd.message.channelData) + sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityPurchased, cmd.message.channelData) } } } @@ -712,7 +712,12 @@ data class Normal( } } - private fun ChannelContext.sendSpliceTxSigs(origins: List, action: InteractiveTxSigningSessionAction.SendTxSigs, remoteChannelData: EncryptedChannelData): Pair> { + private fun ChannelContext.sendSpliceTxSigs( + origins: List, + action: InteractiveTxSigningSessionAction.SendTxSigs, + liquidityPurchase: LiquidityAds.Lease?, + remoteChannelData: EncryptedChannelData + ): Pair> { logger.info { "sending tx_sigs" } // We watch for confirmation in all cases, to allow pruning outdated commitments when transactions confirm. val fundingMinDepth = Helpers.minDepthForFunding(staticParams.nodeParams, action.fundingTx.fundingParams.fundingAmount) @@ -754,13 +759,16 @@ data class Normal( txId = action.fundingTx.txId ) }) - // If we initiated the splice but there are no new inputs or outputs, it's a cpfp - if (action.fundingTx.fundingParams.isInitiator && action.fundingTx.sharedTx.tx.localInputs.isEmpty() && action.fundingTx.fundingParams.localOutputs.isEmpty()) add( + // If we initiated the splice but there are no new inputs on either side and no new output on our side, it's a cpfp + if (action.fundingTx.fundingParams.isInitiator && action.fundingTx.sharedTx.tx.localInputs.isEmpty() && action.fundingTx.sharedTx.tx.remoteInputs.isEmpty() && action.fundingTx.fundingParams.localOutputs.isEmpty()) add( ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceCpfp( miningFees = action.fundingTx.sharedTx.tx.fees, txId = action.fundingTx.txId ) ) + liquidityPurchase?.let { + add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, lease = it)) + } if (staticParams.useZeroConf) { logger.info { "channel is using 0-conf, sending splice_locked right away" } val spliceLocked = SpliceLocked(channelId, action.fundingTx.txId) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt index c453ffb19..550da51ec 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt @@ -8,6 +8,7 @@ import fr.acinq.lightning.payment.FinalFailure import fr.acinq.lightning.payment.PaymentRequest import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.FailureMessage +import fr.acinq.lightning.wire.LiquidityAds interface PaymentsDb : IncomingPaymentsDb, OutgoingPaymentsDb { /** @@ -387,6 +388,21 @@ data class SpliceCpfpOutgoingPayment( override val completedAt: Long? = confirmedAt } +data class InboundLiquidityOutgoingPayment( + override val id: UUID, + override val channelId: ByteVector32, + override val txId: TxId, + val lease: LiquidityAds.Lease, + override val createdAt: Long, + override val confirmedAt: Long?, + override val lockedAt: Long?, +) : OnChainOutgoingPayment() { + override val amount: MilliSatoshi = lease.fees.total.toMilliSatoshi() + override val miningFees: Satoshi = lease.fees.miningFee + override val fees: MilliSatoshi = lease.fees.total.toMilliSatoshi() + override val completedAt: Long? = confirmedAt +} + enum class ChannelClosingType { Mutual, Local, Remote, Revoked, Other; } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 9b826724e..51822ac95 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -724,6 +724,16 @@ class Peer( confirmedAt = null, lockedAt = null ) + is ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest -> + InboundLiquidityOutgoingPayment( + id = UUID.randomUUID(), + channelId = channelId, + txId = action.txId, + lease = action.lease, + createdAt = currentTimestampMillis(), + confirmedAt = null, + lockedAt = null + ) is ChannelAction.Storage.StoreOutgoingPayment.ViaClose -> ChannelCloseOutgoingPayment( id = UUID.randomUUID(), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 98db83189..3c8ec2a3a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.serialization.v4 import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.Input +import fr.acinq.bitcoin.io.readNBytes import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Features import fr.acinq.lightning.ShortChannelId @@ -307,43 +308,60 @@ object Deserialization { else -> error("unknown discriminator $discriminator for class ${SignedSharedTransaction::class}") } - private fun Input.readInteractiveTxSigningSession(): InteractiveTxSigningSession = InteractiveTxSigningSession( - fundingParams = readInteractiveTxParams(), - fundingTxIndex = readNumber(), - fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction, - localCommit = readEither( - readLeft = { - InteractiveTxSigningSession.Companion.UnsignedLocalCommit( - index = readNumber(), - spec = readCommitmentSpecWithHtlcs(), - commitTx = readTransactionWithInputInfo() as CommitTx, - htlcTxs = readCollection { readTransactionWithInputInfo() as HtlcTx }.toList(), - ) - }, - readRight = { - LocalCommit( - index = readNumber(), - spec = readCommitmentSpecWithHtlcs(), - publishableTxs = PublishableTxs( - commitTx = readTransactionWithInputInfo() as CommitTx, - htlcTxsAndSigs = readCollection { - HtlcTxAndSigs( - txinfo = readTransactionWithInputInfo() as HtlcTx, - localSig = readByteVector64(), - remoteSig = readByteVector64() - ) - }.toList() - ) + private fun Input.readUnsignedLocalCommitWithHtlcs(): InteractiveTxSigningSession.Companion.UnsignedLocalCommit = InteractiveTxSigningSession.Companion.UnsignedLocalCommit( + index = readNumber(), + spec = readCommitmentSpecWithHtlcs(), + commitTx = readTransactionWithInputInfo() as CommitTx, + htlcTxs = readCollection { readTransactionWithInputInfo() as HtlcTx }.toList(), + ) + + private fun Input.readLocalCommitWithHtlcs(): LocalCommit = LocalCommit( + index = readNumber(), + spec = readCommitmentSpecWithHtlcs(), + publishableTxs = PublishableTxs( + commitTx = readTransactionWithInputInfo() as CommitTx, + htlcTxsAndSigs = readCollection { + HtlcTxAndSigs( + txinfo = readTransactionWithInputInfo() as HtlcTx, + localSig = readByteVector64(), + remoteSig = readByteVector64() ) - }, + }.toList() + ) + ) + + private fun Input.readLiquidityPurchase(): LiquidityAds.Lease = LiquidityAds.Lease( + amount = readNumber().sat, + fees = LiquidityAds.LeaseFees(miningFee = readNumber().sat, serviceFee = readNumber().sat), + sellerSig = readByteVector64(), + witness = LiquidityAds.LeaseWitness( + fundingScript = readNBytes(readNumber().toInt())!!.toByteVector(), + leaseDuration = readNumber().toInt(), + leaseEnd = readNumber().toInt(), + maxRelayFeeProportional = readNumber().toInt(), + maxRelayFeeBase = readNumber().msat, ), - remoteCommit = RemoteCommit( + ) + + private fun Input.readInteractiveTxSigningSession(): InteractiveTxSigningSession { + val fundingParams = readInteractiveTxParams() + val fundingTxIndex = readNumber() + val fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction + val (liquidityPurchase, localCommit) = when (val discriminator = read()) { + 0 -> Pair(null, Either.Left(readUnsignedLocalCommitWithHtlcs())) + 1 -> Pair(null, Either.Right(readLocalCommitWithHtlcs())) + 2 -> Pair(readLiquidityPurchase(), Either.Left(readUnsignedLocalCommitWithHtlcs())) + 3 -> Pair(readLiquidityPurchase(), Either.Right(readLocalCommitWithHtlcs())) + else -> error("unknown discriminator $discriminator for class ${InteractiveTxSigningSession::class}") + } + val remoteCommit = RemoteCommit( index = readNumber(), spec = readCommitmentSpecWithHtlcs(), txid = readTxId(), remotePerCommitmentPoint = readPublicKey() ) - ) + return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, liquidityPurchase, localCommit, remoteCommit) + } private fun Input.readChannelOrigin(): Origin = when (val discriminator = read()) { 0x01 -> Origin.PayToOpenOrigin( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index 3077b451f..ecb973636 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -12,6 +12,7 @@ import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* import fr.acinq.lightning.utils.Either import fr.acinq.lightning.wire.LightningCodecs import fr.acinq.lightning.wire.LightningMessage +import fr.acinq.lightning.wire.LiquidityAds /** * Serialization for [ChannelStateWithCommitments]. @@ -356,31 +357,68 @@ object Serialization { } } + private fun Output.writeUnsignedLocalCommitWithHtlcs(localCommit: InteractiveTxSigningSession.Companion.UnsignedLocalCommit) { + writeNumber(localCommit.index) + writeCommitmentSpecWithHtlcs(localCommit.spec) + writeTransactionWithInputInfo(localCommit.commitTx) + writeCollection(localCommit.htlcTxs) { writeTransactionWithInputInfo(it) } + } + + private fun Output.writeLocalCommitWithHtlcs(localCommit: LocalCommit) { + writeNumber(localCommit.index) + writeCommitmentSpecWithHtlcs(localCommit.spec) + localCommit.publishableTxs.run { + writeTransactionWithInputInfo(commitTx) + writeCollection(htlcTxsAndSigs) { htlc -> + writeTransactionWithInputInfo(htlc.txinfo) + writeByteVector64(htlc.localSig) + writeByteVector64(htlc.remoteSig) + } + } + } + + private fun Output.writeLiquidityPurchase(lease: LiquidityAds.Lease) { + writeNumber(lease.amount.toLong()) + writeNumber(lease.fees.miningFee.toLong()) + writeNumber(lease.fees.serviceFee.toLong()) + writeByteVector64(lease.sellerSig) + writeNumber(lease.witness.fundingScript.size()) + write(lease.witness.fundingScript.toByteArray()) + writeNumber(lease.witness.leaseDuration) + writeNumber(lease.witness.leaseEnd) + writeNumber(lease.witness.maxRelayFeeProportional) + writeNumber(lease.witness.maxRelayFeeBase.toLong()) + } + private fun Output.writeInteractiveTxSigningSession(s: InteractiveTxSigningSession) = s.run { writeInteractiveTxParams(fundingParams) writeNumber(s.fundingTxIndex) writeSignedSharedTransaction(fundingTx) // We don't bother removing the duplication across HTLCs: this is a short-lived state during which the channel cannot be used for payments. - writeEither(localCommit, - writeLeft = { localCommit -> - writeNumber(localCommit.index) - writeCommitmentSpecWithHtlcs(localCommit.spec) - writeTransactionWithInputInfo(localCommit.commitTx) - writeCollection(localCommit.htlcTxs) { writeTransactionWithInputInfo(it) } - }, - writeRight = { localCommit -> - writeNumber(localCommit.index) - writeCommitmentSpecWithHtlcs(localCommit.spec) - localCommit.publishableTxs.run { - writeTransactionWithInputInfo(commitTx) - writeCollection(htlcTxsAndSigs) { htlc -> - writeTransactionWithInputInfo(htlc.txinfo) - writeByteVector64(htlc.localSig) - writeByteVector64(htlc.remoteSig) - } + when (liquidityPurchased) { + null -> when (localCommit) { + is Either.Left -> { + writeNumber(0) + writeUnsignedLocalCommitWithHtlcs(localCommit.value) + } + is Either.Right -> { + writeNumber(1) + writeLocalCommitWithHtlcs(localCommit.value) } } - ) + else -> when (localCommit) { + is Either.Left -> { + writeNumber(2) + writeLiquidityPurchase(liquidityPurchased) + writeUnsignedLocalCommitWithHtlcs(localCommit.value) + } + is Either.Right -> { + writeNumber(3) + writeLiquidityPurchase(liquidityPurchased) + writeLocalCommitWithHtlcs(localCommit.value) + } + } + } remoteCommit.run { writeNumber(index) writeCommitmentSpecWithHtlcs(spec) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index 0f7117262..339ff2089 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -4,7 +4,6 @@ import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.channel.* -import fr.acinq.lightning.channel.ChannelCommand import fr.acinq.lightning.channel.states.Normal import fr.acinq.lightning.channel.states.PersistedChannelState import fr.acinq.lightning.serialization.Encryption.from @@ -97,4 +96,34 @@ class StateSerializationTestsCommon : LightningTestSuite() { // with 6 incoming payments and 6 outgoing payments, we can still add our encrypted backup to commig_sig messages assertTrue(commitSigSize(6, 6) < 65000) } + + @Test + fun `liquidity ads lease backwards compatibility`() { + // The serialized data was created with lightning-kmp v1.5.12. + run { + val bin = Hex.decode( + "0402b83e19a809cbef04a58caa852693cf7ddebd0fd926875bd1b195fcf5225a869a01010410101000037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c09fe8824646afed5cc44a9fecb08263bfee1c34a83feba92e4e8fe65d93543fecb5ee602fe43ec9100fe80000001fd044cfe59682f000090640116001405e0104aa726e34ff5cd3a6320d05c0862b5b01c13280880000000000000000000001000101a51020362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06fd03e8fe59682f00fd03e89064026843554d5e604ffd3fcabc56cefe5849abbb7fd395f36bcf3e9550594aace9690236633b1e8f7a54ef367482c31c74162f4fd3e4c7d78694e2c6d769af6e33047202e97df1b0423c20ba41a1955e71cfcb96cec4f636b1d310be78e989f92229edb302b3c6959eefecdee406b9b4df0d76126f2c5038811b27abf44738e6db1be0bdf11408220222000000000000000000001000142a5102000000000000000000000100022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b02fd024a02000000000102d70488b7709a2ea05d808ec1f46d6ec100f85b3c1f1fe909d3dc6332b1b9153a0000000000fdffffff3bd4776fba4675b6b2e56d4ef0b81159c4319cf9942918fc29798f06b95a84270000000000fdffffff0140420f00000000002200201aaf87557cdb6e31b338fa3e6be95fc7d8ee72b80ac8147fc35ce975d145a67e03473044022012d7967e817c6f369aa4f9a69f78ac1008a7f0ea8f62e3510b8ec2ed3e9e109302202fd1fd54d104f7e2fe0a5404edccb7b3f786cd5f82447bcca2bd23fee34cb596014830450221009c192a826ecf488216eea24279c6699c774e9c930194a8357a61d2e4e91b9c1e02202a085efb5276f6d34ec310b8d2ede7bf65fe63d4027b93b89902d3c4e179e91d014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b26803483045022100ec3085bef0557c2329c0a9ff1df9374973f42247bab7cdcff10b2fa5fa8b89680220040eeeb7576228bcde8d62f930c368010c8c665467299ea7f3a735140a27ae3301483045022100e628ebd5b4f433c1e4127b7d7fb0f625a6dcb1e4cf8cd62aa4a120312c723138022020f22620ebb280dfc8ad5eb1c1671ce13c4cd9bb7166cd50f3a8577a8b79b167014d21020bc21d9cb5ecc60fc2002195429d55ff4dfd0888e377a1b3226b4dc1ee7cedf3ad2102d8c2f4fe8a017ff3a30eb2a4477f3ebe64ae930f67f907270712a70b18cb8951ac7364024065b268801a0600fd1388cc0047b83e19a809cbef04a58caa852693cf7ddebd0fd926875bd1b195fcf5225a869a1f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f180000fd025b409c192a826ecf488216eea24279c6699c774e9c930194a8357a61d2e4e91b9c1e2a085efb5276f6d34ec310b8d2ede7bf65fe63d4027b93b89902d3c4e179e91dfd025d40ec3085bef0557c2329c0a9ff1df9374973f42247bab7cdcff10b2fa5fa8b8968040eeeb7576228bcde8d62f930c368010c8c665467299ea7f3a735140a27ae33000000fd1388fe2faf0800fe0bebc20000241f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f18000000002b40420f00000000002200201aaf87557cdb6e31b338fa3e6be95fc7d8ee72b80ac8147fc35ce975d145a67e475221022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b2103cb734f5eac1ce94a8d571b17db09bafe974b5ebae76416667bae888a11fbb9a652aefd01bc020000000001011f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f180000000000bc63fb80044a010000000000002200202a962bfb8410b4d8515002cdca69755a4e7b2f35c1d3c8ca23c8c2eb2c663ea84a0100000000000022002086bc033f5435e003d1be7f8d21ffcba84d5177f72d9cab95ddca49557b0db016400d03000000000022002066b48675ced51cc392a9c208c5b6b37173fcd1c88601188c0a55fa5db4a3df87781c0c0000000000220020d4234a5c556b42832ec8985edc4f377809fa254fc509ee5533de099a91579848040048304502210097e686048e14f2e862970d384734ec72d4799afaf7a67f679e5ef1c685e37279022052f6c4647e44bc9ea197431e47903398fc077314e47a19046f5c6f6b130d9fca0147304402204c85f5c533eaf8bfd8fcdbfca7184522eec4d9f3028226450051a7cedd15d0d802202a5ead9a0284bdec30d52326eb2ddc8d6e00a991f14faf5656a05769eb52ce8401475221022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b2103cb734f5eac1ce94a8d571b17db09bafe974b5ebae76416667bae888a11fbb9a652ae34d5bc20000000fd1388fe0bebc200fe2faf080002613a5fffafc39766ca252b1470bc96161211c3bf0533aa04fd7cb23d05bf6e02cd117f0145d8c5780d5fd5145251257cfc0898145a99750c181927bbe526f4a600000001035a3feef004f091d822802e715c2d6e9e75020af11be99fd3d4c30e2c6ffa2a480000fd0760040042686e87cb623bed5376be9b0b6314dc871fe35781bbbccf91d12cd07adf711c72e520bb2ff4cf3fa9611868a324e153ebf63586083258028b322fbce9995ddd2593a122a97b082d0f2d58a895c9fa06fd535089a04e05fcdf0e8907492a6c5244541b806bcc49120e464ec1eca87840b41e694725528fe8d7d94f640d958d0b43c17478977617d134a4a1fa85c45f135bd626e70ca862cca3e4861e88771a120bfa6898971b4dd3b022dc920f481cca8102fc101e69ac2d92e18773ef6b262356370514cad85f6531f3ae1d8f404e04172917483227ad9ca8ac29a0b01302ecb67adf54e8289f6dedf3c323f1e52daee77b3bd2524a2959f5dc0dec361212b77c593b67419adeb7aeb75b39b9daca003755b0fd50653724df439d95e6a00bb6afb303ac8a39bc47ebc6a0b906532fd14140e6ca727c4b85ca970da5b249374ec813d1f78ff7171711bfd2a2bc204fbe29834bbf8b9bdf1be88f987315e2d3cb56b50056feee5970c9939af176d829e08106dc4101f5f18a8f04c8067e375505f7bea0a20ccadccf3ece22eccb873efd221877100e08ab9b1c241ef36176dc0ab7b41c17a5bddbf243e22c2dc5f5f9b410a90b6e77e09bc95d7e9e50c5a8afdc462408c453d37571a695dbf37945565b605b0b13c70ce03580d0c4c36f453c7a0a1a7418fdaf057c1c3cbbd9f3fdbf667f3d7342b24c4cc5b7b078891b2fb31d2a2f37f9beab0a503c34df80c39eb19c9194bf4b04c164dade1b176c0cc1690ff64bcfc3f4365d7f7ab7777ee20374c1707a794e32eb7792b20cab4d67cd0d226eb93643d35dd479567a90245e518ce4150709a7d550d3b175ca880393830fe784aeac55811ccf62ce15bac14630263ba1c182827646a4bbd26ddbad3100b23b04afc042cefb6489fe1c77f38826d8a39c9cdc906d73317eaa33cf6ca2ed8756925c8919622ee80a87d66f3eb2f43534c6ecb749b2c473d32c7eaaff659d84bf680c702c1e13adcfadd8e907b886300e07cd431fab9affb451196e3dfd77cfafb8de0e1fe65e66ddb7ba594b7369aa52113c3d752b312fbc51a17d504244933cee42909c60c517a4411f841af48799e719554a07bdd3ffbeb14e694e913514856656e7fcdfaaf84daf8f0b2ef4639c0682524874dd7eb4c16844074ac0d97354a7e643a2e3220bf30855c54461464c0bf82bbabbba7fe407e1f2fa394f8e3822c507e2d705e32e13f2a50a5f2c8b3d73b63847cf985f06e25de5629e8a570092a92996c655f5ef3871d2a3a4b556c9b52d40b828475c35262c6f9f5bbfbd3e6ebf09864bfb3d3dcf4f78961d4fc85fd9b9c924ce6ba8c6df4c8525ee4c3f67f97e361566b31a9df0c4bc6da36e9e0e47f0b91a67f489fba2d0eddee58bac5ddc4cfde2c74947a27b49e89fa838bbfaeae6605a7e2dad611252a5d30a5c99592de44aad8fb4253880ce16f60c3231f9824898751e99eb4d554bea9042843a56d5239f8d3aae93696583970822429beef912dcd7129693e11ad39ec0191ee5fc06b58544fbed9c6c12ad73690bf64bb78fb16902e97bc8f8fcbdba321ce0241141542cca9235489459b1b50d44d76bb36492241dfa43f5252331556a9c618f14f89f9b7dc9944498a73ce242a0ec0b2953b25cc5b11c25dbf336a6319f479e561c2c4f6f196a43f93ddb22da68bfe3909c3cb21503a554b895ef4dbd0033684b16b974042386eddef9faf63389d6d07bafdc934884589333da2fd0a6e1e15bdcab663c562e00e887c1b9b5296b8bee678a21d11c45005729bb0e6eb225cb9a480673483634ad21ceb0bec52ec78b13058847e750412ab67e3631187c289aeba97371926027b348bc932b600ece0fa5a8fa69a18d44e51eb7857011e72484e1e8393d94382ddd8e012b676dde44da75eda81aa0ba5ed8e474b7465c5af2b1a1de7aa870fdd191de0caf78875880ab6d5d3fcef3057002e17a07f9e870ae13634cef3bf8a60b41104c39145a1b6dd44f37c3b7d3c78f2f6f1fe83d38c2a54c1597270fed60b157fbdf431d51e98899f6894ad41c4142e271af7d5557faaacdc337caa9f3ecae7a0dbfdf27057c437556c9c9442fdca9e9a07e61741ee56a3db89e29d3d4ab7fc3feda7d737d5ceb3787d103efbd72772a1ebf65541d6d7cdb5ac82e834060d1b9f58be80db537f9ca696a57f21d74fdee7947ff90cb238c3f6f7e084012a1c1466c230d841e7b3cc0b670696e7f3b6186770b2e61c3bae625da4232831058ad73c87744be94f301ce839d6fd46d62fec3edd60ea7cf92fd119b98232cd9621f5e5d37bde331e2db7d4742531e93531676150bbcf8dd28e7acd3181128ebfc36c49aa7ced8fb5af96833769deb6d46f49010ce92ba80e5f7e841360ae01f86a39e24383cab02d31af0745f70b752ebf6e149e38c1c4bc7a6f39124555b449e4887fca29bdc51efd56c0f682458a41a5cf28697c3f79c980df742e80aeae1dcc91309389885c3f2386f4edd14956bf562884f5983bb906fea2bc394efbb67de76720209ae47b3a6ed5e00e82287f3586113de2476d514dc58086e03890ce3247a0d2969a32c995ec7b306c8c6b6737310f8d2bf2499d1659b523e0ceb00eba41af9bb32a81fa230a560a866606481a5086da9ea8b61d0d4dd8b0cff00002a00000000008a01023a973e5a95ba9356ebb5d884eda57169e214d46afbe7e0ede00f4bf4a3acc0336825ba58984754922a4f3a28cbcb5fa52a9b983210bb992eef6e2dfe391a806d06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00002a00000000006572fa6b0101009000000000000003e8000000640000000a000000003b9aca000000000001b83e19a809cbef04a58caa852693cf7ddebd0fd926875bd1b195fcf5225a869a01fdc256000101241f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f18000000002b40420f00000000002200201aaf87557cdb6e31b338fa3e6be95fc7d8ee72b80ac8147fc35ce975d145a67e475221022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b2103cb734f5eac1ce94a8d571b17db09bafe974b5ebae76416667bae888a11fbb9a652ae00022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b0369b5f974eb92be821826fe7d0969744283c917f3411655f190b56fc5930c3cfa00fe00061a80fd044cfd00fd0101010102241f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f1800000000fefffffffdfe2faf0800fe0bebc2000104220020431b8ae82b805d47da74e00ecde92e930c666124d5930fae2958ba309885380ffe32a627f0fe0bebc2000102007d02000000011134cd9d56bee35f5db7b8a8e17ae69eabc8738653da42247fad8996e7419b7d0200000000000000000250c3000000000000220020778f86e213a518e8336ec940874ccf4b92910332f5b3c0265374a86dbd34e6a596000000000000001600141240d4b7fcfbfbd7234cf2dedf071673a0c1e5590000000000fefffffffd03d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc49190256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdfd6540000000fe00061a80cc0047b83e19a809cbef04a58caa852693cf7ddebd0fd926875bd1b195fcf5225a869a3448c2e0ddb06dce57bc06c2a3d447d958958b4cef6499a4d0bb1eb36d28e2c60000fd0259406e731b1649d06176d0ecf590b385b0123f685cb93ef518124d6b9cbd7062c4265af87d8986ac6fd525d0e738dff61e00d18ce04fe9cee80a99744a65fcd4fb04fd025b4045e91597bd2826f18f58321051c3e0a6728ebbb0d633eba9428139335460c9da322b01137b5a3b5041fd526d5043ffd586d8ef44b7ccb27c9605ab6bb2943d27000000fd1388fe32a627f0fe0bebc20000243448c2e0ddb06dce57bc06c2a3d447d958958b4cef6499a4d0bb1eb36d28e2c6000000002b9604100000000000220020431b8ae82b805d47da74e00ecde92e930c666124d5930fae2958ba309885380f47522102c3cdf2cd990536f7ac520b3a2f66c0a6e302c2fe15a8c3baee24eba1cb9a8b02210369b5f974eb92be821826fe7d0969744283c917f3411655f190b56fc5930c3cfa52aedf02000000013448c2e0ddb06dce57bc06c2a3d447d958958b4cef6499a4d0bb1eb36d28e2c60000000000bc63fb80044a010000000000002200204725be4ed490e91c4ad5824fcc202c53787b147d4ad28a30aafeff90400d17634a010000000000002200204eea61d0b3215da0c03b103e98d24aa4466e0fb1ba80f8d8d85d33ed0a969da3400d03000000000022002066b48675ced51cc392a9c208c5b6b37173fcd1c88601188c0a55fa5db4a3df87cede0c0000000000220020d4234a5c556b42832ec8985edc4f377809fa254fc509ee5533de099a9157984834d5bc20000000fd1388fe0bebc200fe32a627f05f4cb3d37e1f420f5f39b563929d1a82a8e93ee4d864eaca609508b3ad2b6a5702cd117f0145d8c5780d5fd5145251257cfc0898145a99750c181927bbe526f4a600" + ) + val state = Serialization.deserialize(bin).value + assertIs(state) + val splice = state.spliceStatus + assertIs(splice) + assertNull(splice.session.liquidityPurchased) + assertTrue(splice.session.localCommit.isLeft) + assertContentEquals(bin, Serialization.serialize(state)) + } + run { + val bin = Hex.decode( + "040238ca11203794f6ef69fc7df13b91e7848c837632bd1407bec63d9ade0330a3f701010410101000037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c09fe5b62e3a5fecf5fd832fec7e172c8fe15fff232fe17015da3fe71c99b0afea90a802bfe3993f9cefe80000001fd044cfe59682f000090640116001405e0104aa726e34ff5cd3a6320d05c0862b5b01c13280880000000000000000000001000101a51020362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06fd03e8fe59682f00fd03e890640349b56ccb150862271cdc1b280d484db844d48ee85f07515cc6e847d1d32a147a02d78bdcc7f2160d5ccdcfbc0ba3dcc4b547d06f38cb65ffac7589ae5ad529d08a03e62f14d41cdf68d7ac982dc03e6492d093d7aec4ef7d1765d5a5bcc995e204b602e95f9d9281919ff9cae84e7dc3b5b1ef1161a6c503617e4f3207e05d722c15a71408220222000000000000000000001000142a510200000000000000000000010002e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f02fd02490200000000010256724a067da52a008fa768ad15f2a003054882bf0c09693a2c0f386eb5d8c4340000000000fdffffff3be96364f874547c41cf86f1f57c35029a6e082700bcd25f5b3cbd742417ced80000000000fdffffff0140420f000000000022002026fa4316fb307d348467c0edd3b8bb1eff9ca3ec04367ce5788885712ec400b003483045022100e66fec8848962770b61c9835b4e09954dd6dec98c2cd621a8592defe58796ef4022067aa7588b08614346f544322577b7797116c727f452b06cad40c2f9b2af8774601473044022064df9cd5e6e68bad426f4ffe06ccee9eeb5769f4fe65931bc54c0df618accbf1022018488e51b6c6969771de015e9ac41f9426701c5c961df270675cb1476920de98014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b26803483045022100e9882e40951f43da434e1d86cf9830f5499f8dbb846f5131becfa0fea181ef4602206146db93b26271798d40f453d5255557191d0f45f0b5fb0a0d78ce261afcd386014730440220379f14da69fa108168d32351e5c9127479a8e1858f89cdf1a70c66010348a6c1022039bc27eabe21078071c35c35adc1c11ab8e3f70fc9d4e649f6527618e70ba647014d21020bc21d9cb5ecc60fc2002195429d55ff4dfd0888e377a1b3226b4dc1ee7cedf3ad2102d8c2f4fe8a017ff3a30eb2a4477f3ebe64ae930f67f907270712a70b18cb8951ac7364024065b268801a0600fd1388cc004738ca11203794f6ef69fc7df13b91e7848c837632bd1407bec63d9ade0330a3f7f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d240000fd025b4064df9cd5e6e68bad426f4ffe06ccee9eeb5769f4fe65931bc54c0df618accbf118488e51b6c6969771de015e9ac41f9426701c5c961df270675cb1476920de98fd025d40e9882e40951f43da434e1d86cf9830f5499f8dbb846f5131becfa0fea181ef466146db93b26271798d40f453d5255557191d0f45f0b5fb0a0d78ce261afcd386000000fd1388fe2faf0800fe0bebc2000024f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d24000000002b40420f000000000022002026fa4316fb307d348467c0edd3b8bb1eff9ca3ec04367ce5788885712ec400b04752210243fc4580744090c5009be7bb3b85b91c6dfdfeffc67e37a808a89431b24461bc2102e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f52aefd01bb02000000000101f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d240000000000620f4680044a01000000000000220020ad6e712f2f3ff4a279f7c1cc4bb31d88c98ad807537616a4f53beed64cb5091d4a01000000000000220020c79d8484429b469c3230783f14fd3228d9b6da520dac471f3b3d826c59ad0b52400d0300000000002200202a563e407578aae18800c2388759b5bad0ceef5a3759de39d7acedffd868d30d781c0c0000000000220020bf57f237188dde15a4a0947bf64cd985f95163d77fee3d8372cdce323de1efdf04004730440220178102bdc1fce536c08c0660749208ff2d1e0aa9bb5ad1b98b120e9e5e263324022057c7033283b0f397f98378d0b2666879ed5da822445ed43dbb26563644d397370147304402206dd5be99fe9473e0221aaf2e37a72fbe38f666c4129c0c164cf9bd2eb7d93fe802204ec11e93732a56f4e759c4ea8359cfda3e1db4cef0cf91f79b1d9906e60eb2b3014752210243fc4580744090c5009be7bb3b85b91c6dfdfeffc67e37a808a89431b24461bc2102e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f52ae65031b20000000fd1388fe0bebc200fe2faf0800b3822442b3a5d53e6410fe106e9ec9408a9bb0b6b6f34c0ad39d0811b466f86a03cb5d3b7310e6532d403dc7ad5857869e6ace3a2a1a7cf710c8a9b307a5ebb02c0000000103b6668d222ea88836cd25b40784f759c9cc0ff9ac03ef0408a08543ae185405060000fd076004003749c4912f86c5594f2e9775e78a8292f386ca75711bbb7f89c841e99158d24e1aea0c5bf2f810cedf6ca3842aba47df127ad165b13052c8fbc30aa23feb59d01960f2226127e20affb1637bb17394140be970a45c79fa4dd3ac0ba32c6bba5095be8a5ad1c0d6747788fcf128a8f71378d8921d2b7d2c9e999e2898fcb7ae7a5048900b111c973622ebcbdc5e3232efb330464f4d76b1a0fb2d70ddb3882ae9a45a7f3115ad94acc926d1ed33f940cd7bcd8a296983bb3ff4592009ce498b9d4552e6e019d453210545ac5c2f48a1fe75b5dd93cff4f124c363f22578cd7d3b5a5244a871c37244e79eaa1ffc3966f716520b8cbf38ba1c33ec68939fce45a2519eda4f1029d2e5fa3069e5fed848d9e078ed29af5a10541933db39ed353895f2b269437f2a04ba09528b0ba92bf725ea300752226b888f4cf3c3fa973e4b2017b74c86cdbe81829513bd62f2055076e0463b39c2155635772d80b2f6945319bede15535a3becbd9374122f0f974ef2c9ec990369f2a90dfb7f1355e5e183489880c4a9e63740967dec2a77dbfa003361bfee2f3e4f1e4cc02afe0d82a14a47ba9fac237ed616fc892c1d93387b9a9682a78994cd62074b295afc542b190ef2391e8352e8ada52147b448ee2e2cc8cd5170af58cbf211f7b0d49a6b6b6ec628b0dfb4e4636df58dc5c55b3634457d7f949a1f26abc64db158fa51343a5990d707218b01dabf223361cc4f6ce3cfc6b62c5306ad1bfbabf5c51003551a07bb053e5a419d5d8c8c200feb87ab9dd0802d418068285bfa3f0c0ae717d4671cb9d4b2cb0c12d44985961c259f4433fe732da40458c3903d6191f7a6167132a9db3476dfbfc37f3c5d37b49e3027ab9a981ed788e124ed88abe2f3a10f52fd5ba278e6555acc89d916b30c2dcf3bbcc6cfc1985e66a169a6eb1f251cef9ece3487e88d1f81676d97955ef374465e16ad36abaabd3888236dc0eb27050b9050a396a6d8a2cb451b8d75e480d8afa13ddefcda4c28a8483a441edbc034023fe5332c52e86dde7f71dd1865d471deb7ea04a09f38a9307206e2fc53e205362d95247adc5dc5cb5cb064609f2cd11ecbf005612d12165725799044eb45673a1e9c1a1275dc70ff5992500754c6efd851666b6a5d02d438d01e881b430876245d4bc4b888988471fdad5104e5bc5a518a83a9be98f9a1ea11473b8eb32150714ffa1bdddaf35fb7e0b50cb075a8a38437638cd4803e3e8baea0630420947dfb274a4980fd6c8d4a1a79d033406b7dae6cf68f83cfabe9e5bc9e4ef51c49362017fa835497b909bf0599ec764709a527a8bbf59007602bbeb676a60a3c990bb1630c18f4ba3b3ce71d73ebdff879af9347c7fd9d0789cf7d15fbe3196a4cbaecc3fbd2a5ed2f1d995cc03c6e5bfb48395b317ca4b3ff626e291f6cf877186eddd707b8d5a66de90ced49f276b417032d264d992d6dcf26267bdfbeb37a7e65438ae136bf65ad0da4998a3a331e7593786157562ac0eb4d37e68d41181d79677265b27099d770b4443cbf9d08859e4ac79f9adcbc41000ce203fedb40ceeb5050fa56a5bc9f038d4f13cc860e3e68a5df055ae2df2c09a392435e5770790835e2db2081dd21d28f2bc76eba810d5cdba41c97a8a64512af71eb9bbfc8b7ea17f41710cc034d33e92ca73c02a6e7501e33efe57efb54ecdf36e1e18207994779fe8a8e299ec5ddf186b6c859e5884994ac780d6f800d7e65ab1746e56b9dca3f08a0fd7a86680a53ffc70bb1b3138844a3ae4ee7267c2cdbba2cd8da1af7522fb6eaeb6b737637df1e69c0356ba02ca06a064d80add016c1a5fe804be21250c93dc859313ff0c41a68c351a702b2f24279d197cd1201080edb1006ae100ffa7d660a5439a79bcbda24e2fdf445f010bc49514e5030f10b4760101d07cec44773136f884264a3c0dc465fb950bbc2c11cebfd9a7de7b0f18e77e03e2a2e5199308f21fde4d9092328651d13d9b86cfbadde55d4eb3bd815d3c4349ca4e3944bfad27ef31b6034b3c934f8eeed228845091fbd030858ffbb6448dfb3454a5049bc86e3894814de855627b4cbb9a90515360f9087c7f99b894b7839c6b3beb0a6dcfe102d549cf571e287720b02fac463bddbaf1fd3d4865c9444f36d763d977d6e4741bdd983133112bd2567af10bbeba5944c39f3cd3ea9d5249cfcaa56f762224c1fe4ed7a847303859cb36d642c6bf903012327fa4af7ee0d901e09d4b2443f4036e3c7cf46971b90750fdf2c63f3f10b18ad46da18b62f7320d0d05366aecef0d0281ab8ec80888c332761300eff916a846fa3887e58f76fcbd5122861324e748dc0544b5886aab637188d40d4232587d5f11d9ddb9f7c8a1fd9b084032a6724e786d8ea632dfb85a0e650f8120758c1a58ebe077658f28e1181802ea5a90e1fce99a1e0246e60837048a75e47e6deba2959cec9f63029671dba511ff1a3ebee4a0b5868621c56afa369f17cff651d03cc620984dd1d985184ae7b3d2c9d0e838d9843b6a893e22643ce7057d33075dc937146e7d0194a2bbbf87a427d5235fa361f1eae8f35090b83f4a589205b683f773a2588018ae306383fdff65a857fc13be804b20a13f8982a3652eea9006ad3e4767f520abe82f199dbe74870cec8e466cb98ff00002a00000000008a0102a9c1d59ef327d76207a8a26373c89b9d0d0eb25d6ca7551a3f291c8c6cd3f0b45a36669e8960989d59bb5412a3aea7a6c0d994375d43bf812f38c96eedaf712506226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00002a0000000000657313c00101009000000000000003e8000000640000000a000000003b9aca00000000000138ca11203794f6ef69fc7df13b91e7848c837632bd1407bec63d9ade0330a3f701fdc25600010124f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d24000000002b40420f000000000022002026fa4316fb307d348467c0edd3b8bb1eff9ca3ec04367ce5788885712ec400b04752210243fc4580744090c5009be7bb3b85b91c6dfdfeffc67e37a808a89431b24461bc2102e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f52ae0002e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f029bd74977c75b38711165bb9506c4b2725fbb9a031364d299681e80ec2952285500fe00061a80fd044cfd00fd010101010024f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d2400000000fefffffffdfe2faf0800fe0bebc2000104220020e4187f51de13d85db945c888357da388e2877ec578316fc25ecac3e716f4866bfe32a627f0fe0bebc2000102027d02000000018c19fefe4b851e9da17f94d44b549d87124aec35a4a85c40c566564c51ada7220200000000000000000250c3000000000000220020778f86e213a518e8336ec940874ccf4b92910332f5b3c0265374a86dbd34e6a5960000000000000016001425deb8d8a6cb84452c47904350e79c523dbfefdb0000000000fefffffffd03d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc49190256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdfd6540000000fe00061a80cc004738ca11203794f6ef69fc7df13b91e7848c837632bd1407bec63d9ade0330a3f79ad1a0a72421f2f13a86e069498ee9c1a5687c1b06968776f9699940e624d5300000fd02594066195c52f770b513f87862384c79a4ad543fff3a81fa6e3d45e76ebea9bd319a38706e847ae4e53fb9d97111b69f0cbfc86f20c92d35f155855b3de9de804548fd025b40849a87ab6815902abbc4f3d9773517c18eeddc5d10dd949aeccdad068be0e5d8260b54dff0c783ae0c330157954e371fadd82bb084d2fd9de28867a87ca60b70010000fd1388fe32a627f0fe0bebc20000249ad1a0a72421f2f13a86e069498ee9c1a5687c1b06968776f9699940e624d530000000002b9604100000000000220020e4187f51de13d85db945c888357da388e2877ec578316fc25ecac3e716f4866b475221029bd74977c75b38711165bb9506c4b2725fbb9a031364d299681e80ec2952285521029e2ff8460c1ea74f8892509f4c60340d19a1981c4339e2a728ddef8df08eea4b52aefd01bc020000000001019ad1a0a72421f2f13a86e069498ee9c1a5687c1b06968776f9699940e624d5300000000000620f4680044a0100000000000022002063d3fcf7f93eb1b30ff3d7f185c2889a5fa8c4a584cedd4e0ce3a16e10ea2c994a01000000000000220020a483a289ea09e761fae87c628675e3d52f23fcc6355ff15c3bde6ce2dc86d802400d0300000000002200202a563e407578aae18800c2388759b5bad0ceef5a3759de39d7acedffd868d30dcede0c0000000000220020bf57f237188dde15a4a0947bf64cd985f95163d77fee3d8372cdce323de1efdf04004730440220772d80d88ac5156fb8096ba19129492e66bdf8bea76e750847534f7aafb9621d022075f0480e72f576632ad9b80fbc5e7630951753c8425892adac6eb0352823de12014830450221009ea0e484d4d4c43c46960cb2f182e0460ebe560c036691076e2ddc03e2d87933022042074738e8a9c263694060f5fbcdd42a308d56abca4e5466f8d958d018fc5d0501475221029bd74977c75b38711165bb9506c4b2725fbb9a031364d299681e80ec2952285521029e2ff8460c1ea74f8892509f4c60340d19a1981c4339e2a728ddef8df08eea4b52ae65031b20000000fd1388fe0bebc200fe32a627f0b8b2f694372a292f7822c17f2400184c2f70195afc7523987a14f65e7601042d03cb5d3b7310e6532d403dc7ad5857869e6ace3a2a1a7cf710c8a9b307a5ebb02c00" + ) + val state = Serialization.deserialize(bin).value + assertIs(state) + val splice = state.spliceStatus + assertIs(splice) + assertNull(splice.session.liquidityPurchased) + assertTrue(splice.session.localCommit.isRight) + assertContentEquals(bin, Serialization.serialize(state)) + } + + } } From b7cd46184c358195e15c74e28a28396e23d957fa Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 12 Dec 2023 13:58:45 +0100 Subject: [PATCH 06/15] Abort if balance is too low to pay fees We know beforehand that the interactive-tx session will fail if our current balance is too low to pay the fees to the requested liquidity. --- .../acinq/lightning/channel/states/Normal.kt | 5 ++++ .../channel/states/SpliceTestsCommon.kt | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 8e1dab3b5..9db855617 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -116,6 +116,11 @@ data class Normal( logger.warning { "cannot do splice: invalid splice-out script" } cmd.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript) Pair(this@Normal, emptyList()) + } else if (cmd.requestRemoteFunding?.let { r -> r.rate.fees(cmd.feerate, r.fundingAmount, r.fundingAmount).total <= parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } == false) { + val missing = cmd.requestRemoteFunding.let { r -> r.rate.fees(cmd.feerate, r.fundingAmount, r.fundingAmount).total - parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } + logger.warning { "cannot do splice: balance is too low to pay for inbound liquidity (missing=$missing)" } + cmd.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds) + Pair(this@Normal, emptyList()) } else { val spliceInit = SpliceInit( channelId, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index c412938b4..8c5f086ae 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -20,6 +20,7 @@ import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toMilliSatoshi import fr.acinq.lightning.wire.* import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlin.test.* @@ -151,6 +152,29 @@ class SpliceTestsCommon : LightningTestSuite() { } } + @Test + @OptIn(ExperimentalCoroutinesApi::class) + fun `splice to purchase inbound liquidity -- not enough funds`() { + val (_, bob) = reachNormal(aliceFundingAmount = 100_000.sat, bobFundingAmount = 10_000.sat, alicePushAmount = 0.msat, bobPushAmount = 0.msat) + val leaseRate = LiquidityAds.LeaseRate(0, 0, 100 /* 5% */, 1.sat, 200, 100.msat) + run { + val liquidityRequest = LiquidityAds.RequestRemoteFunding(1_000_000.sat, bob.currentBlockHeight, leaseRate) + assertEquals(10_001.sat, liquidityRequest.rate.fees(FeeratePerKw(1000.sat), liquidityRequest.fundingAmount, liquidityRequest.fundingAmount).total) + val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) + val (_, actions1) = bob.process(cmd) + assertTrue(actions1.isEmpty()) + assertTrue(cmd.replyTo.isCompleted) + assertEquals(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds, cmd.replyTo.getCompleted()) + } + run { + val liquidityRequest = LiquidityAds.RequestRemoteFunding(1_000_000.sat, bob.currentBlockHeight, leaseRate.copy(leaseFeeBase = 0.sat)) + assertEquals(10_000.sat, liquidityRequest.rate.fees(FeeratePerKw(1000.sat), liquidityRequest.fundingAmount, liquidityRequest.fundingAmount).total) + val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) + val (_, actions1) = bob.process(cmd) + actions1.hasOutgoingMessage() + } + } + @Test fun `reject splice_init`() { val cmd = createSpliceOutRequest(25_000.sat) From cb0c0f80900c95f03f4be9c335decc046229958b Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 12 Dec 2023 14:21:14 +0100 Subject: [PATCH 07/15] Improve liquidity ads fee estimation helper --- .../kotlin/fr/acinq/lightning/io/Peer.kt | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 51822ac95..ae7de6b18 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -536,6 +536,25 @@ class Peer( } } + /** + * Estimate the actual feerate to use (and corresponding fee to pay) to purchase inbound liquidity with a splice + * that reaches the target feerate. + */ + suspend fun estimateFeeForInboundLiquidity(amount: Satoshi, targetFeerate: FeeratePerKw): Pair? { + return channels.values + .filterIsInstance() + .firstOrNull() + ?.let { channel -> + val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = channel.commitments.active.first(), walletInputs = emptyList(), localOutputs = emptyList()) + // The mining fee below pays for the shared input and output of the splice transaction. + val (actualFeerate, miningFee) = watcher.client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) + val leaseRate = liquidityRatesFlow.filterNotNull().first { it.leaseDuration == 0 } + // The mining fee in the lease only covers the remote node's inputs and outputs. + val leaseFees = leaseRate.fees(actualFeerate, amount, amount) + Pair(actualFeerate, leaseFees.copy(miningFee = leaseFees.miningFee + miningFee)) + } + } + /** * Do a splice out using any suitable channel * @return [ChannelCommand.Commitment.Splice.Response] if a splice was attempted, or {null} if no suitable @@ -576,11 +595,6 @@ class Peer( } } - suspend fun estimateFeeForInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw): LiquidityAds.LeaseFees { - val leaseRate = liquidityRatesFlow.filterNotNull().first { it.leaseDuration == 0 } - return leaseRate.fees(feerate, amount, amount) - } - suspend fun requestInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw): ChannelCommand.Commitment.Splice.Response? { return channels.values .filterIsInstance() From 3541777350fdae4b0ac92cbf55d4e52ae1c200c5 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 12 Dec 2023 14:25:19 +0100 Subject: [PATCH 08/15] Add comments for the serialization trick --- .../lightning/serialization/v4/Deserialization.kt | 2 ++ .../lightning/serialization/v4/Serialization.kt | 15 ++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 3c8ec2a3a..ec4c6afcd 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -347,6 +347,8 @@ object Deserialization { val fundingParams = readInteractiveTxParams() val fundingTxIndex = readNumber() val fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction + // liquidityPurchase and localCommit are logically independent, this is just a serialization trick for backwards + // compatibility since the liquidityPurchase field was introduced later. val (liquidityPurchase, localCommit) = when (val discriminator = read()) { 0 -> Pair(null, Either.Left(readUnsignedLocalCommitWithHtlcs())) 1 -> Pair(null, Either.Right(readLocalCommitWithHtlcs())) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index ecb973636..3fa978a92 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -394,26 +394,31 @@ object Serialization { writeInteractiveTxParams(fundingParams) writeNumber(s.fundingTxIndex) writeSignedSharedTransaction(fundingTx) - // We don't bother removing the duplication across HTLCs: this is a short-lived state during which the channel cannot be used for payments. + // The liquidity purchase field was added afterwards. For backwards-compatibility, we extend the discriminator + // we previously used for the local commit to insert the liquidity purchase if available. + // Note that we don't bother removing the duplication across HTLCs in the local commit: this is a short-lived + // state during which the channel cannot be used for payments. when (liquidityPurchased) { + // Before introducing the liquidity purchase field, we serialized the local commit as an Either, with + // discriminators 0 and 1. null -> when (localCommit) { is Either.Left -> { - writeNumber(0) + write(0) writeUnsignedLocalCommitWithHtlcs(localCommit.value) } is Either.Right -> { - writeNumber(1) + write(1) writeLocalCommitWithHtlcs(localCommit.value) } } else -> when (localCommit) { is Either.Left -> { - writeNumber(2) + write(2) writeLiquidityPurchase(liquidityPurchased) writeUnsignedLocalCommitWithHtlcs(localCommit.value) } is Either.Right -> { - writeNumber(3) + write(3) writeLiquidityPurchase(liquidityPurchased) writeLocalCommitWithHtlcs(localCommit.value) } From 20b3b71a3cefb5cac6538904b148ac69b3818aa4 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 12 Dec 2023 15:01:59 +0100 Subject: [PATCH 09/15] Introduce a SpliceFees class --- .../fr/acinq/lightning/channel/ChannelCommand.kt | 6 ++++++ .../kotlin/fr/acinq/lightning/io/Peer.kt | 16 +++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt index 9578ec3af..d76694a25 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt @@ -92,6 +92,12 @@ sealed class ChannelCommand { data class SpliceOut(val amount: Satoshi, val scriptPubKey: ByteVector) } + /** + * @param miningFee on-chain fee that will be paid for the splice transaction. + * @param serviceFee service-fee that will be paid to the remote node for a service they provide with the splice transaction. + */ + data class Fees(val miningFee: Satoshi, val serviceFee: MilliSatoshi) + sealed class Response { /** * This response doesn't fully guarantee that the splice will confirm, because our peer may potentially double-spend diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index ae7de6b18..99d61c79b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -508,13 +508,14 @@ class Peer( * Estimate the actual feerate to use (and corresponding fee to pay) in order to reach the target feerate * for a splice out, taking into account potential unconfirmed parent splices. */ - suspend fun estimateFeeForSpliceOut(amount: Satoshi, scriptPubKey: ByteVector, targetFeerate: FeeratePerKw): Pair? { + suspend fun estimateFeeForSpliceOut(amount: Satoshi, scriptPubKey: ByteVector, targetFeerate: FeeratePerKw): Pair? { return channels.values .filterIsInstance() .firstOrNull { it.commitments.availableBalanceForSend() > amount } ?.let { channel -> val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = channel.commitments.active.first(), walletInputs = emptyList(), localOutputs = listOf(TxOut(amount, scriptPubKey))) - watcher.client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) + val (actualFeerate, miningFee) = watcher.client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) + Pair(actualFeerate, ChannelCommand.Commitment.Splice.Fees(miningFee, 0.msat)) } } @@ -526,13 +527,14 @@ class Peer( * NB: if the output feerate is equal to the input feerate then the cpfp is useless and * should not be attempted. */ - suspend fun estimateFeeForSpliceCpfp(channelId: ByteVector32, targetFeerate: FeeratePerKw): Pair? { + suspend fun estimateFeeForSpliceCpfp(channelId: ByteVector32, targetFeerate: FeeratePerKw): Pair? { return channels.values .filterIsInstance() .find { it.channelId == channelId } ?.let { channel -> val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = channel.commitments.active.first(), walletInputs = emptyList(), localOutputs = emptyList()) - watcher.client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) + val (actualFeerate, miningFee) = watcher.client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) + Pair(actualFeerate, ChannelCommand.Commitment.Splice.Fees(miningFee, 0.msat)) } } @@ -540,7 +542,7 @@ class Peer( * Estimate the actual feerate to use (and corresponding fee to pay) to purchase inbound liquidity with a splice * that reaches the target feerate. */ - suspend fun estimateFeeForInboundLiquidity(amount: Satoshi, targetFeerate: FeeratePerKw): Pair? { + suspend fun estimateFeeForInboundLiquidity(amount: Satoshi, targetFeerate: FeeratePerKw): Pair? { return channels.values .filterIsInstance() .firstOrNull() @@ -549,9 +551,9 @@ class Peer( // The mining fee below pays for the shared input and output of the splice transaction. val (actualFeerate, miningFee) = watcher.client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) val leaseRate = liquidityRatesFlow.filterNotNull().first { it.leaseDuration == 0 } - // The mining fee in the lease only covers the remote node's inputs and outputs. + // The mining fee in the lease covers the remote node's inputs and outputs, depending on the weight they're requesting. val leaseFees = leaseRate.fees(actualFeerate, amount, amount) - Pair(actualFeerate, leaseFees.copy(miningFee = leaseFees.miningFee + miningFee)) + Pair(actualFeerate, ChannelCommand.Commitment.Splice.Fees(miningFee + leaseFees.miningFee, leaseFees.serviceFee.toMilliSatoshi())) } } From 92eb6c2c5be7c82b26a97589da668ce377f97ec2 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 12 Dec 2023 17:16:12 +0100 Subject: [PATCH 10/15] Store liquidity purchases in `Normal` state We use a dedicated discriminator to allow replacing this data entirely in future versions without backwards-compatibility issues. --- .../states/LegacyWaitForFundingLocked.kt | 3 ++- .../acinq/lightning/channel/states/Normal.kt | 5 +++-- .../channel/states/WaitForChannelReady.kt | 3 ++- .../acinq/lightning/json/JsonSerializers.kt | 16 +++++++++++++- .../serialization/v2/ChannelState.kt | 3 ++- .../serialization/v3/ChannelState.kt | 3 ++- .../serialization/v4/Deserialization.kt | 10 +++++++++ .../serialization/v4/Serialization.kt | 5 +++++ .../StateSerializationTestsCommon.kt | 22 ++++++++++++++++++- .../nonreg/v2/Normal_748a735b/data.json | 4 +++- .../nonreg/v2/Normal_e2253ddd/data.json | 4 +++- .../nonreg/v2/Normal_ff248f8d/data.json | 4 +++- .../nonreg/v2/Normal_ff4a71b6/data.json | 4 +++- .../nonreg/v2/Normal_ffd9f5db/data.json | 4 +++- .../nonreg/v3/Normal_fd10d3cc/data.json | 4 +++- .../nonreg/v3/Normal_fe897b64/data.json | 4 +++- .../nonreg/v3/Normal_ff248f8d/data.json | 4 +++- .../nonreg/v3/Normal_ff4a71b6/data.json | 4 +++- 18 files changed, 89 insertions(+), 17 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/LegacyWaitForFundingLocked.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/LegacyWaitForFundingLocked.kt index 24feabf49..63f1e41f5 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/LegacyWaitForFundingLocked.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/LegacyWaitForFundingLocked.kt @@ -47,7 +47,8 @@ data class LegacyWaitForFundingLocked( null, null, null, - SpliceStatus.None + SpliceStatus.None, + listOf(), ) val actions = listOf( ChannelAction.Storage.StoreState(nextState), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 9db855617..bb84fb97e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -24,7 +24,8 @@ data class Normal( val localShutdown: Shutdown?, val remoteShutdown: Shutdown?, val closingFeerates: ClosingFeerates?, - val spliceStatus: SpliceStatus + val spliceStatus: SpliceStatus, + val liquidityPurchases: List, ) : ChannelStateWithCommitments() { override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) @@ -728,7 +729,7 @@ data class Normal( val fundingMinDepth = Helpers.minDepthForFunding(staticParams.nodeParams, action.fundingTx.fundingParams.fundingAmount) val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, action.commitment.commitInput.txOut.publicKeyScript, fundingMinDepth.toLong(), BITCOIN_FUNDING_DEPTHOK) val commitments = commitments.add(action.commitment).copy(remoteChannelData = remoteChannelData) - val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None) + val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None, liquidityPurchases = liquidityPurchases + listOfNotNull(liquidityPurchase)) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt index f4949101a..9df8f13f4 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt @@ -80,7 +80,8 @@ data class WaitForChannelReady( null, null, null, - SpliceStatus.None + SpliceStatus.None, + listOf(), ) val actions = listOf( ChannelAction.Storage.StoreState(nextState), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt b/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt index 068288b85..a9a9bd3d4 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt @@ -61,6 +61,9 @@ JsonSerializers.InteractiveTxSigningSessionSerializer::class, JsonSerializers.RbfStatusSerializer::class, JsonSerializers.SpliceStatusSerializer::class, + JsonSerializers.LiquidityLeaseFeesSerializer::class, + JsonSerializers.LiquidityLeaseWitnessSerializer::class, + JsonSerializers.LiquidityLeaseSerializer::class, JsonSerializers.ChannelParamsSerializer::class, JsonSerializers.ChannelOriginSerializer::class, JsonSerializers.CommitmentChangesSerializer::class, @@ -105,7 +108,9 @@ import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.json.JsonSerializers.LongSerializer import fr.acinq.lightning.json.JsonSerializers.StringSerializer import fr.acinq.lightning.json.JsonSerializers.SurrogateSerializer -import fr.acinq.lightning.transactions.* +import fr.acinq.lightning.transactions.CommitmentSpec +import fr.acinq.lightning.transactions.IncomingHtlc +import fr.acinq.lightning.transactions.OutgoingHtlc import fr.acinq.lightning.utils.Either import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.wire.* @@ -278,6 +283,15 @@ object JsonSerializers { object SpliceStatusSerializer : StringSerializer({ it::class.simpleName!! }) + @Serializer(forClass = LiquidityAds.LeaseFees::class) + object LiquidityLeaseFeesSerializer + + @Serializer(forClass = LiquidityAds.LeaseWitness::class) + object LiquidityLeaseWitnessSerializer + + @Serializer(forClass = LiquidityAds.Lease::class) + object LiquidityLeaseSerializer + @Serializer(forClass = ChannelParams::class) object ChannelParamsSerializer diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt index 686d35035..0236186c2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt @@ -510,7 +510,8 @@ internal data class Normal( localShutdown, remoteShutdown, null, - SpliceStatus.None + SpliceStatus.None, + listOf(), ) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt index 0a1121c66..dc7becec6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt @@ -512,7 +512,8 @@ internal data class Normal( localShutdown, remoteShutdown, closingFeerates?.export(), - SpliceStatus.None + SpliceStatus.None, + listOf(), ) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index ec4c6afcd..0dd162368 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -100,6 +100,16 @@ object Deserialization { origins = readCollection { readChannelOrigin() as Origin.PayToOpenOrigin }.toList() ) else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") + }, + liquidityPurchases = when { + availableBytes == 0 -> listOf() + else -> when (val discriminator = read()) { + 0x01 -> { + val liquidityPurchaseCount = readNumber() + (0 until liquidityPurchaseCount).map { readLiquidityPurchase() } + } + else -> error("unknown discriminator $discriminator for class ${Normal::class}") + } } ) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index 3fa978a92..c05ccb72c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -145,6 +145,11 @@ object Serialization { write(0x00) } } + if (liquidityPurchases.isNotEmpty()) { + write(0x01) + writeNumber(liquidityPurchases.size) + liquidityPurchases.forEach { writeLiquidityPurchase(it) } + } } private fun Output.writeShuttingDown(o: ShuttingDown) = o.run { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index 339ff2089..95ccac2aa 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -1,6 +1,9 @@ package fr.acinq.lightning.serialization +import fr.acinq.bitcoin.byteVector import fr.acinq.lightning.Feature +import fr.acinq.lightning.Lightning.randomBytes +import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.channel.* @@ -8,10 +11,13 @@ import fr.acinq.lightning.channel.states.Normal import fr.acinq.lightning.channel.states.PersistedChannelState import fr.acinq.lightning.serialization.Encryption.from import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.value import fr.acinq.lightning.wire.CommitSig import fr.acinq.lightning.wire.EncryptedChannelData import fr.acinq.lightning.wire.LightningMessage +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.secp256k1.Hex import kotlin.math.max import kotlin.test.* @@ -111,6 +117,13 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertNull(splice.session.liquidityPurchased) assertTrue(splice.session.localCommit.isLeft) assertContentEquals(bin, Serialization.serialize(state)) + assertTrue(state.liquidityPurchases.isEmpty()) + val state1 = state.copy( + liquidityPurchases = listOf( + LiquidityAds.Lease(50_000.sat, LiquidityAds.LeaseFees(1337.sat, 1329.sat), randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(23).byteVector(), 0, 850_000, 100, 1_000.msat)) + ) + ) + assertEquals(state1, Serialization.deserialize(Serialization.serialize(state1)).value) } run { val bin = Hex.decode( @@ -123,7 +136,14 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertNull(splice.session.liquidityPurchased) assertTrue(splice.session.localCommit.isRight) assertContentEquals(bin, Serialization.serialize(state)) + assertTrue(state.liquidityPurchases.isEmpty()) + val state1 = state.copy( + liquidityPurchases = listOf( + LiquidityAds.Lease(50_000.sat, LiquidityAds.LeaseFees(1337.sat, 1329.sat), randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(23).byteVector(), 0, 850_000, 100, 1_000.msat)), + LiquidityAds.Lease(37_000.sat, LiquidityAds.LeaseFees(2500.sat, 4001.sat), randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(23).byteVector(), 0, 900_000, 100, 1_000.msat)) + ) + ) + assertEquals(state1, Serialization.deserialize(Serialization.serialize(state1)).value) } - } } diff --git a/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json b/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json index ba3b527c2..200dc3473 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json @@ -186,5 +186,7 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None" + "spliceStatus": "None", + "liquidityPurchases": [ + ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json b/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json index 329b43d0a..013c1fd76 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json @@ -361,5 +361,7 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None" + "spliceStatus": "None", + "liquidityPurchases": [ + ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json b/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json index c47184a79..27a63bc3f 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json @@ -199,5 +199,7 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None" + "spliceStatus": "None", + "liquidityPurchases": [ + ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json b/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json index 43ba5c258..9e782c82e 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json @@ -369,5 +369,7 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None" + "spliceStatus": "None", + "liquidityPurchases": [ + ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json b/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json index 4509cfc83..5ef6ddfd4 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json @@ -218,5 +218,7 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None" + "spliceStatus": "None", + "liquidityPurchases": [ + ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json b/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json index 70a9ea4cd..d17e7fce6 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json @@ -259,5 +259,7 @@ }, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None" + "spliceStatus": "None", + "liquidityPurchases": [ + ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json b/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json index 19d128f35..278e1e18f 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json @@ -230,5 +230,7 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None" + "spliceStatus": "None", + "liquidityPurchases": [ + ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json b/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json index 30f347162..de7298c4f 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json @@ -197,5 +197,7 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None" + "spliceStatus": "None", + "liquidityPurchases": [ + ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json b/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json index b2cee2e2d..ab1436545 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json @@ -367,5 +367,7 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None" + "spliceStatus": "None", + "liquidityPurchases": [ + ] } \ No newline at end of file From 15e412ac1e9123ab37430710ffec481ccdee0bf7 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 12 Dec 2023 17:43:02 +0100 Subject: [PATCH 11/15] Rename liquidityPurchase -> liquidityLease --- .../acinq/lightning/channel/ChannelCommand.kt | 2 +- .../acinq/lightning/channel/InteractiveTx.kt | 10 +++---- .../acinq/lightning/channel/states/Normal.kt | 28 +++++++++---------- .../channel/states/WaitForFundingConfirmed.kt | 2 +- .../channel/states/WaitForFundingCreated.kt | 2 +- .../serialization/v4/Deserialization.kt | 20 ++++++------- .../serialization/v4/Serialization.kt | 14 +++++----- .../StateSerializationTestsCommon.kt | 12 ++++---- .../nonreg/v2/Normal_748a735b/data.json | 2 +- .../nonreg/v2/Normal_e2253ddd/data.json | 2 +- .../nonreg/v2/Normal_ff248f8d/data.json | 2 +- .../nonreg/v2/Normal_ff4a71b6/data.json | 2 +- .../nonreg/v2/Normal_ffd9f5db/data.json | 2 +- .../nonreg/v3/Normal_fd10d3cc/data.json | 2 +- .../nonreg/v3/Normal_fe897b64/data.json | 2 +- .../nonreg/v3/Normal_ff248f8d/data.json | 2 +- .../nonreg/v3/Normal_ff4a71b6/data.json | 2 +- 17 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt index d76694a25..9cbe825ae 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt @@ -109,7 +109,7 @@ sealed class ChannelCommand { val fundingTxId: TxId, val capacity: Satoshi, val balance: MilliSatoshi, - val liquidityPurchased: LiquidityAds.Lease?, + val liquidityLease: LiquidityAds.Lease?, ) : Response() sealed class Failure : Response() { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index a8c6f3a13..301f4aa7f 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -751,7 +751,7 @@ data class InteractiveTxSigningSession( val fundingParams: InteractiveTxParams, val fundingTxIndex: Long, val fundingTx: PartiallySignedSharedTransaction, - val liquidityPurchased: LiquidityAds.Lease?, + val liquidityLease: LiquidityAds.Lease?, val localCommit: Either, val remoteCommit: RemoteCommit, ) { @@ -827,7 +827,7 @@ data class InteractiveTxSigningSession( sharedTx: SharedTransaction, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, - liquidityPurchased: LiquidityAds.Lease?, + liquidityLease: LiquidityAds.Lease?, localCommitmentIndex: Long, remoteCommitmentIndex: Long, commitTxFeerate: FeeratePerKw, @@ -836,7 +836,7 @@ data class InteractiveTxSigningSession( val channelKeys = channelParams.localParams.channelKeys(keyManager) val unsignedTx = sharedTx.buildUnsignedTx() val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) } - val liquidityFees = liquidityPurchased?.fees?.total?.toMilliSatoshi() ?: 0.msat + val liquidityFees = liquidityLease?.fees?.total?.toMilliSatoshi() ?: 0.msat return Helpers.Funding.makeCommitTxsWithoutHtlcs( channelKeys, channelParams.channelId, @@ -872,7 +872,7 @@ data class InteractiveTxSigningSession( val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf()) val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint) val signedFundingTx = sharedTx.sign(keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) - Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, liquidityPurchased, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) + Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, liquidityLease, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) } } @@ -908,7 +908,7 @@ sealed class SpliceStatus { val spliceSession: InteractiveTxSession, val localPushAmount: MilliSatoshi, val remotePushAmount: MilliSatoshi, - val liquidityPurchased: LiquidityAds.Lease?, + val liquidityLease: LiquidityAds.Lease?, val origins: List ) : SpliceStatus() data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List) : SpliceStatus() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index bb84fb97e..10322fd74 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -25,7 +25,7 @@ data class Normal( val remoteShutdown: Shutdown?, val closingFeerates: ClosingFeerates?, val spliceStatus: SpliceStatus, - val liquidityPurchases: List, + val liquidityLeases: List, ) : ChannelStateWithCommitments() { override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) @@ -199,7 +199,7 @@ data class Normal( logger.info { "waiting for tx_sigs" } Pair(this@Normal.copy(spliceStatus = spliceStatus.copy(session = signingSession1)), listOf()) } - is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityPurchased, cmd.message.channelData) + is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityLease, cmd.message.channelData) } } ignoreRetransmittedCommitSig(cmd.message) -> { @@ -392,7 +392,7 @@ data class Normal( session, localPushAmount = 0.msat, remotePushAmount = cmd.message.pushAmount, - liquidityPurchased = null, + liquidityLease = null, origins = cmd.message.origins ) ) @@ -413,7 +413,7 @@ data class Normal( is SpliceAck -> when (spliceStatus) { is SpliceStatus.Requested -> { logger.info { "our peer accepted our splice request and will contribute ${cmd.message.fundingContribution} to the funding transaction" } - when (val liquidityPurchased = LiquidityAds.validateLease( + when (val liquidityLease = LiquidityAds.validateLease( spliceStatus.command.requestRemoteFunding, remoteNodeId, channelId, @@ -423,9 +423,9 @@ data class Normal( cmd.message.willFund, )) { is Either.Left -> { - logger.error { "rejecting liquidity proposal: ${liquidityPurchased.value.message}" } - spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds(liquidityPurchased.value)) - Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, liquidityPurchased.value.message)))) + logger.error { "rejecting liquidity proposal: ${liquidityLease.value.message}" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds(liquidityLease.value)) + Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, liquidityLease.value.message)))) } is Either.Right -> { val parentCommitment = commitments.active.first() @@ -474,7 +474,7 @@ data class Normal( interactiveTxSession, localPushAmount = spliceStatus.spliceInit.pushAmount, remotePushAmount = cmd.message.pushAmount, - liquidityPurchased = liquidityPurchased.value, + liquidityLease = liquidityLease.value, origins = spliceStatus.spliceInit.origins ) ) @@ -511,7 +511,7 @@ data class Normal( interactiveTxAction.sharedTx, localPushAmount = spliceStatus.localPushAmount, remotePushAmount = spliceStatus.remotePushAmount, - liquidityPurchased = spliceStatus.liquidityPurchased, + liquidityLease = spliceStatus.liquidityLease, localCommitmentIndex = parentCommitment.localCommit.index, remoteCommitmentIndex = parentCommitment.remoteCommit.index, parentCommitment.localCommit.spec.feerate, @@ -536,7 +536,7 @@ data class Normal( fundingTxId = session.fundingTx.txId, capacity = session.fundingParams.fundingAmount, balance = session.localCommit.fold({ it.spec }, { it.spec }).toLocal, - liquidityPurchased = spliceStatus.liquidityPurchased, + liquidityLease = spliceStatus.liquidityLease, ) ) val nextState = this@Normal.copy(spliceStatus = SpliceStatus.WaitingForSigs(session, spliceStatus.origins)) @@ -571,7 +571,7 @@ data class Normal( } is Either.Right -> { val action: InteractiveTxSigningSessionAction.SendTxSigs = res.value - sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityPurchased, cmd.message.channelData) + sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityLease, cmd.message.channelData) } } } @@ -721,7 +721,7 @@ data class Normal( private fun ChannelContext.sendSpliceTxSigs( origins: List, action: InteractiveTxSigningSessionAction.SendTxSigs, - liquidityPurchase: LiquidityAds.Lease?, + liquidityLease: LiquidityAds.Lease?, remoteChannelData: EncryptedChannelData ): Pair> { logger.info { "sending tx_sigs" } @@ -729,7 +729,7 @@ data class Normal( val fundingMinDepth = Helpers.minDepthForFunding(staticParams.nodeParams, action.fundingTx.fundingParams.fundingAmount) val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, action.commitment.commitInput.txOut.publicKeyScript, fundingMinDepth.toLong(), BITCOIN_FUNDING_DEPTHOK) val commitments = commitments.add(action.commitment).copy(remoteChannelData = remoteChannelData) - val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None, liquidityPurchases = liquidityPurchases + listOfNotNull(liquidityPurchase)) + val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None, liquidityLeases = liquidityLeases + listOfNotNull(liquidityLease)) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) } @@ -772,7 +772,7 @@ data class Normal( txId = action.fundingTx.txId ) ) - liquidityPurchase?.let { + liquidityLease?.let { add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, lease = it)) } if (staticParams.useZeroConf) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt index 6eee49c62..5730f9604 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -176,7 +176,7 @@ data class WaitForFundingConfirmed( interactiveTxAction.sharedTx, localPushAmount, remotePushAmount, - liquidityPurchased = null, + liquidityLease = null, localCommitmentIndex = replacedCommitment.localCommit.index, remoteCommitmentIndex = replacedCommitment.remoteCommit.index, replacedCommitment.localCommit.spec.feerate, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt index 5147083fb..7facc1db0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -62,7 +62,7 @@ data class WaitForFundingCreated( interactiveTxAction.sharedTx, localPushAmount, remotePushAmount, - liquidityPurchased = null, + liquidityLease = null, localCommitmentIndex = 0, remoteCommitmentIndex = 0, commitTxFeerate, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 0dd162368..8daba03d2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -101,12 +101,12 @@ object Deserialization { ) else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") }, - liquidityPurchases = when { + liquidityLeases = when { availableBytes == 0 -> listOf() else -> when (val discriminator = read()) { 0x01 -> { - val liquidityPurchaseCount = readNumber() - (0 until liquidityPurchaseCount).map { readLiquidityPurchase() } + val leaseCount = readNumber() + (0 until leaseCount).map { readLiquidityLease() } } else -> error("unknown discriminator $discriminator for class ${Normal::class}") } @@ -340,7 +340,7 @@ object Deserialization { ) ) - private fun Input.readLiquidityPurchase(): LiquidityAds.Lease = LiquidityAds.Lease( + private fun Input.readLiquidityLease(): LiquidityAds.Lease = LiquidityAds.Lease( amount = readNumber().sat, fees = LiquidityAds.LeaseFees(miningFee = readNumber().sat, serviceFee = readNumber().sat), sellerSig = readByteVector64(), @@ -357,13 +357,13 @@ object Deserialization { val fundingParams = readInteractiveTxParams() val fundingTxIndex = readNumber() val fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction - // liquidityPurchase and localCommit are logically independent, this is just a serialization trick for backwards - // compatibility since the liquidityPurchase field was introduced later. - val (liquidityPurchase, localCommit) = when (val discriminator = read()) { + // liquidityLease and localCommit are logically independent, this is just a serialization trick for backwards + // compatibility since the liquidityLease field was introduced later. + val (liquidityLease, localCommit) = when (val discriminator = read()) { 0 -> Pair(null, Either.Left(readUnsignedLocalCommitWithHtlcs())) 1 -> Pair(null, Either.Right(readLocalCommitWithHtlcs())) - 2 -> Pair(readLiquidityPurchase(), Either.Left(readUnsignedLocalCommitWithHtlcs())) - 3 -> Pair(readLiquidityPurchase(), Either.Right(readLocalCommitWithHtlcs())) + 2 -> Pair(readLiquidityLease(), Either.Left(readUnsignedLocalCommitWithHtlcs())) + 3 -> Pair(readLiquidityLease(), Either.Right(readLocalCommitWithHtlcs())) else -> error("unknown discriminator $discriminator for class ${InteractiveTxSigningSession::class}") } val remoteCommit = RemoteCommit( @@ -372,7 +372,7 @@ object Deserialization { txid = readTxId(), remotePerCommitmentPoint = readPublicKey() ) - return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, liquidityPurchase, localCommit, remoteCommit) + return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, liquidityLease, localCommit, remoteCommit) } private fun Input.readChannelOrigin(): Origin = when (val discriminator = read()) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index c05ccb72c..8885e0127 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -145,10 +145,10 @@ object Serialization { write(0x00) } } - if (liquidityPurchases.isNotEmpty()) { + if (liquidityLeases.isNotEmpty()) { write(0x01) - writeNumber(liquidityPurchases.size) - liquidityPurchases.forEach { writeLiquidityPurchase(it) } + writeNumber(liquidityLeases.size) + liquidityLeases.forEach { writeLiquidityLease(it) } } } @@ -382,7 +382,7 @@ object Serialization { } } - private fun Output.writeLiquidityPurchase(lease: LiquidityAds.Lease) { + private fun Output.writeLiquidityLease(lease: LiquidityAds.Lease) { writeNumber(lease.amount.toLong()) writeNumber(lease.fees.miningFee.toLong()) writeNumber(lease.fees.serviceFee.toLong()) @@ -403,7 +403,7 @@ object Serialization { // we previously used for the local commit to insert the liquidity purchase if available. // Note that we don't bother removing the duplication across HTLCs in the local commit: this is a short-lived // state during which the channel cannot be used for payments. - when (liquidityPurchased) { + when (liquidityLease) { // Before introducing the liquidity purchase field, we serialized the local commit as an Either, with // discriminators 0 and 1. null -> when (localCommit) { @@ -419,12 +419,12 @@ object Serialization { else -> when (localCommit) { is Either.Left -> { write(2) - writeLiquidityPurchase(liquidityPurchased) + writeLiquidityLease(liquidityLease) writeUnsignedLocalCommitWithHtlcs(localCommit.value) } is Either.Right -> { write(3) - writeLiquidityPurchase(liquidityPurchased) + writeLiquidityLease(liquidityLease) writeLocalCommitWithHtlcs(localCommit.value) } } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index 95ccac2aa..3c83897bb 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -114,12 +114,12 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertIs(state) val splice = state.spliceStatus assertIs(splice) - assertNull(splice.session.liquidityPurchased) + assertNull(splice.session.liquidityLease) assertTrue(splice.session.localCommit.isLeft) assertContentEquals(bin, Serialization.serialize(state)) - assertTrue(state.liquidityPurchases.isEmpty()) + assertTrue(state.liquidityLeases.isEmpty()) val state1 = state.copy( - liquidityPurchases = listOf( + liquidityLeases = listOf( LiquidityAds.Lease(50_000.sat, LiquidityAds.LeaseFees(1337.sat, 1329.sat), randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(23).byteVector(), 0, 850_000, 100, 1_000.msat)) ) ) @@ -133,12 +133,12 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertIs(state) val splice = state.spliceStatus assertIs(splice) - assertNull(splice.session.liquidityPurchased) + assertNull(splice.session.liquidityLease) assertTrue(splice.session.localCommit.isRight) assertContentEquals(bin, Serialization.serialize(state)) - assertTrue(state.liquidityPurchases.isEmpty()) + assertTrue(state.liquidityLeases.isEmpty()) val state1 = state.copy( - liquidityPurchases = listOf( + liquidityLeases = listOf( LiquidityAds.Lease(50_000.sat, LiquidityAds.LeaseFees(1337.sat, 1329.sat), randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(23).byteVector(), 0, 850_000, 100, 1_000.msat)), LiquidityAds.Lease(37_000.sat, LiquidityAds.LeaseFees(2500.sat, 4001.sat), randomBytes64(), LiquidityAds.LeaseWitness(randomBytes(23).byteVector(), 0, 900_000, 100, 1_000.msat)) ) diff --git a/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json b/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json index 200dc3473..1972b21d2 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json @@ -187,6 +187,6 @@ "remoteShutdown": null, "closingFeerates": null, "spliceStatus": "None", - "liquidityPurchases": [ + "liquidityLeases": [ ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json b/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json index 013c1fd76..f5e6d6451 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json @@ -362,6 +362,6 @@ "remoteShutdown": null, "closingFeerates": null, "spliceStatus": "None", - "liquidityPurchases": [ + "liquidityLeases": [ ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json b/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json index 27a63bc3f..85e46b5a8 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json @@ -200,6 +200,6 @@ "remoteShutdown": null, "closingFeerates": null, "spliceStatus": "None", - "liquidityPurchases": [ + "liquidityLeases": [ ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json b/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json index 9e782c82e..fdf1580ff 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json @@ -370,6 +370,6 @@ "remoteShutdown": null, "closingFeerates": null, "spliceStatus": "None", - "liquidityPurchases": [ + "liquidityLeases": [ ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json b/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json index 5ef6ddfd4..77e09eece 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json @@ -219,6 +219,6 @@ "remoteShutdown": null, "closingFeerates": null, "spliceStatus": "None", - "liquidityPurchases": [ + "liquidityLeases": [ ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json b/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json index d17e7fce6..60ae64d06 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json @@ -260,6 +260,6 @@ "remoteShutdown": null, "closingFeerates": null, "spliceStatus": "None", - "liquidityPurchases": [ + "liquidityLeases": [ ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json b/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json index 278e1e18f..4add406ad 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json @@ -231,6 +231,6 @@ "remoteShutdown": null, "closingFeerates": null, "spliceStatus": "None", - "liquidityPurchases": [ + "liquidityLeases": [ ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json b/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json index de7298c4f..4ab6752e6 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json @@ -198,6 +198,6 @@ "remoteShutdown": null, "closingFeerates": null, "spliceStatus": "None", - "liquidityPurchases": [ + "liquidityLeases": [ ] } \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json b/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json index ab1436545..43df4d86f 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json @@ -368,6 +368,6 @@ "remoteShutdown": null, "closingFeerates": null, "spliceStatus": "None", - "liquidityPurchases": [ + "liquidityLeases": [ ] } \ No newline at end of file From f735f45eee0b765ed04563ecfad0f5a0b061a774 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 12 Dec 2023 18:51:18 +0100 Subject: [PATCH 12/15] fixup! Store liquidity purchases in `Normal` state --- .../fr/acinq/lightning/serialization/v4/Deserialization.kt | 5 +---- .../fr/acinq/lightning/serialization/v4/Serialization.kt | 7 ++----- .../serialization/StateSerializationTestsCommon.kt | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 8daba03d2..c825933f0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -104,10 +104,7 @@ object Deserialization { liquidityLeases = when { availableBytes == 0 -> listOf() else -> when (val discriminator = read()) { - 0x01 -> { - val leaseCount = readNumber() - (0 until leaseCount).map { readLiquidityLease() } - } + 0x01 -> readCollection { readLiquidityLease() }.toList() else -> error("unknown discriminator $discriminator for class ${Normal::class}") } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index 8885e0127..eede7f3cc 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -145,11 +145,8 @@ object Serialization { write(0x00) } } - if (liquidityLeases.isNotEmpty()) { - write(0x01) - writeNumber(liquidityLeases.size) - liquidityLeases.forEach { writeLiquidityLease(it) } - } + write(0x01) + writeCollection(liquidityLeases) { writeLiquidityLease(it) } } private fun Output.writeShuttingDown(o: ShuttingDown) = o.run { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index 3c83897bb..4dace9e8d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -116,7 +116,7 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertIs(splice) assertNull(splice.session.liquidityLease) assertTrue(splice.session.localCommit.isLeft) - assertContentEquals(bin, Serialization.serialize(state)) + assertContentEquals(bin, Serialization.serialize(state).dropLast(2).toByteArray()) // we add a discriminator byte and the liquidity lease count (0x0100) assertTrue(state.liquidityLeases.isEmpty()) val state1 = state.copy( liquidityLeases = listOf( @@ -135,7 +135,7 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertIs(splice) assertNull(splice.session.liquidityLease) assertTrue(splice.session.localCommit.isRight) - assertContentEquals(bin, Serialization.serialize(state)) + assertContentEquals(bin, Serialization.serialize(state).dropLast(2).toByteArray()) // we add a discriminator byte and the liquidity lease count (0x0100) assertTrue(state.liquidityLeases.isEmpty()) val state1 = state.copy( liquidityLeases = listOf( From df15490d105fc02d4e2fc9fa3ace49cfa409469e Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 13 Dec 2023 08:47:35 +0100 Subject: [PATCH 13/15] Fix `miningFees` in payments DB --- .../kotlin/fr/acinq/lightning/channel/ChannelAction.kt | 4 ++-- .../kotlin/fr/acinq/lightning/channel/states/Normal.kt | 2 +- src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt | 7 ++++--- src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt | 1 + 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index 07e0dfccb..c76104068 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -87,8 +87,8 @@ sealed class ChannelAction { 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 ViaInboundLiquidityRequest(override val txId: TxId, val lease: LiquidityAds.Lease) : StoreOutgoingPayment() { - override val miningFees: Satoshi = lease.fees.miningFee + data class ViaInboundLiquidityRequest(override val txId: TxId, val spliceFees: Satoshi, val lease: LiquidityAds.Lease) : StoreOutgoingPayment() { + override val miningFees: Satoshi = spliceFees + lease.fees.miningFee } data class ViaClose(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId, val isSentToDefaultAddress: Boolean, val closingType: ChannelClosingType) : StoreOutgoingPayment() } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 10322fd74..0d30fd98a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -773,7 +773,7 @@ data class Normal( ) ) liquidityLease?.let { - add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, lease = it)) + add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, spliceFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), lease = it)) } if (staticParams.useZeroConf) { logger.info { "channel is using 0-conf, sending splice_locked right away" } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt index 550da51ec..947b0e7eb 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt @@ -392,14 +392,15 @@ data class InboundLiquidityOutgoingPayment( override val id: UUID, override val channelId: ByteVector32, override val txId: TxId, + val spliceFees: Satoshi, val lease: LiquidityAds.Lease, override val createdAt: Long, override val confirmedAt: Long?, override val lockedAt: Long?, ) : OnChainOutgoingPayment() { - override val amount: MilliSatoshi = lease.fees.total.toMilliSatoshi() - override val miningFees: Satoshi = lease.fees.miningFee - override val fees: MilliSatoshi = lease.fees.total.toMilliSatoshi() + override val miningFees: Satoshi = spliceFees + lease.fees.miningFee + override val fees: MilliSatoshi = (miningFees + lease.fees.serviceFee).toMilliSatoshi() + override val amount: MilliSatoshi = fees override val completedAt: Long? = confirmedAt } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 99d61c79b..c71398f17 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -745,6 +745,7 @@ class Peer( id = UUID.randomUUID(), channelId = channelId, txId = action.txId, + spliceFees = action.spliceFees, lease = action.lease, createdAt = currentTimestampMillis(), confirmedAt = null, From b77df8cfcc9a7446eb0c203ceffef622c9648c1f Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 13 Dec 2023 11:10:18 +0100 Subject: [PATCH 14/15] fixup! Fix `miningFees` in payments DB --- .../acinq/lightning/channel/ChannelAction.kt | 4 +--- .../acinq/lightning/channel/states/Normal.kt | 22 +++++++++---------- .../fr/acinq/lightning/db/PaymentsDb.kt | 3 +-- .../kotlin/fr/acinq/lightning/io/Peer.kt | 2 +- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index c76104068..7a2689776 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -87,9 +87,7 @@ sealed class ChannelAction { 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 ViaInboundLiquidityRequest(override val txId: TxId, val spliceFees: Satoshi, val lease: LiquidityAds.Lease) : StoreOutgoingPayment() { - override val miningFees: Satoshi = spliceFees + lease.fees.miningFee - } + data class ViaInboundLiquidityRequest(override val txId: TxId, override val miningFees: Satoshi, val lease: LiquidityAds.Lease) : 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: TxId) : Storage() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 0d30fd98a..ef8f10e83 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -749,9 +749,9 @@ data class Normal( // If we added some funds ourselves it's a swap-in if (action.fundingTx.sharedTx.tx.localInputs.isNotEmpty()) add( ChannelAction.Storage.StoreIncomingPayment.ViaSpliceIn( - amount = action.fundingTx.sharedTx.tx.localInputs.map { i -> i.txOut.amount }.sum().toMilliSatoshi() - action.fundingTx.sharedTx.tx.fees.toMilliSatoshi(), + amount = action.fundingTx.sharedTx.tx.localInputs.map { i -> i.txOut.amount }.sum().toMilliSatoshi() - action.fundingTx.sharedTx.tx.localFees, serviceFee = 0.msat, - miningFee = action.fundingTx.sharedTx.tx.fees, + miningFee = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), localInputs = action.fundingTx.sharedTx.tx.localInputs.map { it.outPoint }.toSet(), txId = action.fundingTx.txId, origin = null @@ -760,20 +760,20 @@ data class Normal( addAll(action.fundingTx.fundingParams.localOutputs.map { txOut -> ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceOut( amount = txOut.amount, - miningFees = action.fundingTx.sharedTx.tx.fees, + miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), address = Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, txOut.publicKeyScript.toByteArray()).result ?: "unknown", txId = action.fundingTx.txId ) }) // If we initiated the splice but there are no new inputs on either side and no new output on our side, it's a cpfp - if (action.fundingTx.fundingParams.isInitiator && action.fundingTx.sharedTx.tx.localInputs.isEmpty() && action.fundingTx.sharedTx.tx.remoteInputs.isEmpty() && action.fundingTx.fundingParams.localOutputs.isEmpty()) add( - ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceCpfp( - miningFees = action.fundingTx.sharedTx.tx.fees, - txId = action.fundingTx.txId - ) - ) - liquidityLease?.let { - add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, spliceFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), lease = it)) + if (action.fundingTx.fundingParams.isInitiator && action.fundingTx.sharedTx.tx.localInputs.isEmpty() && action.fundingTx.sharedTx.tx.remoteInputs.isEmpty() && action.fundingTx.fundingParams.localOutputs.isEmpty()) { + add(ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceCpfp(miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), txId = action.fundingTx.txId)) + } + liquidityLease?.let { lease -> + // The actual mining fees contain the inputs and outputs we paid for in the interactive-tx transaction, + // and what we refunded the remote peer for some of their inputs and outputs via the lease. + val miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi() + lease.fees.miningFee + add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, miningFees = miningFees, lease = lease)) } if (staticParams.useZeroConf) { logger.info { "channel is using 0-conf, sending splice_locked right away" } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt index 947b0e7eb..cccd4cd02 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt @@ -392,13 +392,12 @@ data class InboundLiquidityOutgoingPayment( override val id: UUID, override val channelId: ByteVector32, override val txId: TxId, - val spliceFees: Satoshi, + override val miningFees: Satoshi, val lease: LiquidityAds.Lease, override val createdAt: Long, override val confirmedAt: Long?, override val lockedAt: Long?, ) : OnChainOutgoingPayment() { - override val miningFees: Satoshi = spliceFees + lease.fees.miningFee override val fees: MilliSatoshi = (miningFees + lease.fees.serviceFee).toMilliSatoshi() override val amount: MilliSatoshi = fees override val completedAt: Long? = confirmedAt diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index c71398f17..68d4060d8 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -745,7 +745,7 @@ class Peer( id = UUID.randomUUID(), channelId = channelId, txId = action.txId, - spliceFees = action.spliceFees, + miningFees = action.miningFees, lease = action.lease, createdAt = currentTimestampMillis(), confirmedAt = null, From 8d0c92a1dcbac51b3ad0be20f6463ec2a650d627 Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 13 Dec 2023 11:43:24 +0100 Subject: [PATCH 15/15] Add lease starting height --- src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt index d8ea946a1..03682d10b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt @@ -133,6 +133,7 @@ object LiquidityAds { * routing fees above the values they signed up for. */ data class Lease(val amount: Satoshi, val fees: LeaseFees, val sellerSig: ByteVector64, val witness: LeaseWitness) { + val start: Int = witness.leaseEnd - witness.leaseDuration val expiry: Int = witness.leaseEnd }