From 1a271dc0e44cfcfc7b76e36fed7683a313083da5 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 24 May 2024 15:20:12 +0200 Subject: [PATCH] Add liquidity ads to the channel opening flow We previously only used liquidity ads with splicing: we now support it during the initial channel opening flow as well. This lets us add more unit tests, including tests for the case where the node receiving the `open_channel` message is responsible for paying the commitment fees. We also update liquidity ads to use the latest version of the spec from https://github.com/lightning/bolts/pull/1153. This introduces more ways of paying the liquidity fees, to support on-the-fly funding without existing channel balance (not implemented in this commit). Note that we need some backwards-compatibility with the previous liquidity ads types in our state serialization code: when we're in the middle of signing a splice transaction, we may have a legacy liquidity lease in our splice status. We ignore it when finalizing the splice: the only consequence is that we won't store an entry in our DB for that lease, but the channel will otherwise work correctly. --- .../kotlin/fr/acinq/lightning/NodeParams.kt | 5 +- .../acinq/lightning/channel/ChannelAction.kt | 2 +- .../acinq/lightning/channel/ChannelCommand.kt | 9 +- .../lightning/channel/ChannelException.kt | 1 - .../acinq/lightning/channel/InteractiveTx.kt | 25 +- .../states/LegacyWaitForFundingLocked.kt | 1 - .../acinq/lightning/channel/states/Normal.kt | 54 +-- .../channel/states/WaitForAcceptChannel.kt | 89 +++-- .../channel/states/WaitForChannelReady.kt | 1 - .../channel/states/WaitForFundingConfirmed.kt | 2 +- .../channel/states/WaitForFundingCreated.kt | 4 +- .../channel/states/WaitForFundingSigned.kt | 9 + .../lightning/channel/states/WaitForInit.kt | 5 +- .../channel/states/WaitForOpenChannel.kt | 19 +- .../fr/acinq/lightning/db/PaymentsDb.kt | 4 +- .../kotlin/fr/acinq/lightning/io/Peer.kt | 27 +- .../acinq/lightning/json/JsonSerializers.kt | 18 +- .../serialization/v2/ChannelState.kt | 1 - .../serialization/v3/ChannelState.kt | 1 - .../serialization/v4/Deserialization.kt | 101 ++++-- .../serialization/v4/Serialization.kt | 76 ++-- .../fr/acinq/lightning/wire/ChannelTlv.kt | 52 +-- .../kotlin/fr/acinq/lightning/wire/InitTlv.kt | 19 +- .../acinq/lightning/wire/LightningMessages.kt | 42 ++- .../fr/acinq/lightning/wire/LiquidityAds.kt | 324 +++++++++++++----- .../channel/InteractiveTxTestsCommon.kt | 12 +- .../fr/acinq/lightning/channel/TestsHelper.kt | 16 +- .../channel/states/ClosingTestsCommon.kt | 31 ++ .../channel/states/NegotiatingTestsCommon.kt | 36 ++ .../channel/states/NormalTestsCommon.kt | 37 ++ .../channel/states/ShutdownTestsCommon.kt | 19 + .../channel/states/SpliceTestsCommon.kt | 52 ++- .../states/WaitForAcceptChannelTestsCommon.kt | 53 ++- .../states/WaitForChannelReadyTestsCommon.kt | 5 +- .../WaitForFundingConfirmedTestsCommon.kt | 2 + .../WaitForFundingCreatedTestsCommon.kt | 3 +- .../states/WaitForFundingSignedTestsCommon.kt | 117 +++++-- .../crypto/LocalKeyManagerTestsCommon.kt | 8 +- .../fr/acinq/lightning/io/peer/PeerTest.kt | 2 +- .../OutgoingPaymentHandlerTestsCommon.kt | 2 +- .../StateSerializationTestsCommon.kt | 37 +- .../fr/acinq/lightning/tests/TestConstants.kt | 26 +- .../wire/LightningCodecsTestsCommon.kt | 129 ++++--- .../lightning/wire/LiquidityAdsTestsCommon.kt | 56 +++ .../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 +- 53 files changed, 1054 insertions(+), 516 deletions(-) create mode 100644 src/commonTest/kotlin/fr/acinq/lightning/wire/LiquidityAdsTestsCommon.kt diff --git a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt index ac4f9d547..24536f4fa 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -70,14 +70,15 @@ object DefaultSwapInParams { * @param trampolineFees ordered list of trampoline fees to try when making an outgoing payment. * @param invoiceDefaultRoutingFees default routing fees set in invoices when we don't have any channel. * @param swapInParams parameters for swap-in transactions. - * @param leaseRate rate at which our peer sells their liquidity. + * @param remoteFundingRates rates at which our peer sells their liquidity. */ data class WalletParams( val trampolineNode: NodeUri, val trampolineFees: List, val invoiceDefaultRoutingFees: InvoiceDefaultRoutingFees, val swapInParams: SwapInParams, - val leaseRate: LiquidityAds.LeaseRate, + // TODO: once standardized, we should get this data from our peer's init message. + val remoteFundingRates: LiquidityAds.WillFundRates, ) /** diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index 86d469615..d2f919eeb 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -87,7 +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, override val miningFees: Satoshi, val lease: LiquidityAds.Lease) : StoreOutgoingPayment() + data class ViaInboundLiquidityRequest(override val txId: TxId, override val miningFees: Satoshi, val purchase: LiquidityAds.Purchase) : 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/ChannelCommand.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt index a8ed4a5e9..25e2a90e3 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt @@ -35,7 +35,7 @@ sealed class ChannelCommand { val channelFlags: ChannelFlags, val channelConfig: ChannelConfig, val channelType: ChannelType.SupportedChannelType, - val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, + val requestRemoteFunding: LiquidityAds.RequestFunding?, val channelOrigin: Origin?, ) : Init() { fun temporaryChannelId(keyManager: KeyManager): ByteVector32 = keyManager.channelKeys(localParams.fundingKeyPath).temporaryChannelId @@ -48,7 +48,8 @@ sealed class ChannelCommand { val walletInputs: List, val localParams: LocalParams, val channelConfig: ChannelConfig, - val remoteInit: InitMessage + val remoteInit: InitMessage, + val fundingRates: LiquidityAds.WillFundRates? ) : Init() data class Restore(val state: PersistedChannelState) : Init() @@ -86,7 +87,7 @@ sealed class ChannelCommand { data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice, ForbiddenDuringQuiescence data object CheckHtlcTimeout : Commitment() sealed class Splice : Commitment() { - data class Request(val replyTo: CompletableDeferred, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, val feerate: FeeratePerKw, val origins: List) : Splice() { + data class Request(val replyTo: CompletableDeferred, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestFunding?, val feerate: FeeratePerKw, val origins: List) : Splice() { val pushAmount: MilliSatoshi = spliceIn?.pushAmount ?: 0.msat val spliceOutputs: List = spliceOut?.let { listOf(TxOut(it.amount, it.scriptPubKey)) } ?: emptyList() @@ -105,7 +106,7 @@ sealed class ChannelCommand { val fundingTxId: TxId, val capacity: Satoshi, val balance: MilliSatoshi, - val liquidityLease: LiquidityAds.Lease?, + val liquidityPurchase: LiquidityAds.Purchase?, ) : Response() sealed class Failure : Response() { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt index d6f3f1724..49fc17312 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt @@ -28,7 +28,6 @@ data class ToSelfDelayTooHigh (override val channelId: Byte 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 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/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 97a374cac..3e8e09806 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -673,8 +673,7 @@ data class InteractiveTxSession( val isComplete: Boolean = txCompleteSent != null && txCompleteReceived != null fun send(): Pair { - val msg = toSend.firstOrNull() - return when (msg) { + return when (val msg = toSend.firstOrNull()) { null -> { val localSwapIns = localInputs.filterIsInstance() val remoteSwapIns = remoteInputs.filterIsInstance() @@ -987,7 +986,6 @@ data class InteractiveTxSigningSession( val fundingParams: InteractiveTxParams, val fundingTxIndex: Long, val fundingTx: PartiallySignedSharedTransaction, - val liquidityLease: LiquidityAds.Lease?, val localCommit: Either, val remoteCommit: RemoteCommit, ) { @@ -1065,7 +1063,7 @@ data class InteractiveTxSigningSession( sharedTx: SharedTransaction, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, - liquidityLease: LiquidityAds.Lease?, + liquidityPurchase: LiquidityAds.Purchase?, localCommitmentIndex: Long, remoteCommitmentIndex: Long, commitTxFeerate: FeeratePerKw, @@ -1075,7 +1073,16 @@ 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 = liquidityLease?.fees?.total?.toMilliSatoshi() ?: 0.msat + val liquidityFees = liquidityPurchase?.let { l -> + val fees = l.fees.total.toMilliSatoshi() + when (l.paymentDetails) { + is LiquidityAds.PaymentDetails.FromChannelBalance -> if (fundingParams.isInitiator) fees else -fees + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> if (fundingParams.isInitiator) fees else -fees + // Fees will be paid later, from relayed HTLCs. + is LiquidityAds.PaymentDetails.FromFutureHtlc -> 0.msat + is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> 0.msat + } + } ?: 0.msat return Helpers.Funding.makeCommitTxs( channelKeys, channelParams.channelId, @@ -1120,7 +1127,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(session, keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) - Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, liquidityLease, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) + Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) } } @@ -1168,7 +1175,7 @@ sealed class SpliceStatus { /** Our peer has asked us to stop sending new updates and wait for our updates to be added to the local and remote commitments. */ data class ReceivedStfu(val stfu: Stfu) : QuiescenceNegotiation.NonInitiator() /** Our updates have been added to the local and remote commitments, we wait for our peer to use the now quiescent channel. */ - object NonInitiatorQuiescent : QuiescentSpliceStatus() + data object NonInitiatorQuiescent : QuiescentSpliceStatus() /** We told our peer we want to splice funds in the channel. */ data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : QuiescentSpliceStatus() /** We both agreed to splice and are building the splice transaction. */ @@ -1177,11 +1184,11 @@ sealed class SpliceStatus { val spliceSession: InteractiveTxSession, val localPushAmount: MilliSatoshi, val remotePushAmount: MilliSatoshi, - val liquidityLease: LiquidityAds.Lease?, + val liquidityPurchase: LiquidityAds.Purchase?, val origins: List ) : QuiescentSpliceStatus() /** The splice transaction has been negotiated, we're exchanging signatures. */ - data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List) : QuiescentSpliceStatus() + data class WaitingForSigs(val session: InteractiveTxSigningSession, val liquidityPurchase: LiquidityAds.Purchase?, val origins: List) : QuiescentSpliceStatus() /** The splice attempt was aborted by us, we're waiting for our peer to ack. */ data object Aborted : QuiescentSpliceStatus() } 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 338300031..d1963c119 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/LegacyWaitForFundingLocked.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/LegacyWaitForFundingLocked.kt @@ -48,7 +48,6 @@ data class LegacyWaitForFundingLocked( null, null, 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 6c847588b..83ff59a0a 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,6 @@ data class Normal( val remoteShutdown: Shutdown?, val closingFeerates: ClosingFeerates?, val spliceStatus: SpliceStatus, - val liquidityLeases: List, ) : ChannelStateWithCommitments() { override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) @@ -179,7 +178,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.liquidityLease, cmd.message.channelData) + is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityPurchase, cmd.message.channelData) } } ignoreRetransmittedCommitSig(cmd.message) -> { @@ -406,8 +405,8 @@ data class Normal( add(ChannelAction.Disconnect) } Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions) - } else if (spliceStatus.command.requestRemoteFunding?.let { r -> r.rate.fees(spliceStatus.command.feerate, r.fundingAmount, r.fundingAmount).total <= parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } == false) { - val missing = spliceStatus.command.requestRemoteFunding.let { r -> r.rate.fees(spliceStatus.command.feerate, r.fundingAmount, r.fundingAmount).total - parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } + } else if (!canAffordSpliceLiquidityFees(spliceStatus.command, parentCommitment)) { + val missing = spliceStatus.command.requestRemoteFunding?.let { r -> r.fees(spliceStatus.command.feerate).total - parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } logger.warning { "cannot do splice: balance is too low to pay for inbound liquidity (missing=$missing)" } spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds) Pair(this@Normal, emptyList()) @@ -419,7 +418,7 @@ data class Normal( feerate = spliceStatus.command.feerate, fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), pushAmount = spliceStatus.command.pushAmount, - requestFunds = spliceStatus.command.requestRemoteFunding?.requestFunds, + requestFunding = spliceStatus.command.requestRemoteFunding, ) logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution} local.push=${spliceInit.pushAmount}" } Pair(this@Normal.copy(spliceStatus = SpliceStatus.Requested(spliceStatus.command, spliceInit)), listOf(ChannelAction.Message.Send(spliceInit))) @@ -490,7 +489,7 @@ data class Normal( session, localPushAmount = 0.msat, remotePushAmount = cmd.message.pushAmount, - liquidityLease = null, + liquidityPurchase = null, origins = listOf() ) ) @@ -511,7 +510,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 liquidityLease = LiquidityAds.validateLease( + when (val liquidityPurchase = LiquidityAds.validateRemoteFunding( spliceStatus.command.requestRemoteFunding, remoteNodeId, channelId, @@ -521,11 +520,11 @@ data class Normal( cmd.message.willFund, )) { is Either.Left -> { - 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)))) + logger.error { "rejecting liquidity proposal: ${liquidityPurchase.value.message}" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds(liquidityPurchase.value)) + Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, liquidityPurchase.value.message)))) } - is Either.Right -> { + is Either.Right -> { val parentCommitment = commitments.active.first() val sharedInput = SharedFundingInput.Multisig2of2(parentCommitment) val fundingParams = InteractiveTxParams( @@ -575,7 +574,7 @@ data class Normal( interactiveTxSession, localPushAmount = spliceStatus.spliceInit.pushAmount, remotePushAmount = cmd.message.pushAmount, - liquidityLease = liquidityLease.value, + liquidityPurchase = liquidityPurchase.value, origins = spliceStatus.command.origins, ) ) @@ -613,7 +612,7 @@ data class Normal( interactiveTxAction.sharedTx, localPushAmount = spliceStatus.localPushAmount, remotePushAmount = spliceStatus.remotePushAmount, - liquidityLease = spliceStatus.liquidityLease, + liquidityPurchase = spliceStatus.liquidityPurchase, localCommitmentIndex = parentCommitment.localCommit.index, remoteCommitmentIndex = parentCommitment.remoteCommit.index, parentCommitment.localCommit.spec.feerate, @@ -639,10 +638,10 @@ data class Normal( fundingTxId = session.fundingTx.txId, capacity = session.fundingParams.fundingAmount, balance = session.localCommit.fold({ it.spec }, { it.spec }).toLocal, - liquidityLease = spliceStatus.liquidityLease, + liquidityPurchase = spliceStatus.liquidityPurchase, ) ) - val nextState = this@Normal.copy(spliceStatus = SpliceStatus.WaitingForSigs(session, spliceStatus.origins)) + val nextState = this@Normal.copy(spliceStatus = SpliceStatus.WaitingForSigs(session, spliceStatus.liquidityPurchase, spliceStatus.origins)) val actions = buildList { interactiveTxAction.txComplete?.let { add(ChannelAction.Message.Send(it)) } add(ChannelAction.Storage.StoreState(nextState)) @@ -674,7 +673,7 @@ data class Normal( } is Either.Right -> { val action: InteractiveTxSigningSessionAction.SendTxSigs = res.value - sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityLease, cmd.message.channelData) + sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityPurchase, cmd.message.channelData) } } } @@ -840,10 +839,23 @@ data class Normal( } } + private fun canAffordSpliceLiquidityFees(splice: ChannelCommand.Commitment.Splice.Request, parentCommitment: Commitment): Boolean { + return when (val request = splice.requestRemoteFunding) { + null -> true + else -> when (request.paymentDetails) { + is LiquidityAds.PaymentDetails.FromChannelBalance -> request.fees(splice.feerate).total <= parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> request.fees(splice.feerate).total <= parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() + // Fees don't need to be paid during the splice, they will be deducted from relayed HTLCs. + is LiquidityAds.PaymentDetails.FromFutureHtlc -> true + is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> true + } + } + } + private fun ChannelContext.sendSpliceTxSigs( origins: List, action: InteractiveTxSigningSessionAction.SendTxSigs, - liquidityLease: LiquidityAds.Lease?, + liquidityPurchase: LiquidityAds.Purchase?, remoteChannelData: EncryptedChannelData ): Pair> { logger.info { "sending tx_sigs" } @@ -851,7 +863,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, liquidityLeases = liquidityLeases + listOfNotNull(liquidityLease)) + val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) } @@ -891,11 +903,11 @@ data class Normal( 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 -> + liquidityPurchase?.let { purchase -> // 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)) + val miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi() + purchase.fees.miningFee + add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, miningFees = miningFees, purchase = purchase)) } addAll(origins.map { origin -> when (origin) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt index d7337f11c..843b77cf6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt @@ -8,6 +8,7 @@ import fr.acinq.lightning.channel.Helpers.Funding.computeChannelId import fr.acinq.lightning.utils.msat import fr.acinq.lightning.wire.AcceptDualFundedChannel import fr.acinq.lightning.wire.Error +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.lightning.wire.OpenDualFundedChannel /* @@ -53,40 +54,64 @@ data class WaitForAcceptChannel( val remoteFundingPubkey = accept.fundingPubkey val dustLimit = accept.dustLimit.max(init.localParams.dustLimit) val fundingParams = InteractiveTxParams(channelId, true, init.fundingAmount, accept.fundingAmount, remoteFundingPubkey, lastSent.lockTime, dustLimit, lastSent.fundingFeerate) - when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, init.walletInputs)) { + when (val liquidityPurchase = LiquidityAds.validateRemoteFunding( + lastSent.requestFunding, + staticParams.remoteNodeId, + channelId, + fundingParams.fundingPubkeyScript(channelKeys), + accept.fundingAmount, + lastSent.fundingFeerate, + accept.willFund + )) { is Either.Left -> { - logger.error { "could not fund channel: ${fundingContributions.value}" } - Pair(Aborted, listOf(ChannelAction.Message.Send(Error(channelId, ChannelFundingError(channelId).message)))) + logger.error { "rejecting liquidity proposal: ${liquidityPurchase.value.message}" } + Pair(Aborted, listOf(ChannelAction.Message.Send(Error(cmd.message.temporaryChannelId, liquidityPurchase.value.message)))) } - is Either.Right -> { - // The channel initiator always sends the first interactive-tx message. - val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value).send() - when (interactiveTxAction) { - is InteractiveTxSessionAction.SendMessage -> { - val nextState = WaitForFundingCreated( - init.localParams, - remoteParams, - interactiveTxSession, - lastSent.pushAmount, - accept.pushAmount, - lastSent.commitmentFeerate, - accept.firstPerCommitmentPoint, - accept.secondPerCommitmentPoint, - lastSent.channelFlags, - init.channelConfig, - channelFeatures, - channelOrigin - ) - val actions = listOf( - ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId), - ChannelAction.Message.Send(interactiveTxAction.msg), - ChannelAction.EmitEvent(ChannelEvents.Creating(nextState)) - ) - Pair(nextState, actions) - } - else -> { - logger.error { "could not start interactive-tx session: $interactiveTxAction" } - Pair(Aborted, listOf(ChannelAction.Message.Send(Error(channelId, ChannelFundingError(channelId).message)))) + is Either.Right -> when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, init.walletInputs)) { + is Either.Left -> { + logger.error { "could not fund channel: ${fundingContributions.value}" } + Pair(Aborted, listOf(ChannelAction.Message.Send(Error(channelId, ChannelFundingError(channelId).message)))) + } + is Either.Right -> { + // The channel initiator always sends the first interactive-tx message. + val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession( + staticParams.remoteNodeId, + channelKeys, + keyManager.swapInOnChainWallet, + fundingParams, + 0.msat, + 0.msat, + emptySet(), + fundingContributions.value + ).send() + when (interactiveTxAction) { + is InteractiveTxSessionAction.SendMessage -> { + val nextState = WaitForFundingCreated( + init.localParams, + remoteParams, + interactiveTxSession, + lastSent.pushAmount, + accept.pushAmount, + lastSent.commitmentFeerate, + accept.firstPerCommitmentPoint, + accept.secondPerCommitmentPoint, + lastSent.channelFlags, + init.channelConfig, + channelFeatures, + liquidityPurchase.value, + channelOrigin + ) + val actions = listOf( + ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId), + ChannelAction.Message.Send(interactiveTxAction.msg), + ChannelAction.EmitEvent(ChannelEvents.Creating(nextState)) + ) + Pair(nextState, actions) + } + else -> { + logger.error { "could not start interactive-tx session: $interactiveTxAction" } + Pair(Aborted, listOf(ChannelAction.Message.Send(Error(channelId, ChannelFundingError(channelId).message)))) + } } } } 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 c5f5aba61..cff67ea29 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt @@ -81,7 +81,6 @@ data class WaitForChannelReady( null, null, SpliceStatus.None, - listOf(), ) val actions = listOf( ChannelAction.Storage.StoreState(nextState), 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 c65b7acb7..a7d9f2ef3 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -177,7 +177,7 @@ data class WaitForFundingConfirmed( interactiveTxAction.sharedTx, localPushAmount, remotePushAmount, - liquidityLease = null, + liquidityPurchase = 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 28f88518e..41839bb0c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -41,6 +41,7 @@ data class WaitForFundingCreated( val channelFlags: ChannelFlags, val channelConfig: ChannelConfig, val channelFeatures: ChannelFeatures, + val liquidityPurchase: LiquidityAds.Purchase?, val channelOrigin: Origin? ) : ChannelState() { val channelId: ByteVector32 = interactiveTxSession.fundingParams.channelId @@ -63,7 +64,7 @@ data class WaitForFundingCreated( interactiveTxAction.sharedTx, localPushAmount, remotePushAmount, - liquidityLease = null, + liquidityPurchase, localCommitmentIndex = 0, remoteCommitmentIndex = 0, commitTxFeerate, @@ -83,6 +84,7 @@ data class WaitForFundingCreated( localPushAmount, remotePushAmount, remoteSecondPerCommitmentPoint, + liquidityPurchase, channelOrigin ) val actions = buildList { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt index 4c98007e7..c80bc3d81 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt @@ -41,6 +41,7 @@ data class WaitForFundingSigned( val localPushAmount: MilliSatoshi, val remotePushAmount: MilliSatoshi, val remoteSecondPerCommitmentPoint: PublicKey, + val liquidityPurchase: LiquidityAds.Purchase?, val channelOrigin: Origin?, val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty ) : PersistedChannelState() { @@ -129,6 +130,14 @@ data class WaitForFundingSigned( origin = channelOrigin ) ) + liquidityPurchase?.let { purchase -> + if (channelParams.localParams.isChannelOpener) { + // 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() + purchase.fees.miningFee + add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, miningFees = miningFees, purchase = purchase)) + } + } channelOrigin?.let { when (it) { is Origin.OffChainPayment -> add(ChannelAction.EmitEvent(LiquidityEvents.Accepted(it.amount, it.fees.total.toMilliSatoshi(), LiquidityEvents.Source.OffChainPayment))) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt index 57930820a..dcdd8b513 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt @@ -24,7 +24,8 @@ data object WaitForInit : ChannelState() { cmd.walletInputs, cmd.localParams, cmd.channelConfig, - cmd.remoteInit + cmd.remoteInit, + cmd.fundingRates, ) Pair(nextState, listOf()) } @@ -53,7 +54,7 @@ data object WaitForInit : ChannelState() { tlvStream = TlvStream( buildSet { add(ChannelTlv.ChannelTypeTlv(cmd.channelType)) - cmd.requestRemoteFunding?.let { add(it.requestFunds) } + cmd.requestRemoteFunding?.let { add(ChannelTlv.RequestFundingTlv(it)) } if (cmd.pushAmount > 0.msat) add(ChannelTlv.PushAmountTlv(cmd.pushAmount)) } ) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt index 4106abea7..e103af1e7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt @@ -27,7 +27,8 @@ data class WaitForOpenChannel( val walletInputs: List, val localParams: LocalParams, val channelConfig: ChannelConfig, - val remoteInit: Init + val remoteInit: Init, + val fundingRates: LiquidityAds.WillFundRates? ) : ChannelState() { override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> { return when (cmd) { @@ -40,6 +41,15 @@ data class WaitForOpenChannel( val channelFeatures = ChannelFeatures(channelType, localFeatures = localParams.features, remoteFeatures = remoteInit.features) val minimumDepth = if (staticParams.useZeroConf) 0 else Helpers.minDepthForFunding(staticParams.nodeParams, open.fundingAmount) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) + val localFundingPubkey = channelKeys.fundingPubKey(0) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey) + val requestFunding = open.requestFunding + val willFund = when { + fundingRates == null -> null + requestFunding == null -> null + requestFunding.requestedAmount > fundingAmount -> null + else -> fundingRates.validateRequest(staticParams.nodeParams.nodePrivateKey, fundingScript, open.fundingFeerate, requestFunding) + } val accept = AcceptDualFundedChannel( temporaryChannelId = open.temporaryChannelId, fundingAmount = fundingAmount, @@ -49,7 +59,7 @@ data class WaitForOpenChannel( minimumDepth = minimumDepth.toLong(), toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = channelKeys.fundingPubKey(0), + fundingPubkey = localFundingPubkey, revocationBasepoint = channelKeys.revocationBasepoint, paymentBasepoint = channelKeys.paymentBasepoint, delayedPaymentBasepoint = channelKeys.delayedPaymentBasepoint, @@ -59,6 +69,7 @@ data class WaitForOpenChannel( tlvStream = TlvStream( buildSet { add(ChannelTlv.ChannelTypeTlv(channelType)) + willFund?.let { add(ChannelTlv.ProvideFundingTlv(it.willFund)) } if (pushAmount > 0.msat) add(ChannelTlv.PushAmountTlv(pushAmount)) } ), @@ -88,7 +99,8 @@ data class WaitForOpenChannel( is Either.Right -> { val interactiveTxSession = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value) val nextState = WaitForFundingCreated( - localParams, + // If our peer asks us to pay the commit tx fees, we accept (only used in tests, as we're otherwise always the channel opener). + localParams.copy(paysCommitTxFees = open.channelFlags.nonInitiatorPaysCommitFees), remoteParams, interactiveTxSession, pushAmount, @@ -99,6 +111,7 @@ data class WaitForOpenChannel( open.channelFlags, channelConfig, channelFeatures, + willFund?.purchase, channelOrigin = null, ) val actions = listOf( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt index ba6661fab..6feb9bc33 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt @@ -418,12 +418,12 @@ data class InboundLiquidityOutgoingPayment( override val channelId: ByteVector32, override val txId: TxId, override val miningFees: Satoshi, - val lease: LiquidityAds.Lease, + val purchase: LiquidityAds.Purchase, override val createdAt: Long, override val confirmedAt: Long?, override val lockedAt: Long?, ) : OnChainOutgoingPayment() { - override val fees: MilliSatoshi = (miningFees + lease.fees.serviceFee).toMilliSatoshi() + override val fees: MilliSatoshi = (miningFees + purchase.fees.serviceFee).toMilliSatoshi() override val amount: MilliSatoshi = fees override val completedAt: Long? = lockedAt } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 0934b5054..7d18b5eea 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -577,17 +577,17 @@ 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, leaseRate: LiquidityAds.LeaseRate): Pair? { + suspend fun estimateFeeForInboundLiquidity(amount: Satoshi, targetFeerate: FeeratePerKw, fundingRate: LiquidityAds.FundingRate): Pair? { return channels.values .filterIsInstance() .firstOrNull() ?.let { channel -> - val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = channel.commitments.active.first(), walletInputs = emptyList(), localOutputs = emptyList()) + leaseRate.fundingWeight + val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = channel.commitments.active.first(), walletInputs = emptyList(), localOutputs = emptyList()) + fundingRate.fundingWeight // The mining fee below pays for the entirety of the splice transaction, including inputs and outputs from the liquidity provider. val (actualFeerate, miningFee) = client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) // The mining fee in the lease only covers the remote node's inputs and outputs, they are already included in the mining fee above. - val leaseFees = leaseRate.fees(actualFeerate, amount, amount) - Pair(actualFeerate, ChannelManagementFees(miningFee, leaseFees.serviceFee)) + val fundingFees = fundingRate.fees(actualFeerate, amount, amount) + Pair(actualFeerate, ChannelManagementFees(miningFee, fundingFees.serviceFee)) } } @@ -633,17 +633,16 @@ class Peer( } } - suspend fun requestInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw, leaseRate: LiquidityAds.LeaseRate): ChannelCommand.Commitment.Splice.Response? { + suspend fun requestInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw, fundingRate: LiquidityAds.FundingRate): ChannelCommand.Commitment.Splice.Response? { return channels.values .filterIsInstance() .firstOrNull() ?.let { channel -> - val leaseStart = currentTipFlow.filterNotNull().first() val spliceCommand = ChannelCommand.Commitment.Splice.Request( replyTo = CompletableDeferred(), spliceIn = null, spliceOut = null, - requestRemoteFunding = LiquidityAds.RequestRemoteFunding(amount, leaseStart, leaseRate), + requestRemoteFunding = LiquidityAds.RequestFunding(amount, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance), feerate = feerate, origins = listOf(), ) @@ -849,7 +848,7 @@ class Peer( channelId = channelId, txId = action.txId, miningFees = action.miningFees, - lease = action.lease, + purchase = action.purchase, createdAt = currentTimestampMillis(), confirmedAt = null, lockedAt = null @@ -1028,7 +1027,7 @@ class Peer( val localParams = LocalParams(nodeParams, isChannelOpener = false, payCommitTxFees = msg.channelFlags.nonInitiatorPaysCommitFees) val state = WaitForInit val channelConfig = ChannelConfig.standard - val (state1, actions1) = state.process(ChannelCommand.Init.NonInitiator(msg.temporaryChannelId, 0.sat, 0.msat, listOf(), localParams, channelConfig, theirInit!!)) + val (state1, actions1) = state.process(ChannelCommand.Init.NonInitiator(msg.temporaryChannelId, 0.sat, 0.msat, listOf(), localParams, channelConfig, theirInit!!, fundingRates = null)) val (state2, actions2) = state1.process(ChannelCommand.MessageReceived(msg)) _channels = _channels + (msg.temporaryChannelId to state2) processActions(msg.temporaryChannelId, peerConnection, actions1 + actions2) @@ -1258,7 +1257,9 @@ class Peer( is LiquidityPolicy.Disable -> LiquidityPolicy.minInboundLiquidityTarget is LiquidityPolicy.Auto -> policy.inboundLiquidityTarget ?: LiquidityPolicy.minInboundLiquidityTarget } - LiquidityAds.RequestRemoteFunding(inboundLiquidityTarget, currentTipFlow.filterNotNull().first(), walletParams.leaseRate) + // We assume that the liquidity policy is correctly configured to match a funding lease offered by our peer. + val fundingRate = walletParams.remoteFundingRates.findRate(inboundLiquidityTarget)!! + LiquidityAds.RequestFunding(inboundLiquidityTarget, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance) } val (localFundingAmount, fees) = run { // We need to know the local channel funding amount to be able use channel opening messages. @@ -1267,17 +1268,17 @@ class Peer( val dummyFundingScript = Script.write(Scripts.multiSig2of2(Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey)).byteVector() val localMiningFee = Transactions.weight2fee(currentFeerates.fundingFeerate, FundingContributions.computeWeightPaid(isInitiator = true, null, dummyFundingScript, cmd.walletInputs, emptyList())) val localFundingAmount = cmd.totalAmount - localMiningFee - val leaseFees = walletParams.leaseRate.fees(currentFeerates.fundingFeerate, requestRemoteFunding.fundingAmount, requestRemoteFunding.fundingAmount) + val fundingFees = requestRemoteFunding.fees(currentFeerates.fundingFeerate) // We also refund the liquidity provider for some of the on-chain fees they will pay for their inputs/outputs of the transaction. // This will be taken from our channel balance during the interactive-tx construction, they shouldn't be deducted from our funding amount. - val totalFees = ChannelManagementFees(miningFee = localMiningFee + leaseFees.miningFee, serviceFee = leaseFees.serviceFee) + val totalFees = ChannelManagementFees(miningFee = localMiningFee + fundingFees.miningFee, serviceFee = fundingFees.serviceFee) Pair(localFundingAmount, totalFees) } if (cmd.totalAmount - fees.total < nodeParams.dustLimit) { logger.warning { "cannot create channel, not enough funds to pay fees (fees=${fees.total}, available=${cmd.totalAmount})" } swapInCommands.trySend(SwapInCommand.UnlockWalletInputs(cmd.walletInputs.map { it.outPoint }.toSet())) } else { - when (val rejected = nodeParams.liquidityPolicy.first().maybeReject(requestRemoteFunding.fundingAmount.toMilliSatoshi(), fees.total.toMilliSatoshi(), LiquidityEvents.Source.OnChainWallet, logger)) { + when (val rejected = nodeParams.liquidityPolicy.first().maybeReject(requestRemoteFunding.requestedAmount.toMilliSatoshi(), fees.total.toMilliSatoshi(), LiquidityEvents.Source.OnChainWallet, logger)) { is LiquidityEvents.Rejected -> { logger.info { "rejecting channel open: reason=${rejected.reason}" } nodeParams._nodeEvents.emit(rejected) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt b/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt index d50a7ec17..6dc462c26 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt @@ -63,9 +63,9 @@ JsonSerializers.InteractiveTxSigningSessionSerializer::class, JsonSerializers.RbfStatusSerializer::class, JsonSerializers.SpliceStatusSerializer::class, - JsonSerializers.LiquidityLeaseFeesSerializer::class, - JsonSerializers.LiquidityLeaseWitnessSerializer::class, - JsonSerializers.LiquidityLeaseSerializer::class, + JsonSerializers.LiquidityFeesSerializer::class, + JsonSerializers.LiquidityPaymentDetailsSerializer::class, + JsonSerializers.LiquidityPurchaseSerializer::class, JsonSerializers.ChannelFlagsSerializer::class, JsonSerializers.ChannelParamsSerializer::class, JsonSerializers.ChannelOriginSerializer::class, @@ -301,14 +301,14 @@ object JsonSerializers { object SpliceStatusSerializer : StringSerializer({ it::class.simpleName!! }) - @Serializer(forClass = LiquidityAds.LeaseFees::class) - object LiquidityLeaseFeesSerializer + @Serializer(forClass = LiquidityAds.Fees::class) + object LiquidityFeesSerializer - @Serializer(forClass = LiquidityAds.LeaseWitness::class) - object LiquidityLeaseWitnessSerializer + @Serializer(forClass = LiquidityAds.PaymentDetails::class) + object LiquidityPaymentDetailsSerializer - @Serializer(forClass = LiquidityAds.Lease::class) - object LiquidityLeaseSerializer + @Serializer(forClass = LiquidityAds.Purchase::class) + object LiquidityPurchaseSerializer @Serializer(forClass = ChannelFlags::class) object ChannelFlagsSerializer 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 b4667924e..16f5aea97 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt @@ -536,7 +536,6 @@ internal data class Normal( remoteShutdown, null, 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 1cec553df..512da5ce2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt @@ -538,7 +538,6 @@ internal data class Normal( remoteShutdown, closingFeerates?.export(), 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 07ba38a1b..494314312 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -35,13 +35,15 @@ object Deserialization { 0x09 -> readLegacyWaitForFundingLocked() 0x00 -> readWaitForFundingConfirmed() 0x01 -> readWaitForChannelReady() - 0x02 -> readNormal() + 0x02 -> readNormalLegacy() 0x03 -> readShuttingDown() 0x04 -> readNegotiating() 0x05 -> readClosing() 0x06 -> readWaitForRemotePublishFutureCommitment() 0x07 -> readClosed() - 0x0a -> readWaitForFundingSigned() + 0x0a -> readWaitForFundingSignedLegacy() + 0x0b -> readNormal() + 0x0c -> readWaitForFundingSigned() else -> error("unknown discriminator $discriminator for class ${PersistedChannelState::class}") } @@ -68,6 +70,17 @@ object Deserialization { localPushAmount = readNumber().msat, remotePushAmount = readNumber().msat, remoteSecondPerCommitmentPoint = readPublicKey(), + liquidityPurchase = readNullable { readLiquidityPurchase() }, + channelOrigin = readNullable { readChannelOrigin() } + ) + + private fun Input.readWaitForFundingSignedLegacy() = WaitForFundingSigned( + channelParams = readChannelParams(), + signingSession = readInteractiveTxSigningSession(), + localPushAmount = readNumber().msat, + remotePushAmount = readNumber().msat, + remoteSecondPerCommitmentPoint = readPublicKey(), + liquidityPurchase = null, channelOrigin = readNullable { readChannelOrigin() } ) @@ -100,16 +113,24 @@ object Deserialization { closingFeerates = readNullable { readClosingFeerates() }, spliceStatus = when (val discriminator = read()) { 0x00 -> SpliceStatus.None - 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(), readCollection { readChannelOrigin() }.toList()) + 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(), readNullable { readLiquidityPurchase() }, readCollection { readChannelOrigin() }.toList()) + else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") + }, + ) + + private fun Input.readNormalLegacy(): Normal = Normal( + commitments = readCommitments(), + shortChannelId = ShortChannelId(readNumber()), + channelUpdate = readLightningMessage() as ChannelUpdate, + remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate }, + localShutdown = readNullable { readLightningMessage() as Shutdown }, + remoteShutdown = readNullable { readLightningMessage() as Shutdown }, + closingFeerates = readNullable { readClosingFeerates() }, + spliceStatus = when (val discriminator = read()) { + 0x00 -> SpliceStatus.None + 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(), null, readCollection { readChannelOrigin() }.toList()) else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") }, - liquidityLeases = when { - availableBytes == 0 -> listOf() - else -> when (val discriminator = read()) { - 0x01 -> readCollection { readLiquidityLease() }.toList() - else -> error("unknown discriminator $discriminator for class ${Normal::class}") - } - } ) private fun Input.readShuttingDown(): ShuttingDown = ShuttingDown( @@ -388,30 +409,50 @@ object Deserialization { ) ) - private fun Input.readLiquidityLease(): 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, - ), - ) + private fun Input.readLiquidityFees(): LiquidityAds.Fees = LiquidityAds.Fees(miningFee = readNumber().sat, serviceFee = readNumber().sat) + + private fun Input.readLiquidityPurchase(): LiquidityAds.Purchase = when (val discriminator = read()) { + 0x00 -> LiquidityAds.Purchase.Standard( + amount = readNumber().sat, + fees = readLiquidityFees(), + paymentDetails = when (val paymentDetailsDiscriminator = read()) { + 0x00 -> LiquidityAds.PaymentDetails.FromChannelBalance + 0x80 -> LiquidityAds.PaymentDetails.FromFutureHtlc(readCollection { readByteVector32() }.toList()) + 0x81 -> LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage(readCollection { readByteVector32() }.toList()) + 0x82 -> LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(readCollection { readByteVector32() }.toList()) + else -> error("unknown discriminator $paymentDetailsDiscriminator for class ${LiquidityAds.PaymentDetails::class}") + } + ) + else -> error("unknown discriminator $discriminator for class ${LiquidityAds.Purchase::class}") + } + + private fun Input.skipLegacyLiquidityLease() { + readNumber() // amount + readNumber() // mining fee + readNumber() // service fee + readByteVector64() // seller signature + readNBytes(readNumber().toInt()) // funding script + readNumber() // lease duration + readNumber() // lease end + readNumber() // maximum proportional relay fee + readNumber() // maximum base relay fee + } private fun Input.readInteractiveTxSigningSession(): InteractiveTxSigningSession { val fundingParams = readInteractiveTxParams() val fundingTxIndex = readNumber() val fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction - // 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(readLiquidityLease(), Either.Left(readUnsignedLocalCommitWithHtlcs())) - 3 -> Pair(readLiquidityLease(), Either.Right(readLocalCommitWithHtlcs())) + val localCommit = when (val discriminator = read()) { + 0 -> Either.Left(readUnsignedLocalCommitWithHtlcs()) + 1 -> Either.Right(readLocalCommitWithHtlcs()) + 2 -> { + skipLegacyLiquidityLease() + Either.Left(readUnsignedLocalCommitWithHtlcs()) + } + 3 -> { + skipLegacyLiquidityLease() + Either.Right(readLocalCommitWithHtlcs()) + } else -> error("unknown discriminator $discriminator for class ${InteractiveTxSigningSession::class}") } val remoteCommit = RemoteCommit( @@ -420,7 +461,7 @@ object Deserialization { txid = readTxId(), remotePerCommitmentPoint = readPublicKey() ) - return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, liquidityLease, localCommit, remoteCommit) + return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, 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 b61b0e58d..9dac84ef3 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -57,7 +57,7 @@ object Serialization { write(0x01); writeWaitForChannelReady(o) } is Normal -> { - write(0x02); writeNormal(o) + write(0x0b); writeNormal(o) } is ShuttingDown -> { write(0x03); writeShuttingDown(o) @@ -75,7 +75,7 @@ object Serialization { write(0x07); writeClosed(o) } is WaitForFundingSigned -> { - write(0x0a); writeWaitForFundingSigned(o) + write(0x0c); writeWaitForFundingSigned(o) } } @@ -102,6 +102,7 @@ object Serialization { writeNumber(localPushAmount.toLong()) writeNumber(remotePushAmount.toLong()) writePublicKey(remoteSecondPerCommitmentPoint) + writeNullable(liquidityPurchase) { writeLiquidityPurchase(it) } writeNullable(channelOrigin) { writeChannelOrigin(it) } } @@ -140,14 +141,13 @@ object Serialization { is SpliceStatus.WaitingForSigs -> { write(0x01) writeInteractiveTxSigningSession(spliceStatus.session) + writeNullable(spliceStatus.liquidityPurchase) { writeLiquidityPurchase(it) } writeCollection(spliceStatus.origins) { writeChannelOrigin(it) } } else -> { write(0x00) } } - write(0x01) - writeCollection(liquidityLeases) { writeLiquidityLease(it) } } private fun Output.writeShuttingDown(o: ShuttingDown) = o.run { @@ -406,53 +406,43 @@ object Serialization { } } - private fun Output.writeLiquidityLease(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.writeLiquidityFees(fees: LiquidityAds.Fees) { + writeNumber(fees.miningFee.toLong()) + writeNumber(fees.serviceFee.toLong()) + } + + private fun Output.writeLiquidityPurchase(purchase: LiquidityAds.Purchase) { + when (purchase) { + is LiquidityAds.Purchase.Standard -> { + write(0x00) // discriminator + writeNumber(purchase.amount.toLong()) + writeLiquidityFees(purchase.fees) + when (val paymentDetails = purchase.paymentDetails) { + is LiquidityAds.PaymentDetails.FromChannelBalance -> write(0x00) + is LiquidityAds.PaymentDetails.FromFutureHtlc -> { + write(0x80) + writeCollection(paymentDetails.paymentHashes) { writeByteVector32(it) } + } + is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> { + write(0x81) + writeCollection(paymentDetails.preimages) { writeByteVector32(it) } + } + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> { + write(0x82) + writeCollection(paymentDetails.paymentHashes) { writeByteVector32(it) } + } + } + } + } } private fun Output.writeInteractiveTxSigningSession(s: InteractiveTxSigningSession) = s.run { writeInteractiveTxParams(fundingParams) writeNumber(s.fundingTxIndex) writeSignedSharedTransaction(fundingTx) - // 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 (liquidityLease) { - // 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 -> { - write(0) - writeUnsignedLocalCommitWithHtlcs(localCommit.value) - } - is Either.Right -> { - write(1) - writeLocalCommitWithHtlcs(localCommit.value) - } - } - else -> when (localCommit) { - is Either.Left -> { - write(2) - writeLiquidityLease(liquidityLease) - writeUnsignedLocalCommitWithHtlcs(localCommit.value) - } - is Either.Right -> { - write(3) - writeLiquidityLease(liquidityLease) - writeLocalCommitWithHtlcs(localCommit.value) - } - } - } + writeEither(localCommit, { localCommit -> writeUnsignedLocalCommitWithHtlcs(localCommit) }, { localCommit -> writeLocalCommitWithHtlcs(localCommit) }) remoteCommit.run { writeNumber(index) writeCommitmentSpecWithHtlcs(spec) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt index 7b5d3d0c9..85e80b62f 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt @@ -66,52 +66,26 @@ sealed class ChannelTlv : Tlv { } /** Request inbound liquidity from our peer. */ - data class RequestFunds(val amount: Satoshi, val leaseDuration: Int, val leaseExpiry: Int) : ChannelTlv() { - override val tag: Long get() = RequestFunds.tag + data class RequestFundingTlv(val request: LiquidityAds.RequestFunding) : ChannelTlv() { + override val tag: Long get() = RequestFundingTlv.tag - override fun write(out: Output) { - LightningCodecs.writeU64(amount.toLong(), out) - LightningCodecs.writeU16(leaseDuration, out) - LightningCodecs.writeU32(leaseExpiry, out) - } - - companion object : TlvValueReader { - const val tag: Long = 1337 + override fun write(out: Output) = request.write(out) - override fun read(input: Input): RequestFunds = RequestFunds( - amount = LightningCodecs.u64(input).sat, - leaseDuration = LightningCodecs.u16(input), - leaseExpiry = LightningCodecs.u32(input), - ) + companion object : TlvValueReader { + const val tag: Long = 1339 + override fun read(input: Input): RequestFundingTlv = RequestFundingTlv(LiquidityAds.RequestFunding.read(input)) } } - /** Liquidity rates applied to an incoming [[RequestFunds]]. */ - 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 + /** Accept inbound liquidity request. */ + data class ProvideFundingTlv(val willFund: LiquidityAds.WillFund) : ChannelTlv() { + override val tag: Long get() = ProvideFundingTlv.tag - fun leaseRate(leaseDuration: Int): LiquidityAds.LeaseRate = LiquidityAds.LeaseRate(leaseDuration, fundingWeight, leaseFeeProportional, leaseFeeBase, maxRelayFeeProportional, maxRelayFeeBase) - - override fun write(out: Output) { - LightningCodecs.writeBytes(sig, 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) - } + override fun write(out: Output) = willFund.write(out) - companion object : TlvValueReader { - const val tag: Long = 1337 - - override fun read(input: Input): WillFund = WillFund( - sig = LightningCodecs.bytes(input, 64).toByteVector64(), - fundingWeight = LightningCodecs.u16(input), - leaseFeeProportional = LightningCodecs.u16(input), - leaseFeeBase = LightningCodecs.u32(input).sat, - maxRelayFeeProportional = LightningCodecs.u16(input), - maxRelayFeeBase = LightningCodecs.u32(input).msat, - ) + companion object : TlvValueReader { + const val tag: Long = 1339 + override fun read(input: Input): ProvideFundingTlv = ProvideFundingTlv(LiquidityAds.WillFund.read(input)) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InitTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InitTlv.kt index a4ae87672..acae643ba 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InitTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InitTlv.kt @@ -32,21 +32,14 @@ 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 + data class OptionWillFund(val rates: LiquidityAds.WillFundRates) : InitTlv() { + override val tag: Long get() = OptionWillFund.tag - override fun write(out: Output) { - leaseRates.forEach { it.write(out) } - } + override fun write(out: Output) = rates.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) - } + companion object : TlvValueReader { + const val tag: Long = 1339 + override fun read(input: Input): OptionWillFund = OptionWillFund(LiquidityAds.WillFundRates.read(input)) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 6609298fc..192da74c1 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -188,14 +188,15 @@ 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() + val liquidityRates = tlvs.get()?.rates?.fundingRates ?: listOf() + val liquidityPaymentTypes = tlvs.get()?.rates?.paymentTypes ?: setOf() - constructor(features: Features, chainHashs: List, liquidityRates: List) : this( + constructor(features: Features, chainHashs: List, liquidityRates: LiquidityAds.WillFundRates?) : this( features, TlvStream( setOfNotNull( if (chainHashs.isNotEmpty()) InitTlv.Networks(chainHashs) else null, - if (liquidityRates.isNotEmpty()) InitTlv.LiquidityAdsRates(liquidityRates) else null, + liquidityRates?.let { InitTlv.OptionWillFund(it) }, ) ) ) @@ -217,7 +218,7 @@ data class Init(val features: Features, val tlvs: TlvStream = TlvStream @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.OptionWillFund.tag to InitTlv.OptionWillFund.Companion as TlvValueReader, InitTlv.PhoenixAndroidLegacyNodeId.tag to InitTlv.PhoenixAndroidLegacyNodeId.Companion as TlvValueReader, ) @@ -674,7 +675,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 requestFunding: LiquidityAds.RequestFunding? get() = tlvStream.get()?.request override val type: Long get() = OpenDualFundedChannel.type @@ -711,7 +712,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.RequestFundingTlv.tag to ChannelTlv.RequestFundingTlv as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, ) @@ -782,7 +783,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 willFund: LiquidityAds.WillFund? get() = tlvStream.get()?.willFund val pushAmount: MilliSatoshi get() = tlvStream.get()?.amount ?: 0.msat override val type: Long get() = AcceptDualFundedChannel.type @@ -814,7 +815,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.ProvideFundingTlv.tag to ChannelTlv.ProvideFundingTlv as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, ) @@ -952,16 +953,21 @@ 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 requestFunding: LiquidityAds.RequestFunding? = tlvStream.get()?.request val pushAmount: MilliSatoshi = tlvStream.get()?.amount ?: 0.msat - constructor(channelId: ByteVector32, fundingContribution: Satoshi, pushAmount: MilliSatoshi, feerate: FeeratePerKw, lockTime: Long, fundingPubkey: PublicKey, requestFunds: ChannelTlv.RequestFunds?) : this( + constructor(channelId: ByteVector32, fundingContribution: Satoshi, pushAmount: MilliSatoshi, feerate: FeeratePerKw, lockTime: Long, fundingPubkey: PublicKey, requestFunding: LiquidityAds.RequestFunding?) : this( channelId, fundingContribution, feerate, lockTime, fundingPubkey, - TlvStream(setOfNotNull(if (pushAmount > 0.msat) ChannelTlv.PushAmountTlv(pushAmount) else null, requestFunds)) + TlvStream( + setOfNotNull( + if (pushAmount > 0.msat) ChannelTlv.PushAmountTlv(pushAmount) else null, + requestFunding?.let { ChannelTlv.RequestFundingTlv(it) }, + ) + ) ) override fun write(out: Output) { @@ -979,7 +985,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.RequestFundingTlv.tag to ChannelTlv.RequestFundingTlv as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, ) @@ -1002,14 +1008,18 @@ 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 willFund: LiquidityAds.WillFund? = tlvStream.get()?.willFund val pushAmount: MilliSatoshi = tlvStream.get()?.amount ?: 0.msat - constructor(channelId: ByteVector32, fundingContribution: Satoshi, pushAmount: MilliSatoshi, fundingPubkey: PublicKey, willFund: ChannelTlv.WillFund?) : this( + constructor(channelId: ByteVector32, fundingContribution: Satoshi, pushAmount: MilliSatoshi, fundingPubkey: PublicKey, willFund: LiquidityAds.WillFund?) : this( channelId, fundingContribution, fundingPubkey, - TlvStream(setOfNotNull(if (pushAmount > 0.msat) ChannelTlv.PushAmountTlv(pushAmount) else null, willFund)) + TlvStream( + setOfNotNull( + if (pushAmount > 0.msat) ChannelTlv.PushAmountTlv(pushAmount) else null, + willFund?.let { ChannelTlv.ProvideFundingTlv(it) } + )) ) override fun write(out: Output) { @@ -1025,7 +1035,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.ProvideFundingTlv.tag to ChannelTlv.ProvideFundingTlv 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 index cc75d2b95..75f8836ea 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt @@ -5,16 +5,18 @@ import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output import fr.acinq.bitcoin.utils.Either -import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.* +import fr.acinq.lightning.channel.ChannelException +import fr.acinq.lightning.channel.InvalidLiquidityAdsAmount +import fr.acinq.lightning.channel.InvalidLiquidityAdsSig +import fr.acinq.lightning.channel.MissingLiquidityAds import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.BitField 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. + * Nodes advertise funding 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 { @@ -23,137 +25,289 @@ object LiquidityAds { * @param miningFee we refund the liquidity provider for some of the fee they 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) { + data class Fees(val miningFee: Satoshi, val serviceFee: Satoshi) { val total: Satoshi = miningFee + serviceFee } /** - * Liquidity is leased using the following rates: + * Rate at which a liquidity seller sells its liquidity. + * Liquidity fees are computed based on multiple components. * - * - 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]. - * This cannot be enforced, but if the buyer notices the seller cheating, they should blacklist them and can prove - * that they misbehaved using the seller's signature of the [LeaseWitness]. + * @param minAmount minimum amount that can be purchased at this rate. + * @param maxAmount maximum amount that can be purchased at this rate. + * @param fundingWeight the seller will have to add inputs/outputs to the transaction and pay on-chain fees for them. + * The buyer refunds those on-chain fees for the given vbytes. + * @param feeProportional proportional fee (expressed in basis points) based on the amount contributed by the seller. + * @param feeBase flat fee that must be paid regardless of the amount contributed by the seller. */ - 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): LeaseFees { + data class FundingRate(val minAmount: Satoshi, val maxAmount: Satoshi, val fundingWeight: Int, val feeProportional: Int, val feeBase: Satoshi) { + /** Fees paid by the liquidity buyer. */ + fun fees(feerate: FeeratePerKw, requestedAmount: Satoshi, contributedAmount: Satoshi): Fees { 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 LeaseFees(onChainFees, leaseFeeBase + proportionalFee) + val proportionalFee = requestedAmount.min(contributedAmount) * feeProportional / 10_000 + return Fees(onChainFees, feeBase + proportionalFee) } - 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, fundingWeight, leaseFeeProportional, leaseFeeBase, maxRelayFeeProportional, maxRelayFeeBase) + /** When liquidity is purchased, the seller provides a signature of the funding rate and funding script. */ + fun signedData(fundingScript: ByteVector): ByteArray { + // We use a tagged hash to ensure that our signature cannot be reused in a different context. + val tag = "liquidity_ads_purchase" + val tmp = ByteArrayOutput() + write(tmp) + return Crypto.sha256(tag.encodeToByteArray() + tmp.toByteArray() + fundingScript.toByteArray()) } fun write(out: Output) { - LightningCodecs.writeU16(leaseDuration, out) + LightningCodecs.writeU32(minAmount.sat.toInt(), out) + LightningCodecs.writeU32(maxAmount.sat.toInt(), 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) + LightningCodecs.writeU16(feeProportional, out) + LightningCodecs.writeU32(feeBase.sat.toInt(), out) } companion object { - fun read(input: Input): LeaseRate = LeaseRate( - leaseDuration = LightningCodecs.u16(input), + fun read(input: Input): FundingRate = FundingRate( + minAmount = LightningCodecs.u32(input).sat, + maxAmount = LightningCodecs.u32(input).sat, fundingWeight = LightningCodecs.u16(input), - leaseFeeProportional = LightningCodecs.u16(input), - leaseFeeBase = LightningCodecs.u32(input).sat, - maxRelayFeeProportional = LightningCodecs.u16(input), - maxRelayFeeBase = LightningCodecs.u32(input).msat, + feeProportional = LightningCodecs.u16(input), + feeBase = LightningCodecs.u32(input).sat, + ) + } + } + + /** The fees associated with a given [FundingRate] can be paid using various options. */ + sealed class PaymentType { + /** Fees are transferred from the buyer's channel balance to the seller's during the interactive-tx construction. */ + data object FromChannelBalance : PaymentType() + /** Fees will be deducted from future HTLCs that will be relayed to the buyer. */ + data object FromFutureHtlc : PaymentType() + /** Fees will be deducted from future HTLCs that will be relayed to the buyer, but the preimage is revealed immediately. */ + data object FromFutureHtlcWithPreimage : PaymentType() + /** Similar to [FromChannelBalance] but expects HTLCs to be relayed after funding. */ + data object FromChannelBalanceForFutureHtlc : PaymentType() + /** Sellers may support unknown payment types, which we must ignore. */ + data class Unknown(val bitIndex: Int) : PaymentType() + + companion object { + fun encode(paymentTypes: Set): ByteArray { + val bitIndices = paymentTypes.map { + when (it) { + is FromChannelBalance -> 0 + is FromFutureHtlc -> 128 + is FromFutureHtlcWithPreimage -> 129 + is FromChannelBalanceForFutureHtlc -> 130 + is Unknown -> it.bitIndex + } + } + val bits = BitField.forAtMost(bitIndices.max() + 1) + bitIndices.forEach { bits.setRight(it) } + return bits.bytes + } + + fun decode(bytes: ByteArray): Set { + return BitField.from(bytes).asRightSequence().withIndex().mapNotNull { + when { + it.value && it.index == 0 -> FromChannelBalance + it.value && it.index == 128 -> FromFutureHtlc + it.value && it.index == 129 -> FromFutureHtlcWithPreimage + it.value && it.index == 130 -> FromChannelBalanceForFutureHtlc + it.value -> Unknown(it.index) + else -> null + } + }.toSet() + } + } + } + + /** When purchasing liquidity, we provide payment details matching one of the [PaymentType]s supported by the seller. */ + sealed class PaymentDetails { + abstract val paymentType: PaymentType + + // @formatter:off + data object FromChannelBalance : PaymentDetails() { override val paymentType: PaymentType = PaymentType.FromChannelBalance } + data class FromFutureHtlc(val paymentHashes: List) : PaymentDetails() { override val paymentType: PaymentType = PaymentType.FromFutureHtlc } + data class FromFutureHtlcWithPreimage(val preimages: List) : PaymentDetails() { override val paymentType: PaymentType = PaymentType.FromFutureHtlcWithPreimage } + data class FromChannelBalanceForFutureHtlc(val paymentHashes: List) : PaymentDetails() { override val paymentType: PaymentType = PaymentType.FromChannelBalanceForFutureHtlc } + // @formatter:on + + fun write(out: Output) = when (this) { + is FromChannelBalance -> { + LightningCodecs.writeBigSize(0, out) // tag + LightningCodecs.writeBigSize(0, out) // length + } + is FromFutureHtlc -> { + LightningCodecs.writeBigSize(128, out) // tag + LightningCodecs.writeBigSize(32 * paymentHashes.size.toLong(), out) // length + paymentHashes.forEach { LightningCodecs.writeBytes(it, out) } + } + is FromFutureHtlcWithPreimage -> { + LightningCodecs.writeBigSize(129, out) // tag + LightningCodecs.writeBigSize(32 * preimages.size.toLong(), out) // length + preimages.forEach { LightningCodecs.writeBytes(it, out) } + } + is FromChannelBalanceForFutureHtlc -> { + LightningCodecs.writeBigSize(130, out) // tag + LightningCodecs.writeBigSize(32 * paymentHashes.size.toLong(), out) // length + paymentHashes.forEach { LightningCodecs.writeBytes(it, out) } + } + } + + companion object { + fun read(input: Input): PaymentDetails = when (val tag = LightningCodecs.bigSize(input)) { + 0L -> { + require(LightningCodecs.bigSize(input) == 0L) { "invalid length for from_channel_balance payment details" } + FromChannelBalance + } + 128L -> { + val count = LightningCodecs.bigSize(input) / 32 + val paymentHashes = (0 until count).map { LightningCodecs.bytes(input, 32).byteVector32() } + FromFutureHtlc(paymentHashes) + } + 129L -> { + val count = LightningCodecs.bigSize(input) / 32 + val preimages = (0 until count).map { LightningCodecs.bytes(input, 32).byteVector32() } + FromFutureHtlcWithPreimage(preimages) + } + 130L -> { + val count = LightningCodecs.bigSize(input) / 32 + val paymentHashes = (0 until count).map { LightningCodecs.bytes(input, 32).byteVector32() } + FromChannelBalanceForFutureHtlc(paymentHashes) + } + else -> throw IllegalArgumentException("unknown payment details (tag=$tag)") + } + } + } + + /** Sellers offer various rates and payment options. */ + data class WillFundRates(val fundingRates: List, val paymentTypes: Set) { + fun validateRequest(nodeKey: PrivateKey, fundingScript: ByteVector, fundingFeerate: FeeratePerKw, request: RequestFunding): WillFundPurchase? { + val paymentTypeOk = paymentTypes.contains(request.paymentDetails.paymentType) + val rateOk = fundingRates.contains(request.fundingRate) + val amountOk = request.fundingRate.minAmount <= request.requestedAmount && request.requestedAmount <= request.fundingRate.maxAmount + return when { + paymentTypeOk && rateOk && amountOk -> { + val sig = Crypto.sign(request.fundingRate.signedData(fundingScript), nodeKey) + val purchase = Purchase.Standard(request.requestedAmount, request.fees(fundingFeerate), request.paymentDetails) + WillFundPurchase(WillFund(request.fundingRate, fundingScript, sig), purchase) + } + else -> null + } + } + + fun findRate(requestedAmount: Satoshi): FundingRate? { + return fundingRates.firstOrNull { it.minAmount <= requestedAmount && requestedAmount <= it.maxAmount } + } + + fun write(out: Output) { + LightningCodecs.writeU16(fundingRates.size, out) + fundingRates.forEach { it.write(out) } + val encoded = PaymentType.encode(paymentTypes) + LightningCodecs.writeU16(encoded.size, out) + LightningCodecs.writeBytes(encoded, out) + } + + companion object { + fun read(input: Input): WillFundRates { + val fundingRatesCount = LightningCodecs.u16(input) + val fundingRates = (0 until fundingRatesCount).mapNotNull { FundingRate.read(input) } + val paymentTypes = PaymentType.decode(LightningCodecs.bytes(input, LightningCodecs.u16(input))) + return WillFundRates(fundingRates, paymentTypes) + } + } + } + + /** Provide inbound liquidity to a remote peer that wants to purchase liquidity. */ + data class WillFund(val fundingRate: FundingRate, val fundingScript: ByteVector, val signature: ByteVector64) { + fun write(out: Output) { + fundingRate.write(out) + LightningCodecs.writeU16(fundingScript.size(), out) + LightningCodecs.writeBytes(fundingScript, out) + LightningCodecs.writeBytes(signature, out) + } + + companion object { + fun read(input: Input): WillFund = WillFund( + fundingRate = FundingRate.read(input), + fundingScript = LightningCodecs.bytes(input, LightningCodecs.u16(input)).byteVector(), + signature = LightningCodecs.bytes(input, 64).byteVector64(), ) } } /** Request inbound liquidity from a remote peer that supports liquidity ads. */ - 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) + data class RequestFunding(val requestedAmount: Satoshi, val fundingRate: FundingRate, val paymentDetails: PaymentDetails) { + fun fees(feerate: FeeratePerKw): Fees = fundingRate.fees(feerate, requestedAmount, requestedAmount) - fun validateLease( + fun validateRemoteFunding( remoteNodeId: PublicKey, channelId: ByteVector32, fundingScript: ByteVector, remoteFundingAmount: Satoshi, fundingFeerate: FeeratePerKw, - willFund: ChannelTlv.WillFund? - ): Either { + willFund: 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(fundingScript, rate.leaseDuration, leaseExpiry, willFund.maxRelayFeeProportional, willFund.maxRelayFeeBase) - return if (!witness.verify(remoteNodeId, willFund.sig)) { - Either.Left(InvalidLiquidityAdsSig(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) - val leaseFees = rate.fees(fundingFeerate, fundingAmount, remoteFundingAmount) - Either.Right(Lease(leaseAmount, leaseFees, willFund.sig, witness)) + else -> when { + !Crypto.verifySignature(fundingRate.signedData(fundingScript), willFund.signature, remoteNodeId) -> Either.Left(InvalidLiquidityAdsSig(channelId)) + remoteFundingAmount < requestedAmount -> Either.Left(InvalidLiquidityAdsAmount(channelId, remoteFundingAmount, requestedAmount)) + else -> { + val purchasedAmount = requestedAmount.min(remoteFundingAmount) + val fees = fundingRate.fees(fundingFeerate, requestedAmount, remoteFundingAmount) + Either.Right(Purchase.Standard(purchasedAmount, fees, paymentDetails)) } } } } + + fun write(out: Output) { + LightningCodecs.writeU64(requestedAmount.toLong(), out) + fundingRate.write(out) + paymentDetails.write(out) + } + + companion object { + fun chooseRate(requestedAmount: Satoshi, paymentDetails: PaymentDetails, rates: WillFundRates): RequestFunding? = when { + rates.paymentTypes.contains(paymentDetails.paymentType) -> rates.findRate(requestedAmount)?.let { RequestFunding(requestedAmount, it, paymentDetails) } + else -> null + } + + fun read(input: Input): RequestFunding = RequestFunding( + requestedAmount = LightningCodecs.u64(input).sat, + fundingRate = FundingRate.read(input), + paymentDetails = PaymentDetails.read(input), + ) + } } - fun validateLease( - request: RequestRemoteFunding?, + fun validateRemoteFunding( + request: RequestFunding?, remoteNodeId: PublicKey, channelId: ByteVector32, fundingScript: ByteVector, remoteFundingAmount: Satoshi, fundingFeerate: FeeratePerKw, - willFund: ChannelTlv.WillFund?, - ): Either { + willFund: WillFund?, + ): Either { return when (request) { null -> Either.Right(null) - else -> request.validateLease(remoteNodeId, channelId, fundingScript, remoteFundingAmount, fundingFeerate, willFund) + else -> request.validateRemoteFunding(remoteNodeId, channelId, fundingScript, 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. - */ - 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 - } - - /** The seller signs the lease parameters: if they cheat, the buyer can use that signature to prove they cheated. */ - 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) + /** Once a liquidity ads has been purchased, we keep track of the fees paid and the payment details. */ + sealed class Purchase { + abstract val amount: Satoshi + abstract val fees: Fees + abstract val paymentDetails: PaymentDetails - fun encode(): ByteArray { - val out = ByteArrayOutput() - LightningCodecs.writeBytes("option_will_fund".encodeToByteArray(), out) - LightningCodecs.writeU16(fundingScript.size(), out) - LightningCodecs.writeBytes(fundingScript, out) - LightningCodecs.writeU16(leaseDuration, out) - LightningCodecs.writeU32(leaseEnd, out) - LightningCodecs.writeU16(maxRelayFeeProportional, out) - LightningCodecs.writeU32(maxRelayFeeBase.msat.toInt(), out) - return out.toByteArray() - } + data class Standard(override val amount: Satoshi, override val fees: Fees, override val paymentDetails: PaymentDetails) : Purchase() } + data class WillFundPurchase(val willFund: WillFund, val purchase: Purchase) + } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index 955293a92..8efd14986 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -1188,8 +1188,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { ): Fixture { val channelId = randomBytes32() val fundingTxIndex = 0L - val localParamsA = TestConstants.Alice.channelParams() - val localParamsB = TestConstants.Bob.channelParams() + val localParamsA = TestConstants.Alice.channelParams(payCommitTxFees = true) + val localParamsB = TestConstants.Bob.channelParams(payCommitTxFees = false) val channelKeysA = localParamsA.channelKeys(TestConstants.Alice.keyManager) val channelKeysB = localParamsB.channelKeys(TestConstants.Bob.keyManager) val swapInKeysA = TestConstants.Alice.keyManager.swapInOnChainWallet @@ -1219,8 +1219,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { lockTime: Long ): Either { val channelId = randomBytes32() - val localParamsA = TestConstants.Alice.channelParams() - val localParamsB = TestConstants.Bob.channelParams() + val localParamsA = TestConstants.Alice.channelParams(payCommitTxFees = true) + val localParamsB = TestConstants.Bob.channelParams(payCommitTxFees = false) val channelKeysA = localParamsA.channelKeys(TestConstants.Alice.keyManager) val channelKeysB = localParamsB.channelKeys(TestConstants.Bob.keyManager) val swapInKeysA = TestConstants.Alice.keyManager.swapInOnChainWallet @@ -1252,8 +1252,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { ): Fixture { val channelId = randomBytes32() val fundingTxIndex = 0L - val localParamsA = TestConstants.Alice.channelParams() - val localParamsB = TestConstants.Bob.channelParams() + val localParamsA = TestConstants.Alice.channelParams(payCommitTxFees = true) + val localParamsB = TestConstants.Bob.channelParams(payCommitTxFees = false) val channelKeysA = localParamsA.channelKeys(TestConstants.Alice.keyManager) val channelKeysB = localParamsB.channelKeys(TestConstants.Bob.keyManager) val swapInKeysA = TestConstants.Alice.keyManager.swapInOnChainWallet diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index a1f12a4f5..90e38efc1 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -149,6 +149,7 @@ object TestsHelper { bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, + requestRemoteFunding: Satoshi? = null, zeroConf: Boolean = false, channelOrigin: Origin? = null ): Triple, LNChannel, OpenDualFundedChannel> { @@ -181,8 +182,9 @@ object TestsHelper { WaitForInit ) - val aliceChannelParams = TestConstants.Alice.channelParams().copy(features = aliceFeatures.initFeatures()) - val bobChannelParams = TestConstants.Bob.channelParams().copy(features = bobFeatures.initFeatures()) + val channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = requestRemoteFunding != null) + val aliceChannelParams = TestConstants.Alice.channelParams(payCommitTxFees = !channelFlags.nonInitiatorPaysCommitFees).copy(features = aliceFeatures.initFeatures()) + val bobChannelParams = TestConstants.Bob.channelParams(payCommitTxFees = channelFlags.nonInitiatorPaysCommitFees).copy(features = bobFeatures.initFeatures()) val aliceInit = Init(aliceFeatures) val bobInit = Init(bobFeatures) val (alice1, actionsAlice1) = alice.process( @@ -194,16 +196,17 @@ object TestsHelper { TestConstants.feeratePerKw, aliceChannelParams, bobInit, - ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), + channelFlags, ChannelConfig.standard, channelType, - requestRemoteFunding = null, + requestRemoteFunding?.let { LiquidityAds.RequestFunding(it, TestConstants.fundingRates.findRate(it)!!, LiquidityAds.PaymentDetails.FromChannelBalance) }, channelOrigin, ) ) assertIs>(alice1) + val temporaryChannelId = aliceChannelParams.channelKeys(alice.ctx.keyManager).temporaryChannelId val bobWallet = if (bobFundingAmount > 0.sat) createWallet(bobNodeParams.keyManager, bobFundingAmount + 1500.sat).second else listOf() - val (bob1, _) = bob.process(ChannelCommand.Init.NonInitiator(aliceChannelParams.channelKeys(alice.ctx.keyManager).temporaryChannelId, bobFundingAmount, bobPushAmount, bobWallet, bobChannelParams, ChannelConfig.standard, aliceInit)) + val (bob1, _) = bob.process(ChannelCommand.Init.NonInitiator(temporaryChannelId, bobFundingAmount, bobPushAmount, bobWallet, bobChannelParams, ChannelConfig.standard, aliceInit, TestConstants.fundingRates)) assertIs>(bob1) val open = actionsAlice1.findOutgoingMessage() return Triple(alice1, bob1, open) @@ -218,9 +221,10 @@ object TestsHelper { bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, + requestRemoteFunding: Satoshi? = null, zeroConf: Boolean = false, ): Triple, LNChannel, Transaction> { - val (alice, channelReadyAlice, bob, channelReadyBob) = WaitForChannelReadyTestsCommon.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, zeroConf) + val (alice, channelReadyAlice, bob, channelReadyBob) = WaitForChannelReadyTestsCommon.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, requestRemoteFunding, zeroConf) val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(channelReadyBob)) assertIs>(alice1) actionsAlice1.has() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt index 29b7f026e..8abcc3a5b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt @@ -268,6 +268,21 @@ class ClosingTestsCommon : LightningTestSuite() { assertContains(actions, ChannelAction.Storage.SetLocked(localCommitPublished.commitTx.txid)) } + @Test + fun `recv BITCOIN_TX_CONFIRMED -- local commit -- non-initiator pays commit fees`() { + val (alice0, bob0) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) + assertFalse(alice0.commitments.params.localParams.paysCommitTxFees) + assertTrue(bob0.commitments.params.localParams.paysCommitTxFees) + val (alice1, localCommitPublished) = localClose(alice0) + val (alice2, _) = alice1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice0.channelId, BITCOIN_TX_CONFIRMED(localCommitPublished.commitTx), 42, 7, localCommitPublished.commitTx))) + val claimMain = localCommitPublished.claimMainDelayedOutputTx!!.tx + val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice0.state.channelId, BITCOIN_TX_CONFIRMED(claimMain), 43, 3, claimMain))) + assertIs(alice3.state) + assertEquals(2, actions3.size) + actions3.has() + actions3.find().also { assertEquals(localCommitPublished.commitTx.txid, it.txId) } + } + @Test fun `recv BITCOIN_TX_CONFIRMED -- local commit with multiple htlcs for the same payment`() { val (alice0, bob0) = reachNormal() @@ -607,6 +622,22 @@ class ClosingTestsCommon : LightningTestSuite() { assertEquals(3, actions.size) } + @Test + fun `recv BITCOIN_TX_CONFIRMED -- remote commit -- non-initiator pays commit fees`() { + val (alice0, bob0) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) + assertFalse(alice0.commitments.params.localParams.paysCommitTxFees) + assertTrue(bob0.commitments.params.localParams.paysCommitTxFees) + val remoteCommitTx = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val (alice1, remoteCommitPublished) = remoteClose(remoteCommitTx, alice0) + val (alice2, _) = alice1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice0.channelId, BITCOIN_TX_CONFIRMED(remoteCommitTx), 42, 7, remoteCommitTx))) + val claimMain = remoteCommitPublished.claimMainOutputTx!!.tx + val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice0.state.channelId, BITCOIN_TX_CONFIRMED(claimMain), 43, 3, claimMain))) + assertIs(alice3.state) + assertEquals(2, actions3.size) + actions3.has() + actions3.find().also { assertEquals(remoteCommitTx.txid, it.txId) } + } + @Test fun `recv BITCOIN_TX_CONFIRMED -- remote commit with multiple htlcs for the same payment`() { val (alice0, bob0) = reachNormal() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt index ac2d1f30c..114fe4286 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt @@ -146,6 +146,42 @@ class NegotiatingTestsCommon : LightningTestSuite() { testClosingSignedSameFees(alice, bob, bobInitiates = true) } + @Test + fun `recv ClosingSigned -- theirCloseFee == ourCloseFee -- non-initiator pays commit fees`() { + val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, aliceFundingAmount = 200_000.sat, alicePushAmount = 0.msat, requestRemoteFunding = TestConstants.bobFundingAmount) + assertFalse(alice.commitments.params.localParams.paysCommitTxFees) + assertTrue(bob.commitments.params.localParams.paysCommitTxFees) + // Alice sends all of her balance to Bob. + val (nodes1, r, htlc) = TestsHelper.addHtlc(alice.commitments.availableBalanceForSend(), alice, bob) + val (alice1, bob1) = TestsHelper.crossSign(nodes1.first, nodes1.second) + val (alice2, bob2) = TestsHelper.fulfillHtlc(htlc.id, r, alice1, bob1) + val (bob3, alice3) = TestsHelper.crossSign(bob2, alice2) + assertEquals(0.msat, alice3.commitments.latest.localCommit.spec.toLocal) + // Alice and Bob agree on the current feerate. + val alice4 = alice3.updateFeerate(FeeratePerKw(3_000.sat)) + val bob4 = bob3.updateFeerate(FeeratePerKw(3_000.sat)) + // Bob initiates the mutual close. + val (bob5, actionsBob5) = bob4.process(ChannelCommand.Close.MutualClose(null, null)) + assertIs>(bob5) + val shutdownBob = actionsBob5.findOutgoingMessage() + assertNull(actionsBob5.findOutgoingMessageOpt()) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(shutdownBob)) + assertIs>(alice5) + val shutdownAlice = actionsAlice5.findOutgoingMessage() + assertNull(actionsAlice5.findOutgoingMessageOpt()) + val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(shutdownAlice)) + assertIs>(bob6) + val closingSignedBob = actionsBob6.findOutgoingMessage() + val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(closingSignedBob)) + assertIs(alice6.state) + val closingSignedAlice = actionsAlice6.findOutgoingMessage() + val mutualCloseTx = actionsAlice6.findPublishTxs().first() + assertEquals(1, mutualCloseTx.txOut.size) + val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(closingSignedAlice)) + assertIs(bob7.state) + actionsBob7.hasPublishTx(mutualCloseTx) + } + @Test fun `override on-chain fee estimator -- initiator`() { val (alice, bob) = reachNormal() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt index 4b5335731..9451f3e37 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt @@ -819,6 +819,27 @@ class NormalTestsCommon : LightningTestSuite() { assertEquals(3, alice9.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) } + @Test + fun `recv CommitSig -- multiple htlcs in both directions -- non-initiator pays commit fees`() { + val (alice0, bob0) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) + assertFalse(alice0.commitments.params.localParams.paysCommitTxFees) + assertTrue(bob0.commitments.params.localParams.paysCommitTxFees) + val (nodes1, _, _) = addHtlc(75_000_000.msat, alice0, bob0) + val (alice1, bob1) = nodes1 + val (nodes2, _, _) = addHtlc(500_000.msat, alice1, bob1) + val (alice2, bob2) = nodes2 + val (nodes3, _, _) = addHtlc(10_000_000.msat, bob2, alice2) + val (bob3, alice3) = nodes3 + val (nodes4, _, _) = addHtlc(100_000.msat, bob3, alice3) + val (bob4, alice4) = nodes4 + val (alice5, bob5) = crossSign(alice4, bob4) + assertEquals(2, alice5.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) + assertEquals(2, bob5.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) + // Alice opened the channel, but Bob is paying the commitment fees. + assertEquals(alice5.commitments.latest.localCommit.spec.toLocal - alice5.commitments.latest.localChannelReserve.toMilliSatoshi(), alice5.commitments.availableBalanceForSend()) + assertTrue(bob5.commitments.availableBalanceForSend() < bob5.commitments.latest.localCommit.spec.toLocal - bob5.commitments.latest.localChannelReserve.toMilliSatoshi()) + } + @Test fun `recv CommitSig -- only fee update`() { val (alice0, bob0) = reachNormal() @@ -1422,6 +1443,22 @@ class NormalTestsCommon : LightningTestSuite() { assertEquals(bob.commitments.copy(changes = bob.commitments.changes.copy(remoteChanges = bob.commitments.changes.remoteChanges.copy(proposed = bob.commitments.changes.remoteChanges.proposed + fee))), bob1.commitments) } + @Test + fun `recv UpdateFee -- non-initiator pays commit fees`() { + val (alice, bob) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) + val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(7_500.sat)) + run { + val (alice1, _) = alice.process(ChannelCommand.MessageReceived(fee)) + assertIs>(alice1) + assertTrue(alice1.commitments.changes.remoteChanges.proposed.contains(fee)) + } + run { + val (bob1, actions1) = bob.process(ChannelCommand.MessageReceived(fee)) + assertIs>(bob1) + actions1.findOutgoingMessage().also { assertEquals(NonInitiatorCannotSendUpdateFee(alice.channelId).message, it.toAscii()) } + } + } + @Test fun `recv UpdateFee -- 2 in a row`() { val (_, bob) = reachNormal() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt index 407147edb..661444daf 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt @@ -367,6 +367,25 @@ class ShutdownTestsCommon : LightningTestSuite() { assertEquals(blob, shutdown.channelData) } + @Test + fun `recv Shutdown with non-initiator paying commit fees`() { + val (alice, bob) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) + assertFalse(alice.commitments.params.localParams.paysCommitTxFees) + assertTrue(bob.commitments.params.localParams.paysCommitTxFees) + // Alice can initiate a mutual close, even though she's not paying the commitment fees. + // Bob will send closing_signed first since he's paying the commitment fees. + val (alice1, actionsAlice1) = alice.process(ChannelCommand.Close.MutualClose(null, null)) + assertIs>(alice1) + val shutdownAlice = actionsAlice1.findOutgoingMessage() + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(shutdownAlice)) + assertIs>(bob1) + val shutdownBob = actionsBob1.findOutgoingMessage() + actionsBob1.findOutgoingMessage() + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) + assertIs>(alice2) + assertNull(actionsAlice2.findOutgoingMessageOpt()) + } + @Test fun `recv CheckHtlcTimeout -- no htlc timed out`() { val (alice, _) = init() 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 1713e3651..95340b309 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -1,7 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* -import fr.acinq.lightning.Lightning +import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.* import fr.acinq.lightning.blockchain.electrum.WalletState @@ -189,11 +189,14 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice to purchase inbound liquidity`() { val (alice, bob) = reachNormal() - 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 fundingRates = LiquidityAds.WillFundRates( + fundingRates = listOf(LiquidityAds.FundingRate(100_000.sat, 500_000.sat, 0, 250 /* 2.5% */, 0.sat)), + paymentTypes = setOf(LiquidityAds.PaymentType.FromChannelBalance), + ) + val liquidityRequest = LiquidityAds.RequestFunding(200_000.sat, fundingRates.findRate(200_000.sat)!!, LiquidityAds.PaymentDetails.FromChannelBalance) val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat), listOf()) val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) - assertEquals(spliceInit.requestFunds, liquidityRequest.requestFunds) + assertEquals(spliceInit.requestFunding, liquidityRequest) // 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. @@ -202,18 +205,19 @@ class SpliceTestsCommon : LightningTestSuite() { assertNull(defaultSpliceAck.willFund) val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey) run { - val willFund = leaseRate.signLease(bob.staticParams.nodeParams.nodePrivateKey, fundingScript, spliceInit.requestFunds!!) - val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, defaultSpliceAck.fundingPubkey, willFund) + val willFund = fundingRates.validateRequest(bob.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!)?.willFund + assertNotNull(willFund) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.requestedAmount, 0.msat, defaultSpliceAck.fundingPubkey, willFund) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) assertIs(alice2.state) assertIs(alice2.state.spliceStatus) actionsAlice2.hasOutgoingMessage() } run { - // 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) + // Bob uses a different funding script than what Alice expects. + val willFund = fundingRates.validateRequest(bob.staticParams.nodeParams.nodePrivateKey, ByteVector("deadbeef"), cmd.feerate, spliceInit.requestFunding!!)?.willFund + assertNotNull(willFund) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.requestedAmount, 0.msat, defaultSpliceAck.fundingPubkey, willFund) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) assertIs(alice2.state) assertIs(alice2.state.spliceStatus) @@ -221,7 +225,7 @@ class SpliceTestsCommon : LightningTestSuite() { } run { // Bob doesn't fund the splice. - val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, defaultSpliceAck.fundingPubkey, willFund = null) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.requestedAmount, 0.msat, defaultSpliceAck.fundingPubkey, willFund = null) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) assertIs(alice2.state) assertIs(alice2.state.spliceStatus) @@ -233,10 +237,10 @@ class SpliceTestsCommon : LightningTestSuite() { @OptIn(ExperimentalCoroutinesApi::class) fun `splice to purchase inbound liquidity -- not enough funds`() { val (alice, 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) + val fundingRate = LiquidityAds.FundingRate(100_000.sat, 10_000_000.sat, 0, 100 /* 1% */, 1.sat) 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 liquidityRequest = LiquidityAds.RequestFunding(1_000_000.sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance) + assertEquals(10_001.sat, liquidityRequest.fees(FeeratePerKw(1000.sat)).total) val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat), listOf()) val (bob1, actionsBob1) = bob.process(cmd) val bobStfu = actionsBob1.findOutgoingMessage() @@ -248,15 +252,27 @@ class SpliceTestsCommon : LightningTestSuite() { 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 liquidityRequest = LiquidityAds.RequestFunding(1_000_000.sat, fundingRate.copy(feeBase = 0.sat), LiquidityAds.PaymentDetails.FromChannelBalance) + assertEquals(10_000.sat, liquidityRequest.fees(FeeratePerKw(1000.sat)).total) + val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat), listOf()) + val (bob1, actionsBob1) = bob.process(cmd) + val bobStfu = actionsBob1.findOutgoingMessage() + val (_, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(bobStfu)) + val aliceStfu = actionsAlice1.findOutgoingMessage() + val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(aliceStfu)) + actionsBob2.hasOutgoingMessage().also { assertEquals(liquidityRequest, it.requestFunding) } + } + run { + // When we don't have enough funds in our channel balance, fees can be paid via future HTLCs. + val liquidityRequest = LiquidityAds.RequestFunding(1_000_000.sat, fundingRate, LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(randomBytes32()))) + assertEquals(10_001.sat, liquidityRequest.fees(FeeratePerKw(1000.sat)).total) val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat), listOf()) val (bob1, actionsBob1) = bob.process(cmd) val bobStfu = actionsBob1.findOutgoingMessage() val (_, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(bobStfu)) val aliceStfu = actionsAlice1.findOutgoingMessage() val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(aliceStfu)) - actionsBob2.hasOutgoingMessage() + actionsBob2.hasOutgoingMessage().also { assertEquals(liquidityRequest, it.requestFunding) } } } @@ -1513,7 +1529,7 @@ class SpliceTestsCommon : LightningTestSuite() { private fun createWalletWithFunds(keyManager: KeyManager, amounts: List): List { val script = keyManager.swapInOnChainWallet.legacySwapInProtocol.pubkeyScript return amounts.map { amount -> - val txIn = listOf(TxIn(OutPoint(TxId(Lightning.randomBytes32()), 2), 0)) + val txIn = listOf(TxIn(OutPoint(TxId(randomBytes32()), 2), 0)) val txOut = listOf(TxOut(amount, script), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) val parentTx = Transaction(2, txIn, txOut, 0) WalletState.Utxo(parentTx.txid, 0, 42, parentTx, WalletState.AddressMeta.Single) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt index 074ff3328..5b5fe0a36 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt @@ -4,6 +4,7 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Chain import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.* +import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.channel.* import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite @@ -29,6 +30,17 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) } + @Test + fun `recv AcceptChannel -- liquidity ads`() { + val (alice, _, accept) = init(requestRemoteFunding = TestConstants.bobFundingAmount) + val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(accept)) + assertIs>(alice1) + val purchase = alice1.state.liquidityPurchase + assertNotNull(purchase) + assertTrue(purchase.fees.total > 0.sat) + actions1.hasOutgoingMessage() + } + @Test fun `recv AcceptChannel -- without non-initiator contribution`() { val (alice, _, accept) = init(bobFundingAmount = 0.sat) @@ -81,6 +93,36 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { assertIs>(alice1) } + @Test + fun `recv AcceptChannel -- missing liquidity ads`() { + val (alice, _, accept) = init(requestRemoteFunding = TestConstants.bobFundingAmount) + val accept1 = accept.copy(tlvStream = accept.tlvStream.copy(records = accept.tlvStream.records.filterNot { it is ChannelTlv.ProvideFundingTlv }.toSet())) + val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(accept1)) + assertIs>(alice1) + val error = actions1.hasOutgoingMessage() + assertEquals(error, Error(accept.temporaryChannelId, MissingLiquidityAds(accept.temporaryChannelId).message)) + } + + @Test + fun `recv AcceptChannel -- invalid liquidity ads amount`() { + val (alice, _, accept) = init(requestRemoteFunding = TestConstants.bobFundingAmount) + val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(accept.copy(fundingAmount = TestConstants.bobFundingAmount - 100.sat))) + assertIs>(alice1) + val error = actions1.hasOutgoingMessage() + assertEquals(error, Error(accept.temporaryChannelId, InvalidLiquidityAdsAmount(accept.temporaryChannelId, TestConstants.bobFundingAmount - 100.sat, TestConstants.bobFundingAmount).message)) + } + + @Test + fun `recv AcceptChannel -- invalid liquidity ads signature`() { + val (alice, _, accept) = init(requestRemoteFunding = TestConstants.bobFundingAmount) + val willFund = ChannelTlv.ProvideFundingTlv(accept.willFund!!.copy(signature = randomBytes64())) + val accept1 = accept.copy(tlvStream = accept.tlvStream.copy(records = accept.tlvStream.records.filterNot { it is ChannelTlv.ProvideFundingTlv }.toSet() + willFund)) + val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(accept1)) + assertIs>(alice1) + val error = actions1.hasOutgoingMessage() + assertEquals(error, Error(accept.temporaryChannelId, InvalidLiquidityAdsSig(accept.temporaryChannelId).message)) + } + @Test fun `recv AcceptChannel -- invalid max accepted htlcs`() { val (alice, _, accept) = init() @@ -154,19 +196,24 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, + requestRemoteFunding: Satoshi? = null, zeroConf: Boolean = false, ): Triple, LNChannel, AcceptDualFundedChannel> { - val (alice, bob, open) = TestsHelper.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, zeroConf) + val (alice, bob, open) = TestsHelper.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, requestRemoteFunding, zeroConf) assertEquals(open.fundingAmount, aliceFundingAmount) assertEquals(open.pushAmount, alicePushAmount) - assertEquals(open.tlvStream.get(), ChannelTlv.ChannelTypeTlv(channelType)) + assertEquals(open.channelType, channelType) + requestRemoteFunding?.let { + assertTrue(open.channelFlags.nonInitiatorPaysCommitFees) + assertNotNull(open.requestFunding) + } val (bob1, actions) = bob.process(ChannelCommand.MessageReceived(open)) assertIs>(bob1) val accept = actions.hasOutgoingMessage() assertEquals(open.temporaryChannelId, accept.temporaryChannelId) assertEquals(accept.fundingAmount, bobFundingAmount) assertEquals(accept.pushAmount, bobPushAmount) - assertEquals(accept.tlvStream.get(), ChannelTlv.ChannelTypeTlv(channelType)) + assertEquals(accept.channelType, channelType) when (zeroConf) { true -> assertEquals(0, accept.minimumDepth) false -> assertEquals(3, accept.minimumDepth) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt index a26995701..c5f5f8375 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt @@ -201,10 +201,11 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, + requestRemoteFunding: Satoshi? = null, zeroConf: Boolean = false, ): Fixture { return if (zeroConf) { - val (alice, commitAlice, bob, commitBob) = WaitForFundingSignedTestsCommon.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, zeroConf) + val (alice, commitAlice, bob, commitBob) = WaitForFundingSignedTestsCommon.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, requestRemoteFunding, zeroConf) val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitBob)) assertIs>(alice1) assertTrue(actionsAlice1.isEmpty()) @@ -221,7 +222,7 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { actionsAlice2.has() Fixture(alice2, channelReadyAlice, bob1, channelReadyBob) } else { - val (alice, bob, fundingTx) = WaitForFundingConfirmedTestsCommon.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount) + val (alice, bob, fundingTx) = WaitForFundingConfirmedTestsCommon.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, requestRemoteFunding) val (alice1, actionsAlice1) = alice.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_FUNDING_DEPTHOK, 42, 0, fundingTx))) assertIs>(alice1) val channelReadyAlice = actionsAlice1.findOutgoingMessage() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt index ea7881698..4d0324fee 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt @@ -398,6 +398,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, + requestRemoteFunding: Satoshi? = null, ): Fixture { val (alice, commitAlice, bob, commitBob, walletAlice) = WaitForFundingSignedTestsCommon.init( channelType, @@ -408,6 +409,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { bobFundingAmount, alicePushAmount, bobPushAmount, + requestRemoteFunding, zeroConf = false ) val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitBob)) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt index 2ae04aa62..1160524d9 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt @@ -302,10 +302,11 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, + requestRemoteFunding: Satoshi? = null, zeroConf: Boolean = false, channelOrigin: Origin? = null ): Fixture { - val (a, b, open) = TestsHelper.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, zeroConf, channelOrigin) + val (a, b, open) = TestsHelper.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, requestRemoteFunding, zeroConf, channelOrigin) val (b1, actions) = b.process(ChannelCommand.MessageReceived(open)) val accept = actions.findOutgoingMessage() assertIs>(b1) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt index f888e9bfa..b95cfbe0b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt @@ -25,23 +25,21 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { val (alice, commitSigAlice, bob, commitSigBob) = init() val commitInput = alice.state.signingSession.commitInput run { - val (_, _) = alice.process(ChannelCommand.MessageReceived(commitSigBob)) - .also { (state, actions) -> - assertIs(state.state) - assertTrue(actions.isEmpty()) - } + alice.process(ChannelCommand.MessageReceived(commitSigBob)).also { (state, actions) -> + assertIs(state.state) + assertTrue(actions.isEmpty()) + } } run { - val (_, _) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) - .also { (state, actions) -> - assertIs(state.state) - assertEquals(actions.size, 5) - actions.hasOutgoingMessage().also { assertFalse(it.channelData.isEmpty()) } - actions.findWatch().also { assertEquals(WatchConfirmed(state.channelId, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, 3, BITCOIN_FUNDING_DEPTHOK), it) } - actions.find().also { assertEquals(TestConstants.bobFundingAmount.toMilliSatoshi() + TestConstants.alicePushAmount - TestConstants.bobPushAmount, it.amount) } - actions.has() - actions.find().also { assertEquals(ChannelEvents.Created(state.state), it.event) } - } + bob.process(ChannelCommand.MessageReceived(commitSigAlice)).also { (state, actions) -> + assertIs(state.state) + assertEquals(actions.size, 5) + actions.hasOutgoingMessage().also { assertFalse(it.channelData.isEmpty()) } + actions.findWatch().also { assertEquals(WatchConfirmed(state.channelId, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, 3, BITCOIN_FUNDING_DEPTHOK), it) } + actions.find().also { assertEquals(TestConstants.bobFundingAmount.toMilliSatoshi() + TestConstants.alicePushAmount - TestConstants.bobPushAmount, it.amount) } + actions.has() + actions.find().also { assertEquals(ChannelEvents.Created(state.state), it.event) } + } } } @@ -49,24 +47,49 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { fun `recv CommitSig -- zero conf`() { val (alice, commitSigAlice, bob, commitSigBob) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) run { - val (_, _) = alice.process(ChannelCommand.MessageReceived(commitSigBob)) - .also { (state, actions) -> - assertIs(state.state) - assertTrue(actions.isEmpty()) - } + alice.process(ChannelCommand.MessageReceived(commitSigBob)).also { (state, actions) -> + assertIs(state.state) + assertTrue(actions.isEmpty()) + } } run { - val (_, _) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) - .also { (state, actions) -> - assertIs(state.state) - assertEquals(actions.size, 6) - actions.hasOutgoingMessage().also { assertFalse(it.channelData.isEmpty()) } - actions.hasOutgoingMessage().also { assertEquals(ShortChannelId.peerId(bob.staticParams.nodeParams.nodeId), it.alias) } - actions.findWatch().also { assertEquals(state.commitments.latest.fundingTxId, it.txId) } - actions.find().also { assertEquals(TestConstants.bobFundingAmount.toMilliSatoshi() + TestConstants.alicePushAmount - TestConstants.bobPushAmount, it.amount) } - actions.has() - actions.find().also { assertEquals(ChannelEvents.Created(state.state), it.event) } - } + bob.process(ChannelCommand.MessageReceived(commitSigAlice)).also { (state, actions) -> + assertIs(state.state) + assertEquals(actions.size, 6) + actions.hasOutgoingMessage().also { assertFalse(it.channelData.isEmpty()) } + actions.hasOutgoingMessage().also { assertEquals(ShortChannelId.peerId(bob.staticParams.nodeParams.nodeId), it.alias) } + actions.findWatch().also { assertEquals(state.commitments.latest.fundingTxId, it.txId) } + actions.find().also { assertEquals(TestConstants.bobFundingAmount.toMilliSatoshi() + TestConstants.alicePushAmount - TestConstants.bobPushAmount, it.amount) } + actions.has() + actions.find().also { assertEquals(ChannelEvents.Created(state.state), it.event) } + } + } + } + + @Test + fun `recv CommitSig -- liquidity ads`() { + val (alice, commitSigAlice, bob, commitSigBob) = init(requestRemoteFunding = TestConstants.bobFundingAmount, alicePushAmount = 0.msat, bobPushAmount = 0.msat) + val purchase = alice.process(ChannelCommand.MessageReceived(commitSigBob)).let { (state, actions) -> + assertIs(state.state) + assertTrue(actions.isEmpty()) + val purchase = state.state.liquidityPurchase + assertNotNull(purchase) + assertEquals(TestConstants.bobFundingAmount / 100, purchase.fees.serviceFee) + val localCommit = state.state.signingSession.localCommit.right!! + assertEquals(TestConstants.aliceFundingAmount - purchase.fees.total, localCommit.spec.toLocal.truncateToSatoshi()) + assertEquals(TestConstants.bobFundingAmount + purchase.fees.total, localCommit.spec.toRemote.truncateToSatoshi()) + purchase + } + bob.process(ChannelCommand.MessageReceived(commitSigAlice)).also { (state, actions) -> + assertIs(state.state) + assertEquals(actions.size, 5) + assertEquals(TestConstants.bobFundingAmount + purchase.fees.total, state.commitments.latest.localCommit.spec.toLocal.truncateToSatoshi()) + assertEquals(TestConstants.aliceFundingAmount - purchase.fees.total, state.commitments.latest.localCommit.spec.toRemote.truncateToSatoshi()) + actions.hasOutgoingMessage().also { assertFalse(it.channelData.isEmpty()) } + actions.findWatch().also { assertEquals(BITCOIN_FUNDING_DEPTHOK, it.event) } + actions.find().also { assertEquals((TestConstants.bobFundingAmount + purchase.fees.total).toMilliSatoshi(), it.amount) } + actions.has() + actions.find().also { assertEquals(ChannelEvents.Created(state.state), it.event) } } } @@ -156,6 +179,36 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { } } + @Test + fun `recv TxSignatures -- liquidity ads`() { + val (alice, commitSigAlice, bob, commitSigBob) = init(requestRemoteFunding = TestConstants.bobFundingAmount, alicePushAmount = 0.msat, bobPushAmount = 0.msat) + val commitInput = alice.state.signingSession.commitInput + val txSigsBob = run { + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) + assertIs(bob1.state) + actionsBob1.hasOutgoingMessage() + } + run { + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitSigBob)) + assertIs(alice1.state) + assertTrue(actionsAlice1.isEmpty()) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(txSigsBob)) + assertIs(alice2.state) + assertEquals(7, actionsAlice2.size) + assertTrue(actionsAlice2.hasOutgoingMessage().channelData.isEmpty()) + actionsAlice2.has() + val watchConfirmedAlice = actionsAlice2.findWatch() + assertEquals(WatchConfirmed(alice2.channelId, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, 3, BITCOIN_FUNDING_DEPTHOK), watchConfirmedAlice) + val liquidityPurchase = actionsAlice2.find() + assertEquals(liquidityPurchase.txId, txSigsBob.txId) + assertIs(liquidityPurchase.purchase.paymentDetails) + assertEquals(ChannelEvents.Created(alice2.state), actionsAlice2.find().event) + val fundingTx = actionsAlice2.find().tx + assertEquals(fundingTx.txid, txSigsBob.txId) + assertEquals(commitInput.outPoint.txid, fundingTx.txid) + } + } + @Test fun `recv TxSignatures -- zero-conf`() { val (alice, commitSigAlice, bob, commitSigBob) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) @@ -298,6 +351,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, + requestRemoteFunding: Satoshi? = null, zeroConf: Boolean = false, channelOrigin: Origin? = null ): Fixture { @@ -310,6 +364,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { bobFundingAmount, alicePushAmount, bobPushAmount, + requestRemoteFunding, zeroConf, channelOrigin ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt index 1d245a177..b13d82fae 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -87,7 +87,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val keyManager = LocalKeyManager(seed, Chain.Regtest, TestConstants.aliceSwapInServerXpub) val fundingKeyPath = makeFundingKeyPath(ByteVector("be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb"), isInitiator = true) - val localParams = TestConstants.Alice.channelParams().copy(fundingKeyPath = fundingKeyPath) + val localParams = TestConstants.Alice.channelParams(payCommitTxFees = true).copy(fundingKeyPath = fundingKeyPath) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) assertEquals(channelKeys.fundingPubKey(0), PrivateKey.fromHex("730c0f99408dbfbff00146acf84183ce539fabeeb22c143212f459d71374f715").publicKey()) @@ -104,7 +104,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val keyManager = LocalKeyManager(seed, Chain.Regtest, TestConstants.aliceSwapInServerXpub) val fundingKeyPath = makeFundingKeyPath(ByteVector("06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488"), isInitiator = false) - val localParams = TestConstants.Alice.channelParams().copy(fundingKeyPath = fundingKeyPath) + val localParams = TestConstants.Alice.channelParams(payCommitTxFees = true).copy(fundingKeyPath = fundingKeyPath) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) assertEquals(channelKeys.fundingPubKey(0), PrivateKey.fromHex("cd85f39fad742e5c742eeab16f5f1acaa9d9c48977767c7daa4708a47b7222ec").publicKey()) @@ -121,7 +121,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val keyManager = LocalKeyManager(seed, Chain.Mainnet, DeterministicWallet.encode(dummyExtendedPubkey, testnet = false)) val fundingKeyPath = makeFundingKeyPath(ByteVector("ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322"), isInitiator = true) - val localParams = TestConstants.Alice.channelParams().copy(fundingKeyPath = fundingKeyPath) + val localParams = TestConstants.Alice.channelParams(payCommitTxFees = true).copy(fundingKeyPath = fundingKeyPath) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) assertEquals(channelKeys.fundingPubKey(0), PrivateKey.fromHex("b3b3f1af2ef961ee7aa62451a93a1fd57ea126c81008e5d95ced822cca30da6e").publicKey()) @@ -138,7 +138,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val keyManager = LocalKeyManager(seed, Chain.Mainnet, DeterministicWallet.encode(dummyExtendedPubkey, testnet = false)) val fundingKeyPath = makeFundingKeyPath(ByteVector("2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b"), isInitiator = false) - val localParams = TestConstants.Alice.channelParams().copy(fundingKeyPath = fundingKeyPath) + val localParams = TestConstants.Alice.channelParams(payCommitTxFees = true).copy(fundingKeyPath = fundingKeyPath) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) assertEquals(channelKeys.fundingPubKey(0), PrivateKey.fromHex("033880995016c275e725da625e4a78ea8c3215ab8ea54145fa3124bbb2e4a3d4").publicKey()) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt b/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt index 09fcd88d4..9897f4bb2 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt @@ -231,7 +231,7 @@ class PeerTest : LightningTestSuite() { val open = bob2alice.expect() assertTrue(open.fundingAmount < 500_000.sat) // we pay the mining fees assertTrue(open.channelFlags.nonInitiatorPaysCommitFees) - assertEquals(open.requestFunds?.amount, 100_000.sat) // we always request funds from the remote, because we ask them to pay the commit tx fees + assertEquals(open.requestFunding?.requestedAmount, 100_000.sat) // we always request funds from the remote, because we ask them to pay the commit tx fees assertEquals(open.channelType, ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) // We cannot test the rest of the flow as lightning-kmp doesn't implement the LSP side that responds to the liquidity ads request. } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt index ca9214aad..0c7101fe9 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt @@ -31,7 +31,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { TestConstants.trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144)), TestConstants.swapInParams, - TestConstants.leaseRate + TestConstants.fundingRates, ) @Test diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index f15a933fc..5742b35eb 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -1,9 +1,6 @@ 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.* @@ -18,7 +15,6 @@ import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.value 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.* @@ -127,8 +123,8 @@ class StateSerializationTestsCommon : LightningTestSuite() { @Test fun `liquidity ads lease backwards compatibility`() { - // The serialized data was created with lightning-kmp v1.5.12. run { + // The serialized data was created with lightning-kmp v1.5.12. val bin = Hex.decode( "0402b83e19a809cbef04a58caa852693cf7ddebd0fd926875bd1b195fcf5225a869a01010410101000037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c09fe8824646afed5cc44a9fecb08263bfee1c34a83feba92e4e8fe65d93543fecb5ee602fe43ec9100fe80000001fd044cfe59682f000090640116001405e0104aa726e34ff5cd3a6320d05c0862b5b01c13280880000000000000000000001000101a51020362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06fd03e8fe59682f00fd03e89064026843554d5e604ffd3fcabc56cefe5849abbb7fd395f36bcf3e9550594aace9690236633b1e8f7a54ef367482c31c74162f4fd3e4c7d78694e2c6d769af6e33047202e97df1b0423c20ba41a1955e71cfcb96cec4f636b1d310be78e989f92229edb302b3c6959eefecdee406b9b4df0d76126f2c5038811b27abf44738e6db1be0bdf11408220222000000000000000000001000142a5102000000000000000000000100022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b02fd024a02000000000102d70488b7709a2ea05d808ec1f46d6ec100f85b3c1f1fe909d3dc6332b1b9153a0000000000fdffffff3bd4776fba4675b6b2e56d4ef0b81159c4319cf9942918fc29798f06b95a84270000000000fdffffff0140420f00000000002200201aaf87557cdb6e31b338fa3e6be95fc7d8ee72b80ac8147fc35ce975d145a67e03473044022012d7967e817c6f369aa4f9a69f78ac1008a7f0ea8f62e3510b8ec2ed3e9e109302202fd1fd54d104f7e2fe0a5404edccb7b3f786cd5f82447bcca2bd23fee34cb596014830450221009c192a826ecf488216eea24279c6699c774e9c930194a8357a61d2e4e91b9c1e02202a085efb5276f6d34ec310b8d2ede7bf65fe63d4027b93b89902d3c4e179e91d014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b26803483045022100ec3085bef0557c2329c0a9ff1df9374973f42247bab7cdcff10b2fa5fa8b89680220040eeeb7576228bcde8d62f930c368010c8c665467299ea7f3a735140a27ae3301483045022100e628ebd5b4f433c1e4127b7d7fb0f625a6dcb1e4cf8cd62aa4a120312c723138022020f22620ebb280dfc8ad5eb1c1671ce13c4cd9bb7166cd50f3a8577a8b79b167014d21020bc21d9cb5ecc60fc2002195429d55ff4dfd0888e377a1b3226b4dc1ee7cedf3ad2102d8c2f4fe8a017ff3a30eb2a4477f3ebe64ae930f67f907270712a70b18cb8951ac7364024065b268801a0600fd1388cc0047b83e19a809cbef04a58caa852693cf7ddebd0fd926875bd1b195fcf5225a869a1f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f180000fd025b409c192a826ecf488216eea24279c6699c774e9c930194a8357a61d2e4e91b9c1e2a085efb5276f6d34ec310b8d2ede7bf65fe63d4027b93b89902d3c4e179e91dfd025d40ec3085bef0557c2329c0a9ff1df9374973f42247bab7cdcff10b2fa5fa8b8968040eeeb7576228bcde8d62f930c368010c8c665467299ea7f3a735140a27ae33000000fd1388fe2faf0800fe0bebc20000241f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f18000000002b40420f00000000002200201aaf87557cdb6e31b338fa3e6be95fc7d8ee72b80ac8147fc35ce975d145a67e475221022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b2103cb734f5eac1ce94a8d571b17db09bafe974b5ebae76416667bae888a11fbb9a652aefd01bc020000000001011f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f180000000000bc63fb80044a010000000000002200202a962bfb8410b4d8515002cdca69755a4e7b2f35c1d3c8ca23c8c2eb2c663ea84a0100000000000022002086bc033f5435e003d1be7f8d21ffcba84d5177f72d9cab95ddca49557b0db016400d03000000000022002066b48675ced51cc392a9c208c5b6b37173fcd1c88601188c0a55fa5db4a3df87781c0c0000000000220020d4234a5c556b42832ec8985edc4f377809fa254fc509ee5533de099a91579848040048304502210097e686048e14f2e862970d384734ec72d4799afaf7a67f679e5ef1c685e37279022052f6c4647e44bc9ea197431e47903398fc077314e47a19046f5c6f6b130d9fca0147304402204c85f5c533eaf8bfd8fcdbfca7184522eec4d9f3028226450051a7cedd15d0d802202a5ead9a0284bdec30d52326eb2ddc8d6e00a991f14faf5656a05769eb52ce8401475221022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b2103cb734f5eac1ce94a8d571b17db09bafe974b5ebae76416667bae888a11fbb9a652ae34d5bc20000000fd1388fe0bebc200fe2faf080002613a5fffafc39766ca252b1470bc96161211c3bf0533aa04fd7cb23d05bf6e02cd117f0145d8c5780d5fd5145251257cfc0898145a99750c181927bbe526f4a600000001035a3feef004f091d822802e715c2d6e9e75020af11be99fd3d4c30e2c6ffa2a480000fd0760040042686e87cb623bed5376be9b0b6314dc871fe35781bbbccf91d12cd07adf711c72e520bb2ff4cf3fa9611868a324e153ebf63586083258028b322fbce9995ddd2593a122a97b082d0f2d58a895c9fa06fd535089a04e05fcdf0e8907492a6c5244541b806bcc49120e464ec1eca87840b41e694725528fe8d7d94f640d958d0b43c17478977617d134a4a1fa85c45f135bd626e70ca862cca3e4861e88771a120bfa6898971b4dd3b022dc920f481cca8102fc101e69ac2d92e18773ef6b262356370514cad85f6531f3ae1d8f404e04172917483227ad9ca8ac29a0b01302ecb67adf54e8289f6dedf3c323f1e52daee77b3bd2524a2959f5dc0dec361212b77c593b67419adeb7aeb75b39b9daca003755b0fd50653724df439d95e6a00bb6afb303ac8a39bc47ebc6a0b906532fd14140e6ca727c4b85ca970da5b249374ec813d1f78ff7171711bfd2a2bc204fbe29834bbf8b9bdf1be88f987315e2d3cb56b50056feee5970c9939af176d829e08106dc4101f5f18a8f04c8067e375505f7bea0a20ccadccf3ece22eccb873efd221877100e08ab9b1c241ef36176dc0ab7b41c17a5bddbf243e22c2dc5f5f9b410a90b6e77e09bc95d7e9e50c5a8afdc462408c453d37571a695dbf37945565b605b0b13c70ce03580d0c4c36f453c7a0a1a7418fdaf057c1c3cbbd9f3fdbf667f3d7342b24c4cc5b7b078891b2fb31d2a2f37f9beab0a503c34df80c39eb19c9194bf4b04c164dade1b176c0cc1690ff64bcfc3f4365d7f7ab7777ee20374c1707a794e32eb7792b20cab4d67cd0d226eb93643d35dd479567a90245e518ce4150709a7d550d3b175ca880393830fe784aeac55811ccf62ce15bac14630263ba1c182827646a4bbd26ddbad3100b23b04afc042cefb6489fe1c77f38826d8a39c9cdc906d73317eaa33cf6ca2ed8756925c8919622ee80a87d66f3eb2f43534c6ecb749b2c473d32c7eaaff659d84bf680c702c1e13adcfadd8e907b886300e07cd431fab9affb451196e3dfd77cfafb8de0e1fe65e66ddb7ba594b7369aa52113c3d752b312fbc51a17d504244933cee42909c60c517a4411f841af48799e719554a07bdd3ffbeb14e694e913514856656e7fcdfaaf84daf8f0b2ef4639c0682524874dd7eb4c16844074ac0d97354a7e643a2e3220bf30855c54461464c0bf82bbabbba7fe407e1f2fa394f8e3822c507e2d705e32e13f2a50a5f2c8b3d73b63847cf985f06e25de5629e8a570092a92996c655f5ef3871d2a3a4b556c9b52d40b828475c35262c6f9f5bbfbd3e6ebf09864bfb3d3dcf4f78961d4fc85fd9b9c924ce6ba8c6df4c8525ee4c3f67f97e361566b31a9df0c4bc6da36e9e0e47f0b91a67f489fba2d0eddee58bac5ddc4cfde2c74947a27b49e89fa838bbfaeae6605a7e2dad611252a5d30a5c99592de44aad8fb4253880ce16f60c3231f9824898751e99eb4d554bea9042843a56d5239f8d3aae93696583970822429beef912dcd7129693e11ad39ec0191ee5fc06b58544fbed9c6c12ad73690bf64bb78fb16902e97bc8f8fcbdba321ce0241141542cca9235489459b1b50d44d76bb36492241dfa43f5252331556a9c618f14f89f9b7dc9944498a73ce242a0ec0b2953b25cc5b11c25dbf336a6319f479e561c2c4f6f196a43f93ddb22da68bfe3909c3cb21503a554b895ef4dbd0033684b16b974042386eddef9faf63389d6d07bafdc934884589333da2fd0a6e1e15bdcab663c562e00e887c1b9b5296b8bee678a21d11c45005729bb0e6eb225cb9a480673483634ad21ceb0bec52ec78b13058847e750412ab67e3631187c289aeba97371926027b348bc932b600ece0fa5a8fa69a18d44e51eb7857011e72484e1e8393d94382ddd8e012b676dde44da75eda81aa0ba5ed8e474b7465c5af2b1a1de7aa870fdd191de0caf78875880ab6d5d3fcef3057002e17a07f9e870ae13634cef3bf8a60b41104c39145a1b6dd44f37c3b7d3c78f2f6f1fe83d38c2a54c1597270fed60b157fbdf431d51e98899f6894ad41c4142e271af7d5557faaacdc337caa9f3ecae7a0dbfdf27057c437556c9c9442fdca9e9a07e61741ee56a3db89e29d3d4ab7fc3feda7d737d5ceb3787d103efbd72772a1ebf65541d6d7cdb5ac82e834060d1b9f58be80db537f9ca696a57f21d74fdee7947ff90cb238c3f6f7e084012a1c1466c230d841e7b3cc0b670696e7f3b6186770b2e61c3bae625da4232831058ad73c87744be94f301ce839d6fd46d62fec3edd60ea7cf92fd119b98232cd9621f5e5d37bde331e2db7d4742531e93531676150bbcf8dd28e7acd3181128ebfc36c49aa7ced8fb5af96833769deb6d46f49010ce92ba80e5f7e841360ae01f86a39e24383cab02d31af0745f70b752ebf6e149e38c1c4bc7a6f39124555b449e4887fca29bdc51efd56c0f682458a41a5cf28697c3f79c980df742e80aeae1dcc91309389885c3f2386f4edd14956bf562884f5983bb906fea2bc394efbb67de76720209ae47b3a6ed5e00e82287f3586113de2476d514dc58086e03890ce3247a0d2969a32c995ec7b306c8c6b6737310f8d2bf2499d1659b523e0ceb00eba41af9bb32a81fa230a560a866606481a5086da9ea8b61d0d4dd8b0cff00002a00000000008a01023a973e5a95ba9356ebb5d884eda57169e214d46afbe7e0ede00f4bf4a3acc0336825ba58984754922a4f3a28cbcb5fa52a9b983210bb992eef6e2dfe391a806d06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00002a00000000006572fa6b0101009000000000000003e8000000640000000a000000003b9aca000000000001b83e19a809cbef04a58caa852693cf7ddebd0fd926875bd1b195fcf5225a869a01fdc256000101241f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f18000000002b40420f00000000002200201aaf87557cdb6e31b338fa3e6be95fc7d8ee72b80ac8147fc35ce975d145a67e475221022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b2103cb734f5eac1ce94a8d571b17db09bafe974b5ebae76416667bae888a11fbb9a652ae00022b1d92a4eb38c6897dd9f9c6931be595d10037831dc20bdf71b8aa5f0f739a8b0369b5f974eb92be821826fe7d0969744283c917f3411655f190b56fc5930c3cfa00fe00061a80fd044cfd00fd0101010102241f89a5180b0bee658784378bf354a475d4ae359997e2d93964c892dfacb16f1800000000fefffffffdfe2faf0800fe0bebc2000104220020431b8ae82b805d47da74e00ecde92e930c666124d5930fae2958ba309885380ffe32a627f0fe0bebc2000102007d02000000011134cd9d56bee35f5db7b8a8e17ae69eabc8738653da42247fad8996e7419b7d0200000000000000000250c3000000000000220020778f86e213a518e8336ec940874ccf4b92910332f5b3c0265374a86dbd34e6a596000000000000001600141240d4b7fcfbfbd7234cf2dedf071673a0c1e5590000000000fefffffffd03d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc49190256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdfd6540000000fe00061a80cc0047b83e19a809cbef04a58caa852693cf7ddebd0fd926875bd1b195fcf5225a869a3448c2e0ddb06dce57bc06c2a3d447d958958b4cef6499a4d0bb1eb36d28e2c60000fd0259406e731b1649d06176d0ecf590b385b0123f685cb93ef518124d6b9cbd7062c4265af87d8986ac6fd525d0e738dff61e00d18ce04fe9cee80a99744a65fcd4fb04fd025b4045e91597bd2826f18f58321051c3e0a6728ebbb0d633eba9428139335460c9da322b01137b5a3b5041fd526d5043ffd586d8ef44b7ccb27c9605ab6bb2943d27000000fd1388fe32a627f0fe0bebc20000243448c2e0ddb06dce57bc06c2a3d447d958958b4cef6499a4d0bb1eb36d28e2c6000000002b9604100000000000220020431b8ae82b805d47da74e00ecde92e930c666124d5930fae2958ba309885380f47522102c3cdf2cd990536f7ac520b3a2f66c0a6e302c2fe15a8c3baee24eba1cb9a8b02210369b5f974eb92be821826fe7d0969744283c917f3411655f190b56fc5930c3cfa52aedf02000000013448c2e0ddb06dce57bc06c2a3d447d958958b4cef6499a4d0bb1eb36d28e2c60000000000bc63fb80044a010000000000002200204725be4ed490e91c4ad5824fcc202c53787b147d4ad28a30aafeff90400d17634a010000000000002200204eea61d0b3215da0c03b103e98d24aa4466e0fb1ba80f8d8d85d33ed0a969da3400d03000000000022002066b48675ced51cc392a9c208c5b6b37173fcd1c88601188c0a55fa5db4a3df87cede0c0000000000220020d4234a5c556b42832ec8985edc4f377809fa254fc509ee5533de099a9157984834d5bc20000000fd1388fe0bebc200fe32a627f05f4cb3d37e1f420f5f39b563929d1a82a8e93ee4d864eaca609508b3ad2b6a5702cd117f0145d8c5780d5fd5145251257cfc0898145a99750c181927bbe526f4a600" ) @@ -136,17 +132,11 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertIs(state) val splice = state.spliceStatus assertIs(splice) - assertNull(splice.session.liquidityLease) + assertNull(splice.liquidityPurchase) assertTrue(splice.session.localCommit.isLeft) - assertTrue(state.liquidityLeases.isEmpty()) - val state1 = state.copy( - 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)) - ) - ) - assertEquals(state1, Serialization.deserialize(Serialization.serialize(state1)).value) } run { + // The serialized data was created with lightning-kmp v1.5.12. val bin = Hex.decode( "040238ca11203794f6ef69fc7df13b91e7848c837632bd1407bec63d9ade0330a3f701010410101000037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c09fe5b62e3a5fecf5fd832fec7e172c8fe15fff232fe17015da3fe71c99b0afea90a802bfe3993f9cefe80000001fd044cfe59682f000090640116001405e0104aa726e34ff5cd3a6320d05c0862b5b01c13280880000000000000000000001000101a51020362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06fd03e8fe59682f00fd03e890640349b56ccb150862271cdc1b280d484db844d48ee85f07515cc6e847d1d32a147a02d78bdcc7f2160d5ccdcfbc0ba3dcc4b547d06f38cb65ffac7589ae5ad529d08a03e62f14d41cdf68d7ac982dc03e6492d093d7aec4ef7d1765d5a5bcc995e204b602e95f9d9281919ff9cae84e7dc3b5b1ef1161a6c503617e4f3207e05d722c15a71408220222000000000000000000001000142a510200000000000000000000010002e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f02fd02490200000000010256724a067da52a008fa768ad15f2a003054882bf0c09693a2c0f386eb5d8c4340000000000fdffffff3be96364f874547c41cf86f1f57c35029a6e082700bcd25f5b3cbd742417ced80000000000fdffffff0140420f000000000022002026fa4316fb307d348467c0edd3b8bb1eff9ca3ec04367ce5788885712ec400b003483045022100e66fec8848962770b61c9835b4e09954dd6dec98c2cd621a8592defe58796ef4022067aa7588b08614346f544322577b7797116c727f452b06cad40c2f9b2af8774601473044022064df9cd5e6e68bad426f4ffe06ccee9eeb5769f4fe65931bc54c0df618accbf1022018488e51b6c6969771de015e9ac41f9426701c5c961df270675cb1476920de98014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b26803483045022100e9882e40951f43da434e1d86cf9830f5499f8dbb846f5131becfa0fea181ef4602206146db93b26271798d40f453d5255557191d0f45f0b5fb0a0d78ce261afcd386014730440220379f14da69fa108168d32351e5c9127479a8e1858f89cdf1a70c66010348a6c1022039bc27eabe21078071c35c35adc1c11ab8e3f70fc9d4e649f6527618e70ba647014d21020bc21d9cb5ecc60fc2002195429d55ff4dfd0888e377a1b3226b4dc1ee7cedf3ad2102d8c2f4fe8a017ff3a30eb2a4477f3ebe64ae930f67f907270712a70b18cb8951ac7364024065b268801a0600fd1388cc004738ca11203794f6ef69fc7df13b91e7848c837632bd1407bec63d9ade0330a3f7f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d240000fd025b4064df9cd5e6e68bad426f4ffe06ccee9eeb5769f4fe65931bc54c0df618accbf118488e51b6c6969771de015e9ac41f9426701c5c961df270675cb1476920de98fd025d40e9882e40951f43da434e1d86cf9830f5499f8dbb846f5131becfa0fea181ef466146db93b26271798d40f453d5255557191d0f45f0b5fb0a0d78ce261afcd386000000fd1388fe2faf0800fe0bebc2000024f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d24000000002b40420f000000000022002026fa4316fb307d348467c0edd3b8bb1eff9ca3ec04367ce5788885712ec400b04752210243fc4580744090c5009be7bb3b85b91c6dfdfeffc67e37a808a89431b24461bc2102e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f52aefd01bb02000000000101f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d240000000000620f4680044a01000000000000220020ad6e712f2f3ff4a279f7c1cc4bb31d88c98ad807537616a4f53beed64cb5091d4a01000000000000220020c79d8484429b469c3230783f14fd3228d9b6da520dac471f3b3d826c59ad0b52400d0300000000002200202a563e407578aae18800c2388759b5bad0ceef5a3759de39d7acedffd868d30d781c0c0000000000220020bf57f237188dde15a4a0947bf64cd985f95163d77fee3d8372cdce323de1efdf04004730440220178102bdc1fce536c08c0660749208ff2d1e0aa9bb5ad1b98b120e9e5e263324022057c7033283b0f397f98378d0b2666879ed5da822445ed43dbb26563644d397370147304402206dd5be99fe9473e0221aaf2e37a72fbe38f666c4129c0c164cf9bd2eb7d93fe802204ec11e93732a56f4e759c4ea8359cfda3e1db4cef0cf91f79b1d9906e60eb2b3014752210243fc4580744090c5009be7bb3b85b91c6dfdfeffc67e37a808a89431b24461bc2102e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f52ae65031b20000000fd1388fe0bebc200fe2faf0800b3822442b3a5d53e6410fe106e9ec9408a9bb0b6b6f34c0ad39d0811b466f86a03cb5d3b7310e6532d403dc7ad5857869e6ace3a2a1a7cf710c8a9b307a5ebb02c0000000103b6668d222ea88836cd25b40784f759c9cc0ff9ac03ef0408a08543ae185405060000fd076004003749c4912f86c5594f2e9775e78a8292f386ca75711bbb7f89c841e99158d24e1aea0c5bf2f810cedf6ca3842aba47df127ad165b13052c8fbc30aa23feb59d01960f2226127e20affb1637bb17394140be970a45c79fa4dd3ac0ba32c6bba5095be8a5ad1c0d6747788fcf128a8f71378d8921d2b7d2c9e999e2898fcb7ae7a5048900b111c973622ebcbdc5e3232efb330464f4d76b1a0fb2d70ddb3882ae9a45a7f3115ad94acc926d1ed33f940cd7bcd8a296983bb3ff4592009ce498b9d4552e6e019d453210545ac5c2f48a1fe75b5dd93cff4f124c363f22578cd7d3b5a5244a871c37244e79eaa1ffc3966f716520b8cbf38ba1c33ec68939fce45a2519eda4f1029d2e5fa3069e5fed848d9e078ed29af5a10541933db39ed353895f2b269437f2a04ba09528b0ba92bf725ea300752226b888f4cf3c3fa973e4b2017b74c86cdbe81829513bd62f2055076e0463b39c2155635772d80b2f6945319bede15535a3becbd9374122f0f974ef2c9ec990369f2a90dfb7f1355e5e183489880c4a9e63740967dec2a77dbfa003361bfee2f3e4f1e4cc02afe0d82a14a47ba9fac237ed616fc892c1d93387b9a9682a78994cd62074b295afc542b190ef2391e8352e8ada52147b448ee2e2cc8cd5170af58cbf211f7b0d49a6b6b6ec628b0dfb4e4636df58dc5c55b3634457d7f949a1f26abc64db158fa51343a5990d707218b01dabf223361cc4f6ce3cfc6b62c5306ad1bfbabf5c51003551a07bb053e5a419d5d8c8c200feb87ab9dd0802d418068285bfa3f0c0ae717d4671cb9d4b2cb0c12d44985961c259f4433fe732da40458c3903d6191f7a6167132a9db3476dfbfc37f3c5d37b49e3027ab9a981ed788e124ed88abe2f3a10f52fd5ba278e6555acc89d916b30c2dcf3bbcc6cfc1985e66a169a6eb1f251cef9ece3487e88d1f81676d97955ef374465e16ad36abaabd3888236dc0eb27050b9050a396a6d8a2cb451b8d75e480d8afa13ddefcda4c28a8483a441edbc034023fe5332c52e86dde7f71dd1865d471deb7ea04a09f38a9307206e2fc53e205362d95247adc5dc5cb5cb064609f2cd11ecbf005612d12165725799044eb45673a1e9c1a1275dc70ff5992500754c6efd851666b6a5d02d438d01e881b430876245d4bc4b888988471fdad5104e5bc5a518a83a9be98f9a1ea11473b8eb32150714ffa1bdddaf35fb7e0b50cb075a8a38437638cd4803e3e8baea0630420947dfb274a4980fd6c8d4a1a79d033406b7dae6cf68f83cfabe9e5bc9e4ef51c49362017fa835497b909bf0599ec764709a527a8bbf59007602bbeb676a60a3c990bb1630c18f4ba3b3ce71d73ebdff879af9347c7fd9d0789cf7d15fbe3196a4cbaecc3fbd2a5ed2f1d995cc03c6e5bfb48395b317ca4b3ff626e291f6cf877186eddd707b8d5a66de90ced49f276b417032d264d992d6dcf26267bdfbeb37a7e65438ae136bf65ad0da4998a3a331e7593786157562ac0eb4d37e68d41181d79677265b27099d770b4443cbf9d08859e4ac79f9adcbc41000ce203fedb40ceeb5050fa56a5bc9f038d4f13cc860e3e68a5df055ae2df2c09a392435e5770790835e2db2081dd21d28f2bc76eba810d5cdba41c97a8a64512af71eb9bbfc8b7ea17f41710cc034d33e92ca73c02a6e7501e33efe57efb54ecdf36e1e18207994779fe8a8e299ec5ddf186b6c859e5884994ac780d6f800d7e65ab1746e56b9dca3f08a0fd7a86680a53ffc70bb1b3138844a3ae4ee7267c2cdbba2cd8da1af7522fb6eaeb6b737637df1e69c0356ba02ca06a064d80add016c1a5fe804be21250c93dc859313ff0c41a68c351a702b2f24279d197cd1201080edb1006ae100ffa7d660a5439a79bcbda24e2fdf445f010bc49514e5030f10b4760101d07cec44773136f884264a3c0dc465fb950bbc2c11cebfd9a7de7b0f18e77e03e2a2e5199308f21fde4d9092328651d13d9b86cfbadde55d4eb3bd815d3c4349ca4e3944bfad27ef31b6034b3c934f8eeed228845091fbd030858ffbb6448dfb3454a5049bc86e3894814de855627b4cbb9a90515360f9087c7f99b894b7839c6b3beb0a6dcfe102d549cf571e287720b02fac463bddbaf1fd3d4865c9444f36d763d977d6e4741bdd983133112bd2567af10bbeba5944c39f3cd3ea9d5249cfcaa56f762224c1fe4ed7a847303859cb36d642c6bf903012327fa4af7ee0d901e09d4b2443f4036e3c7cf46971b90750fdf2c63f3f10b18ad46da18b62f7320d0d05366aecef0d0281ab8ec80888c332761300eff916a846fa3887e58f76fcbd5122861324e748dc0544b5886aab637188d40d4232587d5f11d9ddb9f7c8a1fd9b084032a6724e786d8ea632dfb85a0e650f8120758c1a58ebe077658f28e1181802ea5a90e1fce99a1e0246e60837048a75e47e6deba2959cec9f63029671dba511ff1a3ebee4a0b5868621c56afa369f17cff651d03cc620984dd1d985184ae7b3d2c9d0e838d9843b6a893e22643ce7057d33075dc937146e7d0194a2bbbf87a427d5235fa361f1eae8f35090b83f4a589205b683f773a2588018ae306383fdff65a857fc13be804b20a13f8982a3652eea9006ad3e4767f520abe82f199dbe74870cec8e466cb98ff00002a00000000008a0102a9c1d59ef327d76207a8a26373c89b9d0d0eb25d6ca7551a3f291c8c6cd3f0b45a36669e8960989d59bb5412a3aea7a6c0d994375d43bf812f38c96eedaf712506226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00002a0000000000657313c00101009000000000000003e8000000640000000a000000003b9aca00000000000138ca11203794f6ef69fc7df13b91e7848c837632bd1407bec63d9ade0330a3f701fdc25600010124f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d24000000002b40420f000000000022002026fa4316fb307d348467c0edd3b8bb1eff9ca3ec04367ce5788885712ec400b04752210243fc4580744090c5009be7bb3b85b91c6dfdfeffc67e37a808a89431b24461bc2102e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f52ae0002e5b062a9484e793eaf759f5ab4879577e5d1af684866decf0603f1c56a18de0f029bd74977c75b38711165bb9506c4b2725fbb9a031364d299681e80ec2952285500fe00061a80fd044cfd00fd010101010024f62dbfbfc3ba1da02c846abbf48de14377d8a73d8b69b9e7145c231e2b060d2400000000fefffffffdfe2faf0800fe0bebc2000104220020e4187f51de13d85db945c888357da388e2877ec578316fc25ecac3e716f4866bfe32a627f0fe0bebc2000102027d02000000018c19fefe4b851e9da17f94d44b549d87124aec35a4a85c40c566564c51ada7220200000000000000000250c3000000000000220020778f86e213a518e8336ec940874ccf4b92910332f5b3c0265374a86dbd34e6a5960000000000000016001425deb8d8a6cb84452c47904350e79c523dbfefdb0000000000fefffffffd03d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc49190256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdfd6540000000fe00061a80cc004738ca11203794f6ef69fc7df13b91e7848c837632bd1407bec63d9ade0330a3f79ad1a0a72421f2f13a86e069498ee9c1a5687c1b06968776f9699940e624d5300000fd02594066195c52f770b513f87862384c79a4ad543fff3a81fa6e3d45e76ebea9bd319a38706e847ae4e53fb9d97111b69f0cbfc86f20c92d35f155855b3de9de804548fd025b40849a87ab6815902abbc4f3d9773517c18eeddc5d10dd949aeccdad068be0e5d8260b54dff0c783ae0c330157954e371fadd82bb084d2fd9de28867a87ca60b70010000fd1388fe32a627f0fe0bebc20000249ad1a0a72421f2f13a86e069498ee9c1a5687c1b06968776f9699940e624d530000000002b9604100000000000220020e4187f51de13d85db945c888357da388e2877ec578316fc25ecac3e716f4866b475221029bd74977c75b38711165bb9506c4b2725fbb9a031364d299681e80ec2952285521029e2ff8460c1ea74f8892509f4c60340d19a1981c4339e2a728ddef8df08eea4b52aefd01bc020000000001019ad1a0a72421f2f13a86e069498ee9c1a5687c1b06968776f9699940e624d5300000000000620f4680044a0100000000000022002063d3fcf7f93eb1b30ff3d7f185c2889a5fa8c4a584cedd4e0ce3a16e10ea2c994a01000000000000220020a483a289ea09e761fae87c628675e3d52f23fcc6355ff15c3bde6ce2dc86d802400d0300000000002200202a563e407578aae18800c2388759b5bad0ceef5a3759de39d7acedffd868d30dcede0c0000000000220020bf57f237188dde15a4a0947bf64cd985f95163d77fee3d8372cdce323de1efdf04004730440220772d80d88ac5156fb8096ba19129492e66bdf8bea76e750847534f7aafb9621d022075f0480e72f576632ad9b80fbc5e7630951753c8425892adac6eb0352823de12014830450221009ea0e484d4d4c43c46960cb2f182e0460ebe560c036691076e2ddc03e2d87933022042074738e8a9c263694060f5fbcdd42a308d56abca4e5466f8d958d018fc5d0501475221029bd74977c75b38711165bb9506c4b2725fbb9a031364d299681e80ec2952285521029e2ff8460c1ea74f8892509f4c60340d19a1981c4339e2a728ddef8df08eea4b52ae65031b20000000fd1388fe0bebc200fe32a627f0b8b2f694372a292f7822c17f2400184c2f70195afc7523987a14f65e7601042d03cb5d3b7310e6532d403dc7ad5857869e6ace3a2a1a7cf710c8a9b307a5ebb02c00" ) @@ -154,16 +144,21 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertIs(state) val splice = state.spliceStatus assertIs(splice) - assertNull(splice.session.liquidityLease) + assertNull(splice.liquidityPurchase) assertTrue(splice.session.localCommit.isRight) - assertTrue(state.liquidityLeases.isEmpty()) - val state1 = state.copy( - 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)) - ) + } + run { + // The serialized data was created with lightning-kmp v1.6.1 and contained legacy liquidity leases. + val bin = Hex.decode( + "040213081f6d86356f02bbc63032527acc16f247306626923ac644d268c6d388acc201010410101000037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c09fe21f607fafe59f292aafea0974737fe11ba0ae3fe832735b0fe3f644273fedfd83bd0fe0efc0570fe80000001fd044cfe59682f000090640116001405e0104aa726e34ff5cd3a6320d05c0862b5b01c13280880000000000000000000001000121a51020362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06fd03e8fe59682f00fd03e890640260f378afd291d92f6fe86c27178aef4caf84fb545c1e63f9c9a1898298becaf9029f1c8b58528763c71c3a2e1526ec903c5e75a61c870050011fd7ccdd0a803089030c2c6185254249b67c1426f79652be21a26e093bc2ffcbac93e73ee7cf1e7a4b03e22dcab5315ab54d7ccd6cc496273abffcda59bf7e835c0c4fe7fa3884c330bf1408220222000000000000000000001004162a510200000000000000000000010003d0dd6515d07459f110be9c3165b950916df51e1dd342d673278784f4964e135302fd010d020000000001024bd7d5d15958ededa6e71e049765ffa30471938e615ae06af6e9ec88ddd1dcbc0000000000fdffffffb6a76d06353bcfd53969c1c444befc11d460d2a696f1f64104fe6c2dc0a9b6790000000000fdffffff0140420f0000000000220020cfde05ca43faf792db30fbd8511b29068f3f0bd610d850f5ba6d55018511d69a01405925f4175684907db9feb2f110fb67be493d0336043a7950d7cbc282825cb06e71fe2479ae803379113affc37a993b3efd65551e5bfd31c2e57059c2c4cf26680140e30f414054e2b6b8e875ba78f42471be44467d11382e247b8ea6b24163f43628e2ec0979a7a446c4dc6651314ab80e4300bb60497f70ea7914b4eda86428d9f9801a0600fd1388fd0194004713081f6d86356f02bbc63032527acc16f247306626923ac644d268c6d388acc2555a8f256dd2cd0f98bd8f64df22bc7470116f7edad3e51d2ebc61b6e69b67bb0000fd025fa4628dd41f5d8a4efdd98d316d9d857786000804515b25107a8d27a8e74d7b6bda031bbec27357117fe7efb2a70dfcf6cca04f4b26fe94049993334a5412ba2f209303ddb49212eae6b925dbc941924e994a3b81e000342e6f45966f8435eabaaa0f2c03744f6eb266cf5e21110cb0adcebf8eb1daa936b48a21f3cad3628514b991e66002afc74c8668b444b03d503955befd0406dcd191ccd7b7c5a243d2f6893ba27a3bfd0261a4f48ed47669759026cd1f7a08d68f6837741807bb3ea1d5a6a058ec14dca1e1540274440233607f320c1b193d1c1c3d3f5de40df28cb4dba3c187d1139ba1f1ed0d02b50805c457b12fe9c7e2863dba2c723523bc5de831f447d12fff7e34d16b78df03cb03a6802d32f58d7961fbfe1329f5c1a108514993733dedc4e63ea01064a9540384f3da08590794bebf4f07581f94f746e2cf45f865b9634c6d3b9a9d0dfac9ff000000fd1388fe2faf0800fe0bebc2000024555a8f256dd2cd0f98bd8f64df22bc7470116f7edad3e51d2ebc61b6e69b67bb000000002b40420f0000000000220020cfde05ca43faf792db30fbd8511b29068f3f0bd610d850f5ba6d55018511d69a4752210218bb3f1452c4036155e8f06bbb714e62b1dd4185a83c5cd069665782a04b85852103d0dd6515d07459f110be9c3165b950916df51e1dd342d673278784f4964e135352aefd01bd02000000000101555a8f256dd2cd0f98bd8f64df22bc7470116f7edad3e51d2ebc61b6e69b67bb0000000000e3033a80044a01000000000000220020a3aeecbd2707fd89c1339ca772a327ddac438b64c8a8cc6dbe5180456671abb54a01000000000000220020e5258298238c1e7605fa3f2b97ec1dd1a30c01b2e10930a39a2a11cc2ac01ad2400d03000000000022002021d519f1ce1e3ae039dacb110411dfe477ccf4b6c2168a141919ca482b6ae317781c0c00000000002200203cf8e1d032035e81667c393f5f6638143284a4add6efd656b9641c04eb29ff6c0400483045022100825aeb88370fc80925c23230f99ba96949b6a2548d63036d630319c47bb53a8e022035f6fc69b40936e90102a82147a1e4a6b1fcc7b6ebae83c7a83baf4f153dc00e01483045022100e5e07dd084cc3dbc2e6885088769388111978943bbd42975176871baae70634302206c3deb31f4f02bdf208be8d0396908a320d7e259aa02b310dd40845f8478079f014752210218bb3f1452c4036155e8f06bbb714e62b1dd4185a83c5cd069665782a04b85852103d0dd6515d07459f110be9c3165b950916df51e1dd342d673278784f4964e135352aefe3fa020000000fd1388fe0bebc200fe2faf08006639a3d97853ba424ca83d402055fc00c90ee2671ac43c20675f2d206f48826303b93622e8424a4d6fb07b934730afe5d61c98e888d51f9fcffa2cd0aebc6f68890000000103ccfff8d148c8b109d9bccb1fdc9846c4de2605c81faab2d580db7102657bb16d0000fd086d04000e4f35a4e36333583c23eee2d42bbab50a6987a60b082d35fa9c48847b8b0efe13b59f2dc3e00bbb34ef9f499814525189580f167b5acbea567efe287d2e5c11d426eb3d2c47c30feacd3d99520df9f359114e57470bd382937dd0f9ba6953ac9401f543a0fbb56022fbad18bd931c511f70d67293ec864d7943875fc2c8498553cbb023c380e41ad9394d858be52a89d11f85c2e221fa0662f1dd8b9a4634f5a88ebc38456afe48490f74a42b262cf80bc4c9a67bfa8180dc5168f874a1ea43a9915bb02657bebd0ce54d5bfa1a08f8b29df57ebd1d1efd774bac2644f2fe28f20f92b8d79af92efe9a65bf51dcfb753ca7355a7d250496c5cee0db43e750a089f63d044389ddf5339d27288c4103514cfa6a46caaf6fb8b173eb0720d94b7bd5ec458c7367d1d88b9de235fefe7d7f21f71f32f651651adbad47668709c1074a98b129ee65115e5171dbb70c80ccebe6b56091dd214634a5d8e06d3075af927eb97327d71eb7d0397953986acd31c089b93f4a62a0ba6977ccedb7b80af9de2e786be0e37ab3a2e82c42a7d6c788917e800c1bd86f281db9deaf209a440c7cdc159eddfc8f2105fa276a485655d3d2d710bbf7da45bb8e834ab7f470ad569001dfdf36504f5a9e5efd123a8415b3d74a3c493e03c1550bb6aad295d39e7204e4d6908456341717d1760cc3b0d9cbe546af7b4ab471da0dfc89f2d6386d4aa932386e51e9bc3f7d6d140a4126c3a5a3974872df4567875eb277ccc1c0717621a058bff4fb980d6b006a5f540dfff0da403aa2573ba95c42472c04b20c94dc349e58df57476ad65b4604f55acafba905c427d77d1175f35454c368ec0cf78f49f7dd43cd3e275604069f9dd4777923676c40adc0bd2e15f07ac7eebb536a3d653226d14aa094dc7cfe37793ea4a3704b567ab841660e11c621a6e86d426abec6dcf3903423a53552c4ea099e56d590a49f58b6695071382bbf1eba2e36f0d55daa1bbe3d31995acbdcbec8658afc57d8a2ce12d43818ea6303f83f0bde85c5994228a5e7056d5b6828937b5378b5fe4e30e9ac0fd247bafdf35bf5a905d0d38b734398f7a8197e58e2b4ffb20f5a15140bf777de4e599175b24a627c0cdb2b9ad34869ee4f2a4e5072e85c6395be6a34f18f2f4e4b598b54700948d51b7497c66986fab1999ab1f95bc87c11aea4e0f266165fce87a0874981d4246063d8970101165134479f37b0c5d88dacde6f34871effa43600251f9ad1708fa300f03e34a8d965a66624312ba6f9f3bbc4b81417507367ea8be82f3dfda069f4cbc98b2eb09d9e35f5c21972ed366b0fcbdb53154a3772ff3dd00a1d7cdae5859a9c134a2b443297b62ccd95a70c61ef3242c4a94217ce3a04862d5f4c76de922a4a4b41cb6d201de0615524a9cc4f7cbd1b34094661cf49884aea386fd89f5e322ffbd3280a768d7f447b714e68464d00d32bf3fdb12390d6ea79ca7a684ec389ce09902ce71cd980e5bb1ad7902e77729965e4f64d27b40f805c0cc9fe6f21459beaba1fda10331b4dce8cc5eed8e3a15201d00e5ae507875d68f484bc98a8b03a8c8adbad6cadc5a1057d4495effcbc9e677ba11ef5fa34c8c0c5ed36d54e206c3c621806667a1f4265a63db73906969dddd94fc2293dcd03b66013772a81a8d1a391206209209649d0a583752487f5700474dc4ac13d3250b10f5c0e836fac6d07c851a1e98ec4dd2d203a31b04699212e29109224fecf96ffee85e347db4218295957604033b22fc682067651311a20514438a05ea7ebaf0ec8bca155278e6e5635a685b015fa3016325e4714540ee79e841e2cf30012d014be86d6f704d6bfd5746509a1dd3e162a390886b1d1fc8ba0dd63acefcffa3ed15a17df99cf9c13abf610b0c85ca31fd2f8af18c62f96259702fb93c5209e875f0c8e357af1a9928f10537997bb1288a6993f182f38d0604220b2d988307fb4427cdde499412470fe518757111cd862c2efad5985887a93fb94940e4c8ebb10356da48161b330713e890655afbafbb146f61f930b090028b934fd3b67d3b43e2e20c77874fd60773da913a0a72d96c4567e4119f33619347cd686c1d098856c6c0f01dc93388c6bd4ae52ac8ddd2335465aebcb198d8207edb15c61fba8b7bccae3ddf0180d96c61e32823487782ce1cf922da3d0472737c5a65801f3de65a370f82dfa0312ed2f673726ff256b30207652292c76c00f4b639b02733c11496a051716569ddcd99fed72159476bd63f59699b1ecffadc0c60e132b08c115f4b15e7874531e12c326c25058da980318f103b441293661156ec97b59be85bdc6049525ba23a77bff1eaa23936a926a7bddf8a4922967a7552034cbfcaed2411d02c0d889566d8d1998a2b8b33c7d1799364ae56ed0e08533862f2559dece76b188edc9c11f3c52469202a1b1a12de40b6e521a45371bebe3fcc89b9f43ee32c4565db64c471d5492a068d7c210919310df59239688fb0f847b157612f8f9683b92aedfbb7260a3fe6d4f124311b248b22f678c58a96ec5e10ae7bb797420923fac991409ef96aae3b8ab8eb53398bc24b12e867269b18e840eae115f1b5b9425015d24d8614bfefe2c4c6d19651f4fbb9a3aabe05975115496b323746afc5fcdf3666cdbd249fc68ae8564521afc65e53436118978f6b057eb5e4da6466f769c2cfa15b423726d3b9f6074bcc90e25663b0e49977e9eb3ff770e24d6ce2e57e75d76a96b4143a1c8f3620428f0b43bf4566b1ca7435551f693c1b41c5df63df6ba2ac9b58c93edeab7d1389fb8afdff9c1c519009ef2e3c06dac4700c868eca6847a5a4aca409a6397184b9671869412818d9a397b52db95c556d2a78b363690c85f9d86d697e89a3cd4f06d649ad7ea6e55eab2c2a163a7e589a3fff35d520c1795e45c9388e8769b9494a6b49914bba368e481d07e26f2136a43c2862fc4cf794f645204a9ee890c3d23c6b628f32da5f93c206732bffd20c2cbd00a1d667d8d3ecda1295800325003d737e9a7f1cbbca73fe3f030ff00002a00000000008a01021d81bc063000918971c3ef36d6fdf7d2bef977660d8e5e61fe0457864c1330ed1568299ee459d01a9dfd08a3e7a0c74002d36e84e186a5bff4340b183923d65206226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00002a000000000066505be50101009000000000000003e8000000640000000a000000003b9aca00000000000113081f6d86356f02bbc63032527acc16f247306626923ac644d268c6d388acc201ffffffffffffff3bf900010124555a8f256dd2cd0f98bd8f64df22bc7470116f7edad3e51d2ebc61b6e69b67bb000000002b40420f0000000000220020cfde05ca43faf792db30fbd8511b29068f3f0bd610d850f5ba6d55018511d69a4752210218bb3f1452c4036155e8f06bbb714e62b1dd4185a83c5cd069665782a04b85852103d0dd6515d07459f110be9c3165b950916df51e1dd342d673278784f4964e135352ae0003d0dd6515d07459f110be9c3165b950916df51e1dd342d673278784f4964e1353023820147f58f10fc823579e9ec16bcedddfad27a0a502b5fd1c7b1f89b38f0eb9011f50c30000000000001600146dfb6b029bde323412ab8ab96a06be54693e5c19fe00061a80fd044cfd00fd010101030024555a8f256dd2cd0f98bd8f64df22bc7470116f7edad3e51d2ebc61b6e69b67bb00000000220020cfde05ca43faf792db30fbd8511b29068f3f0bd610d850f5ba6d55018511d69afefffffffdfe2faf0800fe0bebc2000002042200200772d4d7fe05d7bdc102093ffed8bbfcab6ddefed3577bc7c41d16a6fdc5ce1dfe2cb14ca8fe0bebc200000000010202fdc3501600146dfb6b029bde323412ab8ab96a06be54693e5c1900fe00061a8088004713081f6d86356f02bbc63032527acc16f247306626923ac644d268c6d388acc28b280cf870f5064b7dff2fcee3975747922856de2fe7275d959e3bb81ac5e00c0000fd025940a332b7f1f74710ad15bb4d1d3f307d305022ac84b953bfb7f7494c998182e835736f20d226ab927138635b157e0068664b887fc897e4481f78dea0c1e1bd193702fe00030d4064fd015e967c5fc29a6804cca3b35f63ccefbea17035999856483744cb5722f94f04020b890b31e27d192ea79ff17fe3ab38c12adf4bb0997a60295ce6f2525542deda772200200772d4d7fe05d7bdc102093ffed8bbfcab6ddefed3577bc7c41d16a6fdc5ce1d00fe00061a8064fd03e80000fd1388fe2cb14ca8fe0bebc20000248b280cf870f5064b7dff2fcee3975747922856de2fe7275d959e3bb81ac5e00c010000002b397e0e00000000002200200772d4d7fe05d7bdc102093ffed8bbfcab6ddefed3577bc7c41d16a6fdc5ce1d475221023820147f58f10fc823579e9ec16bcedddfad27a0a502b5fd1c7b1f89b38f0eb92103d1d020a4766149a6fbc8eb6a0d4b05e9a4ba0241fd5baf6459534d0c673d69a952aedf02000000018b280cf870f5064b7dff2fcee3975747922856de2fe7275d959e3bb81ac5e00c0100000000e3033a80044a01000000000000220020294e81c524fb7b4e8735d24263c0b586ac131b8fa95651015cfb5e6b1b46f66f4a0100000000000022002043675f52248a7d3765165f8dee4d46e08fa1a57604c13fa991e280d92ad2e6ea400d03000000000022002021d519f1ce1e3ae039dacb110411dfe477ccf4b6c2168a141919ca482b6ae31771580b00000000002200203cf8e1d032035e81667c393f5f6638143284a4add6efd656b9641c04eb29ff6cfe3fa020000000fd1388fe0bebc200fe2cb14ca88b01a0e7d1c61c58cbff673636189d334e654bb72d4d6d1f10d528a1bd69f42603b93622e8424a4d6fb07b934730afe5d61c98e888d51f9fcffa2cd0aebc6f6889010156b38e125574b4454e656a3e546c16cdaedfdffc077d3e807fee4946e8ec15876464fd01f40101fe0007a120fafd02ee4745caee03439c2d96c16f2e43f8de1b408f02404a78a2a75e1e2f9074888c11c8e6a1613bd1223f1d965e4ef31d3e26f7769f818b235ee914f80f6b3dbe4f2d220020cfde05ca43faf792db30fbd8511b29068f3f0bd610d850f5ba6d55018511d69a00fe00061a8064fd03e8" ) - assertEquals(state1, Serialization.deserialize(Serialization.serialize(state1)).value) + val state = Serialization.deserialize(bin).value + assertIs(state) + val splice = state.spliceStatus + assertIs(splice) + assertNull(splice.liquidityPurchase) + assertTrue(splice.session.localCommit.isLeft) } } + } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt b/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt index bc9caa989..a6f5ee145 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt @@ -38,13 +38,17 @@ object TestConstants { TrampolineFees(5.sat, 1200, CltvExpiryDelta(576)) ) - val leaseRate = LiquidityAds.LeaseRate( - leaseDuration = 0, - fundingWeight = 500, - leaseFeeProportional = 100, // 1% - leaseFeeBase = 0.sat, - maxRelayFeeProportional = 50, // 0.5% - maxRelayFeeBase = 1_000.msat, + val fundingRates = LiquidityAds.WillFundRates( + fundingRates = listOf( + LiquidityAds.FundingRate(100_000.sat, 500_000.sat, 500, 100, 0.sat), + LiquidityAds.FundingRate(500_000.sat, 10_000_000.sat, 750, 100, 0.sat) + ), + paymentTypes = setOf( + LiquidityAds.PaymentType.FromChannelBalance, + LiquidityAds.PaymentType.FromFutureHtlc, + LiquidityAds.PaymentType.FromFutureHtlcWithPreimage, + LiquidityAds.PaymentType.FromChannelBalanceForFutureHtlc, + ) ) const val aliceSwapInServerXpub = "tpubDCvYeHUZisCMVTSfWDa1yevTf89NeF6TWxXUQwqkcmFrNvNdNvZQh1j4m4uTA4QcmPEwcrKVF8bJih1v16zDZacRr4j9MCAFQoSydKKy66q" @@ -56,7 +60,7 @@ object TestConstants { private val seed = MnemonicCode.toSeed(mnemonics, "").toByteVector32() val keyManager = LocalKeyManager(seed, Chain.Regtest, bobSwapInServerXpub) - val walletParams = WalletParams(NodeUri(Bob.keyManager.nodeKeys.nodeKey.publicKey, "bob.com", 9735), trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144)), swapInParams, leaseRate) + val walletParams = WalletParams(NodeUri(Bob.keyManager.nodeKeys.nodeKey.publicKey, "bob.com", 9735), trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144)), swapInParams, fundingRates) val nodeParams = NodeParams( chain = Chain.Regtest, loggerFactory = testLoggerFactory, @@ -96,7 +100,7 @@ object TestConstants { paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(0), CltvExpiryDelta(0)), ) - fun channelParams(): LocalParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = true) + fun channelParams(payCommitTxFees: Boolean): LocalParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = payCommitTxFees) } object Bob { @@ -105,7 +109,7 @@ object TestConstants { private val seed = MnemonicCode.toSeed(mnemonics, "").toByteVector32() val keyManager = LocalKeyManager(seed, Chain.Regtest, aliceSwapInServerXpub) - val walletParams = WalletParams(NodeUri(Alice.keyManager.nodeKeys.nodeKey.publicKey, "alice.com", 9735), trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144)), swapInParams, leaseRate) + val walletParams = WalletParams(NodeUri(Alice.keyManager.nodeKeys.nodeKey.publicKey, "alice.com", 9735), trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144)), swapInParams, fundingRates) val nodeParams = NodeParams( chain = Chain.Regtest, loggerFactory = testLoggerFactory, @@ -127,7 +131,7 @@ object TestConstants { paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(0), CltvExpiryDelta(0)), ) - fun channelParams(): LocalParams = LocalParams(nodeParams, isChannelOpener = false, payCommitTxFees = false) + fun channelParams(payCommitTxFees: Boolean): LocalParams = LocalParams(nodeParams, isChannelOpener = false, payCommitTxFees = payCommitTxFees) } } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index bad5dae79..0e4781c22 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -9,7 +9,6 @@ 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.ChannelFlags import fr.acinq.lightning.channel.ChannelType @@ -216,10 +215,10 @@ 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), listOf())), // single network + TestCase(ByteVector("0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101"), Init(Features(ByteVector("088a")), listOf(chainHash1), null)), // single network TestCase( ByteVector("0000 0002088a 014001010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202"), - Init(Features(ByteVector("088a")), listOf(chainHash1, chainHash2), listOf()) + Init(Features(ByteVector("088a")), listOf(chainHash1, chainHash2), null) ), // multiple networks TestCase( ByteVector("0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 03012a"), @@ -227,17 +226,36 @@ class LightningCodecsTestsCommon : LightningTestSuite() { ), // network and unknown odd 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 + ByteVector("0000 0002088a fd053b150001000186a00007a1200226006400001388000101"), + Init( + Features(ByteVector("088a")), + chainHashs = listOf(), + liquidityRates = LiquidityAds.WillFundRates( + fundingRates = listOf(LiquidityAds.FundingRate(100_000.sat, 500_000.sat, 550, 100, 5_000.sat)), + paymentTypes = setOf(LiquidityAds.PaymentType.FromChannelBalance) + ) + ), + ), // one liquidity ads with the default payment type TestCase( - ByteVector("0000 0002088a fd05392003f0019000c8000061a80064000186a00fc001f401f4000027100096000249f0"), + ByteVector("0000 0002088a fd053b3f0002000186a00007a12002260064000013880007a120004c4b40044c004b00000000001b080000000000000000000700000000000000000000000000000001"), 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)) + liquidityRates = LiquidityAds.WillFundRates( + fundingRates = listOf( + LiquidityAds.FundingRate(100_000.sat, 500_000.sat, 550, 100, 5_000.sat), + LiquidityAds.FundingRate(500_000.sat, 5_000_000.sat, 1100, 75, 0.sat), + ), + paymentTypes = setOf( + LiquidityAds.PaymentType.FromChannelBalance, + LiquidityAds.PaymentType.FromFutureHtlc, + LiquidityAds.PaymentType.FromFutureHtlcWithPreimage, + LiquidityAds.PaymentType.FromChannelBalanceForFutureHtlc, + LiquidityAds.PaymentType.Unknown(211) + ) + ) ), - ), // two liquidity ads + ), // two liquidity ads with multiple payment types ) for (testCase in testCases) { @@ -292,6 +310,26 @@ class LightningCodecsTestsCommon : LightningTestSuite() { @Test fun `encode - decode open_channel`() { + val fundingRates = LiquidityAds.WillFundRates( + fundingRates = listOf( + LiquidityAds.FundingRate(100_000.sat, 500_000.sat, 550, 100, 5_000.sat), + LiquidityAds.FundingRate(500_000.sat, 5_000_000.sat, 1100, 75, 0.sat), + ), + paymentTypes = setOf( + LiquidityAds.PaymentType.FromChannelBalance, + LiquidityAds.PaymentType.FromFutureHtlc, + LiquidityAds.PaymentType.FromFutureHtlcWithPreimage, + LiquidityAds.PaymentType.FromChannelBalanceForFutureHtlc, + LiquidityAds.PaymentType.Unknown(211) + ) + ) + val requestFundsFromChannelBalance = LiquidityAds.RequestFunding.chooseRate(750_000.sat, LiquidityAds.PaymentDetails.FromChannelBalance, fundingRates)!! + val paymentHashes = listOf( + ByteVector32.fromValidHex("80417c0c91deb72606958425ea1552a045a55a250e91870231b486dcb2106734"), + ByteVector32.fromValidHex("d662b36d54c6d1c2a0227cdc114d12c578c25ab6ec664eebaa440d7e493eba47"), + ) + val requestFundsFromHtlc = LiquidityAds.RequestFunding.chooseRate(500_000.sat, LiquidityAds.PaymentDetails.FromFutureHtlc(paymentHashes), fundingRates)!! + val requestFundsFromBalanceForHtlc = LiquidityAds.RequestFunding.chooseRate(500_000.sat, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(paymentHashes), fundingRates)!! // @formatter:off val defaultOpen = OpenDualFundedChannel(BlockHash(ByteVector32.Zeroes), ByteVector32.One, FeeratePerKw(5000.sat), FeeratePerKw(4000.sat), 250_000.sat, 500.sat, 50_000, 15.msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), publicKey(7), ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false)) val defaultEncoded = ByteVector("0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f 00") @@ -300,7 +338,9 @@ 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, 2500, 500_000))) to (defaultEncoded + ByteVector("0103101000 fd05390e000000000000c35009c40007a120")), + defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequestFundingTlv(requestFundsFromChannelBalance))) to (defaultEncoded + ByteVector("0103101000 fd053b1a00000000000b71b00007a120004c4b40044c004b000000000000")), + defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequestFundingTlv(requestFundsFromHtlc))) to (defaultEncoded + ByteVector("0103101000 fd053b5a000000000007a120000186a00007a1200226006400001388804080417c0c91deb72606958425ea1552a045a55a250e91870231b486dcb2106734d662b36d54c6d1c2a0227cdc114d12c578c25ab6ec664eebaa440d7e493eba47")), + defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.RequestFundingTlv(requestFundsFromBalanceForHtlc))) to (defaultEncoded + ByteVector("0103101000 fd053b5a000000000007a120000186a00007a1200226006400001388824080417c0c91deb72606958425ea1552a045a55a250e91870231b486dcb2106734d662b36d54c6d1c2a0227cdc114d12c578c25ab6ec664eebaa440d7e493eba47")), 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")), ) // @formatter:on @@ -336,6 +376,11 @@ class LightningCodecsTestsCommon : LightningTestSuite() { @Test fun `encode - decode accept_channel`() { + val nodeKey = PrivateKey.fromHex("57ac961f1b80ebfb610037bf9c96c6333699bde42257919a53974811c34649e3") + val fundingLease = LiquidityAds.FundingRate(500_000.sat, 5_000_000.sat, 1100, 75, 0.sat) + val requestFunds = LiquidityAds.RequestFunding(750_000.sat, fundingLease, LiquidityAds.PaymentDetails.FromChannelBalance) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(publicKey(1), publicKey(1)) + val willFund = LiquidityAds.WillFundRates(listOf(fundingLease), setOf(LiquidityAds.PaymentType.FromChannelBalance)).validateRequest(nodeKey, fundingScript, FeeratePerKw(5000.sat), requestFunds)!!.willFund // @formatter:off val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000.sat, 473.sat, 100_000_000, 1.msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), publicKey(7)) val defaultEncoded = ByteVector("0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f") @@ -343,7 +388,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, 750, 150, 250.sat, 100, 5.msat))) to (defaultEncoded + ByteVector("0103101000 fd05394e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee0096000000fa006400000005")), + defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), ChannelTlv.ProvideFundingTlv(willFund))) to (defaultEncoded + ByteVector("0103101000 fd053b740007a120004c4b40044c004b00000000002200202ec38203f4cf37a3b377d9a55c7ae0153c643046dbdbe2ffccfb11b74420103c35962783e077e3c5214ba829752be2a3994a7c5e0e9d735ef5a9dab3ce1d6dda6282c3252b20af52e58c33c0e164167fd59e19114a8a8f9eb76b33008205dcb6")), 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")), @@ -482,6 +527,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val channelId = ByteVector32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") val fundingTxId = TxId(TxHash("24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566")) val fundingPubkey = PublicKey(ByteVector.fromHex("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")) + val fundingRate = LiquidityAds.FundingRate(100_000.sat, 100_000.sat, 400, 150, 0.sat) val testCases = listOf( // @formatter:off Stfu(channelId, false) to ByteVector("0002 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00"), @@ -490,12 +536,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, 4000, 850_000)) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd05390e00000000000186a00fa0000cf850"), + SpliceInit(channelId, 100_000.sat, 0.msat, FeeratePerKw(2500.sat), 100, fundingPubkey, LiquidityAds.RequestFunding(100_000.sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance)) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1a00000000000186a0000186a0000186a001900096000000000000"), 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, 750, 150, 250.sat, 100, 0.msat)) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd05394e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ee0096000000fa006400000000"), + SpliceAck(channelId, 25_000.sat, 0.msat, fundingPubkey, LiquidityAds.WillFund(fundingRate, ByteVector("deadbeef"), ByteVector64.Zeroes)) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b56000186a0000186a001900096000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), SpliceLocked(channelId, fundingTxId) to ByteVector("908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566"), // @formatter:on ) @@ -832,54 +878,31 @@ class LightningCodecsTestsCommon : LightningTestSuite() { } @Test - fun `validate liquidity ads lease`() { - // The following lease has been signed by eclair. - val channelId = randomBytes32() - val remoteNodeId = PublicKey.fromHex("024dd1d24f950df788c124fe855d5a48c632d5fb6e59cf95f7ea6bee2ad47e5bc8") - val fundingScript = ByteVector.fromHex("00202395c9c52c02ca069f1d56a3c6124bf8b152a617328c76e6b31f83ace370c2ff") - val remoteWillFund = ChannelTlv.WillFund( - sig = ByteVector64("a1b9850389d21b49e074f183e6e1e2d0416e47b4c031843f4cf6f02f68e44ebd5f6ad1baee0b49098c517ac1f04fee6c58335e64ed45f5b0e4ce4b8546cbba09"), - fundingWeight = 500, - leaseFeeProportional = 100, - leaseFeeBase = 10.sat, - maxRelayFeeProportional = 250, - maxRelayFeeBase = 2000.msat, - ) - 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?) - - 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 = InvalidLiquidityAdsAmount(channelId, 0.sat, 500_000.sat)), + fun `decode unknown liquidity ads types`() { + val fundingRate = LiquidityAds.FundingRate(100_000.sat, 500_000.sat, 550, 100, 5_000.sat) + val testCases = mapOf( + // @formatter:off + ByteVector("0001 000186a00007a1200226006400001388 0001 01") to LiquidityAds.WillFundRates(listOf(fundingRate), setOf(LiquidityAds.PaymentType.FromChannelBalance)), + ByteVector("0001 000186a00007a1200226006400001388 001b 080000000000000000000000000000000008000000000000000001") to LiquidityAds.WillFundRates(listOf(fundingRate), setOf(LiquidityAds.PaymentType.FromChannelBalance, LiquidityAds.PaymentType.Unknown(75), LiquidityAds.PaymentType.Unknown(211))), + // @formatter:on ) testCases.forEach { - 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) + val decoded = LiquidityAds.WillFundRates.read(ByteArrayInput(it.key.toByteArray())) + assertEquals(it.value, decoded) } - } @Test fun `encoded node id`() { val testCases = mapOf( - ByteVector.fromHex("00 0d950b0001c80000") to - EncodedNodeId.ShortChannelIdDir(isNode1 = true, ShortChannelId(890123, 456, 0)), - ByteVector.fromHex("01 0c0a14000d800005") to - EncodedNodeId.ShortChannelIdDir(isNode1 = false, ShortChannelId(789012, 3456, 5)), - ByteVector.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73") to - EncodedNodeId.WithPublicKey.Plain(PublicKey.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73")), - ByteVector.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922") to - EncodedNodeId.WithPublicKey.Plain(PublicKey.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922")), - ByteVector.fromHex("042d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73") to - EncodedNodeId.WithPublicKey.Wallet(PublicKey.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73")), - ByteVector.fromHex("05ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922") to - EncodedNodeId.WithPublicKey.Wallet(PublicKey.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922")), + // @formatter:off + ByteVector.fromHex("00 0d950b0001c80000") to EncodedNodeId.ShortChannelIdDir(isNode1 = true, ShortChannelId(890123, 456, 0)), + ByteVector.fromHex("01 0c0a14000d800005") to EncodedNodeId.ShortChannelIdDir(isNode1 = false, ShortChannelId(789012, 3456, 5)), + ByteVector.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73") to EncodedNodeId.WithPublicKey.Plain(PublicKey.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73")), + ByteVector.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922") to EncodedNodeId.WithPublicKey.Plain(PublicKey.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922")), + ByteVector.fromHex("042d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73") to EncodedNodeId.WithPublicKey.Wallet(PublicKey.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73")), + ByteVector.fromHex("05ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922") to EncodedNodeId.WithPublicKey.Wallet(PublicKey.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922")), + // @formatter:on ) for (testCase in testCases) { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LiquidityAdsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LiquidityAdsTestsCommon.kt new file mode 100644 index 000000000..b94138c7b --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LiquidityAdsTestsCommon.kt @@ -0,0 +1,56 @@ +package fr.acinq.lightning.wire + +import fr.acinq.bitcoin.* +import fr.acinq.lightning.Lightning.randomBytes32 +import fr.acinq.lightning.Lightning.randomBytes64 +import fr.acinq.lightning.blockchain.fee.FeeratePerByte +import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.ChannelException +import fr.acinq.lightning.channel.InvalidLiquidityAdsAmount +import fr.acinq.lightning.channel.InvalidLiquidityAdsSig +import fr.acinq.lightning.channel.MissingLiquidityAds +import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.utils.sat +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class LiquidityAdsTestsCommon : LightningTestSuite() { + + @Test + fun `validate liquidity ads funding attempt`() { + val nodeKey = PrivateKey.fromHex("57ac961f1b80ebfb610037bf9c96c6333699bde42257919a53974811c34649e3") + assertEquals(PublicKey.fromHex("03ca9b880627d2d4e3b33164f66946349f820d26aa9572fe0e525e534850cbd413"), nodeKey.publicKey()) + + val fundingRate = LiquidityAds.FundingRate(100_000.sat, 1_000_000.sat, 500, 100, 10.sat) + assertEquals(fundingRate.fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 500_000.sat).total, 5635.sat) + assertEquals(fundingRate.fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 600_000.sat).total, 5635.sat) + assertEquals(fundingRate.fees(FeeratePerKw(FeeratePerByte(5.sat)), 500_000.sat, 400_000.sat).total, 4635.sat) + assertEquals(fundingRate.fees(FeeratePerKw(FeeratePerByte(10.sat)), 500_000.sat, 500_000.sat).total, 6260.sat) + + val fundingRates = LiquidityAds.WillFundRates(listOf(fundingRate), setOf(LiquidityAds.PaymentType.FromChannelBalance)) + val request = LiquidityAds.RequestFunding.chooseRate(500_000.sat, LiquidityAds.PaymentDetails.FromChannelBalance, fundingRates) + assertNotNull(request) + val fundingScript = ByteVector.fromHex("00202395c9c52c02ca069f1d56a3c6124bf8b152a617328c76e6b31f83ace370c2ff") + val willFund = fundingRates.validateRequest(nodeKey, fundingScript, FeeratePerKw(1000.sat), request)?.willFund + assertNotNull(willFund) + assertEquals(fundingScript, willFund.fundingScript) + assertEquals(fundingRate, willFund.fundingRate) + assertEquals(ByteVector64.fromValidHex("0d99b73ecc32a81581cb761d8737e8bccf2358a01f7dea8e2f2579f32db42e94668786a2245287848c550b502fee9aca232c0c343afb16ac44d9be9c59d16f70"), willFund.signature) + + data class TestCase(val remoteFundingAmount: Satoshi, val willFund: LiquidityAds.WillFund?, val failure: ChannelException?) + + val channelId = randomBytes32() + val testCases = listOf( + TestCase(500_000.sat, willFund, failure = null), + TestCase(500_000.sat, willFund = null, failure = MissingLiquidityAds(channelId)), + TestCase(500_000.sat, willFund.copy(signature = randomBytes64()), failure = InvalidLiquidityAdsSig(channelId)), + TestCase(0.sat, willFund, failure = InvalidLiquidityAdsAmount(channelId, 0.sat, 500_000.sat)), + ) + testCases.forEach { + val result = request.validateRemoteFunding(nodeKey.publicKey(), channelId, fundingScript, it.remoteFundingAmount, FeeratePerKw(2500.sat), it.willFund) + assertEquals(it.failure, result.left) + } + } + +} \ No newline at end of file diff --git a/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json b/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json index d5532bbfe..dc5f9ac36 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json @@ -190,7 +190,5 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None", - "liquidityLeases": [ - ] + "spliceStatus": "None" } \ 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 e3811aa3f..19c16c145 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json @@ -365,7 +365,5 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None", - "liquidityLeases": [ - ] + "spliceStatus": "None" } \ 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 432053cea..6e05cb781 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json @@ -203,7 +203,5 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None", - "liquidityLeases": [ - ] + "spliceStatus": "None" } \ 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 6ec3d386a..074594c09 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json @@ -373,7 +373,5 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None", - "liquidityLeases": [ - ] + "spliceStatus": "None" } \ 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 8cf311417..379308d25 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json @@ -222,7 +222,5 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None", - "liquidityLeases": [ - ] + "spliceStatus": "None" } \ 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 0630c17dd..bc1a7bcdf 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json @@ -263,7 +263,5 @@ }, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None", - "liquidityLeases": [ - ] + "spliceStatus": "None" } \ 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 39c521ac4..01a0bcfe3 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json @@ -234,7 +234,5 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None", - "liquidityLeases": [ - ] + "spliceStatus": "None" } \ 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 8f4df8b4d..dec32f7ff 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json @@ -201,7 +201,5 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None", - "liquidityLeases": [ - ] + "spliceStatus": "None" } \ 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 424aa610b..5fe2fa192 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json @@ -371,7 +371,5 @@ "localShutdown": null, "remoteShutdown": null, "closingFeerates": null, - "spliceStatus": "None", - "liquidityLeases": [ - ] + "spliceStatus": "None" } \ No newline at end of file