From 7bace0ae4b5455ab11aaad4048af0a4a6f9216d0 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 1 Mar 2024 17:53:30 +0100 Subject: [PATCH] Remove `please_open_channel` It is usually the wallet that decides that it needs a channel, but we want the LSP to pay the commit fees to allow the wallet user to empty its wallet over lightning. We previously used a `please_open_channel` message that was sent by the wallet to the LSP, but it doesn't work well with liquidity ads. We remove that message and instead send `open_channel` from the wallet but with a custom channel flag that tells the LSP that they should be paying the commit fees. This only works if the LSP adds funds on their side of the channel, so we couple that with liquidity ads to request funds from the LSP. We also add a `recommended_feerates` message from the LSP which lets the wallet know the on-chain feerates that the LSP will accept for on-chain funding operations, since those feerates are set in the `open_channel` message that is now sent by the wallet. --- .../kotlin/fr/acinq/lightning/NodeEvents.kt | 18 +- .../kotlin/fr/acinq/lightning/NodeParams.kt | 5 +- .../blockchain/electrum/SwapInManager.kt | 18 +- .../acinq/lightning/channel/ChannelAction.kt | 8 +- .../acinq/lightning/channel/ChannelCommand.kt | 7 +- .../fr/acinq/lightning/channel/ChannelData.kt | 37 ++- .../fr/acinq/lightning/channel/Commitments.kt | 34 +-- .../fr/acinq/lightning/channel/Helpers.kt | 12 +- .../acinq/lightning/channel/InteractiveTx.kt | 6 +- .../acinq/lightning/channel/states/Channel.kt | 4 +- .../lightning/channel/states/Negotiating.kt | 6 +- .../acinq/lightning/channel/states/Normal.kt | 31 +- .../lightning/channel/states/ShuttingDown.kt | 4 +- .../acinq/lightning/channel/states/Syncing.kt | 2 +- .../channel/states/WaitForAcceptChannel.kt | 5 +- .../channel/states/WaitForFundingConfirmed.kt | 6 +- .../channel/states/WaitForFundingCreated.kt | 2 +- .../channel/states/WaitForFundingSigned.kt | 15 +- .../lightning/channel/states/WaitForInit.kt | 9 +- .../channel/states/WaitForOpenChannel.kt | 2 +- .../kotlin/fr/acinq/lightning/io/Peer.kt | 268 +++++++++--------- .../acinq/lightning/json/JsonSerializers.kt | 4 + .../payment/IncomingPaymentHandler.kt | 2 +- .../lightning/payment/LiquidityPolicy.kt | 31 +- .../serialization/v2/ChannelState.kt | 6 +- .../serialization/v3/ChannelState.kt | 6 +- .../serialization/v4/Deserialization.kt | 103 ++++--- .../serialization/v4/Serialization.kt | 30 +- .../lightning/transactions/Transactions.kt | 14 +- .../fr/acinq/lightning/wire/ChannelTlv.kt | 110 +------ .../acinq/lightning/wire/LightningMessages.kt | 131 +++++---- .../fr/acinq/lightning/wire/LiquidityAds.kt | 2 +- .../lightning/wire/RecommendedFeeratesTlv.kt | 48 ++++ .../electrum/SwapInManagerTestsCommon.kt | 40 +-- .../channel/CommitmentsTestsCommon.kt | 19 +- .../fr/acinq/lightning/channel/TestsHelper.kt | 6 +- .../channel/states/QuiescenceTestsCommon.kt | 3 +- .../channel/states/SpliceTestsCommon.kt | 18 +- .../states/WaitForFundingSignedTestsCommon.kt | 53 ++-- .../fr/acinq/lightning/io/peer/PeerTest.kt | 133 +++------ .../IncomingPaymentHandlerTestsCommon.kt | 3 +- .../payment/LiquidityPolicyTestsCommon.kt | 16 +- .../OutgoingPaymentHandlerTestsCommon.kt | 8 +- .../fr/acinq/lightning/tests/TestConstants.kt | 18 +- .../acinq/lightning/tests/io/peer/builders.kt | 10 +- .../transactions/AnchorOutputsTestsCommon.kt | 4 +- .../transactions/TransactionsTestsCommon.kt | 10 +- .../wire/LightningCodecsTestsCommon.kt | 53 ++-- .../lightning/wire/OpenTlvTestsCommon.kt | 31 -- .../nonreg/v2/Closing_0ba41d17/data.json | 8 +- .../nonreg/v2/Closing_0ed6ff68/data.json | 8 +- .../nonreg/v2/Closing_0efffae3/data.json | 8 +- .../nonreg/v2/Closing_2fd2a3fa/data.json | 8 +- .../nonreg/v2/Closing_3bb07fb6/data.json | 8 +- .../nonreg/v2/Closing_8f1a524e/data.json | 8 +- .../nonreg/v2/Closing_ef682e2e/data.json | 8 +- .../nonreg/v2/Negotiating_c8d15808/data.json | 8 +- .../nonreg/v2/Negotiating_d9b4cd96/data.json | 8 +- .../nonreg/v2/Negotiating_ee10091c/data.json | 8 +- .../nonreg/v2/Negotiating_f52b19b8/data.json | 8 +- .../nonreg/v2/Normal_748a735b/data.json | 8 +- .../nonreg/v2/Normal_e2253ddd/data.json | 8 +- .../nonreg/v2/Normal_ff248f8d/data.json | 8 +- .../nonreg/v2/Normal_ff4a71b6/data.json | 8 +- .../nonreg/v2/Normal_ffd9f5db/data.json | 8 +- .../nonreg/v2/ShuttingDown_c321b947/data.json | 8 +- .../nonreg/v2/ShuttingDown_f89ecd50/data.json | 8 +- .../data.json | 8 +- .../data.json | 8 +- .../data.json | 8 +- .../WaitForFundingLocked_f3437082/data.json | 8 +- .../data.json | 8 +- .../data.json | 8 +- .../nonreg/v3/Closing_029bf8f3/data.json | 8 +- .../nonreg/v3/Closing_0ba41d17/data.json | 8 +- .../nonreg/v3/Closing_0ed6ff68/data.json | 8 +- .../nonreg/v3/Closing_0efffae3/data.json | 8 +- .../nonreg/v3/Closing_ebbd24bc/data.json | 8 +- .../nonreg/v3/Closing_f137669f/data.json | 8 +- .../nonreg/v3/Negotiating_da44c6e2/data.json | 8 +- .../nonreg/v3/Negotiating_dabbed55/data.json | 8 +- .../nonreg/v3/Negotiating_fadb50c1/data.json | 8 +- .../nonreg/v3/Normal_fd10d3cc/data.json | 8 +- .../nonreg/v3/Normal_fe897b64/data.json | 8 +- .../nonreg/v3/Normal_ff248f8d/data.json | 8 +- .../nonreg/v3/Normal_ff4a71b6/data.json | 8 +- .../nonreg/v3/ShuttingDown_ef41a1a5/data.json | 8 +- .../nonreg/v3/ShuttingDown_ef7081a1/data.json | 8 +- .../data.json | 8 +- .../data.json | 8 +- .../WaitForFundingLocked_f3437082/data.json | 8 +- .../data.json | 8 +- .../data.json | 8 +- 93 files changed, 945 insertions(+), 818 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/wire/RecommendedFeeratesTlv.kt diff --git a/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt b/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt index 225bc9c91..f8e882653 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt @@ -2,7 +2,10 @@ package fr.acinq.lightning import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PublicKey +import fr.acinq.bitcoin.OutPoint import fr.acinq.bitcoin.Satoshi +import fr.acinq.lightning.blockchain.electrum.WalletState +import fr.acinq.lightning.channel.ChannelManagementFees import fr.acinq.lightning.channel.InteractiveTxParams import fr.acinq.lightning.channel.SharedFundingInput import fr.acinq.lightning.channel.states.ChannelStateWithCommitments @@ -11,16 +14,18 @@ import fr.acinq.lightning.channel.states.WaitForFundingCreated import fr.acinq.lightning.db.IncomingPayment import fr.acinq.lightning.utils.sum import fr.acinq.lightning.wire.Init -import fr.acinq.lightning.wire.PleaseOpenChannel -import kotlinx.coroutines.CompletableDeferred sealed interface NodeEvents data class PeerConnected(val remoteNodeId: PublicKey, val theirInit: Init) : NodeEvents sealed interface SwapInEvents : NodeEvents { - data class Requested(val req: PleaseOpenChannel) : SwapInEvents - data class Accepted(val requestId: ByteVector32, val serviceFee: MilliSatoshi, val miningFee: Satoshi) : SwapInEvents + data class Requested(val walletInputs: List) : SwapInEvents { + val totalAmount: Satoshi = walletInputs.map { it.amount }.sum() + } + data class Accepted(val inputs: Set, val amount: Satoshi, val fees: ChannelManagementFees) : SwapInEvents { + val receivedAmount: Satoshi = amount - fees.total + } } sealed interface ChannelEvents : NodeEvents { @@ -30,6 +35,7 @@ sealed interface ChannelEvents : NodeEvents { } sealed interface LiquidityEvents : NodeEvents { + /** Amount of the liquidity event, before fees are paid. */ val amount: MilliSatoshi val fee: MilliSatoshi val source: Source @@ -45,8 +51,7 @@ sealed interface LiquidityEvents : NodeEvents { data object ChannelInitializing : Reason() } } - - data class ApprovalRequested(override val amount: MilliSatoshi, override val fee: MilliSatoshi, override val source: Source, val replyTo: CompletableDeferred) : LiquidityEvents + data class Accepted(override val amount: MilliSatoshi, override val fee: MilliSatoshi, override val source: Source) : LiquidityEvents } /** This is useful on iOS to ask the OS for time to finish some sensitive tasks. */ @@ -59,7 +64,6 @@ sealed interface SensitiveTaskEvents : NodeEvents { } data class TaskStarted(val id: TaskIdentifier) : SensitiveTaskEvents data class TaskEnded(val id: TaskIdentifier) : SensitiveTaskEvents - } /** This will be emitted in a corner case where the user restores a wallet on an older version of the app, which is unable to read the channel data. */ diff --git a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt index 6abd258e2..ac4f9d547 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -12,6 +12,7 @@ import fr.acinq.lightning.payment.LiquidityPolicy import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toMilliSatoshi +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.lightning.wire.OfferTypes import io.ktor.utils.io.charsets.* import io.ktor.utils.io.core.* @@ -69,12 +70,14 @@ 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. */ data class WalletParams( val trampolineNode: NodeUri, val trampolineFees: List, val invoiceDefaultRoutingFees: InvoiceDefaultRoutingFees, val swapInParams: SwapInParams, + val leaseRate: LiquidityAds.LeaseRate, ) /** @@ -229,7 +232,7 @@ data class NodeParams( maxPaymentAttempts = 5, zeroConfPeers = emptySet(), paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(75), CltvExpiryDelta(200)), - liquidityPolicy = MutableStateFlow(LiquidityPolicy.Auto(maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)), + liquidityPolicy = MutableStateFlow(LiquidityPolicy.Auto(inboundLiquidityTarget = null, maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)), minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA, maxFinalCltvExpiryDelta = CltvExpiryDelta(360), bolt12invoiceExpiry = 60.seconds, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt index 6f9737c23..8d3581dcd 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt @@ -2,21 +2,18 @@ package fr.acinq.lightning.blockchain.electrum import fr.acinq.bitcoin.OutPoint import fr.acinq.bitcoin.Transaction -import fr.acinq.bitcoin.TxId -import fr.acinq.lightning.Lightning import fr.acinq.lightning.SwapInParams import fr.acinq.lightning.channel.FundingContributions.Companion.stripInputWitnesses import fr.acinq.lightning.channel.LocalFundingStatus import fr.acinq.lightning.channel.RbfStatus -import fr.acinq.lightning.channel.SignedSharedTransaction import fr.acinq.lightning.channel.SpliceStatus import fr.acinq.lightning.channel.states.* -import fr.acinq.lightning.io.RequestChannelOpen +import fr.acinq.lightning.io.AddWalletInputsToChannel import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.utils.sat internal sealed class SwapInCommand { - data class TrySwapIn(val currentBlockHeight: Int, val wallet: WalletState, val swapInParams: SwapInParams, val trustedTxs: Set) : SwapInCommand() + data class TrySwapIn(val currentBlockHeight: Int, val wallet: WalletState, val swapInParams: SwapInParams) : SwapInCommand() data class UnlockWalletInputs(val inputs: Set) : SwapInCommand() } @@ -33,20 +30,15 @@ internal sealed class SwapInCommand { class SwapInManager(private var reservedUtxos: Set, private val logger: MDCLogger) { constructor(bootChannels: List, logger: MDCLogger) : this(reservedWalletInputs(bootChannels), logger) - internal fun process(cmd: SwapInCommand): RequestChannelOpen? = when (cmd) { + internal fun process(cmd: SwapInCommand): AddWalletInputsToChannel? = when (cmd) { is SwapInCommand.TrySwapIn -> { val availableWallet = cmd.wallet.withoutReservedUtxos(reservedUtxos).withConfirmations(cmd.currentBlockHeight, cmd.swapInParams) logger.info { "swap-in wallet balance: deeplyConfirmed=${availableWallet.deeplyConfirmed.balance}, weaklyConfirmed=${availableWallet.weaklyConfirmed.balance}, unconfirmed=${availableWallet.unconfirmed.balance}" } - val utxos = buildSet { - // some utxos may be used for swap-in even if they are not confirmed, for example when migrating from the legacy phoenix android app - addAll(availableWallet.unconfirmed.filter { cmd.trustedTxs.contains(it.outPoint.txid) }) - addAll(availableWallet.weaklyConfirmed.filter { cmd.trustedTxs.contains(it.outPoint.txid) }) - addAll(availableWallet.deeplyConfirmed.filter { Transaction.write(it.previousTx.stripInputWitnesses()).size < 65_000 }) - }.toList() + val utxos = availableWallet.deeplyConfirmed.filter { Transaction.write(it.previousTx.stripInputWitnesses()).size < 65_000 } if (utxos.balance > 0.sat) { logger.info { "swap-in wallet: requesting channel using ${utxos.size} utxos with balance=${utxos.balance}" } reservedUtxos = reservedUtxos.union(utxos.map { it.outPoint }) - RequestChannelOpen(Lightning.randomBytes32(), utxos) + AddWalletInputsToChannel(utxos) } else { null } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index 6b2d5aa08..86d469615 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -1,9 +1,9 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* -import fr.acinq.lightning.ChannelEvents import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.NodeEvents import fr.acinq.lightning.blockchain.Watch import fr.acinq.lightning.channel.states.PersistedChannelState import fr.acinq.lightning.db.ChannelClosingType @@ -79,7 +79,7 @@ sealed class ChannelAction { abstract val txId: TxId abstract val localInputs: Set data class ViaNewChannel(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment() - data class ViaSpliceIn(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set, override val txId: TxId, override val origin: Origin.PayToOpenOrigin?) : StoreIncomingPayment() + data class ViaSpliceIn(val amount: MilliSatoshi, val serviceFee: MilliSatoshi, val miningFee: Satoshi, override val localInputs: Set, override val txId: TxId, override val origin: Origin?) : StoreIncomingPayment() } /** Payment sent through on-chain operations (channel close or splice-out) */ sealed class StoreOutgoingPayment : Storage() { @@ -128,8 +128,8 @@ sealed class ChannelAction { } } - data class EmitEvent(val event: ChannelEvents) : ChannelAction() + data class EmitEvent(val event: NodeEvents) : ChannelAction() - object Disconnect : ChannelAction() + data object Disconnect : ChannelAction() // @formatter:on } \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt index 9ec5ebf68..a8ed4a5e9 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt @@ -32,10 +32,11 @@ sealed class ChannelCommand { val fundingTxFeerate: FeeratePerKw, val localParams: LocalParams, val remoteInit: InitMessage, - val channelFlags: Byte, + val channelFlags: ChannelFlags, val channelConfig: ChannelConfig, val channelType: ChannelType.SupportedChannelType, - val channelOrigin: Origin? = null + val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, + val channelOrigin: Origin?, ) : Init() { fun temporaryChannelId(keyManager: KeyManager): ByteVector32 = keyManager.channelKeys(localParams.fundingKeyPath).temporaryChannelId } @@ -85,7 +86,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 = emptyList()) : Splice() { + data class Request(val replyTo: CompletableDeferred, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, 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() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt index 6bea9342b..e4f7984b7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt @@ -350,23 +350,29 @@ data class LocalParams( val htlcMinimum: MilliSatoshi, val toSelfDelay: CltvExpiryDelta, val maxAcceptedHtlcs: Int, - val isInitiator: Boolean, + val isChannelOpener: Boolean, + val paysCommitTxFees: Boolean, val defaultFinalScriptPubKey: ByteVector, val features: Features ) { - constructor(nodeParams: NodeParams, isInitiator: Boolean) : this( + constructor(nodeParams: NodeParams, isChannelOpener: Boolean, payCommitTxFees: Boolean) : this( nodeId = nodeParams.nodeId, - fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isInitiator), // we make sure that initiator and non-initiator key path end differently + fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isChannelOpener), // we make sure that initiator and non-initiator key path end differently dustLimit = nodeParams.dustLimit, maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, htlcMinimum = nodeParams.htlcMinimum, toSelfDelay = nodeParams.toRemoteDelayBlocks, // we choose their delay maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, - isInitiator = isInitiator, + isChannelOpener = isChannelOpener, + paysCommitTxFees = payCommitTxFees, defaultFinalScriptPubKey = nodeParams.keyManager.finalOnChainWallet.pubkeyScript(addressIndex = 0), // the default closing address is the same for all channels features = nodeParams.features.initFeatures() ) + // The node responsible for the commit tx fees is also the node paying the mutual close fees. + // The other node's balance may be empty, which wouldn't allow them to pay the closing fees. + val paysClosingFees: Boolean = paysCommitTxFees + fun channelKeys(keyManager: KeyManager) = keyManager.channelKeys(fundingKeyPath) } @@ -384,10 +390,11 @@ data class RemoteParams( val features: Features ) -object ChannelFlags { - const val AnnounceChannel = 0x01.toByte() - const val Empty = 0x00.toByte() -} +/** + * The [nonInitiatorPaysCommitFees] parameter can be set to true when the sender wants the receiver to pay the commitment transaction fees. + * This is not part of the BOLTs and won't be needed anymore once commitment transactions don't pay any on-chain fees. + */ +data class ChannelFlags(val announceChannel: Boolean, val nonInitiatorPaysCommitFees: Boolean) data class ClosingTxProposed(val unsignedTx: ClosingTx, val localClosingSigned: ClosingSigned) @@ -399,13 +406,17 @@ data class ChannelManagementFees(val miningFee: Satoshi, val serviceFee: Satoshi val total: Satoshi = miningFee + serviceFee } -/** Reason for creating a new channel or a splice. */ +/** Reason for creating a new channel or splicing into an existing channel. */ // @formatter:off sealed class Origin { + /** Amount of the origin payment, before fees are paid. */ abstract val amount: MilliSatoshi - abstract val serviceFee: MilliSatoshi - abstract val miningFee: Satoshi - data class PayToOpenOrigin(val paymentHash: ByteVector32, override val serviceFee: MilliSatoshi, override val miningFee: Satoshi, override val amount: MilliSatoshi) : Origin() - data class PleaseOpenChannelOrigin(val requestId: ByteVector32, override val serviceFee: MilliSatoshi, override val miningFee: Satoshi, override val amount: MilliSatoshi) : Origin() + /** Fees applied for the channel funding transaction. */ + abstract val fees: ChannelManagementFees + + data class OnChainWallet(val inputs: Set, override val amount: MilliSatoshi, override val fees: ChannelManagementFees) : Origin() + data class OffChainPayment(val paymentPreimage: ByteVector32, override val amount: MilliSatoshi, override val fees: ChannelManagementFees) : Origin() { + val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage).byteVector32() + } } // @formatter:on diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt index c1b728c9b..a094f456c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt @@ -40,7 +40,7 @@ data class ChannelParams( val channelConfig: ChannelConfig, val channelFeatures: ChannelFeatures, val localParams: LocalParams, val remoteParams: RemoteParams, - val channelFlags: Byte + val channelFlags: ChannelFlags ) { init { require(channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) { "FundingPubKeyBasedChannelKeyPath option must be enabled" } @@ -252,7 +252,7 @@ data class Commitment( val remoteCommit1 = nextRemoteCommit?.commit ?: remoteCommit val reduced = CommitmentSpec.reduce(remoteCommit1.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val balanceNoFees = (reduced.toRemote - localChannelReserve(params).toMilliSatoshi()).coerceAtLeast(0.msat) - return if (params.localParams.isInitiator) { + return if (params.localParams.paysCommitTxFees) { // The initiator always pays the on-chain fees, so we must subtract that from the amount we can send. val commitFees = commitTxFeeMsat(params.remoteParams.dustLimit, reduced) // the initiator needs to keep a "initiator fee buffer" (see explanation above) @@ -278,7 +278,7 @@ data class Commitment( fun availableBalanceForReceive(params: ChannelParams, changes: CommitmentChanges): MilliSatoshi { val reduced = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) val balanceNoFees = (reduced.toRemote - remoteChannelReserve(params).toMilliSatoshi()).coerceAtLeast(0.msat) - return if (params.localParams.isInitiator) { + return if (params.localParams.paysCommitTxFees) { // The non-initiator doesn't pay on-chain fees so we don't take those into account when receiving. balanceNoFees } else { @@ -357,7 +357,7 @@ data class Commitment( val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. - val missingForSender = reduced.toRemote - localChannelReserve(params).toMilliSatoshi() - (if (params.localParams.isInitiator) fees.toMilliSatoshi().coerceAtLeast(initiatorFeeBuffer) else 0.msat) + val missingForSender = reduced.toRemote - localChannelReserve(params).toMilliSatoshi() - (if (params.localParams.paysCommitTxFees) fees.toMilliSatoshi().coerceAtLeast(initiatorFeeBuffer) else 0.msat) // According to BOLT 2, we should also subtract the channel reserve from the calculation below. // But this creates issues with splicing in the following scenario: // - Alice opened a channel to Bob, and her balance is slightly above the reserve @@ -366,12 +366,12 @@ data class Commitment( // - The liquidity is mostly on Bob's side, but since he's unable to send HTLCs the channel is stuck // We instead only check that the channel initiator is able to pay the fees for the commit tx. // We are sending an outgoing HTLC, so once it's fulfilled it will increase their balance which is good for the channel reserve. - val missingForReceiver = reduced.toLocal - (if (params.localParams.isInitiator) 0.msat else fees.toMilliSatoshi()) + val missingForReceiver = reduced.toLocal - (if (params.localParams.paysCommitTxFees) 0.msat else fees.toMilliSatoshi()) if (missingForSender < 0.msat) { - val actualFees = if (params.localParams.isInitiator) fees else 0.sat + val actualFees = if (params.localParams.paysCommitTxFees) fees else 0.sat return Either.Left(InsufficientFunds(params.channelId, amount, -missingForSender.truncateToSatoshi(), localChannelReserve(params), actualFees)) } else if (missingForReceiver < 0.msat) { - if (params.localParams.isInitiator) { + if (params.localParams.paysCommitTxFees) { // receiver is not the initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment } else { return Either.Left(RemoteCannotAffordFeesForNewHtlc(params.channelId, amount = amount, missing = -missingForReceiver.truncateToSatoshi(), fees = fees)) @@ -406,14 +406,14 @@ data class Commitment( val fees = commitTxFee(params.localParams.dustLimit, reduced) // NB: we don't enforce the initiatorFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. - val missingForSender = reduced.toRemote - remoteChannelReserve(params).toMilliSatoshi() - (if (params.localParams.isInitiator) 0.sat else fees).toMilliSatoshi() + val missingForSender = reduced.toRemote - remoteChannelReserve(params).toMilliSatoshi() - (if (params.localParams.paysCommitTxFees) 0.sat else fees).toMilliSatoshi() // We diverge from Bolt 2 and don't subtract the channel reserve: see `canSendAdd` for details. - val missingForReceiver = reduced.toLocal - (if (params.localParams.isInitiator) fees else 0.sat).toMilliSatoshi() + val missingForReceiver = reduced.toLocal - (if (params.localParams.paysCommitTxFees) fees else 0.sat).toMilliSatoshi() if (missingForSender < 0.sat) { - val actualFees = if (params.localParams.isInitiator) 0.sat else fees + val actualFees = if (params.localParams.paysCommitTxFees) 0.sat else fees return Either.Left(InsufficientFunds(params.channelId, amount, -missingForSender.truncateToSatoshi(), remoteChannelReserve(params), actualFees)) } else if (missingForReceiver < 0.sat) { - if (params.localParams.isInitiator) { + if (params.localParams.paysCommitTxFees) { return Either.Left(CannotAffordFees(params.channelId, missing = -missingForReceiver.truncateToSatoshi(), reserve = localChannelReserve(params), fees = fees)) } else { // receiver is not the initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment @@ -737,7 +737,7 @@ data class Commitments( } fun sendFee(cmd: ChannelCommand.Commitment.UpdateFee): Either> { - if (!params.localParams.isInitiator) return Either.Left(NonInitiatorCannotSendUpdateFee(channelId)) + if (!params.localParams.paysCommitTxFees) return Either.Left(NonInitiatorCannotSendUpdateFee(channelId)) // let's compute the current commitment *as seen by them* with this change taken into account val fee = UpdateFee(channelId, cmd.feerate) // update_fee replace each other, so we can remove previous ones @@ -747,7 +747,7 @@ data class Commitments( } fun receiveFee(fee: UpdateFee, feerateTolerance: FeerateTolerance): Either { - if (params.localParams.isInitiator) return Either.Left(NonInitiatorCannotSendUpdateFee(channelId)) + if (params.localParams.paysCommitTxFees) return Either.Left(NonInitiatorCannotSendUpdateFee(channelId)) if (fee.feeratePerKw < FeeratePerKw.MinimumFeeratePerKw) return Either.Left(FeerateTooSmall(channelId, remoteFeeratePerKw = fee.feeratePerKw)) if (Helpers.isFeeDiffTooHigh(FeeratePerKw.CommitmentFeerate, fee.feeratePerKw, feerateTolerance)) return Either.Left(FeerateTooDifferent(channelId, FeeratePerKw.CommitmentFeerate, fee.feeratePerKw)) val changes1 = changes.copy(remoteChanges = changes.remoteChanges.copy(proposed = changes.remoteChanges.proposed.filterNot { it is UpdateFee } + fee)) @@ -1031,7 +1031,7 @@ data class Commitments( val outputs = makeCommitTxOutputs( channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, - localParams.isInitiator, + localParams.paysCommitTxFees, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, @@ -1041,7 +1041,7 @@ data class Commitments( remoteHtlcPubkey, spec ) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isInitiator, outputs) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isChannelOpener, outputs) val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feerate, outputs) return Pair(commitTx, htlcTxs) } @@ -1065,7 +1065,7 @@ data class Commitments( val outputs = makeCommitTxOutputs( remoteFundingPubKey, channelKeys.fundingPubKey(fundingTxIndex), - !localParams.isInitiator, + !localParams.paysCommitTxFees, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, @@ -1076,7 +1076,7 @@ data class Commitments( spec ) // NB: we are creating the remote commit tx, so local/remote parameters are inverted. - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentPubkey, !localParams.isInitiator, outputs) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentPubkey, !localParams.isChannelOpener, outputs) val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feerate, outputs) return Pair(commitTx, htlcTxs) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt index c37a4e884..64d6a770d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt @@ -258,8 +258,8 @@ object Helpers { val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote) val remoteSpec = CommitmentSpec(localHtlcs.map{ it.opposite() }.toSet(), commitTxFeerate, toLocal = toRemote, toRemote = toLocal) - if (!localParams.isInitiator) { - // They initiated the channel open, therefore they pay the fee: we need to make sure they can afford it! + if (!localParams.paysCommitTxFees) { + // They are responsible for paying the commitment transaction fee: we need to make sure they can afford it! // Note that the reserve may not be always be met: we could be using dual funding with a large funding amount on // our side and a small funding amount on their side. But we shouldn't care as long as they can pay the fees for // the commitment transaction. @@ -324,7 +324,7 @@ object Helpers { private fun firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, requestedFeerate: ClosingFeerates): ClosingFees { // this is just to estimate the weight which depends on the size of the pubkey scripts - val dummyClosingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.params.localParams.isInitiator, Satoshi(0), Satoshi(0), commitment.localCommit.spec) + val dummyClosingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.params.localParams.paysClosingFees, 0.sat, 0.sat, commitment.localCommit.spec) val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, commitment.remoteFundingPubkey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) return requestedFeerate.computeFees(closingWeight) } @@ -356,7 +356,7 @@ object Helpers { require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit)) { "invalid localScriptPubkey" } require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit)) { "invalid remoteScriptPubkey" } val dustLimit = commitment.params.localParams.dustLimit.max(commitment.params.remoteParams.dustLimit) - val closingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.params.localParams.isInitiator, dustLimit, closingFees.preferred, commitment.localCommit.spec) + val closingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.params.localParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) val localClosingSig = Transactions.sign(closingTx, channelKeys.fundingKey(commitment.fundingTxIndex)) val closingSigned = ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) return Pair(closingTx, closingSigned) @@ -510,7 +510,7 @@ object Helpers { val outputs = makeCommitTxOutputs( commitment.remoteFundingPubkey, channelKeys.fundingPubKey(commitment.fundingTxIndex), - !localParams.isInitiator, + !localParams.paysCommitTxFees, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, @@ -624,7 +624,7 @@ object Helpers { val obscuredTxNumber = Transactions.decodeTxNumber(sequence, tx.lockTime) val localPaymentPoint = channelKeys.paymentBasepoint // this tx has been published by remote, so we need to invert local/remote params - val commitmentNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !params.localParams.isInitiator, params.remoteParams.paymentBasepoint, localPaymentPoint) + val commitmentNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !params.localParams.isChannelOpener, params.remoteParams.paymentBasepoint, localPaymentPoint) if (commitmentNumber > 0xffffffffffffL) { // txNumber must be lesser than 48 bits long return null diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 5074949ee..97a374cac 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -339,7 +339,7 @@ data class FundingContributions(val inputs: List, v fun Transaction.stripInputWitnesses(): Transaction = copy(txIn = txIn.map { it.updateWitness(ScriptWitness.empty) }) /** Compute the weight we need to pay on-chain fees for. */ - private fun computeWeightPaid(isInitiator: Boolean, sharedInput: SharedFundingInput?, sharedOutputScript: ByteVector, walletInputs: List, localOutputs: List): Int { + fun computeWeightPaid(isInitiator: Boolean, sharedInput: SharedFundingInput?, sharedOutputScript: ByteVector, walletInputs: List, localOutputs: List): Int { val walletInputsWeight = weight(walletInputs) val localOutputsWeight = localOutputs.sumOf { it.weight() } return if (isInitiator) { @@ -1178,10 +1178,10 @@ sealed class SpliceStatus { val localPushAmount: MilliSatoshi, val remotePushAmount: MilliSatoshi, val liquidityLease: LiquidityAds.Lease?, - val origins: List + 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 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/Channel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt index 1982c8edd..3083829f0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt @@ -341,7 +341,9 @@ sealed class PersistedChannelState : ChannelState() { sealed class ChannelStateWithCommitments : PersistedChannelState() { abstract val commitments: Commitments override val channelId: ByteVector32 get() = commitments.channelId - val isInitiator: Boolean get() = commitments.params.localParams.isInitiator + val isChannelOpener: Boolean get() = commitments.params.localParams.isChannelOpener + val paysCommitTxFees: Boolean get() = commitments.params.localParams.paysCommitTxFees + val paysClosingFees: Boolean get() = commitments.params.localParams.paysClosingFees val remoteNodeId: PublicKey get() = commitments.remoteNodeId fun ChannelContext.channelKeys(): KeyManager.ChannelKeys = commitments.params.localParams.channelKeys(keyManager) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt index 5aea88133..321f2062a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt @@ -23,7 +23,7 @@ data class Negotiating( ) : ChannelStateWithCommitments() { init { require(closingTxProposed.isNotEmpty()) { "there must always be a list for the current negotiation" } - require(!commitments.params.localParams.isInitiator || !closingTxProposed.any { it.isEmpty() }) { "initiator must have at least one closing signature for every negotiation attempt because it initiates the closing" } + require(!paysClosingFees || !closingTxProposed.any { it.isEmpty() }) { "the node paying the closing fees must have at least one closing signature for every negotiation attempt because it initiates the closing" } } override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) @@ -62,8 +62,8 @@ data class Negotiating( val theirFeeRange = cmd.message.tlvStream.get() val ourFeeRange = closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate) when { - theirFeeRange != null && !commitments.params.localParams.isInitiator -> { - // if we are not the initiator and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation + theirFeeRange != null && !paysClosingFees -> { + // if we are not paying the on-chain fees and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation // we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation val closingFees = Helpers.Closing.firstClosingFee(commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, ourFeeRange) val closingFee = when { 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 c88ec47c9..6c847588b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -4,9 +4,7 @@ import fr.acinq.bitcoin.Bitcoin import fr.acinq.bitcoin.SigHash import fr.acinq.bitcoin.TxId import fr.acinq.bitcoin.utils.Either -import fr.acinq.lightning.Feature -import fr.acinq.lightning.Features -import fr.acinq.lightning.ShortChannelId +import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchEventConfirmed @@ -242,7 +240,7 @@ data class Normal( ShuttingDown(commitments1, localShutdown, remoteShutdown, closingFeerates) } else { logger.warning { "we have no htlcs but have not replied with our Shutdown yet, this should never happen" } - val closingTxProposed = if (isInitiator) { + val closingTxProposed = if (paysClosingFees) { val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( channelKeys(), commitments1.latest, @@ -313,7 +311,7 @@ data class Normal( if (this@Normal.localShutdown == null) actions.add(ChannelAction.Message.Send(localShutdown)) val commitments1 = commitments.copy(remoteChannelData = cmd.message.channelData) when { - commitments1.hasNoPendingHtlcsOrFeeUpdate() && commitments1.params.localParams.isInitiator -> { + commitments1.hasNoPendingHtlcsOrFeeUpdate() && paysClosingFees -> { val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( channelKeys(), commitments1.latest, @@ -378,7 +376,7 @@ data class Normal( } is SpliceStatus.InitiatorQuiescent -> { // if both sides send stfu at the same time, the quiescence initiator is the channel initiator - if (!cmd.message.initiator || commitments.params.localParams.isInitiator) { + if (!cmd.message.initiator || isChannelOpener) { if (commitments.isQuiescent()) { val parentCommitment = commitments.active.first() val fundingContribution = FundingContributions.computeSpliceContribution( @@ -389,7 +387,7 @@ data class Normal( targetFeerate = spliceStatus.command.feerate ) val commitTxFees = when { - commitments.params.localParams.isInitiator -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec) + paysCommitTxFees -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec) else -> 0.sat } if (parentCommitment.localCommit.spec.toLocal + fundingContribution.toMilliSatoshi() < parentCommitment.localChannelReserve(commitments.params).max(commitTxFees)) { @@ -493,7 +491,7 @@ data class Normal( localPushAmount = 0.msat, remotePushAmount = cmd.message.pushAmount, liquidityLease = null, - origins = cmd.message.origins + origins = listOf() ) ) Pair(nextState, listOf(ChannelAction.Message.Send(spliceAck))) @@ -566,7 +564,8 @@ data class Normal( previousLocalBalance = parentCommitment.localCommit.spec.toLocal, previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, localHtlcs = parentCommitment.localCommit.spec.htlcs, - fundingContributions.value, previousTxs = emptyList() + fundingContributions = fundingContributions.value, + previousTxs = emptyList() ).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { @@ -577,7 +576,7 @@ data class Normal( localPushAmount = spliceStatus.spliceInit.pushAmount, remotePushAmount = cmd.message.pushAmount, liquidityLease = liquidityLease.value, - origins = spliceStatus.spliceInit.origins + origins = spliceStatus.command.origins, ) ) Pair(nextState, listOf(ChannelAction.Message.Send(interactiveTxAction.msg))) @@ -842,7 +841,7 @@ data class Normal( } private fun ChannelContext.sendSpliceTxSigs( - origins: List, + origins: List, action: InteractiveTxSigningSessionAction.SendTxSigs, liquidityLease: LiquidityAds.Lease?, remoteChannelData: EncryptedChannelData @@ -862,8 +861,8 @@ data class Normal( addAll(origins.map { origin -> ChannelAction.Storage.StoreIncomingPayment.ViaSpliceIn( amount = origin.amount, - serviceFee = origin.serviceFee, - miningFee = origin.miningFee, + serviceFee = origin.fees.serviceFee.toMilliSatoshi(), + miningFee = origin.fees.miningFee, localInputs = action.fundingTx.sharedTx.tx.localInputs.map { it.outPoint }.toSet(), txId = action.fundingTx.txId, origin = origin @@ -898,6 +897,12 @@ data class Normal( val miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi() + lease.fees.miningFee add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, miningFees = miningFees, lease = lease)) } + addAll(origins.map { origin -> + when (origin) { + is Origin.OffChainPayment -> ChannelAction.EmitEvent(LiquidityEvents.Accepted(origin.amount, origin.fees.total.toMilliSatoshi(), LiquidityEvents.Source.OffChainPayment)) + is Origin.OnChainWallet -> ChannelAction.EmitEvent(SwapInEvents.Accepted(origin.inputs, origin.amount.truncateToSatoshi(), origin.fees)) + } + }) if (staticParams.useZeroConf) { logger.info { "channel is using 0-conf, sending splice_locked right away" } val spliceLocked = SpliceLocked(channelId, action.fundingTx.txId) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt index c713de36e..44a0d0d39 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt @@ -45,7 +45,7 @@ data class ShuttingDown( is Either.Right -> { val (commitments1, revocation) = result.value when { - commitments1.hasNoPendingHtlcsOrFeeUpdate() && commitments1.params.localParams.isInitiator -> { + commitments1.hasNoPendingHtlcsOrFeeUpdate() && paysClosingFees -> { val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( channelKeys(), commitments1.latest, @@ -93,7 +93,7 @@ data class ShuttingDown( val (commitments1, actions) = result.value val actions1 = actions.toMutableList() when { - commitments1.hasNoPendingHtlcsOrFeeUpdate() && commitments1.params.localParams.isInitiator -> { + commitments1.hasNoPendingHtlcsOrFeeUpdate() && paysClosingFees -> { val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( channelKeys(), commitments1.latest, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt index aece9a49e..8056161b1 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt @@ -228,7 +228,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // negotiation restarts from the beginning, and is initialized by the initiator // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them is Negotiating -> - if (state.commitments.params.localParams.isInitiator) { + if (state.paysClosingFees) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( channelKeys(), 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 c00720b28..d7337f11c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt @@ -21,7 +21,8 @@ import fr.acinq.lightning.wire.OpenDualFundedChannel */ data class WaitForAcceptChannel( val init: ChannelCommand.Init.Initiator, - val lastSent: OpenDualFundedChannel + val lastSent: OpenDualFundedChannel, + val channelOrigin: Origin?, ) : ChannelState() { val temporaryChannelId: ByteVector32 get() = lastSent.temporaryChannelId @@ -74,7 +75,7 @@ data class WaitForAcceptChannel( lastSent.channelFlags, init.channelConfig, channelFeatures, - null + channelOrigin ) val actions = listOf( ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId), 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 02e092358..c65b7acb7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -75,7 +75,7 @@ data class WaitForFundingConfirmed( } } is TxInitRbf -> { - if (isInitiator) { + if (isChannelOpener) { logger.info { "rejecting tx_init_rbf, we're the initiator, not them!" } Pair(this@WaitForFundingConfirmed, listOf(ChannelAction.Message.Send(Error(channelId, InvalidRbfNonInitiator(channelId).message)))) } else { @@ -95,7 +95,7 @@ data class WaitForFundingConfirmed( logger.info { "our peer wants to raise the feerate of the funding transaction (previous=${latestFundingTx.fundingParams.targetFeerate} target=${cmd.message.feerate})" } val fundingParams = InteractiveTxParams( channelId, - isInitiator, + isChannelOpener, latestFundingTx.fundingParams.localContribution, // we don't change our funding contribution cmd.message.fundingContribution, latestFundingTx.fundingParams.remoteFundingPubkey, @@ -128,7 +128,7 @@ data class WaitForFundingConfirmed( logger.info { "our peer accepted our rbf attempt and will contribute ${cmd.message.fundingContribution} to the funding transaction" } val fundingParams = InteractiveTxParams( channelId, - isInitiator, + isChannelOpener, rbfStatus.command.fundingAmount, cmd.message.fundingContribution, latestFundingTx.fundingParams.remoteFundingPubkey, 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 304ba5f8f..28f88518e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -38,7 +38,7 @@ data class WaitForFundingCreated( val commitTxFeerate: FeeratePerKw, val remoteFirstPerCommitmentPoint: PublicKey, val remoteSecondPerCommitmentPoint: PublicKey, - val channelFlags: Byte, + val channelFlags: ChannelFlags, val channelConfig: ChannelConfig, val channelFeatures: ChannelFeatures, val channelOrigin: Origin? 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 e1fba47e5..4c98007e7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt @@ -4,14 +4,13 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PublicKey import fr.acinq.bitcoin.crypto.Pack import fr.acinq.bitcoin.utils.Either -import fr.acinq.lightning.ChannelEvents -import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.ShortChannelId +import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.channel.* import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.toMilliSatoshi import fr.acinq.lightning.wire.* import kotlin.math.absoluteValue @@ -123,13 +122,19 @@ data class WaitForFundingSigned( if (action.commitment.localCommit.spec.toLocal > 0.msat) add( ChannelAction.Storage.StoreIncomingPayment.ViaNewChannel( amount = action.commitment.localCommit.spec.toLocal, - serviceFee = channelOrigin?.serviceFee ?: 0.msat, - miningFee = channelOrigin?.miningFee ?: action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), + serviceFee = channelOrigin?.fees?.serviceFee?.toMilliSatoshi() ?: 0.msat, + miningFee = channelOrigin?.fees?.miningFee ?: action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), localInputs = action.fundingTx.sharedTx.tx.localInputs.map { it.outPoint }.toSet(), txId = action.fundingTx.txId, origin = channelOrigin ) ) + channelOrigin?.let { + when (it) { + is Origin.OffChainPayment -> add(ChannelAction.EmitEvent(LiquidityEvents.Accepted(it.amount, it.fees.total.toMilliSatoshi(), LiquidityEvents.Source.OffChainPayment))) + is Origin.OnChainWallet -> add(ChannelAction.EmitEvent(SwapInEvents.Accepted(it.inputs, it.amount.truncateToSatoshi(), it.fees))) + } + } } return if (staticParams.useZeroConf) { logger.info { "channel is using 0-conf, we won't wait for the funding tx to confirm" } 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 c97ce807a..57930820a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt @@ -4,7 +4,10 @@ import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_SPENT import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchSpent -import fr.acinq.lightning.channel.* +import fr.acinq.lightning.channel.ChannelAction +import fr.acinq.lightning.channel.ChannelCommand +import fr.acinq.lightning.channel.Helpers +import fr.acinq.lightning.channel.LocalFundingStatus import fr.acinq.lightning.utils.msat import fr.acinq.lightning.wire.ChannelTlv import fr.acinq.lightning.wire.OpenDualFundedChannel @@ -50,12 +53,12 @@ data object WaitForInit : ChannelState() { tlvStream = TlvStream( buildSet { add(ChannelTlv.ChannelTypeTlv(cmd.channelType)) + cmd.requestRemoteFunding?.let { add(it.requestFunds) } if (cmd.pushAmount > 0.msat) add(ChannelTlv.PushAmountTlv(cmd.pushAmount)) - if (cmd.channelOrigin != null) add(ChannelTlv.OriginTlv(cmd.channelOrigin)) } ) ) - val nextState = WaitForAcceptChannel(cmd, open) + val nextState = WaitForAcceptChannel(cmd, open, cmd.channelOrigin) Pair(nextState, listOf(ChannelAction.Message.Send(open))) } is ChannelCommand.Init.Restore -> { 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 18dcdba66..4106abea7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt @@ -99,7 +99,7 @@ data class WaitForOpenChannel( open.channelFlags, channelConfig, channelFeatures, - open.origin + channelOrigin = null, ) val actions = listOf( ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index ce09b5001..0934b5054 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -22,6 +22,7 @@ import fr.acinq.lightning.logging.withMDC import fr.acinq.lightning.payment.* import fr.acinq.lightning.serialization.Encryption.from import fr.acinq.lightning.serialization.Serialization.DeserializationResult +import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* @@ -38,11 +39,6 @@ import kotlin.time.Duration.Companion.seconds sealed class PeerCommand -/** - * Try to open a channel, consuming all the spendable utxos in the wallet state provided. - */ -data class RequestChannelOpen(val requestId: ByteVector32, val walletInputs: List) : PeerCommand() - /** Open a channel, consuming all the spendable utxos in the wallet state provided. */ data class OpenChannel( val fundingAmount: Satoshi, @@ -50,10 +46,14 @@ data class OpenChannel( val walletInputs: List, val commitTxFeerate: FeeratePerKw, val fundingTxFeerate: FeeratePerKw, - val channelFlags: Byte, val channelType: ChannelType.SupportedChannelType ) : PeerCommand() +/** Consume all the spendable utxos in the wallet state provided to open a channel or splice into an existing channel. */ +data class AddWalletInputsToChannel(val walletInputs: List) : PeerCommand() { + val totalAmount: Satoshi = walletInputs.map { it.amount }.sum() +} + data class PeerConnection(val id: Long, val output: Channel, val logger: MDCLogger) { fun send(msg: LightningMessage) { // We can safely use trySend because we use unlimited channel buffers. @@ -129,7 +129,6 @@ data class AddressAssigned(val address: String) : PeerEvent() * @param watcher Watches events from the Electrum client and publishes transactions and events. * @param db Wraps the various databases persisting the channels and payments data related to the Peer. * @param socketBuilder Builds the TCP socket used to connect to the Peer. - * @param trustedSwapInTxs a set of txids that can be used for swap-in even if they are zeroconf (useful when migrating from the legacy phoenix android app). * @param initTlvStream Optional stream of TLV for the [Init] message we send to this Peer after connection. Empty by default. */ @OptIn(ExperimentalStdlibApi::class) @@ -141,7 +140,6 @@ class Peer( val db: Databases, socketBuilder: TcpSocket.Builder?, scope: CoroutineScope, - private val trustedSwapInTxs: Set = emptySet(), private val initTlvStream: TlvStream = TlvStream.empty() ) : CoroutineScope by scope { companion object { @@ -179,9 +177,6 @@ class Peer( private var _channels by _channelsFlow val channels: Map get() = _channelsFlow.value - // pending requests asking our peer to open a channel to us - private var channelRequests: Map = HashMap() - private val _connectionState = MutableStateFlow(Connection.CLOSED(null)) val connectionState: StateFlow get() = _connectionState @@ -201,7 +196,7 @@ class Peer( val currentTipFlow = MutableStateFlow(null) val onChainFeeratesFlow = MutableStateFlow(null) - val swapInFeeratesFlow = MutableStateFlow(null) + val peerFeeratesFlow = MutableStateFlow(null) private val _channelLogger = nodeParams.loggerFactory.newLogger(ChannelState::class) private suspend fun ChannelState.process(cmd: ChannelCommand): Pair> { @@ -428,8 +423,15 @@ class Peer( while (isActive) { val received = session.receive { size -> socket.receiveFully(size) } try { - val msg = LightningMessage.decode(received) - input.send(MessageReceived(peerConnection.id, msg)) + when (val msg = LightningMessage.decode(received)) { + // We treat this message immediately, which ensures that other operations can + // suspend until we receive our peer's feerates without deadlocking. + is RecommendedFeerates -> { + logger.info { "received peer recommended feerates: $msg" } + peerFeeratesFlow.value = msg + } + else -> input.send(MessageReceived(peerConnection.id, msg)) + } } catch (e: Throwable) { logger.warning { "cannot deserialize message: ${received.byteVector().toHex()}" } } @@ -496,17 +498,9 @@ class Peer( swapInJob = launch { swapInWallet.wallet.walletStateFlow .combine(currentTipFlow.filterNotNull()) { walletState, currentTip -> Pair(walletState, currentTip) } - .combine(swapInFeeratesFlow.filterNotNull()) { (walletState, currentTip), feerate -> Triple(walletState, currentTip, feerate) } + .combine(peerFeeratesFlow.filterNotNull()) { (walletState, currentTip), feerates -> Triple(walletState, currentTip, feerates.fundingFeerate) } .combine(nodeParams.liquidityPolicy) { (walletState, currentTip, feerate), policy -> TrySwapInFlow(currentTip, walletState, feerate, policy) } - .collect { w -> - // Local mutual close txs from pre-splice channels can be used as zero-conf inputs for swap-in to facilitate migration - val mutualCloseTxs = channels.values - .filterIsInstance() - .filterNot { it.commitments.params.channelFeatures.hasFeature(Feature.DualFunding) } - .flatMap { state -> state.mutualClosePublished.map { closingTx -> closingTx.tx.txid } } - val trustedTxs = trustedSwapInTxs + mutualCloseTxs - swapInCommands.send(SwapInCommand.TrySwapIn(w.currentBlockHeight, w.walletState, walletParams.swapInParams, trustedTxs)) - } + .collect { w -> swapInCommands.send(SwapInCommand.TrySwapIn(w.currentBlockHeight, w.walletState, walletParams.swapInParams)) } } } } @@ -612,7 +606,8 @@ class Peer( spliceIn = null, spliceOut = ChannelCommand.Commitment.Splice.Request.SpliceOut(amount, scriptPubKey), requestRemoteFunding = null, - feerate = feerate + feerate = feerate, + origins = listOf(), ) send(WrappedChannelCommand(channel.channelId, spliceCommand)) spliceCommand.replyTo.await() @@ -630,7 +625,8 @@ class Peer( spliceIn = null, spliceOut = null, requestRemoteFunding = null, - feerate = feerate + feerate = feerate, + origins = listOf(), ) send(WrappedChannelCommand(channel.channelId, spliceCommand)) spliceCommand.replyTo.await() @@ -648,7 +644,8 @@ class Peer( spliceIn = null, spliceOut = null, requestRemoteFunding = LiquidityAds.RequestRemoteFunding(amount, leaseStart, leaseRate), - feerate = feerate + feerate = feerate, + origins = listOf(), ) send(WrappedChannelCommand(channel.channelId, spliceCommand)) spliceCommand.replyTo.await() @@ -1028,50 +1025,13 @@ class Peer( } else if (_channels.containsKey(msg.temporaryChannelId)) { logger.warning { "ignoring open_channel with duplicate temporaryChannelId=${msg.temporaryChannelId}" } } else { - val (walletInputs, fundingAmount, pushAmount) = when (val origin = msg.origin) { - is Origin.PleaseOpenChannelOrigin -> when (val request = channelRequests[origin.requestId]) { - is RequestChannelOpen -> { - val totalFee = origin.serviceFee + origin.miningFee.toMilliSatoshi() - msg.pushAmount - nodeParams.liquidityPolicy.value.maybeReject(request.walletInputs.balance.toMilliSatoshi(), totalFee, LiquidityEvents.Source.OnChainWallet, logger)?.let { rejected -> - logger.info { "rejecting open_channel2: reason=${rejected.reason}" } - nodeParams._nodeEvents.emit(rejected) - swapInCommands.send(SwapInCommand.UnlockWalletInputs(request.walletInputs.map { it.outPoint }.toSet())) - peerConnection?.send(Error(msg.temporaryChannelId, "cancelling open due to local liquidity policy")) - return - } - val fundingFee = Transactions.weight2fee(msg.fundingFeerate, FundingContributions.weight(request.walletInputs)) - // We have to pay the fees for our inputs, so we deduce them from our funding amount. - val fundingAmount = request.walletInputs.balance - fundingFee - // We pay the other fees by pushing the corresponding amount - val pushAmount = origin.serviceFee + origin.miningFee.toMilliSatoshi() - fundingFee.toMilliSatoshi() - nodeParams._nodeEvents.emit(SwapInEvents.Accepted(request.requestId, serviceFee = origin.serviceFee, miningFee = origin.miningFee)) - Triple(request.walletInputs, fundingAmount, pushAmount) - } - - else -> { - logger.warning { "n:$remoteNodeId c:${msg.temporaryChannelId} rejecting open_channel2: cannot find channel request with requestId=${origin.requestId}" } - peerConnection?.send(Error(msg.temporaryChannelId, "no corresponding channel request")) - return - } - } - else -> Triple(listOf(), 0.sat, 0.msat) - } - if (fundingAmount.toMilliSatoshi() < pushAmount) { - logger.warning { "rejecting open_channel2 with invalid funding and push amounts ($fundingAmount < $pushAmount)" } - peerConnection?.send(Error(msg.temporaryChannelId, InvalidPushAmount(msg.temporaryChannelId, pushAmount, fundingAmount.toMilliSatoshi()).message)) - } else { - val localParams = LocalParams(nodeParams, isInitiator = false) - val state = WaitForInit - val channelConfig = ChannelConfig.standard - val (state1, actions1) = state.process(ChannelCommand.Init.NonInitiator(msg.temporaryChannelId, fundingAmount, pushAmount, walletInputs, localParams, channelConfig, theirInit!!)) - val (state2, actions2) = state1.process(ChannelCommand.MessageReceived(msg)) - _channels = _channels + (msg.temporaryChannelId to state2) - when (val origin = msg.origin) { - is Origin.PleaseOpenChannelOrigin -> channelRequests = channelRequests - origin.requestId - else -> Unit - } - processActions(msg.temporaryChannelId, peerConnection, actions1 + actions2) - } + 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 (state2, actions2) = state1.process(ChannelCommand.MessageReceived(msg)) + _channels = _channels + (msg.temporaryChannelId to state2) + processActions(msg.temporaryChannelId, peerConnection, actions1 + actions2) } } is ChannelReestablish -> { @@ -1227,63 +1187,8 @@ class Peer( _channels = _channels + (cmd.watch.channelId to state1) } } - is RequestChannelOpen -> { - when (val available = selectChannelForSplicing()) { - is SelectChannelResult.Available -> { - // We have a channel and we are connected (otherwise state would be Offline/Syncing). - val targetFeerate = swapInFeeratesFlow.filterNotNull().first() - val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = available.channel.commitments.active.first(), walletInputs = cmd.walletInputs, localOutputs = emptyList()) - val (feerate, fee) = client.computeSpliceCpfpFeerate(available.channel.commitments, targetFeerate, spliceWeight = weight, logger) - - logger.info { "requesting splice-in using balance=${cmd.walletInputs.balance} feerate=$feerate fee=$fee" } - nodeParams.liquidityPolicy.value.maybeReject(cmd.walletInputs.balance.toMilliSatoshi(), fee.toMilliSatoshi(), LiquidityEvents.Source.OnChainWallet, logger)?.let { rejected -> - logger.info { "rejecting splice: reason=${rejected.reason}" } - nodeParams._nodeEvents.emit(rejected) - swapInCommands.send(SwapInCommand.UnlockWalletInputs(cmd.walletInputs.map { it.outPoint }.toSet())) - return - } - - val spliceCommand = ChannelCommand.Commitment.Splice.Request( - replyTo = CompletableDeferred(), - spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(cmd.walletInputs), - spliceOut = null, - requestRemoteFunding = null, - feerate = feerate - ) - // If the splice fails, we immediately unlock the utxos to reuse them in the next attempt. - spliceCommand.replyTo.invokeOnCompletion { ex -> - if (ex == null && spliceCommand.replyTo.getCompleted() is ChannelCommand.Commitment.Splice.Response.Failure) { - swapInCommands.trySend(SwapInCommand.UnlockWalletInputs(cmd.walletInputs.map { it.outPoint }.toSet())) - } - } - input.send(WrappedChannelCommand(available.channel.channelId, spliceCommand)) - } - SelectChannelResult.NotReady -> { - // There are existing channels but not immediately usable (e.g. creating, disconnected), we don't do anything yet. - logger.info { "ignoring channel request, existing channels are not ready for splice-in: ${channels.values.map { it::class.simpleName }}" } - swapInCommands.trySend(SwapInCommand.UnlockWalletInputs(cmd.walletInputs.map { it.outPoint }.toSet())) - } - SelectChannelResult.None -> { - // Either there are no channels, or they will never be suitable for a splice-in: we request a new channel. - // Grandparents are supplied as a proof of migration. - val grandParents = cmd.walletInputs.map { utxo -> utxo.previousTx.txIn.map { txIn -> txIn.outPoint } }.flatten() - val pleaseOpenChannel = PleaseOpenChannel( - nodeParams.chainHash, - cmd.requestId, - cmd.walletInputs.balance, - cmd.walletInputs.size, - FundingContributions.weight(cmd.walletInputs), - TlvStream(PleaseOpenChannelTlv.GrandParents(grandParents)) - ) - logger.info { "sending please_open_channel with ${cmd.walletInputs.size} utxos (amount = ${cmd.walletInputs.balance})" } - peerConnection?.send(pleaseOpenChannel) - nodeParams._nodeEvents.emit(SwapInEvents.Requested(pleaseOpenChannel)) - channelRequests = channelRequests + (pleaseOpenChannel.requestId to cmd) - } - } - } is OpenChannel -> { - val localParams = LocalParams(nodeParams, isInitiator = true) + val localParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = true) val state = WaitForInit val (state1, actions1) = state.process( ChannelCommand.Init.Initiator( @@ -1294,15 +1199,120 @@ class Peer( cmd.fundingTxFeerate, localParams, theirInit!!, - cmd.channelFlags, + ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), ChannelConfig.standard, - cmd.channelType + cmd.channelType, + requestRemoteFunding = null, + channelOrigin = null, ) ) val msg = actions1.filterIsInstance().map { it.message }.filterIsInstance().first() _channels = _channels + (msg.temporaryChannelId to state1) processActions(msg.temporaryChannelId, peerConnection, actions1) } + is AddWalletInputsToChannel -> { + when (val available = selectChannelForSplicing()) { + is SelectChannelResult.Available -> { + // We have a channel and we are connected. + val targetFeerate = peerFeeratesFlow.filterNotNull().first().fundingFeerate + val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = available.channel.commitments.active.first(), walletInputs = cmd.walletInputs, localOutputs = emptyList()) + val (feerate, fee) = client.computeSpliceCpfpFeerate(available.channel.commitments, targetFeerate, spliceWeight = weight, logger) + logger.info { "requesting splice-in using balance=${cmd.walletInputs.balance} feerate=$feerate fee=$fee" } + when (val rejected = nodeParams.liquidityPolicy.value.maybeReject(cmd.walletInputs.balance.toMilliSatoshi(), fee.toMilliSatoshi(), LiquidityEvents.Source.OnChainWallet, logger)) { + is LiquidityEvents.Rejected -> { + logger.info { "rejecting splice: reason=${rejected.reason}" } + nodeParams._nodeEvents.emit(rejected) + swapInCommands.trySend(SwapInCommand.UnlockWalletInputs(cmd.walletInputs.map { it.outPoint }.toSet())) + } + else -> { + val spliceCommand = ChannelCommand.Commitment.Splice.Request( + replyTo = CompletableDeferred(), + spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(cmd.walletInputs), + spliceOut = null, + requestRemoteFunding = null, + feerate = feerate, + origins = listOf(Origin.OnChainWallet(cmd.walletInputs.map { it.outPoint }.toSet(), cmd.totalAmount.toMilliSatoshi(), ChannelManagementFees(fee, 0.sat))) + ) + // If the splice fails, we immediately unlock the utxos to reuse them in the next attempt. + spliceCommand.replyTo.invokeOnCompletion { ex -> + if (ex == null && spliceCommand.replyTo.getCompleted() is ChannelCommand.Commitment.Splice.Response.Failure) { + swapInCommands.trySend(SwapInCommand.UnlockWalletInputs(cmd.walletInputs.map { it.outPoint }.toSet())) + } + } + input.send(WrappedChannelCommand(available.channel.channelId, spliceCommand)) + nodeParams._nodeEvents.emit(SwapInEvents.Requested(cmd.walletInputs)) + } + } + } + SelectChannelResult.NotReady -> { + // There are existing channels but not immediately usable (e.g. creating, disconnected), we don't do anything yet. + logger.info { "ignoring request to add utxos to channel, existing channels are not ready for splice-in: ${channels.values.map { it::class.simpleName }}" } + swapInCommands.trySend(SwapInCommand.UnlockWalletInputs(cmd.walletInputs.map { it.outPoint }.toSet())) + } + SelectChannelResult.None -> { + // Either there are no channels, or they will never be suitable for a splice-in: we open a new channel. + val currentFeerates = peerFeeratesFlow.filterNotNull().first() + val requestRemoteFunding = run { + // We need our peer to contribute, because they must have enough funds to pay the commitment fees. + val inboundLiquidityTarget = when (val policy = nodeParams.liquidityPolicy.first()) { + is LiquidityPolicy.Disable -> LiquidityPolicy.minInboundLiquidityTarget + is LiquidityPolicy.Auto -> policy.inboundLiquidityTarget ?: LiquidityPolicy.minInboundLiquidityTarget + } + LiquidityAds.RequestRemoteFunding(inboundLiquidityTarget, currentTipFlow.filterNotNull().first(), walletParams.leaseRate) + } + val (localFundingAmount, fees) = run { + // We need to know the local channel funding amount to be able use channel opening messages. + // We must pay on-chain fees for our inputs/outputs of the transaction: we compute them first + // and proceed backwards to retrieve the funding amount. + 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) + // 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) + 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)) { + is LiquidityEvents.Rejected -> { + logger.info { "rejecting channel open: reason=${rejected.reason}" } + nodeParams._nodeEvents.emit(rejected) + swapInCommands.trySend(SwapInCommand.UnlockWalletInputs(cmd.walletInputs.map { it.outPoint }.toSet())) + } + else -> { + // We ask our peer to pay the commit tx fees. + val localParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = false) + val channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = true) + val (state, actions) = WaitForInit.process( + ChannelCommand.Init.Initiator( + fundingAmount = localFundingAmount, + pushAmount = 0.msat, + walletInputs = cmd.walletInputs, + commitTxFeerate = currentFeerates.commitmentFeerate, + fundingTxFeerate = currentFeerates.fundingFeerate, + localParams = localParams, + remoteInit = theirInit!!, + channelFlags = channelFlags, + channelConfig = ChannelConfig.standard, + channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, + requestRemoteFunding = requestRemoteFunding, + channelOrigin = Origin.OnChainWallet(cmd.walletInputs.map { it.outPoint }.toSet(), cmd.totalAmount.toMilliSatoshi(), fees), + ) + ) + val msg = actions.filterIsInstance().map { it.message }.filterIsInstance().first() + _channels = _channels + (msg.temporaryChannelId to state) + processActions(msg.temporaryChannelId, peerConnection, actions) + nodeParams._nodeEvents.emit(SwapInEvents.Requested(cmd.walletInputs)) + } + } + } + } + } + } is PayToOpenResponseCommand -> { logger.info { "sending ${cmd.payToOpenResponse::class.simpleName}" } peerConnection?.send(cmd.payToOpenResponse) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt b/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt index d4b963a5e..d50a7ec17 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt @@ -66,6 +66,7 @@ JsonSerializers.LiquidityLeaseFeesSerializer::class, JsonSerializers.LiquidityLeaseWitnessSerializer::class, JsonSerializers.LiquidityLeaseSerializer::class, + JsonSerializers.ChannelFlagsSerializer::class, JsonSerializers.ChannelParamsSerializer::class, JsonSerializers.ChannelOriginSerializer::class, JsonSerializers.CommitmentChangesSerializer::class, @@ -309,6 +310,9 @@ object JsonSerializers { @Serializer(forClass = LiquidityAds.Lease::class) object LiquidityLeaseSerializer + @Serializer(forClass = ChannelFlags::class) + object ChannelFlagsSerializer + @Serializer(forClass = ChannelParams::class) object ChannelParamsSerializer diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt index fd814cabc..2e8b53633 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt @@ -138,7 +138,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPayment ) } when (val origin = action.origin) { - is Origin.PayToOpenOrigin -> { + is Origin.OffChainPayment -> { // there already is a corresponding Lightning invoice in the db db.receivePayment( paymentHash = origin.paymentHash, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/payment/LiquidityPolicy.kt b/src/commonMain/kotlin/fr/acinq/lightning/payment/LiquidityPolicy.kt index 101dc7bb7..1d861ca13 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/payment/LiquidityPolicy.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/payment/LiquidityPolicy.kt @@ -3,24 +3,26 @@ package fr.acinq.lightning.payment import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.LiquidityEvents import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.logging.* +import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toMilliSatoshi - sealed class LiquidityPolicy { - /** Never initiates swap-ins, never accept pay-to-open */ + /** Never initiates swap-ins, never accept on-the-fly funding requests. */ data object Disable : LiquidityPolicy() /** - * Allow automated liquidity managements, within relative and absolute fee limits. Both conditions must be met. + * Allow automated liquidity management, within relative and absolute fee limits. Both conditions must be met. + * + * @param inboundLiquidityTarget amount of inbound liquidity the buyer would like to purchase whenever necessary (can be set to null to disable) * @param maxAbsoluteFee max absolute fee * @param maxRelativeFeeBasisPoints max relative fee (all included: service fee and mining fee) (1_000 bips = 10 %) * @param skipAbsoluteFeeCheck only applies for off-chain payments, being more lax may make sense when the sender doesn't retry payments */ - data class Auto(val maxAbsoluteFee: Satoshi, val maxRelativeFeeBasisPoints: Int, val skipAbsoluteFeeCheck: Boolean) : LiquidityPolicy() + data class Auto(val inboundLiquidityTarget: Satoshi?, val maxAbsoluteFee: Satoshi, val maxRelativeFeeBasisPoints: Int, val skipAbsoluteFeeCheck: Boolean) : LiquidityPolicy() - /** Make decision for a particular liquidity event */ + /** Make a decision for a particular liquidity event. */ fun maybeReject(amount: MilliSatoshi, fee: MilliSatoshi, source: LiquidityEvents.Source, logger: MDCLogger): LiquidityEvents.Rejected? { return when (this) { is Disable -> LiquidityEvents.Rejected.Reason.PolicySetToDisabled @@ -28,13 +30,20 @@ sealed class LiquidityPolicy { val maxAbsoluteFee = if (skipAbsoluteFeeCheck && source == LiquidityEvents.Source.OffChainPayment) Long.MAX_VALUE.msat else this.maxAbsoluteFee.toMilliSatoshi() val maxRelativeFee = amount * maxRelativeFeeBasisPoints / 10_000 logger.info { "liquidity policy check: fee=$fee maxAbsoluteFee=$maxAbsoluteFee maxRelativeFee=$maxRelativeFee policy=$this" } - if (fee > maxRelativeFee) { - LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee(maxRelativeFeeBasisPoints) - } else if (fee > maxAbsoluteFee) { - LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(this.maxAbsoluteFee) - } else null + when { + fee > maxRelativeFee -> LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee(maxRelativeFeeBasisPoints) + fee > maxAbsoluteFee -> LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(this.maxAbsoluteFee) + else -> null // accept + } } }?.let { reason -> LiquidityEvents.Rejected(amount, fee, source, reason) } } + companion object { + /** + * We usually need our peer to contribute to channel funding, because they must have enough funds to pay the commitment fees. + * When we don't have an inbound liquidity target set, we use the following default amount. + */ + val minInboundLiquidityTarget: Satoshi = 100_000.sat + } } \ No newline at end of file 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 c714a277b..b4667924e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt @@ -43,6 +43,7 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.ChannelFlags import fr.acinq.lightning.channel.InteractiveTxOutput import fr.acinq.lightning.channel.SpliceStatus import fr.acinq.lightning.channel.states.* @@ -295,7 +296,7 @@ internal data class RevokedCommitPublished( * This means that they will be recomputed once when we convert serialized data to their "live" counterparts. */ @Serializable -internal data class LocalParams constructor( +internal data class LocalParams( val nodeId: PublicKey, val fundingKeyPath: KeyPath, val dustLimit: Satoshi, @@ -317,6 +318,7 @@ internal data class LocalParams constructor( toSelfDelay, maxAcceptedHtlcs, isFunder, + isFunder, defaultFinalScriptPubKey, features ) @@ -410,7 +412,7 @@ internal data class Commitments( ChannelVersion.channelFeatures, localParams.export(), remoteParams.export(), - channelFlags + ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), ), fr.acinq.lightning.channel.CommitmentChanges( localChanges.export(), 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 38a4e595e..1cec553df 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt @@ -43,6 +43,7 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.ChannelFlags import fr.acinq.lightning.channel.InteractiveTxOutput import fr.acinq.lightning.channel.SpliceStatus import fr.acinq.lightning.channel.states.* @@ -295,7 +296,7 @@ internal data class RevokedCommitPublished( * This means that they will be recomputed once when we convert serialized data to their "live" counterparts. */ @Serializable -internal data class LocalParams constructor( +internal data class LocalParams( val nodeId: PublicKey, val fundingKeyPath: KeyPath, val dustLimit: Satoshi, @@ -317,6 +318,7 @@ internal data class LocalParams constructor( toSelfDelay, maxAcceptedHtlcs, isFunder, + isFunder, defaultFinalScriptPubKey, features ) @@ -403,7 +405,7 @@ internal data class Commitments( channelFeatures.export(), localParams.export(), remoteParams.export(), - channelFlags + ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), ), fr.acinq.lightning.channel.CommitmentChanges( localChanges.export(), 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 d7d5ff33a..07ba38a1b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -15,7 +15,10 @@ import fr.acinq.lightning.channel.states.* import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.transactions.* import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* -import fr.acinq.lightning.utils.* +import fr.acinq.lightning.utils.UUID +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* object Deserialization { @@ -97,10 +100,7 @@ object Deserialization { closingFeerates = readNullable { readClosingFeerates() }, spliceStatus = when (val discriminator = read()) { 0x00 -> SpliceStatus.None - 0x01 -> SpliceStatus.WaitingForSigs( - session = readInteractiveTxSigningSession(), - origins = readCollection { readChannelOrigin() as Origin.PayToOpenOrigin }.toList() - ) + 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(), readCollection { readChannelOrigin() }.toList()) else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") }, liquidityLeases = when { @@ -424,51 +424,76 @@ object Deserialization { } private fun Input.readChannelOrigin(): Origin = when (val discriminator = read()) { - 0x01 -> Origin.PayToOpenOrigin( - paymentHash = readByteVector32(), - serviceFee = readNumber().msat, - miningFee = readNumber().sat, + 0x01 -> { + // Note that we've replaced this field by the payment preimage: old entries will be incorrect, but it's not critical. + val paymentHash = readByteVector32() + val serviceFee = readNumber().msat + val miningFee = readNumber().sat + val amount = readNumber().msat + Origin.OffChainPayment(paymentHash, amount, ChannelManagementFees(miningFee, serviceFee.truncateToSatoshi())) + } + 0x02 -> { + readByteVector32() // unused requestId + val serviceFee = readNumber().msat + val miningFee = readNumber().sat + val amount = readNumber().msat + Origin.OnChainWallet(setOf(), amount, ChannelManagementFees(miningFee, serviceFee.truncateToSatoshi())) + } + 0x03 -> Origin.OffChainPayment( + paymentPreimage = readByteVector32(), amount = readNumber().msat, + fees = ChannelManagementFees(miningFee = readNumber().sat, serviceFee = readNumber().sat), ) - 0x02 -> Origin.PleaseOpenChannelOrigin( - requestId = readByteVector32(), - serviceFee = readNumber().msat, - miningFee = readNumber().sat, + 0x04 -> Origin.OnChainWallet( + inputs = readCollection { readOutPoint() }.toSet(), amount = readNumber().msat, + fees = ChannelManagementFees(miningFee = readNumber().sat, serviceFee = readNumber().sat), ) else -> error("unknown discriminator $discriminator for class ${Origin::class}") } + private fun Input.readLocalParams(): LocalParams { + val nodeId = readPublicKey() + val fundingKeyPath = KeyPath(readCollection { readNumber() }.toList()) + val dustLimit = readNumber().sat + val maxHtlcValueInFlightMsat = readNumber() + val htlcMinimum = readNumber().msat + val toSelfDelay = CltvExpiryDelta(readNumber().toInt()) + val maxAcceptedHtlcs = readNumber().toInt() + val flags = readNumber().toInt() + val isChannelOpener = flags.and(1) != 0 + val payCommitTxFees = flags.and(2) != 0 + val defaultFinalScriptPubKey = readDelimitedByteArray().toByteVector() + val features = Features(readDelimitedByteArray().toByteVector()) + return LocalParams(nodeId, fundingKeyPath, dustLimit, maxHtlcValueInFlightMsat, htlcMinimum, toSelfDelay, maxAcceptedHtlcs, isChannelOpener, payCommitTxFees, defaultFinalScriptPubKey, features) + } + + private fun Input.readRemoteParams(): RemoteParams = RemoteParams( + nodeId = readPublicKey(), + dustLimit = readNumber().sat, + maxHtlcValueInFlightMsat = readNumber(), + htlcMinimum = readNumber().msat, + toSelfDelay = CltvExpiryDelta(readNumber().toInt()), + maxAcceptedHtlcs = readNumber().toInt(), + revocationBasepoint = readPublicKey(), + paymentBasepoint = readPublicKey(), + delayedPaymentBasepoint = readPublicKey(), + htlcBasepoint = readPublicKey(), + features = Features(readDelimitedByteArray().toByteVector()) + ) + + private fun Input.readChannelFlags(): ChannelFlags { + val flags = readNumber().toInt() + return ChannelFlags(announceChannel = flags.and(1) != 0, nonInitiatorPaysCommitFees = flags.and(2) != 0) + } + private fun Input.readChannelParams(): ChannelParams = ChannelParams( channelId = readByteVector32(), channelConfig = ChannelConfig(readDelimitedByteArray()), channelFeatures = ChannelFeatures(Features(readDelimitedByteArray()).activated.keys), - localParams = LocalParams( - nodeId = readPublicKey(), - fundingKeyPath = KeyPath(readCollection { readNumber() }.toList()), - dustLimit = readNumber().sat, - maxHtlcValueInFlightMsat = readNumber(), - htlcMinimum = readNumber().msat, - toSelfDelay = CltvExpiryDelta(readNumber().toInt()), - maxAcceptedHtlcs = readNumber().toInt(), - isInitiator = readBoolean(), - defaultFinalScriptPubKey = readDelimitedByteArray().toByteVector(), - features = Features(readDelimitedByteArray().toByteVector()) - ), - remoteParams = RemoteParams( - nodeId = readPublicKey(), - dustLimit = readNumber().sat, - maxHtlcValueInFlightMsat = readNumber(), - htlcMinimum = readNumber().msat, - toSelfDelay = CltvExpiryDelta(readNumber().toInt()), - maxAcceptedHtlcs = readNumber().toInt(), - revocationBasepoint = readPublicKey(), - paymentBasepoint = readPublicKey(), - delayedPaymentBasepoint = readPublicKey(), - htlcBasepoint = readPublicKey(), - features = Features(readDelimitedByteArray().toByteVector()) - ), - channelFlags = readNumber().toByte(), + localParams = readLocalParams(), + remoteParams = readRemoteParams(), + channelFlags = readChannelFlags(), ) private fun Input.readCommitmentChanges(): CommitmentChanges = CommitmentChanges( 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 48aab0206..b61b0e58d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -462,19 +462,19 @@ object Serialization { } private fun Output.writeChannelOrigin(o: Origin) = when (o) { - is Origin.PayToOpenOrigin -> { - write(0x01) - writeByteVector32(o.paymentHash) - writeNumber(o.serviceFee.toLong()) - writeNumber(o.miningFee.toLong()) + is Origin.OffChainPayment -> { + write(0x03) + writeByteVector32(o.paymentPreimage) writeNumber(o.amount.toLong()) + writeNumber(o.fees.miningFee.toLong()) + writeNumber(o.fees.serviceFee.toLong()) } - is Origin.PleaseOpenChannelOrigin -> { - write(0x02) - writeByteVector32(o.requestId) - writeNumber(o.serviceFee.toLong()) - writeNumber(o.miningFee.toLong()) + is Origin.OnChainWallet -> { + write(0x04) + writeCollection(o.inputs) { writeBtcObject(it) } writeNumber(o.amount.toLong()) + writeNumber(o.fees.miningFee.toLong()) + writeNumber(o.fees.serviceFee.toLong()) } } @@ -490,7 +490,10 @@ object Serialization { writeNumber(htlcMinimum.toLong()) writeNumber(toSelfDelay.toLong()) writeNumber(maxAcceptedHtlcs) - writeBoolean(isInitiator) + // We encode those two booleans in the same byte. + val isOpenerFlag = if (isChannelOpener) 1 else 0 + val payCommitTxFeesFlag = if (paysCommitTxFees) 2 else 0 + writeNumber(isOpenerFlag + payCommitTxFeesFlag) writeDelimited(defaultFinalScriptPubKey.toByteArray()) writeDelimited(features.toByteArray()) } @@ -507,7 +510,10 @@ object Serialization { writePublicKey(htlcBasepoint) writeDelimited(features.toByteArray()) } - writeNumber(channelFlags) + // We encode channel flags in the same byte. + val announceChannelFlag = if (channelFlags.announceChannel) 1 else 0 + val nonInitiatorPaysCommitFeesFlag = if (channelFlags.nonInitiatorPaysCommitFees) 2 else 0 + writeNumber(announceChannelFlag + nonInitiatorPaysCommitFeesFlag) } private fun Output.writeCommitmentChanges(o: CommitmentChanges) = o.run { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index b826695a4..0d30501a5 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -309,7 +309,7 @@ object Transactions { fun makeCommitTxOutputs( localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, - localIsInitiator: Boolean, + localPaysCommitTxFees: Boolean, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, @@ -321,7 +321,7 @@ object Transactions { ): TransactionsCommitmentOutputs { val commitFee = commitTxFee(localDustLimit, spec) - val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsInitiator) { + val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localPaysCommitTxFees) { Pair(spec.toLocal.truncateToSatoshi() - commitFee, spec.toRemote.truncateToSatoshi()) } else { Pair(spec.toLocal.truncateToSatoshi(), spec.toRemote.truncateToSatoshi() - commitFee) @@ -383,11 +383,11 @@ object Transactions { commitTxNumber: Long, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey, - localIsInitiator: Boolean, + localIsChannelOpener: Boolean, outputs: TransactionsCommitmentOutputs ): TransactionWithInputInfo.CommitTx { - val txnumber = obscuredCommitTxNumber(commitTxNumber, localIsInitiator, localPaymentBasePoint, remotePaymentBasePoint) - val (sequence, locktime) = encodeTxNumber(txnumber) + val txNumber = obscuredCommitTxNumber(commitTxNumber, localIsChannelOpener, localPaymentBasePoint, remotePaymentBasePoint) + val (sequence, locktime) = encodeTxNumber(txNumber) val tx = Transaction( version = 2, @@ -739,14 +739,14 @@ object Transactions { commitTxInput: InputInfo, localScriptPubKey: ByteArray, remoteScriptPubKey: ByteArray, - localIsInitiator: Boolean, + localPaysClosingFees: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec ): TransactionWithInputInfo.ClosingTx { require(spec.htlcs.isEmpty()) { "there shouldn't be any pending htlcs" } - val (toLocalAmount, toRemoteAmount) = if (localIsInitiator) { + val (toLocalAmount, toRemoteAmount) = if (localPaysClosingFees) { Pair(spec.toLocal.truncateToSatoshi() - closingFee, spec.toRemote.truncateToSatoshi()) } else { Pair(spec.toLocal.truncateToSatoshi(), spec.toRemote.truncateToSatoshi() - closingFee) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt index 0ac111bab..7b5d3d0c9 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt @@ -8,7 +8,6 @@ import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelType -import fr.acinq.lightning.channel.Origin import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector @@ -116,79 +115,6 @@ sealed class ChannelTlv : Tlv { } } - data class OriginTlv(val origin: Origin) : ChannelTlv() { - override val tag: Long get() = OriginTlv.tag - - override fun write(out: Output) { - when (origin) { - is Origin.PayToOpenOrigin -> { - LightningCodecs.writeU16(1, out) - LightningCodecs.writeBytes(origin.paymentHash, out) - LightningCodecs.writeU64(origin.miningFee.toLong(), out) - LightningCodecs.writeU64(origin.serviceFee.toLong(), out) - LightningCodecs.writeU64(origin.amount.toLong(), out) - } - - is Origin.PleaseOpenChannelOrigin -> { - LightningCodecs.writeU16(4, out) - LightningCodecs.writeBytes(origin.requestId, out) - LightningCodecs.writeU64(origin.miningFee.toLong(), out) - LightningCodecs.writeU64(origin.serviceFee.toLong(), out) - LightningCodecs.writeU64(origin.amount.toLong(), out) - } - } - } - - companion object : TlvValueReader { - const val tag: Long = 0x47000005 - - override fun read(input: Input): OriginTlv { - val origin = when (LightningCodecs.u16(input)) { - 1 -> Origin.PayToOpenOrigin( - paymentHash = LightningCodecs.bytes(input, 32).byteVector32(), - miningFee = LightningCodecs.u64(input).sat, - serviceFee = LightningCodecs.u64(input).msat, - amount = LightningCodecs.u64(input).msat - ) - - 4 -> Origin.PleaseOpenChannelOrigin( - requestId = LightningCodecs.bytes(input, 32).byteVector32(), - miningFee = LightningCodecs.u64(input).sat, - serviceFee = LightningCodecs.u64(input).msat, - amount = LightningCodecs.u64(input).msat - ) - - else -> error("Unsupported channel origin discriminator") - } - return OriginTlv(origin) - } - } - } - - /** With rbfed splices we can have multiple origins*/ - data class OriginsTlv(val origins: List) : ChannelTlv() { - override val tag: Long get() = OriginsTlv.tag - - override fun write(out: Output) { - LightningCodecs.writeU16(origins.size, out) - origins.forEach { OriginTlv(it).write(out) } - } - - companion object : TlvValueReader { - const val tag: Long = 0x47000009 - - override fun read(input: Input): OriginsTlv { - val size = LightningCodecs.u16(input) - val origins = buildList { - for (i in 0 until size) { - add(OriginTlv.read(input).origin) - } - } - return OriginsTlv(origins) - } - } - } - /** Amount that will be offered by the initiator of a dual-funded channel to the non-initiator. */ data class PushAmountTlv(val amount: MilliSatoshi) : ChannelTlv() { override val tag: Long get() = PushAmountTlv.tag @@ -340,40 +266,6 @@ sealed class ClosingSignedTlv : Tlv { } } -sealed class PleaseOpenChannelTlv : Tlv { - // NB: this is a temporary tlv that is only used to ensure a smooth migration to lightning-kmp for the android version of Phoenix. - data class GrandParents(val outpoints: List) : PleaseOpenChannelTlv() { - override val tag: Long get() = GrandParents.tag - override fun write(out: Output) { - outpoints.forEach { outpoint -> - LightningCodecs.writeTxHash(outpoint.hash, out) - LightningCodecs.writeU64(outpoint.index, out) - } - } - - companion object : TlvValueReader { - const val tag: Long = 561 - override fun read(input: Input): GrandParents { - val count = input.availableBytes / 40 - val outpoints = (0 until count).map { OutPoint(LightningCodecs.txHash(input), LightningCodecs.u64(input)) } - return GrandParents(outpoints) - } - } - } -} - -sealed class PleaseOpenChannelRejectedTlv : Tlv { - data class ExpectedFees(val fees: MilliSatoshi) : PleaseOpenChannelRejectedTlv() { - override val tag: Long get() = ExpectedFees.tag - override fun write(out: Output) = LightningCodecs.writeTU64(fees.toLong(), out) - - companion object : TlvValueReader { - const val tag: Long = 1 - override fun read(input: Input): ExpectedFees = ExpectedFees(LightningCodecs.tu64(input).msat) - } - } -} - sealed class PayToOpenRequestTlv : Tlv { /** Blinding ephemeral public key that should be used to derive shared secrets when using route blinding. */ data class Blinding(val publicKey: PublicKey) : PayToOpenRequestTlv() { @@ -386,4 +278,4 @@ sealed class PayToOpenRequestTlv : Tlv { override fun read(input: Input): Blinding = Blinding(PublicKey(LightningCodecs.bytes(input, 33))) } } -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 48db001c1..6609298fc 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -8,9 +8,10 @@ import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.ChannelFlags import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.channel.Origin -import fr.acinq.lightning.logging.* +import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.router.Announcements import fr.acinq.lightning.utils.* import fr.acinq.secp256k1.Hex @@ -86,7 +87,7 @@ interface LightningMessage { DNSAddressRequest.type -> DNSAddressRequest.read(stream) DNSAddressResponse.type -> DNSAddressResponse.read(stream) PhoenixAndroidLegacyInfo.type -> PhoenixAndroidLegacyInfo.read(stream) - PleaseOpenChannel.type -> PleaseOpenChannel.read(stream) + RecommendedFeerates.type -> RecommendedFeerates.read(stream) Stfu.type -> Stfu.read(stream) SpliceInit.type -> SpliceInit.read(stream) SpliceAck.type -> SpliceAck.read(stream) @@ -668,13 +669,12 @@ data class OpenDualFundedChannel( val htlcBasepoint: PublicKey, val firstPerCommitmentPoint: PublicKey, val secondPerCommitmentPoint: PublicKey, - val channelFlags: Byte, + val channelFlags: ChannelFlags, val tlvStream: TlvStream = TlvStream.empty() ) : ChannelMessage, HasTemporaryChannelId, HasChainHash { val channelType: ChannelType? get() = tlvStream.get()?.channelType val pushAmount: MilliSatoshi get() = tlvStream.get()?.amount ?: 0.msat val requestFunds: ChannelTlv.RequestFunds? get() = tlvStream.get() - val origin: Origin? get() = tlvStream.get()?.origin override val type: Long get() = OpenDualFundedChannel.type @@ -697,7 +697,9 @@ data class OpenDualFundedChannel( LightningCodecs.writeBytes(htlcBasepoint.value, out) LightningCodecs.writeBytes(firstPerCommitmentPoint.value, out) LightningCodecs.writeBytes(secondPerCommitmentPoint.value, out) - LightningCodecs.writeByte(channelFlags.toInt(), out) + val announceChannelFlag = if (channelFlags.announceChannel) 1 else 0 + val commitFeesFlag = if (channelFlags.nonInitiatorPaysCommitFees) 2 else 0 + LightningCodecs.writeByte(announceChannelFlag + commitFeesFlag, out) TlvStreamSerializer(false, readers).write(tlvStream, out) } @@ -710,32 +712,54 @@ data class OpenDualFundedChannel( ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader, ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, ChannelTlv.RequestFunds.tag to ChannelTlv.RequestFunds as TlvValueReader, - ChannelTlv.OriginTlv.tag to ChannelTlv.OriginTlv.Companion as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, ) - override fun read(input: Input): OpenDualFundedChannel = OpenDualFundedChannel( - BlockHash(LightningCodecs.bytes(input, 32)), - ByteVector32(LightningCodecs.bytes(input, 32)), - FeeratePerKw(LightningCodecs.u32(input).toLong().sat), - FeeratePerKw(LightningCodecs.u32(input).toLong().sat), - Satoshi(LightningCodecs.u64(input)), - Satoshi(LightningCodecs.u64(input)), - LightningCodecs.u64(input), // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - MilliSatoshi(LightningCodecs.u64(input)), - CltvExpiryDelta(LightningCodecs.u16(input)), - LightningCodecs.u16(input), - LightningCodecs.u32(input).toLong(), - PublicKey(LightningCodecs.bytes(input, 33)), - PublicKey(LightningCodecs.bytes(input, 33)), - PublicKey(LightningCodecs.bytes(input, 33)), - PublicKey(LightningCodecs.bytes(input, 33)), - PublicKey(LightningCodecs.bytes(input, 33)), - PublicKey(LightningCodecs.bytes(input, 33)), - PublicKey(LightningCodecs.bytes(input, 33)), - LightningCodecs.byte(input).toByte(), - TlvStreamSerializer(false, readers).read(input) - ) + override fun read(input: Input): OpenDualFundedChannel { + val chainHash = BlockHash(LightningCodecs.bytes(input, 32)) + val temporaryChannelId = ByteVector32(LightningCodecs.bytes(input, 32)) + val fundingFeerate = FeeratePerKw(LightningCodecs.u32(input).toLong().sat) + val commitmentFeerate = FeeratePerKw(LightningCodecs.u32(input).toLong().sat) + val fundingAmount = Satoshi(LightningCodecs.u64(input)) + val dustLimit = Satoshi(LightningCodecs.u64(input)) + val maxHtlcValueInFlightMsat = LightningCodecs.u64(input) // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + val htlcMinimum = MilliSatoshi(LightningCodecs.u64(input)) + val toSelfDelay = CltvExpiryDelta(LightningCodecs.u16(input)) + val maxAcceptedHtlcs = LightningCodecs.u16(input) + val lockTime = LightningCodecs.u32(input).toLong() + val fundingPubkey = PublicKey(LightningCodecs.bytes(input, 33)) + val revocationBasepoint = PublicKey(LightningCodecs.bytes(input, 33)) + val paymentBasepoint = PublicKey(LightningCodecs.bytes(input, 33)) + val delayedPaymentBasepoint = PublicKey(LightningCodecs.bytes(input, 33)) + val htlcBasepoint = PublicKey(LightningCodecs.bytes(input, 33)) + val firstPerCommitmentPoint = PublicKey(LightningCodecs.bytes(input, 33)) + val secondPerCommitmentPoint = PublicKey(LightningCodecs.bytes(input, 33)) + val encodedChannelFlags = LightningCodecs.byte(input).toByte() + val channelFlags = ChannelFlags(announceChannel = encodedChannelFlags.toInt().and(1) != 0, nonInitiatorPaysCommitFees = encodedChannelFlags.toInt().and(2) != 0) + val tlvs = TlvStreamSerializer(false, readers).read(input) + return OpenDualFundedChannel( + chainHash = chainHash, + temporaryChannelId = temporaryChannelId, + fundingFeerate = fundingFeerate, + commitmentFeerate = commitmentFeerate, + fundingAmount = fundingAmount, + dustLimit = dustLimit, + maxHtlcValueInFlightMsat = maxHtlcValueInFlightMsat, + htlcMinimum = htlcMinimum, + toSelfDelay = toSelfDelay, + maxAcceptedHtlcs = maxAcceptedHtlcs, + lockTime = lockTime, + fundingPubkey = fundingPubkey, + revocationBasepoint = revocationBasepoint, + paymentBasepoint = paymentBasepoint, + delayedPaymentBasepoint = delayedPaymentBasepoint, + htlcBasepoint = htlcBasepoint, + firstPerCommitmentPoint = firstPerCommitmentPoint, + secondPerCommitmentPoint = secondPerCommitmentPoint, + channelFlags = channelFlags, + tlvStream = tlvs + ) + } } } @@ -930,7 +954,6 @@ data class SpliceInit( val requireConfirmedInputs: Boolean = tlvStream.get()?.let { true } ?: false val requestFunds: ChannelTlv.RequestFunds? get() = tlvStream.get() val pushAmount: MilliSatoshi = tlvStream.get()?.amount ?: 0.msat - val origins: List = tlvStream.get()?.origins?.filterIsInstance() ?: emptyList() constructor(channelId: ByteVector32, fundingContribution: Satoshi, pushAmount: MilliSatoshi, feerate: FeeratePerKw, lockTime: Long, fundingPubkey: PublicKey, requestFunds: ChannelTlv.RequestFunds?) : this( channelId, @@ -958,7 +981,6 @@ data class SpliceInit( ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, ChannelTlv.RequestFunds.tag to ChannelTlv.RequestFunds as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, - ChannelTlv.OriginsTlv.tag to ChannelTlv.OriginsTlv.Companion as TlvValueReader ) override fun read(input: Input): SpliceInit = SpliceInit( @@ -1807,48 +1829,35 @@ data class DNSAddressResponse(override val chainHash: BlockHash, val address: St } } -/** - * This message is used to request a channel open from a remote node, with local contributions to the funding transaction. - * If the remote node won't open a channel, it will respond with [PleaseOpenChannelRejected]. - * Otherwise, it will respond with [OpenDualFundedChannel] and a fee that must be paid by a corresponding push_amount - * in the [AcceptDualFundedChannel] message. - */ -data class PleaseOpenChannel( +data class RecommendedFeerates( override val chainHash: BlockHash, - val requestId: ByteVector32, - val localFundingAmount: Satoshi, - val localInputsCount: Int, - val localInputsWeight: Int, - val tlvs: TlvStream = TlvStream.empty(), + val fundingFeerate: FeeratePerKw, + val commitmentFeerate: FeeratePerKw, + val tlvStream: TlvStream = TlvStream.empty(), ) : LightningMessage, HasChainHash { - override val type: Long get() = PleaseOpenChannel.type - - val grandParents: List = tlvs.get()?.outpoints ?: listOf() + override val type: Long get() = RecommendedFeerates.type override fun write(out: Output) { LightningCodecs.writeBytes(chainHash.value, out) - LightningCodecs.writeBytes(requestId.toByteArray(), out) - LightningCodecs.writeU64(localFundingAmount.toLong(), out) - LightningCodecs.writeU16(localInputsCount, out) - LightningCodecs.writeU32(localInputsWeight, out) - TlvStreamSerializer(false, readers).write(tlvs, out) + LightningCodecs.writeU32(fundingFeerate.toLong().toInt(), out) + LightningCodecs.writeU32(commitmentFeerate.toLong().toInt(), out) + TlvStreamSerializer(false, readers).write(tlvStream, out) } - companion object : LightningMessageReader { - const val type: Long = 36001 + companion object : LightningMessageReader { + const val type: Long = 39409 @Suppress("UNCHECKED_CAST") - val readers = mapOf( - PleaseOpenChannelTlv.GrandParents.tag to PleaseOpenChannelTlv.GrandParents.Companion as TlvValueReader, + private val readers = mapOf( + RecommendedFeeratesTlv.FundingFeerateRange.tag to RecommendedFeeratesTlv.FundingFeerateRange as TlvValueReader, + RecommendedFeeratesTlv.CommitmentFeerateRange.tag to RecommendedFeeratesTlv.CommitmentFeerateRange as TlvValueReader, ) - override fun read(input: Input): PleaseOpenChannel = PleaseOpenChannel( - BlockHash(LightningCodecs.bytes(input, 32)), - LightningCodecs.bytes(input, 32).toByteVector32(), - LightningCodecs.u64(input).sat, - LightningCodecs.u16(input), - LightningCodecs.u32(input), - TlvStreamSerializer(false, readers).read(input) + override fun read(input: Input): RecommendedFeerates = RecommendedFeerates( + chainHash = BlockHash(LightningCodecs.bytes(input, 32)), + fundingFeerate = FeeratePerKw(LightningCodecs.u32(input).sat), + commitmentFeerate = FeeratePerKw(LightningCodecs.u32(input).sat), + tlvStream = TlvStreamSerializer(false, readers).read(input) ) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt index b3e0439ce..cc75d2b95 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt @@ -20,7 +20,7 @@ import fr.acinq.lightning.utils.sat object LiquidityAds { /** - * @param miningFee fee paid to miners for the underlying on-chain transaction. + * @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) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/RecommendedFeeratesTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/RecommendedFeeratesTlv.kt new file mode 100644 index 000000000..303f3ea04 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/RecommendedFeeratesTlv.kt @@ -0,0 +1,48 @@ +package fr.acinq.lightning.wire + +import fr.acinq.bitcoin.io.Input +import fr.acinq.bitcoin.io.Output +import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.utils.sat + +sealed class RecommendedFeeratesTlv : Tlv { + + /** Detailed range of values that will be accepted until the next [RecommendedFeerates] message is received. */ + data class FundingFeerateRange(val min: FeeratePerKw, val max: FeeratePerKw) : RecommendedFeeratesTlv() { + override val tag: Long get() = FundingFeerateRange.tag + + override fun write(out: Output) { + LightningCodecs.writeU32(min.toLong().toInt(), out) + LightningCodecs.writeU32(max.toLong().toInt(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 1 + + override fun read(input: Input): FundingFeerateRange = FundingFeerateRange( + min = FeeratePerKw(LightningCodecs.u32(input).sat), + max = FeeratePerKw(LightningCodecs.u32(input).sat), + ) + } + } + + /** Detailed range of values that will be accepted until the next [RecommendedFeerates] message is received. */ + data class CommitmentFeerateRange(val min: FeeratePerKw, val max: FeeratePerKw) : RecommendedFeeratesTlv() { + override val tag: Long get() = CommitmentFeerateRange.tag + + override fun write(out: Output) { + LightningCodecs.writeU32(min.toLong().toInt(), out) + LightningCodecs.writeU32(max.toLong().toInt(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 3 + + override fun read(input: Input): CommitmentFeerateRange = CommitmentFeerateRange( + min = FeeratePerKw(LightningCodecs.u32(input).sat), + max = FeeratePerKw(LightningCodecs.u32(input).sat), + ) + } + } + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt index f9d6ab615..30aac396a 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt @@ -12,7 +12,7 @@ import fr.acinq.lightning.channel.TestsHelper import fr.acinq.lightning.channel.states.Normal import fr.acinq.lightning.channel.states.SpliceTestsCommon import fr.acinq.lightning.channel.states.WaitForFundingSignedTestsCommon -import fr.acinq.lightning.logging.* +import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.SpliceLocked @@ -42,7 +42,7 @@ class SwapInManagerTestsCommon : LightningTestSuite() { val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) WalletState(mapOf(dummyAddress to addressState)) } - val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 3, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) + val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 3, maxConfirmations = 720, refundDelay = 900)) mgr.process(cmd).also { result -> assertNotNull(result) assertEquals(result.walletInputs.map { it.amount }.toSet(), setOf(50_000.sat, 75_000.sat)) @@ -64,7 +64,7 @@ class SwapInManagerTestsCommon : LightningTestSuite() { val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) WalletState(mapOf(dummyAddress to addressState)) } - val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 101, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 3, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) + val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 101, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 3, maxConfirmations = 720, refundDelay = 900)) mgr.process(cmd).also { assertNull(it) } } @@ -83,34 +83,10 @@ class SwapInManagerTestsCommon : LightningTestSuite() { val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) WalletState(mapOf(dummyAddress to addressState)) } - val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 130, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 3, maxConfirmations = 10, refundDelay = 15), trustedTxs = emptySet()) + val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 130, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 3, maxConfirmations = 10, refundDelay = 15)) mgr.process(cmd).also { assertNull(it) } } - @Test - fun `swap funds -- allow unconfirmed in migration`() { - val mgr = SwapInManager(listOf(), logger) - val parentTxs = listOf( - Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 1), 0)), listOf(TxOut(75_000.sat, dummyScript)), 0), - Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 2), 0)), listOf(TxOut(50_000.sat, dummyScript)), 0), - Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(25_000.sat, dummyScript)), 0) - ) - val wallet = run { - val utxos = listOf( - WalletState.Utxo(parentTxs[0].txid, 0, 100, parentTxs[0], WalletState.AddressMeta.Single), // deeply confirmed - WalletState.Utxo(parentTxs[1].txid, 0, 150, parentTxs[1], WalletState.AddressMeta.Single), // recently confirmed - WalletState.Utxo(parentTxs[2].txid, 0, 0, parentTxs[2], WalletState.AddressMeta.Single), // unconfirmed - ) - val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) - WalletState(mapOf(dummyAddress to addressState)) - } - val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = parentTxs.map { it.txid }.toSet()) - mgr.process(cmd).also { result -> - assertNotNull(result) - assertEquals(result.walletInputs.map { it.amount }.toSet(), setOf(25_000.sat, 50_000.sat, 75_000.sat)) - } - } - @Test fun `swap funds -- previously used inputs`() { val mgr = SwapInManager(listOf(), logger) @@ -120,7 +96,7 @@ class SwapInManagerTestsCommon : LightningTestSuite() { val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) WalletState(mapOf(dummyAddress to addressState)) } - val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) + val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900)) mgr.process(cmd).also { assertNotNull(it) } // We cannot reuse the same inputs. @@ -143,7 +119,7 @@ class SwapInManagerTestsCommon : LightningTestSuite() { WalletState(mapOf(dummyAddress to addressState)) } val mgr = SwapInManager(listOf(waitForFundingSigned.state), logger) - val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) + val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900)) mgr.process(cmd).also { assertNull(it) } // The pending channel is aborted: we can reuse those inputs. @@ -164,7 +140,7 @@ class SwapInManagerTestsCommon : LightningTestSuite() { WalletState(mapOf(dummyAddress to addressState)) } val mgr = SwapInManager(listOf(alice1.state), logger) - val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) + val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900)) mgr.process(cmd).also { assertNull(it) } // The channel is aborted: we can reuse those inputs. @@ -195,7 +171,7 @@ class SwapInManagerTestsCommon : LightningTestSuite() { WalletState(mapOf(dummyAddress to addressState)) } val mgr = SwapInManager(listOf(alice3.state), logger) - val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) + val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900)) mgr.process(cmd).also { assertNull(it) } } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt index c775d5937..c0035f0a7 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt @@ -19,14 +19,17 @@ import fr.acinq.lightning.channel.TestsHelper.htlcTimeoutTxs import fr.acinq.lightning.channel.TestsHelper.reachNormal import fr.acinq.lightning.channel.states.Closing import fr.acinq.lightning.crypto.ShaChain -import fr.acinq.lightning.logging.* +import fr.acinq.lightning.logging.LoggingContext +import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.tests.utils.testLoggerFactory import fr.acinq.lightning.transactions.CommitmentSpec import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.utils.* +import fr.acinq.lightning.utils.UUID +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.IncorrectOrUnknownPaymentDetails import fr.acinq.lightning.wire.TxSignatures import fr.acinq.lightning.wire.UpdateAddHtlc @@ -482,9 +485,9 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { } companion object { - fun makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0.sat), dustLimit: Satoshi = 0.sat, isInitiator: Boolean = true, announceChannel: Boolean = true): Commitments { + fun makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0.sat), dustLimit: Satoshi = 0.sat, isInitiator: Boolean = true): Commitments { val localParams = LocalParams( - randomKey().publicKey(), KeyPath("42"), dustLimit, Long.MAX_VALUE, 1.msat, CltvExpiryDelta(144), 50, isInitiator, ByteVector.empty, Features.empty + randomKey().publicKey(), KeyPath("42"), dustLimit, Long.MAX_VALUE, 1.msat, CltvExpiryDelta(144), 50, isInitiator, isInitiator, ByteVector.empty, Features.empty ) val remoteParams = RemoteParams( randomKey().publicKey(), dustLimit, Long.MAX_VALUE, 1.msat, CltvExpiryDelta(144), 50, @@ -503,7 +506,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features), localParams = localParams, remoteParams = remoteParams, - channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty, + channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), ), CommitmentChanges( LocalChanges(listOf(), listOf(), listOf()), @@ -529,9 +532,9 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { ) } - fun makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announceChannel: Boolean): Commitments { + fun makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey): Commitments { val localParams = LocalParams( - localNodeId, KeyPath("42"), 0.sat, Long.MAX_VALUE, 1.msat, CltvExpiryDelta(144), 50, isInitiator = true, ByteVector.empty, Features.empty + localNodeId, KeyPath("42"), 0.sat, Long.MAX_VALUE, 1.msat, CltvExpiryDelta(144), 50, isChannelOpener = true, paysCommitTxFees = true, ByteVector.empty, Features.empty ) val remoteParams = RemoteParams( remoteNodeId, 0.sat, Long.MAX_VALUE, 1.msat, CltvExpiryDelta(144), 50, randomKey().publicKey(), randomKey().publicKey(), randomKey().publicKey(), randomKey().publicKey(), Features.empty @@ -548,7 +551,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features), localParams = localParams, remoteParams = remoteParams, - channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty, + channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), ), CommitmentChanges( LocalChanges(listOf(), listOf(), listOf()), diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index a533f9a8e..a1f12a4f5 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -181,7 +181,6 @@ object TestsHelper { WaitForInit ) - val channelFlags = 0.toByte() val aliceChannelParams = TestConstants.Alice.channelParams().copy(features = aliceFeatures.initFeatures()) val bobChannelParams = TestConstants.Bob.channelParams().copy(features = bobFeatures.initFeatures()) val aliceInit = Init(aliceFeatures) @@ -195,10 +194,11 @@ object TestsHelper { TestConstants.feeratePerKw, aliceChannelParams, bobInit, - channelFlags, + ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), ChannelConfig.standard, channelType, - channelOrigin + requestRemoteFunding = null, + channelOrigin, ) ) assertIs>(alice1) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt index 430b971de..53737c775 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt @@ -519,7 +519,8 @@ class QuiescenceTestsCommon : LightningTestSuite() { spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(createWalletWithFunds(sender.staticParams.nodeParams.keyManager, spliceIn)), spliceOut = spliceOut?.let { ChannelCommand.Commitment.Splice.Request.SpliceOut(it, Script.write(Script.pay2wpkh(Lightning.randomKey().publicKey())).byteVector()) }, feerate = FeeratePerKw(253.sat), - requestRemoteFunding = null + requestRemoteFunding = null, + origins = listOf(), ) } 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 f8d7eb039..1713e3651 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -191,7 +191,7 @@ class SpliceTestsCommon : LightningTestSuite() { 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 cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) + 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) // Alice's contribution is negative: she needs to pay on-chain fees for the splice. @@ -237,7 +237,7 @@ class SpliceTestsCommon : LightningTestSuite() { run { val liquidityRequest = LiquidityAds.RequestRemoteFunding(1_000_000.sat, bob.currentBlockHeight, leaseRate) assertEquals(10_001.sat, liquidityRequest.rate.fees(FeeratePerKw(1000.sat), liquidityRequest.fundingAmount, liquidityRequest.fundingAmount).total) - val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) + val 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)) @@ -250,7 +250,7 @@ class SpliceTestsCommon : LightningTestSuite() { run { val liquidityRequest = LiquidityAds.RequestRemoteFunding(1_000_000.sat, bob.currentBlockHeight, leaseRate.copy(leaseFeeBase = 0.sat)) assertEquals(10_000.sat, liquidityRequest.rate.fees(FeeratePerKw(1000.sat), liquidityRequest.fundingAmount, liquidityRequest.fundingAmount).total) - val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) + val 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)) @@ -1284,7 +1284,8 @@ class SpliceTestsCommon : LightningTestSuite() { spliceIn = null, spliceOut = ChannelCommand.Commitment.Splice.Request.SpliceOut(amount, Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector()), requestRemoteFunding = null, - feerate = spliceFeerate + feerate = spliceFeerate, + origins = listOf(), ) private fun spliceOut(alice: LNChannel, bob: LNChannel, amount: Satoshi): Pair, LNChannel> { @@ -1329,7 +1330,8 @@ class SpliceTestsCommon : LightningTestSuite() { spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(createWalletWithFunds(alice.staticParams.nodeParams.keyManager, amounts)), spliceOut = null, requestRemoteFunding = null, - feerate = spliceFeerate + feerate = spliceFeerate, + origins = listOf(), ) // Negotiate a splice transaction where Alice is the only contributor. val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) @@ -1366,7 +1368,8 @@ class SpliceTestsCommon : LightningTestSuite() { spliceIn = null, spliceOut = null, requestRemoteFunding = null, - feerate = spliceFeerate + feerate = spliceFeerate, + origins = listOf(), ) // Negotiate a splice transaction with no contribution. val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) @@ -1399,7 +1402,8 @@ class SpliceTestsCommon : LightningTestSuite() { spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(createWalletWithFunds(alice.staticParams.nodeParams.keyManager, inAmounts)), spliceOut = ChannelCommand.Commitment.Splice.Request.SpliceOut(outAmount, Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector()), feerate = spliceFeerate, - requestRemoteFunding = null + requestRemoteFunding = null, + origins = listOf(), ) val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) 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 faf65e15e..f888e9bfa 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt @@ -71,40 +71,43 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { } @Test - fun `recv CommitSig -- with channel origin -- pay-to-open`() { - val channelOrigin = Origin.PayToOpenOrigin(randomBytes32(), 1_000_000.msat, 500.sat, TestConstants.alicePushAmount) - val (_, commitSigAlice, bob, _) = init(bobFundingAmount = 0.sat, alicePushAmount = TestConstants.alicePushAmount, bobPushAmount = 0.msat, channelOrigin = channelOrigin) - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) - assertIs(bob1.state) - assertEquals(actionsBob1.size, 5) - assertFalse(actionsBob1.hasOutgoingMessage().channelData.isEmpty()) - actionsBob1.has() - actionsBob1.find().also { - assertEquals(TestConstants.alicePushAmount, it.amount) + fun `recv CommitSig -- with channel origin -- off-chain payment`() { + val channelOrigin = Origin.OffChainPayment(randomBytes32(), 50_000_000.msat, ChannelManagementFees(500.sat, 1_000.sat)) + val (alice, _, _, commitSigBob) = init(aliceFundingAmount = 0.sat, alicePushAmount = 0.msat, bobPushAmount = 50_000_000.msat, channelOrigin = channelOrigin) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitSigBob)) + assertIs(alice1.state) + assertEquals(actionsAlice1.size, 6) + actionsAlice1.hasOutgoingMessage() + actionsAlice1.has() + actionsAlice1.find().also { + assertEquals(50_000_000.msat, it.amount) assertEquals(channelOrigin, it.origin) - assertEquals(bob1.commitments.latest.fundingTxId, it.txId) - assertTrue(it.localInputs.isEmpty()) + assertEquals(alice1.commitments.latest.fundingTxId, it.txId) } - actionsBob1.hasWatch() - actionsBob1.has() + actionsAlice1.hasWatch() + val events = actionsAlice1.filterIsInstance().map { it.event } + assertTrue(events.any { it is ChannelEvents.Created }) + assertTrue(events.any { it is LiquidityEvents.Accepted }) } @Test fun `recv CommitSig -- with channel origin -- dual-swap-in`() { - val channelOrigin = Origin.PleaseOpenChannelOrigin(randomBytes32(), 2500.msat, 0.sat, TestConstants.bobFundingAmount.toMilliSatoshi() - TestConstants.bobPushAmount) - val (_, commitSigAlice, bob, _) = init(alicePushAmount = 0.msat, channelOrigin = channelOrigin) - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) - assertIs(bob1.state) - assertEquals(actionsBob1.size, 5) - assertFalse(actionsBob1.hasOutgoingMessage().channelData.isEmpty()) - actionsBob1.has() - actionsBob1.find().also { - assertEquals(it.amount, TestConstants.bobFundingAmount.toMilliSatoshi() - TestConstants.bobPushAmount) + val channelOrigin = Origin.OnChainWallet(setOf(), 200_000_000.msat, ChannelManagementFees(750.sat, 0.sat)) + val (alice, _, _, commitSigBob) = init(aliceFundingAmount = 200_000.sat, alicePushAmount = 0.msat, bobFundingAmount = 500_000.sat, channelOrigin = channelOrigin) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitSigBob)) + assertIs(alice1.state) + assertEquals(actionsAlice1.size, 6) + actionsAlice1.hasOutgoingMessage() + actionsAlice1.has() + actionsAlice1.find().also { + assertEquals(it.amount, 200_000_000.msat) assertEquals(it.origin, channelOrigin) assertTrue(it.localInputs.isNotEmpty()) } - actionsBob1.hasWatch() - actionsBob1.has() + actionsAlice1.hasWatch() + val events = actionsAlice1.filterIsInstance().map { it.event } + assertTrue(events.any { it is ChannelEvents.Created }) + assertTrue(events.any { it is SwapInEvents.Accepted }) } @Test 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 9a00fa7ed..09fcd88d4 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt @@ -4,25 +4,22 @@ import fr.acinq.bitcoin.Block import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PrivateKey import fr.acinq.bitcoin.utils.Either -import fr.acinq.lightning.CltvExpiryDelta -import fr.acinq.lightning.InvoiceDefaultRoutingFees +import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey -import fr.acinq.lightning.NodeUri -import fr.acinq.lightning.PeerConnected import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK import fr.acinq.lightning.blockchain.WatchEventConfirmed -import fr.acinq.lightning.blockchain.electrum.balance import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.ChannelFlags import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.channel.LNChannel -import fr.acinq.lightning.channel.Origin import fr.acinq.lightning.channel.TestsHelper import fr.acinq.lightning.channel.TestsHelper.createWallet import fr.acinq.lightning.channel.states.* import fr.acinq.lightning.db.InMemoryDatabases import fr.acinq.lightning.db.LightningOutgoingPayment import fr.acinq.lightning.io.* +import fr.acinq.lightning.payment.LiquidityPolicy import fr.acinq.lightning.router.Announcements import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.io.peer.* @@ -31,7 +28,6 @@ import fr.acinq.lightning.tests.utils.runSuspendTest import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.utils.toMilliSatoshi import fr.acinq.lightning.wire.* import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -59,7 +55,7 @@ class PeerTest : LightningTestSuite() { randomKey().publicKey(), randomKey().publicKey(), randomKey().publicKey(), - 0.toByte(), + ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve)) ) @@ -123,7 +119,7 @@ class PeerTest : LightningTestSuite() { val (alice, bob, alice2bob, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false) val wallet = createWallet(nodeParams.first.keyManager, 300_000.sat).second - alice.send(OpenChannel(250_000.sat, 50_000_000.msat, wallet, FeeratePerKw(3000.sat), FeeratePerKw(2500.sat), 0, ChannelType.SupportedChannelType.AnchorOutputsZeroReserve)) + alice.send(OpenChannel(250_000.sat, 50_000_000.msat, wallet, FeeratePerKw(3000.sat), FeeratePerKw(2500.sat), ChannelType.SupportedChannelType.AnchorOutputsZeroReserve)) val open = alice2bob.expect() bob.forward(open) @@ -183,7 +179,7 @@ class PeerTest : LightningTestSuite() { val (alice, bob, alice2bob, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false) val wallet = createWallet(nodeParams.first.keyManager, 300_000.sat).second - alice.send(OpenChannel(250_000.sat, 50_000_000.msat, wallet, FeeratePerKw(3000.sat), FeeratePerKw(2500.sat), 0, ChannelType.SupportedChannelType.AnchorOutputsZeroReserve)) + alice.send(OpenChannel(250_000.sat, 50_000_000.msat, wallet, FeeratePerKw(3000.sat), FeeratePerKw(2500.sat), ChannelType.SupportedChannelType.AnchorOutputsZeroReserve)) val open = alice2bob.expect() bob.forward(open) @@ -225,106 +221,43 @@ class PeerTest : LightningTestSuite() { @Test fun `swap funds into a channel`() = runSuspendTest { val nodeParams = Pair(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams) + nodeParams.second.liquidityPolicy.emit(LiquidityPolicy.Auto(inboundLiquidityTarget = null, maxAbsoluteFee = 20_000.sat, maxRelativeFeeBasisPoints = 1000, skipAbsoluteFeeCheck = false)) val walletParams = Pair(TestConstants.Alice.walletParams, TestConstants.Bob.walletParams) - val (alice, bob, alice2bob, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false) + val (_, bob, _, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false) - val requestId = randomBytes32() - val walletBob = createWallet(nodeParams.second.keyManager, 260_000.sat).second - val internalRequestBob = RequestChannelOpen(requestId, walletBob) - bob.send(internalRequestBob) - val request = bob2alice.expect() - assertEquals(request.localFundingAmount, 260_000.sat) - - val miningFee = 500.sat - val serviceFee = 1_000.sat.toMilliSatoshi() - val walletAlice = createWallet(nodeParams.first.keyManager, 50_000.sat).second - val openAlice = OpenChannel(40_000.sat, 0.msat, walletAlice, FeeratePerKw(3500.sat), FeeratePerKw(2500.sat), 0, ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) - alice.send(openAlice) - val open = alice2bob.expect().copy( - tlvStream = TlvStream( - ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve), - ChannelTlv.OriginTlv(Origin.PleaseOpenChannelOrigin(requestId, serviceFee, miningFee, openAlice.pushAmount)) - ) - ) - bob.forward(open) - val accept = bob2alice.expect() - assertEquals(open.temporaryChannelId, accept.temporaryChannelId) - val fundingFee = walletBob.balance - accept.fundingAmount - assertEquals(accept.pushAmount, serviceFee + miningFee.toMilliSatoshi() - fundingFee.toMilliSatoshi()) - alice.forward(accept) + val walletBob = createWallet(nodeParams.second.keyManager, 500_000.sat).second + bob.send(AddWalletInputsToChannel(walletBob)) - val txAddInputAlice = alice2bob.expect() - bob.forward(txAddInputAlice) - val txAddInputBob = bob2alice.expect() - alice.forward(txAddInputBob) - val txAddOutput = alice2bob.expect() - bob.forward(txAddOutput) - val txCompleteBob = bob2alice.expect() - alice.forward(txCompleteBob) - val txCompleteAlice = alice2bob.expect() - bob.forward(txCompleteAlice) - val commitSigBob = bob2alice.expect() - alice.forward(commitSigBob) - val commitSigAlice = alice2bob.expect() - bob.forward(commitSigAlice) - val txSigsAlice = alice2bob.expect() - bob.forward(txSigsAlice) - val txSigsBob = bob2alice.expect() - alice.forward(txSigsBob) - val (_, aliceState) = alice.expectState() - assertEquals(aliceState.commitments.latest.localCommit.spec.toLocal, openAlice.fundingAmount.toMilliSatoshi() + serviceFee + miningFee.toMilliSatoshi() - fundingFee.toMilliSatoshi()) - val (_, bobState) = bob.expectState() - // Bob has to deduce from its balance: - // - the fees for the channel open (10 000 sat) - // - the miner fees for his input(s) in the funding transaction - assertEquals(bobState.commitments.latest.localCommit.spec.toLocal, walletBob.balance.toMilliSatoshi() - serviceFee - miningFee.toMilliSatoshi()) + 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.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. } @Test fun `reject swap-in -- fee too high`() = runSuspendTest { val nodeParams = Pair(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams) val walletParams = Pair(TestConstants.Alice.walletParams, TestConstants.Bob.walletParams) - val (alice, bob, alice2bob, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false) - - val requestId = randomBytes32() - val walletBob = createWallet(nodeParams.second.keyManager, 260_000.sat).second - val internalRequestBob = RequestChannelOpen(requestId, walletBob) - bob.send(internalRequestBob) - val request = bob2alice.expect() - assertEquals(request.localFundingAmount, 260_000.sat) - val fundingFee = 100.sat - val serviceFee = request.localFundingAmount.toMilliSatoshi() * 0.02 // 2% fee is too high - val walletAlice = createWallet(nodeParams.first.keyManager, 50_000.sat).second - val openAlice = OpenChannel(40_000.sat, 0.msat, walletAlice, FeeratePerKw(3500.sat), FeeratePerKw(2500.sat), 0, ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) - alice.send(openAlice) - val open = alice2bob.expect().copy( - tlvStream = TlvStream( - ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve), - ChannelTlv.OriginTlv(Origin.PleaseOpenChannelOrigin(requestId, serviceFee, fundingFee, openAlice.pushAmount)) - ) - ) - bob.forward(open) - bob2alice.expect() - } - - @Test - fun `reject swap-in -- no associated channel request`() = runSuspendTest { - val nodeParams = Pair(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams) - val walletParams = Pair(TestConstants.Alice.walletParams, TestConstants.Bob.walletParams) - val (alice, bob, alice2bob, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false) - - val requestId = randomBytes32() - val walletAlice = createWallet(nodeParams.first.keyManager, 50_000.sat).second - val openAlice = OpenChannel(40_000.sat, 0.msat, walletAlice, FeeratePerKw(3500.sat), FeeratePerKw(2500.sat), 0, ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) - alice.send(openAlice) - val open = alice2bob.expect().copy( - tlvStream = TlvStream( - ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve), - ChannelTlv.OriginTlv(Origin.PleaseOpenChannelOrigin(requestId, 50.sat.toMilliSatoshi(), 100.sat, openAlice.pushAmount)) - ) + val (_, bob) = newPeers(this, nodeParams, walletParams, automateMessaging = false) + + // Bob's liquidity policy is too restrictive. + val bobPolicy = LiquidityPolicy.Auto( + inboundLiquidityTarget = 500_000.sat, + maxAbsoluteFee = 100.sat, + maxRelativeFeeBasisPoints = 10, + skipAbsoluteFeeCheck = false ) - bob.forward(open) - bob2alice.expect() + nodeParams.second.liquidityPolicy.emit(bobPolicy) + val walletBob = createWallet(nodeParams.second.keyManager, 1_000_000.sat).second + bob.send(AddWalletInputsToChannel(walletBob)) + + val rejected = bob.nodeParams.nodeEvents.first { it is LiquidityEvents } + assertIs(rejected) + assertEquals(500_000_000.msat, rejected.amount) + assertEquals(LiquidityEvents.Source.OnChainWallet, rejected.source) + assertEquals(LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee(maxRelativeFeeBasisPoints = 10), rejected.reason) } @Test diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt index e3d323917..0dd0764e1 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt @@ -172,7 +172,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { miningFee = 0.sat, localInputs = emptySet(), txId = TxId(randomBytes32()), - origin = Origin.PayToOpenOrigin(amount = payToOpenRequest.amountMsat, paymentHash = payToOpenRequest.paymentHash, serviceFee = 0.msat, miningFee = payToOpenRequest.payToOpenFeeSatoshis) + origin = Origin.OffChainPayment(incomingPayment.preimage, payToOpenRequest.amountMsat, ChannelManagementFees(miningFee = payToOpenRequest.payToOpenFeeSatoshis, serviceFee = 0.sat)) ) paymentHandler.process(channelId, amountOrigin) paymentHandler.db.getIncomingPayment(payToOpenRequest.paymentHash).also { dbPayment -> @@ -191,7 +191,6 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(amountOrigin.amount, dbPayment.received?.amount) assertEquals(amountOrigin.serviceFee, dbPayment.received?.fees) } - } @Test diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/LiquidityPolicyTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/LiquidityPolicyTestsCommon.kt index 718d09770..0a7b14a91 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/LiquidityPolicyTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/LiquidityPolicyTestsCommon.kt @@ -1,7 +1,7 @@ package fr.acinq.lightning.payment import fr.acinq.lightning.LiquidityEvents -import fr.acinq.lightning.logging.* +import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat @@ -15,44 +15,34 @@ class LiquidityPolicyTestsCommon : LightningTestSuite() { @Test fun `policy rejection`() { - - val policy = LiquidityPolicy.Auto(maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false) - + val policy = LiquidityPolicy.Auto(maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false, inboundLiquidityTarget = null) // fee over both absolute and relative assertEquals( expected = LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee(policy.maxRelativeFeeBasisPoints), actual = policy.maybeReject(amount = 4_000_000.msat, fee = 3_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger)?.reason ) - // fee over absolute assertEquals( expected = LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(policy.maxAbsoluteFee), actual = policy.maybeReject(amount = 15_000_000.msat, fee = 3_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger)?.reason ) - // fee over relative assertEquals( expected = LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee(policy.maxRelativeFeeBasisPoints), actual = policy.maybeReject(amount = 4_000_000.msat, fee = 2_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger)?.reason ) - assertNull(policy.maybeReject(amount = 10_000_000.msat, fee = 2_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger)) - } @Test fun `policy rejection skip absolute check`() { - - val policy = LiquidityPolicy.Auto(maxAbsoluteFee = 1_000.sat, maxRelativeFeeBasisPoints = 5_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = true) - + val policy = LiquidityPolicy.Auto(maxAbsoluteFee = 1_000.sat, maxRelativeFeeBasisPoints = 5_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = true, inboundLiquidityTarget = null) // fee is over absolute, and it's an offchain payment so the check passes assertNull(policy.maybeReject(amount = 4_000_000.msat, fee = 2_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger)) - // fee is over absolute, but it's an on-chain payment so the check fails assertEquals( expected = LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(policy.maxAbsoluteFee), actual = policy.maybeReject(amount = 4_000_000.msat, fee = 2_000_000.msat, source = LiquidityEvents.Source.OnChainWallet, logger)?.reason ) - } } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt index f634260f8..ca9214aad 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt @@ -26,7 +26,13 @@ import kotlin.test.* class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { - private val defaultWalletParams = WalletParams(NodeUri(TestConstants.Bob.nodeParams.nodeId, "bob.com", 9735), TestConstants.trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144)), TestConstants.swapInParams) + private val defaultWalletParams = WalletParams( + NodeUri(TestConstants.Bob.nodeParams.nodeId, "bob.com", 9735), + TestConstants.trampolineFees, + InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144)), + TestConstants.swapInParams, + TestConstants.leaseRate + ) @Test fun `invalid payment amount`() = runSuspendTest { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt b/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt index f15e9d22f..bc9caa989 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt @@ -11,6 +11,7 @@ import fr.acinq.lightning.tests.utils.testLoggerFactory import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector32 +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.lightning.wire.OnionRoutingPacket import fr.acinq.secp256k1.Hex @@ -37,6 +38,15 @@ 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, + ) + const val aliceSwapInServerXpub = "tpubDCvYeHUZisCMVTSfWDa1yevTf89NeF6TWxXUQwqkcmFrNvNdNvZQh1j4m4uTA4QcmPEwcrKVF8bJih1v16zDZacRr4j9MCAFQoSydKKy66q" const val bobSwapInServerXpub = "tpubDDt5vQap1awkyDXx1z1cP7QFKSZHDCCpbU8nSq9jy7X2grTjUVZDePexf6gc6AHtRRzkgfPW87K6EKUVV6t3Hu2hg7YkHkmMeLSfrP85x41" @@ -46,7 +56,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) + val walletParams = WalletParams(NodeUri(Bob.keyManager.nodeKeys.nodeKey.publicKey, "bob.com", 9735), trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144)), swapInParams, leaseRate) val nodeParams = NodeParams( chain = Chain.Regtest, loggerFactory = testLoggerFactory, @@ -86,7 +96,7 @@ object TestConstants { paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(0), CltvExpiryDelta(0)), ) - fun channelParams(): LocalParams = LocalParams(nodeParams, isInitiator = true) + fun channelParams(): LocalParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = true) } object Bob { @@ -95,7 +105,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) + val walletParams = WalletParams(NodeUri(Alice.keyManager.nodeKeys.nodeKey.publicKey, "alice.com", 9735), trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144)), swapInParams, leaseRate) val nodeParams = NodeParams( chain = Chain.Regtest, loggerFactory = testLoggerFactory, @@ -117,7 +127,7 @@ object TestConstants { paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(0), CltvExpiryDelta(0)), ) - fun channelParams(): LocalParams = LocalParams(nodeParams, isInitiator = false) + fun channelParams(): LocalParams = LocalParams(nodeParams, isChannelOpener = false, payCommitTxFees = false) } } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt b/src/commonTest/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt index d4db2a3e2..65a378e50 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt @@ -1,5 +1,6 @@ package fr.acinq.lightning.tests.io.peer +import fr.acinq.bitcoin.Block import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PrivateKey import fr.acinq.lightning.NodeParams @@ -20,10 +21,7 @@ import fr.acinq.lightning.io.* import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.tests.utils.testLoggerFactory import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.wire.ChannelReady -import fr.acinq.lightning.wire.ChannelReestablish -import fr.acinq.lightning.wire.Init -import fr.acinq.lightning.wire.LightningMessage +import fr.acinq.lightning.wire.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -88,6 +86,10 @@ suspend fun connect( val bobInit = Init(bob.nodeParams.features.initFeatures()) alice.send(MessageReceived(aliceConnection.id, bobInit)) + // Initialize Alice and Bob's current feerates. + alice.peerFeeratesFlow.emit(RecommendedFeerates(Block.RegtestGenesisBlock.hash, fundingFeerate = FeeratePerKw(FeeratePerByte(20.sat)), commitmentFeerate = FeeratePerKw(FeeratePerByte(1.sat)))) + bob.peerFeeratesFlow.emit(RecommendedFeerates(Block.RegtestGenesisBlock.hash, fundingFeerate = FeeratePerKw(FeeratePerByte(20.sat)), commitmentFeerate = FeeratePerKw(FeeratePerByte(1.sat)))) + if (channelsCount > 0) { // When there are multiple channels, the channel_reestablish and channel_ready messages from different channels // may be interleaved, so we cannot guarantee a deterministic ordering and thus need independent coroutines. diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt index e57048707..2aca071fd 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt @@ -18,7 +18,6 @@ import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.Htl import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.UpdateAddHtlc -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import org.kodein.memory.file.FileSystem import org.kodein.memory.file.Path @@ -92,7 +91,8 @@ class AnchorOutputsTestsCommon { val localParams = LocalParams( TestConstants.Alice.nodeParams.nodeId, KeyPath.empty, - 546.sat, 1000000000L, 0.msat, CltvExpiryDelta(144), 1000, true, + 546.sat, 1000000000L, 0.msat, CltvExpiryDelta(144), 1000, + isChannelOpener = true, paysCommitTxFees = true, Script.write(Script.pay2wpkh(randomKey().publicKey())).toByteVector(), TestConstants.Alice.nodeParams.features, ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index eccfc4e4d..6163254b7 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -656,7 +656,7 @@ class TransactionsTestsCommon : LightningTestSuite() { run { // Different amounts, both outputs untrimmed, local is the initiator: val spec = CommitmentSpec(setOf(), feerate, 150_000_000.msat, 250_000_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000.sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000.sat, spec) assertEquals(2, closingTx.tx.txOut.size) assertNotNull(closingTx.toLocalIndex) assertEquals(localPubKeyScript.toByteVector(), closingTx.toLocalOutput!!.publicKeyScript) @@ -667,7 +667,7 @@ class TransactionsTestsCommon : LightningTestSuite() { run { // Same amounts, both outputs untrimmed, local is not the initiator: val spec = CommitmentSpec(setOf(), feerate, 150_000_000.msat, 150_000_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = false, localDustLimit, 1000.sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = false, localDustLimit, 1000.sat, spec) assertEquals(2, closingTx.tx.txOut.size) assertNotNull(closingTx.toLocalIndex) assertEquals(localPubKeyScript.toByteVector(), closingTx.toLocalOutput!!.publicKeyScript) @@ -678,7 +678,7 @@ class TransactionsTestsCommon : LightningTestSuite() { run { // Their output is trimmed: val spec = CommitmentSpec(setOf(), feerate, 150_000_000.msat, 1_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = false, localDustLimit, 1000.sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = false, localDustLimit, 1000.sat, spec) assertEquals(1, closingTx.tx.txOut.size) assertNotNull(closingTx.toLocalOutput) assertEquals(localPubKeyScript.toByteVector(), closingTx.toLocalOutput!!.publicKeyScript) @@ -688,14 +688,14 @@ class TransactionsTestsCommon : LightningTestSuite() { run { // Our output is trimmed: val spec = CommitmentSpec(setOf(), feerate, 50_000.msat, 150_000_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000.sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000.sat, spec) assertEquals(1, closingTx.tx.txOut.size) assertNull(closingTx.toLocalOutput) } run { // Both outputs are trimmed: val spec = CommitmentSpec(setOf(), feerate, 50_000.msat, 10_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000.sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000.sat, spec) assertTrue(closingTx.tx.txOut.isEmpty()) assertNull(closingTx.toLocalOutput) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 1c9706096..bad5dae79 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -11,7 +11,9 @@ 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.* +import fr.acinq.lightning.channel.ChannelFlags +import fr.acinq.lightning.channel.ChannelType +import fr.acinq.lightning.channel.Helpers import fr.acinq.lightning.message.OnionMessages import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat @@ -291,8 +293,8 @@ class LightningCodecsTestsCommon : LightningTestSuite() { @Test fun `encode - decode open_channel`() { // @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), 1.toByte()) - val defaultEncoded = ByteVector("0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f 01") + 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") val testCases = listOf( defaultOpen to defaultEncoded, defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs))) to (defaultEncoded + ByteVector("0103101000")), @@ -300,8 +302,27 @@ class LightningCodecsTestsCommon : LightningTestSuite() { 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(setOf(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs)), setOf(GenericTlv(321, ByteVector("2a2a")), GenericTlv(325, ByteVector("02"))))) to (defaultEncoded + ByteVector("0103101000 fd0141022a2a fd01450102")), - defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.OriginTlv(Origin.PayToOpenOrigin(ByteVector32.fromValidHex("187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281"), 1_000_000.msat, 1234.sat, 200_000_000.msat)))) to (defaultEncoded + ByteVector("fe47000005 3a 0001 187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281 00000000000004d2 00000000000f4240 000000000bebc200")), - defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.OriginTlv(Origin.PleaseOpenChannelOrigin(ByteVector32("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25"), 1_234_567.msat, 321.sat, 1_111_000.msat)))) to (defaultEncoded + ByteVector("fe47000005 3a 0004 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 0000000000000141 000000000012d687 000000000010f3d8")), + ) + // @formatter:on + testCases.forEach { (open, bin) -> + val decoded = LightningMessage.decode(bin.toByteArray()) + assertNotNull(decoded) + assertEquals(decoded, open) + val encoded = LightningMessage.encode(open) + assertEquals(encoded.byteVector(), bin) + } + } + + @Test + fun `encode - decode open_channel flags`() { + // @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 defaultEncodedWithoutFlags = ByteVector("0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f") + val testCases = listOf( + defaultOpen to (defaultEncodedWithoutFlags + ByteVector("00")), + defaultOpen.copy(channelFlags = ChannelFlags(announceChannel = true, nonInitiatorPaysCommitFees = false)) to (defaultEncodedWithoutFlags + ByteVector("01")), + defaultOpen.copy(channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = true)) to (defaultEncodedWithoutFlags + ByteVector("02")), + defaultOpen.copy(channelFlags = ChannelFlags(announceChannel = true, nonInitiatorPaysCommitFees = true)) to (defaultEncodedWithoutFlags + ByteVector("03")), ) // @formatter:on testCases.forEach { (open, bin) -> @@ -776,16 +797,11 @@ class LightningCodecsTestsCommon : LightningTestSuite() { } @Test - fun `encode - decode please-open-channel messages`() { + fun `encode - decode phoenix-android-legacy-info messages`() { val testCases = listOf( - // @formatter:off - PleaseOpenChannel(Block.RegtestGenesisBlock.hash, ByteVector32("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25"), 123_456.sat, 2, 522_000) to Hex.decode("8ca1 06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 000000000001e240 0002 0007f710"), - PleaseOpenChannel(Block.RegtestGenesisBlock.hash, ByteVector32("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25"), 123_456.sat, 2, 522_000, TlvStream(PleaseOpenChannelTlv.GrandParents(listOf()))) to Hex.decode("8ca1 06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 000000000001e240 0002 0007f710 fd023100"), - PleaseOpenChannel(Block.RegtestGenesisBlock.hash, ByteVector32("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25"), 123_456.sat, 2, 522_000, TlvStream(PleaseOpenChannelTlv.GrandParents(listOf(OutPoint(TxHash("d0556c8cc004933f40b9ca5e87e18cb549298fb02d7e64b0c0ee95303485145a"), 5))))) to Hex.decode("8ca1 06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 000000000001e240 0002 0007f710 fd023128d0556c8cc004933f40b9ca5e87e18cb549298fb02d7e64b0c0ee95303485145a0000000000000005"), - PleaseOpenChannel(Block.RegtestGenesisBlock.hash, ByteVector32("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25"), 123_456.sat, 2, 522_000, TlvStream(PleaseOpenChannelTlv.GrandParents(listOf(OutPoint(TxHash("572b045edb5f0e3ff667e914e368273b11a874fae56a735b332b54048b7978c2"), 0), OutPoint(TxHash("cd6ac843158a1c317021de1323cdd2071f0f59744f79b298a8a45fda2dd7989f"), 1105))))) to Hex.decode("8ca1 06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 000000000001e240 0002 0007f710 fd023150572b045edb5f0e3ff667e914e368273b11a874fae56a735b332b54048b7978c20000000000000000cd6ac843158a1c317021de1323cdd2071f0f59744f79b298a8a45fda2dd7989f0000000000000451"), - // @formatter:on + Pair(PhoenixAndroidLegacyInfo(hasChannels = true), Hex.decode("88cfff")), + Pair(PhoenixAndroidLegacyInfo(hasChannels = false), Hex.decode("88cf00")), ) - testCases.forEach { val decoded = LightningMessage.decode(it.second) assertNotNull(decoded) @@ -796,10 +812,15 @@ class LightningCodecsTestsCommon : LightningTestSuite() { } @Test - fun `encode - decode phoenix-android-legacy-info messages`() { + fun `encode - decode recommended feerates messages`() { + val fundingRange = RecommendedFeeratesTlv.FundingFeerateRange(FeeratePerKw(5000.sat), FeeratePerKw(15_000.sat)) + val commitmentRange = RecommendedFeeratesTlv.CommitmentFeerateRange(FeeratePerKw(253.sat), FeeratePerKw(2_000.sat)) val testCases = listOf( - Pair(PhoenixAndroidLegacyInfo(hasChannels = true), Hex.decode("88cfff")), - Pair(PhoenixAndroidLegacyInfo(hasChannels = false), Hex.decode("88cf00")), + // @formatter:off + RecommendedFeerates(Block.TestnetGenesisBlock.hash, FeeratePerKw(2500.sat), FeeratePerKw(2500.sat)) to Hex.decode("99f1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 000009c4 000009c4"), + RecommendedFeerates(Block.TestnetGenesisBlock.hash, FeeratePerKw(5000.sat), FeeratePerKw(253.sat)) to Hex.decode("99f1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 00001388 000000fd"), + RecommendedFeerates(Block.TestnetGenesisBlock.hash, FeeratePerKw(10_000.sat), FeeratePerKw(1000.sat), TlvStream(fundingRange, commitmentRange)) to Hex.decode("99f1 43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000 00002710 000003e8 01080000138800003a98 0308000000fd000007d0"), + // @formatter:on ) testCases.forEach { val decoded = LightningMessage.decode(it.second) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/OpenTlvTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/OpenTlvTestsCommon.kt index 61f208fab..6b326064d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/OpenTlvTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/OpenTlvTestsCommon.kt @@ -1,14 +1,10 @@ package fr.acinq.lightning.wire -import fr.acinq.bitcoin.ByteVector32 import fr.acinq.lightning.Feature import fr.acinq.lightning.FeatureSupport import fr.acinq.lightning.Features import fr.acinq.lightning.channel.ChannelType -import fr.acinq.lightning.channel.Origin import fr.acinq.lightning.tests.utils.LightningTestSuite -import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.utils.sat import fr.acinq.secp256k1.Hex import kotlin.test.* @@ -59,31 +55,4 @@ class OpenTlvTestsCommon : LightningTestSuite() { } } - @Test - fun `channel origin TLV`() { - val testCases = listOf( - Pair( - Origin.PayToOpenOrigin(ByteVector32.fromValidHex("187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281"), 1_000_000.msat, 1234.sat, 200_000_000.msat), - Hex.decode("fe47000005 3a 0001 187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281 00000000000004d2 00000000000f4240 000000000bebc200") - ), - Pair( - Origin.PleaseOpenChannelOrigin(ByteVector32("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25"), 1_234_567.msat, 321.sat, 1_111_000.msat), - Hex.decode("fe47000005 3a 0004 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 0000000000000141 000000000012d687 000000000010f3d8") - ) - ) - - @Suppress("UNCHECKED_CAST") - val readers = mapOf(ChannelTlv.OriginTlv.tag to ChannelTlv.OriginTlv.Companion as TlvValueReader) - val tlvStreamSerializer = TlvStreamSerializer(false, readers) - - testCases.forEach { - val decoded = tlvStreamSerializer.read(it.second) - val encoded = tlvStreamSerializer.write(decoded) - assertContentEquals(it.second, encoded) - val channelOrigin = decoded.get()?.origin - assertNotNull(channelOrigin) - assertEquals(it.first, channelOrigin) - } - } - } diff --git a/src/commonTest/resources/nonreg/v2/Closing_0ba41d17/data.json b/src/commonTest/resources/nonreg/v2/Closing_0ba41d17/data.json index 764aa406e..9df86d5ac 100644 --- a/src/commonTest/resources/nonreg/v2/Closing_0ba41d17/data.json +++ b/src/commonTest/resources/nonreg/v2/Closing_0ba41d17/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Closing_0ed6ff68/data.json b/src/commonTest/resources/nonreg/v2/Closing_0ed6ff68/data.json index ae5f7de63..0594d10ee 100644 --- a/src/commonTest/resources/nonreg/v2/Closing_0ed6ff68/data.json +++ b/src/commonTest/resources/nonreg/v2/Closing_0ed6ff68/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Closing_0efffae3/data.json b/src/commonTest/resources/nonreg/v2/Closing_0efffae3/data.json index 9a5b4c324..d6913f41d 100644 --- a/src/commonTest/resources/nonreg/v2/Closing_0efffae3/data.json +++ b/src/commonTest/resources/nonreg/v2/Closing_0efffae3/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Closing_2fd2a3fa/data.json b/src/commonTest/resources/nonreg/v2/Closing_2fd2a3fa/data.json index 14d3c2f81..efd21a097 100644 --- a/src/commonTest/resources/nonreg/v2/Closing_2fd2a3fa/data.json +++ b/src/commonTest/resources/nonreg/v2/Closing_2fd2a3fa/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Closing_3bb07fb6/data.json b/src/commonTest/resources/nonreg/v2/Closing_3bb07fb6/data.json index 76bc7ee99..01168c1b0 100644 --- a/src/commonTest/resources/nonreg/v2/Closing_3bb07fb6/data.json +++ b/src/commonTest/resources/nonreg/v2/Closing_3bb07fb6/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Closing_8f1a524e/data.json b/src/commonTest/resources/nonreg/v2/Closing_8f1a524e/data.json index 5c5a6ce51..f85fd3400 100644 --- a/src/commonTest/resources/nonreg/v2/Closing_8f1a524e/data.json +++ b/src/commonTest/resources/nonreg/v2/Closing_8f1a524e/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Closing_ef682e2e/data.json b/src/commonTest/resources/nonreg/v2/Closing_ef682e2e/data.json index 8442323e4..de2e5dd2b 100644 --- a/src/commonTest/resources/nonreg/v2/Closing_ef682e2e/data.json +++ b/src/commonTest/resources/nonreg/v2/Closing_ef682e2e/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -81,7 +82,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json b/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json index 897fe83f2..adf1a8f6f 100644 --- a/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json +++ b/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json b/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json index 76501b4ad..1630cbe83 100644 --- a/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json +++ b/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json b/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json index 97299fe3d..7b9031ac8 100644 --- a/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json +++ b/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json b/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json index 9bc7d6500..4e47c6c15 100644 --- a/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json +++ b/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json b/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json index 1972b21d2..d5532bbfe 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -81,7 +82,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json b/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json index f5e6d6451..e3811aa3f 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json b/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json index 85e46b5a8..432053cea 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json b/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json index fdf1580ff..6ec3d386a 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json b/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json index 77e09eece..8cf311417 100644 --- a/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json +++ b/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json b/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json index e3c2516d7..51a957638 100644 --- a/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json +++ b/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json b/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json index fc47ec79b..f259c7f52 100644 --- a/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json +++ b/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_f7421b49/data.json b/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_f7421b49/data.json index a090b6f17..cf60cf169 100644 --- a/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_f7421b49/data.json +++ b/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_f7421b49/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_fe3c5978/data.json b/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_fe3c5978/data.json index 86fbba546..883fb2068 100644 --- a/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_fe3c5978/data.json +++ b/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_fe3c5978/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -83,7 +84,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_ff74dd33/data.json b/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_ff74dd33/data.json index fa73be0a9..404bec841 100644 --- a/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_ff74dd33/data.json +++ b/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_ff74dd33/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/WaitForFundingLocked_f3437082/data.json b/src/commonTest/resources/nonreg/v2/WaitForFundingLocked_f3437082/data.json index d0fca3dd7..21de20e8d 100644 --- a/src/commonTest/resources/nonreg/v2/WaitForFundingLocked_f3437082/data.json +++ b/src/commonTest/resources/nonreg/v2/WaitForFundingLocked_f3437082/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_ae47fde9/data.json b/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_ae47fde9/data.json index 0656d1373..3c0739391 100644 --- a/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_ae47fde9/data.json +++ b/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_ae47fde9/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -81,7 +82,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_d803549f/data.json b/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_d803549f/data.json index c0a1906a9..c8498287c 100644 --- a/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_d803549f/data.json +++ b/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_d803549f/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -81,7 +82,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Closing_029bf8f3/data.json b/src/commonTest/resources/nonreg/v3/Closing_029bf8f3/data.json index ea25e9026..3938b1644 100644 --- a/src/commonTest/resources/nonreg/v3/Closing_029bf8f3/data.json +++ b/src/commonTest/resources/nonreg/v3/Closing_029bf8f3/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Closing_0ba41d17/data.json b/src/commonTest/resources/nonreg/v3/Closing_0ba41d17/data.json index aa3ffa902..84f7f42a6 100644 --- a/src/commonTest/resources/nonreg/v3/Closing_0ba41d17/data.json +++ b/src/commonTest/resources/nonreg/v3/Closing_0ba41d17/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Closing_0ed6ff68/data.json b/src/commonTest/resources/nonreg/v3/Closing_0ed6ff68/data.json index 66d2bbb69..8010f2104 100644 --- a/src/commonTest/resources/nonreg/v3/Closing_0ed6ff68/data.json +++ b/src/commonTest/resources/nonreg/v3/Closing_0ed6ff68/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Closing_0efffae3/data.json b/src/commonTest/resources/nonreg/v3/Closing_0efffae3/data.json index b160fb04b..aae32e267 100644 --- a/src/commonTest/resources/nonreg/v3/Closing_0efffae3/data.json +++ b/src/commonTest/resources/nonreg/v3/Closing_0efffae3/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Closing_ebbd24bc/data.json b/src/commonTest/resources/nonreg/v3/Closing_ebbd24bc/data.json index dfe296fb1..6c50f2218 100644 --- a/src/commonTest/resources/nonreg/v3/Closing_ebbd24bc/data.json +++ b/src/commonTest/resources/nonreg/v3/Closing_ebbd24bc/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Closing_f137669f/data.json b/src/commonTest/resources/nonreg/v3/Closing_f137669f/data.json index 78d80803a..1b3d23107 100644 --- a/src/commonTest/resources/nonreg/v3/Closing_f137669f/data.json +++ b/src/commonTest/resources/nonreg/v3/Closing_f137669f/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -79,7 +80,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json b/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json index 1a0f84e64..65dbbdfbd 100644 --- a/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json +++ b/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json b/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json index 4c122a653..7cadbc73e 100644 --- a/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json +++ b/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json b/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json index 152463972..8269f04df 100644 --- a/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json +++ b/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -82,7 +83,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json b/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json index 60ae64d06..0630c17dd 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json b/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json index 4add406ad..39c521ac4 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json b/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json index 4ab6752e6..8f4df8b4d 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json b/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json index 43df4d86f..424aa610b 100644 --- a/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json +++ b/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json b/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json index 37bfaadfc..397d90e94 100644 --- a/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json +++ b/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json b/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json index 4eccd551b..e63f60088 100644 --- a/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json +++ b/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_fe3c5978/data.json b/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_fe3c5978/data.json index a23eaa96b..29ddcbe28 100644 --- a/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_fe3c5978/data.json +++ b/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_fe3c5978/data.json @@ -21,7 +21,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -83,7 +84,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_ff74dd33/data.json b/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_ff74dd33/data.json index fa16439a2..931e7fdc6 100644 --- a/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_ff74dd33/data.json +++ b/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_ff74dd33/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/WaitForFundingLocked_f3437082/data.json b/src/commonTest/resources/nonreg/v3/WaitForFundingLocked_f3437082/data.json index 8b7a87823..ed1b39903 100644 --- a/src/commonTest/resources/nonreg/v3/WaitForFundingLocked_f3437082/data.json +++ b/src/commonTest/resources/nonreg/v3/WaitForFundingLocked_f3437082/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 1000, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": false, + "isChannelOpener": false, + "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "features": { "activated": { @@ -80,7 +81,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_ae47fde9/data.json b/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_ae47fde9/data.json index c6f0f40da..dceaa38bd 100644 --- a/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_ae47fde9/data.json +++ b/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_ae47fde9/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -79,7 +80,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": { diff --git a/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_d803549f/data.json b/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_d803549f/data.json index 552674510..915c45bc2 100644 --- a/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_d803549f/data.json +++ b/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_d803549f/data.json @@ -19,7 +19,8 @@ "htlcMinimum": 0, "toSelfDelay": 144, "maxAcceptedHtlcs": 100, - "isInitiator": true, + "isChannelOpener": true, + "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "features": { "activated": { @@ -79,7 +80,10 @@ ] } }, - "channelFlags": 0 + "channelFlags": { + "announceChannel": false, + "nonInitiatorPaysCommitFees": false + } }, "changes": { "localChanges": {