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 18be1e559..71327113e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -163,6 +163,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> { @@ -570,17 +571,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)) @@ -840,10 +847,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) }