diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt index 09177e506..de675ae07 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt @@ -7,7 +7,6 @@ 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 import fr.acinq.lightning.channel.states.Normal import fr.acinq.lightning.channel.states.WaitForFundingCreated @@ -58,7 +57,7 @@ sealed interface LiquidityEvents : NodeEvents { sealed interface SensitiveTaskEvents : NodeEvents { sealed class TaskIdentifier { data class InteractiveTx(val channelId: ByteVector32, val fundingTxIndex: Long) : TaskIdentifier() { - constructor(fundingParams: InteractiveTxParams) : this(fundingParams.channelId, (fundingParams.sharedInput as? SharedFundingInput.Multisig2of2)?.fundingTxIndex?.let { it + 1 } ?: 0) + constructor(fundingParams: InteractiveTxParams) : this(fundingParams.channelId, fundingParams.sharedInput?.fundingTxIndex?.let { it + 1 } ?: 0) } data class IncomingMultiPartPayment(val paymentHash: ByteVector32) : TaskIdentifier() } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/IClient.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/IClient.kt index 71d76fbb0..5cb7c0032 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/IClient.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/IClient.kt @@ -31,12 +31,15 @@ suspend fun IClient.computeSpliceCpfpFeerate(commitments: Commitments, targetFee val (parentsWeight, parentsFees) = commitments.all .takeWhile { getConfirmations(it.fundingTxId).let { confirmations -> confirmations == null || confirmations == 0 } } // we check for null in case the tx has been evicted .fold(Pair(0, 0.sat)) { (parentsWeight, parentsFees), commitment -> - val weight = when (commitment.localFundingStatus) { - // weight will be underestimated if the transaction is not fully signed - is LocalFundingStatus.UnconfirmedFundingTx -> commitment.localFundingStatus.signedTx?.weight() ?: commitment.localFundingStatus.sharedTx.tx.buildUnsignedTx().weight() - is LocalFundingStatus.ConfirmedFundingTx -> commitment.localFundingStatus.signedTx.weight() + when (commitment.localFundingStatus) { + is LocalFundingStatus.UnconfirmedFundingTx -> { + // Note that the weight will be underestimated if the transaction is not fully signed. + val weight = commitment.localFundingStatus.signedTx?.weight() ?: commitment.localFundingStatus.sharedTx.tx.buildUnsignedTx().weight() + Pair(parentsWeight + weight, parentsFees + commitment.localFundingStatus.fee) + } + // We filtered confirmed transactions before, so this case shouldn't happen. + is LocalFundingStatus.ConfirmedFundingTx -> Pair(parentsWeight, parentsFees) } - Pair(parentsWeight + weight, parentsFees + commitment.localFundingStatus.fee) } val totalWeight = parentsWeight + spliceWeight val totalFees = Transactions.weight2fee(targetFeerate, totalWeight) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt index 8d3581dcd..97de506a7 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt @@ -73,7 +73,7 @@ class SwapInManager(private var reservedUtxos: Set, private val logger .forEach { fundingStatus -> when (fundingStatus) { is LocalFundingStatus.UnconfirmedFundingTx -> addAll(fundingStatus.sharedTx.tx.localInputs.map { it.outPoint }) - is LocalFundingStatus.ConfirmedFundingTx -> addAll(fundingStatus.signedTx.txIn.map { it.outPoint }) + is LocalFundingStatus.ConfirmedFundingTx -> addAll(fundingStatus.spentInputs) } } else -> {} diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index 282e494b9..a312949b9 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -32,10 +32,9 @@ sealed class ChannelAction { CommitTx, HtlcSuccessTx, HtlcTimeoutTx, + HtlcDelayedTx, ClaimHtlcSuccessTx, ClaimHtlcTimeoutTx, - ClaimLocalAnchorOutputTx, - ClaimRemoteAnchorOutputTx, ClaimLocalDelayedOutputTx, ClaimRemoteDelayedOutputTx, MainPenaltyTx, @@ -47,20 +46,19 @@ sealed class ChannelAction { constructor(txinfo: Transactions.TransactionWithInputInfo) : this( tx = txinfo.tx, txType = when (txinfo) { - is Transactions.TransactionWithInputInfo.CommitTx -> Type.CommitTx - is Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx -> Type.HtlcSuccessTx - is Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx -> Type.HtlcTimeoutTx - is Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx -> Type.ClaimHtlcSuccessTx - is Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx -> Type.ClaimHtlcTimeoutTx - is Transactions.TransactionWithInputInfo.ClaimAnchorOutputTx.ClaimLocalAnchorOutputTx -> Type.ClaimLocalAnchorOutputTx - is Transactions.TransactionWithInputInfo.ClaimAnchorOutputTx.ClaimRemoteAnchorOutputTx -> Type.ClaimRemoteAnchorOutputTx - is Transactions.TransactionWithInputInfo.ClaimLocalDelayedOutputTx -> Type.ClaimLocalDelayedOutputTx - is Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx -> Type.ClaimRemoteDelayedOutputTx - is Transactions.TransactionWithInputInfo.MainPenaltyTx -> Type.MainPenaltyTx - is Transactions.TransactionWithInputInfo.HtlcPenaltyTx -> Type.HtlcPenaltyTx - is Transactions.TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx -> Type.ClaimHtlcDelayedOutputPenaltyTx - is Transactions.TransactionWithInputInfo.ClosingTx -> Type.ClosingTx - is Transactions.TransactionWithInputInfo.SpliceTx -> Type.FundingTx + is Transactions.CommitTx -> Type.CommitTx + is Transactions.HtlcSuccessTx -> Type.HtlcSuccessTx + is Transactions.HtlcTimeoutTx -> Type.HtlcTimeoutTx + is Transactions.HtlcDelayedTx -> Type.HtlcDelayedTx + is Transactions.ClaimHtlcSuccessTx -> Type.ClaimHtlcSuccessTx + is Transactions.ClaimHtlcTimeoutTx -> Type.ClaimHtlcTimeoutTx + is Transactions.ClaimLocalDelayedOutputTx -> Type.ClaimLocalDelayedOutputTx + is Transactions.ClaimRemoteDelayedOutputTx -> Type.ClaimRemoteDelayedOutputTx + is Transactions.MainPenaltyTx -> Type.MainPenaltyTx + is Transactions.HtlcPenaltyTx -> Type.HtlcPenaltyTx + is Transactions.ClaimHtlcDelayedOutputPenaltyTx -> Type.ClaimHtlcDelayedOutputPenaltyTx + is Transactions.ClosingTx -> Type.ClosingTx + is Transactions.SpliceTx -> Type.FundingTx } ) // endregion diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt index 723ee170a..5215f093c 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.lightning.CltvExpiry +import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.WatchTriggered import fr.acinq.lightning.blockchain.electrum.WalletState @@ -28,7 +29,12 @@ sealed class ChannelCommand { val walletInputs: List, val commitTxFeerate: FeeratePerKw, val fundingTxFeerate: FeeratePerKw, - val localParams: LocalParams, + val localChannelParams: LocalChannelParams, + val dustLimit: Satoshi, + val htlcMinimum: MilliSatoshi, + val maxHtlcValueInFlightMsat: Long, + val maxAcceptedHtlcs: Int, + val toRemoteDelay: CltvExpiryDelta, val remoteInit: InitMessage, val channelFlags: ChannelFlags, val channelConfig: ChannelConfig, @@ -44,7 +50,12 @@ sealed class ChannelCommand { val temporaryChannelId: ByteVector32, val fundingAmount: Satoshi, val walletInputs: List, - val localParams: LocalParams, + val localParams: LocalChannelParams, + val dustLimit: Satoshi, + val htlcMinimum: MilliSatoshi, + val maxHtlcValueInFlightMsat: Long, + val maxAcceptedHtlcs: Int, + val toRemoteDelay: CltvExpiryDelta, val channelConfig: ChannelConfig, val remoteInit: InitMessage, val fundingRates: LiquidityAds.WillFundRates? diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt index b552c817e..09c6c3b62 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt @@ -10,28 +10,33 @@ import fr.acinq.lightning.channel.Helpers.watchConfirmedIfNeeded import fr.acinq.lightning.channel.Helpers.watchSpentIfNeeded import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.logging.LoggingContext -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.toMilliSatoshi /** - * Details about a force-close where we published our commitment. + * When a commitment is published, we keep track of all outputs that can be spent (even if we don't yet have the data + * to spend them, for example the preimage for received HTLCs). Once all of those outputs have been spent by a confirmed + * transaction, the channel close is complete. * - * @param commitTx commitment tx. - * @param claimMainDelayedOutputTx tx claiming our main output (if we have one). - * @param htlcTxs txs claiming HTLCs. There will be one entry for each pending HTLC. The value will be null only for - * incoming HTLCs for which we don't have the preimage (we can't claim them yet). - * @param claimHtlcDelayedTxs 3rd-stage txs (spending the output of HTLC txs). - * @param claimAnchorTxs txs spending anchor outputs to bump the feerate of the commitment tx (if applicable). - * @param irrevocablySpent map of relevant outpoints that have been spent and the confirmed transaction that spends them. + * Note that we only store transactions after they have been confirmed: we're using RBF to get transactions confirmed, + * and it would be wasteful to store previous versions of the transactions that have been replaced. */ -data class LocalCommitPublished( - val commitTx: Transaction, - val claimMainDelayedOutputTx: ClaimLocalDelayedOutputTx? = null, - val htlcTxs: Map = emptyMap(), - val claimHtlcDelayedTxs: List = emptyList(), - val claimAnchorTxs: List = emptyList(), - val irrevocablySpent: Map = emptyMap() -) { +sealed class CommitPublished { + /** Fully signed commitment transaction. */ + abstract val commitTx: Transaction + /** Our main output, if we had some balance in the channel. */ + abstract val localOutput: OutPoint? + /** Our anchor output, if one is available to CPFP the [commitTx]. */ + abstract val anchorOutput: OutPoint? + /** + * Outputs corresponding to HTLCs that we may be able to claim (even when we don't have the preimage yet). + * Note that some HTLC outputs of the [commitTx] may not be included, if we know that we will never claim them + * (such as HTLCs that we started failing before the channel closed). + */ + abstract val htlcOutputs: Set + /** Map of outpoints that have been spent and the confirmed transaction that spends them. */ + abstract val irrevocablySpent: Map + /** * In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the * local commit scenario and keep track of it. @@ -40,9 +45,55 @@ data class LocalCommitPublished( * spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't * want to wait forever before declaring that the channel is CLOSED. * - * @param tx a transaction that has been irrevocably confirmed + * @param tx a transaction that has been irrevocably confirmed. + */ + abstract fun updateIrrevocablySpent(tx: Transaction): CommitPublished + + /** Returns true if the commitment transaction is confirmed. */ + val isConfirmed: Boolean get() = run { + // NB: if multiple transactions end up in the same block, the first confirmation we receive may not be the commit tx. + // However if the confirmed tx spends from the commit tx, we know that the commit tx is already confirmed and we know + // the type of closing. + irrevocablySpent.values.any { tx -> tx.txid == commitTx.txid } || irrevocablySpent.keys.any { spent -> spent.txid == commitTx.txid } + } + /** + * Returns true when all outputs that can be claimed have been spent: we can forget the channel at that point. + * Note that some of those outputs may be claimed by our peer (e.g. HTLCs that reached their expiry). */ - fun update(tx: Transaction): LocalCommitPublished { + abstract val isDone: Boolean +} + +/** Transactions spending outputs of our commitment transaction. */ +data class LocalCommitSecondStageTransactions(val mainDelayedTx: Transactions.ClaimLocalDelayedOutputTx?, val htlcTxs: List) + +/** Transactions spending outputs of our HTLC transactions. */ +data class LocalCommitThirdStageTransactions(val htlcDelayedTxs: List) + +/** + * Details about a force-close where we published our commitment. + * + * @param htlcDelayedOutputs when an HTLC transaction confirms, we must claim its output using a 3rd-stage delayed + * transaction. An entry containing the corresponding output must be added to this set to + * ensure that we don't forget the channel too soon, and correctly wait until we've spent it. + */ +data class LocalCommitPublished( + override val commitTx: Transaction, + override val localOutput: OutPoint?, + override val anchorOutput: OutPoint?, + val incomingHtlcs: Map, + val outgoingHtlcs: Map, + val htlcDelayedOutputs: Set, + override val irrevocablySpent: Map +) : CommitPublished() { + override val htlcOutputs: Set = incomingHtlcs.keys + outgoingHtlcs.keys + override val isDone: Boolean = run { + val mainOutputSpent = localOutput?.let { irrevocablySpent.contains(it) } ?: true + val allHtlcsSpent = (htlcOutputs - irrevocablySpent.keys).isEmpty() + val allHtlcTxsSpent = (htlcDelayedOutputs - irrevocablySpent.keys).isEmpty() + isConfirmed && mainOutputSpent && allHtlcsSpent && allHtlcTxsSpent + } + + override fun updateIrrevocablySpent(tx: Transaction): LocalCommitPublished { // even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant val relevantOutpoints = tx.txIn.map { it.outPoint }.filter { outPoint -> @@ -52,150 +103,95 @@ data class LocalCommitPublished( val spendsTheCommitTx = commitTx.txid == outPoint.txid // is the tx one of our 3rd stage delayed txs? (a 3rd stage tx is a tx spending the output of an htlc tx, which // is itself spending the output of the commitment tx) - val is3rdStageDelayedTx = claimHtlcDelayedTxs.map { it.input.outPoint }.contains(outPoint) + val is3rdStageDelayedTx = htlcDelayedOutputs.contains(outPoint) isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx } // then we add the relevant outpoints to the map keeping track of which txid spends which outpoint return this.copy(irrevocablySpent = irrevocablySpent + relevantOutpoints.associateWith { tx }.toMap()) } - /** - * A local commit is considered done when: - * - all commitment tx outputs that we can spend have been spent and confirmed (even if the spending tx was not ours) - * - all 3rd stage txs (txs spending htlc txs) have been confirmed - */ - fun isDone(): Boolean { - val confirmedTxs = irrevocablySpent.values.map { it.txid }.toSet() - // is the commitment tx buried? (we need to check this because we may not have any outputs) - val isCommitTxConfirmed = confirmedTxs.contains(commitTx.txid) - // is our main output confirmed (if we have one)? - val isMainOutputConfirmed = claimMainDelayedOutputTx == null || irrevocablySpent.contains(claimMainDelayedOutputTx.input.outPoint) - // are all htlc outputs from the commitment tx spent (we need to check them all because we may receive preimages later)? - val allHtlcsSpent = (htlcTxs.keys - irrevocablySpent.keys).isEmpty() - // are all outputs from htlc txs spent? - val unconfirmedHtlcDelayedTxs = claimHtlcDelayedTxs.map { it.input.outPoint } - // only the txs which parents are already confirmed may get confirmed (note that this also eliminates outputs that have been double-spent by a competing tx) - .filter { input -> confirmedTxs.contains(input.txid) } - // has the tx already been confirmed? - .filterNot { input -> irrevocablySpent.contains(input) } - return isCommitTxConfirmed && isMainOutputConfirmed && allHtlcsSpent && unconfirmedHtlcDelayedTxs.isEmpty() - } - - fun isConfirmed(): Boolean { - return irrevocablySpent.values.any { it.txid == commitTx.txid } || irrevocablySpent.keys.any { it.txid == commitTx.txid } - } - - internal fun LoggingContext.doPublish(nodeParams: NodeParams, channelId: ByteVector32): List { + internal fun LoggingContext.doPublish(nodeParams: NodeParams, channelId: ByteVector32, txs: LocalCommitSecondStageTransactions): List { val publishQueue = buildList { add(ChannelAction.Blockchain.PublishTx(commitTx, ChannelAction.Blockchain.PublishTx.Type.CommitTx)) - claimMainDelayedOutputTx?.let { add(ChannelAction.Blockchain.PublishTx(it)) } - addAll(htlcTxs.values.filterNotNull().map { ChannelAction.Blockchain.PublishTx(it) }) - addAll(claimHtlcDelayedTxs.map { ChannelAction.Blockchain.PublishTx(it) }) + txs.mainDelayedTx?.let { add(ChannelAction.Blockchain.PublishTx(it)) } + addAll(txs.htlcTxs.map { ChannelAction.Blockchain.PublishTx(it) }) } val publishList = publishIfNeeded(publishQueue, irrevocablySpent) - - // we watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = buildList { - add(commitTx) - claimMainDelayedOutputTx?.let { add(it.tx) } - addAll(claimHtlcDelayedTxs.map { it.tx }) - } - val watchConfirmedList = watchConfirmedIfNeeded(nodeParams, channelId, watchConfirmedQueue, irrevocablySpent) - - // we watch outputs of the commitment tx that both parties may spend - val watchSpentQueue = htlcTxs.keys.toList() + // We watch the commitment transaction: once confirmed, it invalidates other types of force-close. + val watchConfirmedList = watchConfirmedIfNeeded(nodeParams, channelId, listOf(commitTx), irrevocablySpent) + // We watch outputs of the commitment transaction that we may spend: every time we detect a spending transaction, + // we will watch for its confirmation. This ensures that we detect double-spends that could come from: + // - our own RBF attempts + // - remote transactions for outputs that both parties may spend (e.g. HTLCs) + val watchSpentQueue = listOfNotNull(localOutput) + htlcOutputs val watchSpentList = watchSpentIfNeeded(channelId, commitTx, watchSpentQueue, irrevocablySpent) - return buildList { addAll(publishList) addAll(watchConfirmedList) addAll(watchSpentList) } } + + internal fun LoggingContext.doPublish(channelId: ByteVector32, txs: LocalCommitThirdStageTransactions): List { + val publishList = publishIfNeeded(txs.htlcDelayedTxs.map { ChannelAction.Blockchain.PublishTx(it) }, irrevocablySpent) + // We watch the spent outputs to detect our RBF attempts. + val watchSpentList = txs.htlcDelayedTxs.flatMap { tx -> watchSpentIfNeeded(channelId, tx.input, irrevocablySpent) } + return buildList { + addAll(publishList) + addAll(watchSpentList) + } + } } +/** Transactions spending outputs of a remote commitment transaction. */ +data class RemoteCommitSecondStageTransactions(val mainTx: Transactions.ClaimRemoteDelayedOutputTx?, val htlcTxs: List) + /** - * Details about a force-close where they published their commitment. - * - * @param commitTx commitment tx. - * @param claimMainOutputTx tx claiming our main output (if we have one). - * @param claimHtlcTxs txs claiming HTLCs. There will be one entry for each pending HTLC. The value will be null only - * for incoming HTLCs for which we don't have the preimage (we can't claim them yet). - * @param claimAnchorTxs txs spending anchor outputs to bump the feerate of the commitment tx (if applicable). - * @param irrevocablySpent map of relevant outpoints that have been spent and the confirmed transaction that spends them. + * Details about a force-close where they published their commitment (current or next). */ data class RemoteCommitPublished( - val commitTx: Transaction, - val claimMainOutputTx: ClaimRemoteCommitMainOutputTx? = null, - val claimHtlcTxs: Map = emptyMap(), - val claimAnchorTxs: List = emptyList(), - val irrevocablySpent: Map = emptyMap() -) { - /** - * In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the - * remote commit scenario and keep track of it. - * - * We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be - * spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't - * want to wait forever before declaring that the channel is CLOSED. - * - * @param tx a transaction that has been irrevocably confirmed - */ - fun update(tx: Transaction): RemoteCommitPublished { + override val commitTx: Transaction, + override val localOutput: OutPoint?, + override val anchorOutput: OutPoint?, + val incomingHtlcs: Map, + val outgoingHtlcs: Map, + override val irrevocablySpent: Map +) : CommitPublished() { + override val htlcOutputs: Set = incomingHtlcs.keys + outgoingHtlcs.keys + override val isDone: Boolean = run { + val mainOutputSpent = localOutput?.let { irrevocablySpent.contains(it) } ?: true + val allHtlcsSpent = (htlcOutputs - irrevocablySpent.keys).isEmpty() + isConfirmed && mainOutputSpent && allHtlcsSpent + } + + override fun updateIrrevocablySpent(tx: Transaction): RemoteCommitPublished { // even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant - val relevantOutpoints = tx.txIn.map { it.outPoint }.filter { + val relevantOutpoints = tx.txIn.map { it.outPoint }.filter { outPoint -> // is this the commit tx itself? (we could do this outside of the loop...) val isCommitTx = commitTx.txid == tx.txid // does the tx spend an output of the commitment tx? - val spendsTheCommitTx = commitTx.txid == it.txid + val spendsTheCommitTx = commitTx.txid == outPoint.txid isCommitTx || spendsTheCommitTx } // then we add the relevant outpoints to the map keeping track of which txid spends which outpoint return this.copy(irrevocablySpent = irrevocablySpent + relevantOutpoints.associateWith { tx }.toMap()) } - /** - * A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed - * (even if the spending tx was not ours). - */ - fun isDone(): Boolean { - val confirmedTxs = irrevocablySpent.values.map { it.txid }.toSet() - // is the commitment tx buried? (we need to check this because we may not have any outputs) - val isCommitTxConfirmed = confirmedTxs.contains(commitTx.txid) - // is our main output confirmed (if we have one)? - val isMainOutputConfirmed = claimMainOutputTx == null || irrevocablySpent.contains(claimMainOutputTx.input.outPoint) - // are all htlc outputs from the commitment tx spent (we need to check them all because we may receive preimages later)? - val allHtlcsSpent = (claimHtlcTxs.keys - irrevocablySpent.keys).isEmpty() - return isCommitTxConfirmed && isMainOutputConfirmed && allHtlcsSpent - } - - fun isConfirmed(): Boolean { - return irrevocablySpent.values.any { it.txid == commitTx.txid } || irrevocablySpent.keys.any { it.txid == commitTx.txid } - } - - internal fun LoggingContext.doPublish(nodeParams: NodeParams, channelId: ByteVector32): List { + internal fun LoggingContext.doPublish(nodeParams: NodeParams, channelId: ByteVector32, txs: RemoteCommitSecondStageTransactions): List { val publishQueue = buildList { - claimMainOutputTx?.let { add(ChannelAction.Blockchain.PublishTx(it)) } - addAll(claimHtlcTxs.values.filterNotNull().map { ChannelAction.Blockchain.PublishTx(it) }) + txs.mainTx?.let { add(ChannelAction.Blockchain.PublishTx(it)) } + addAll(txs.htlcTxs.map { ChannelAction.Blockchain.PublishTx(it) }) } val publishList = publishIfNeeded(publishQueue, irrevocablySpent) - - // we watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = buildList { - add(commitTx) - claimMainOutputTx?.let { add(it.tx) } - } - val watchEventConfirmedList = watchConfirmedIfNeeded(nodeParams, channelId, watchConfirmedQueue, irrevocablySpent) - - // we watch outputs of the commitment tx that both parties may spend - val watchSpentQueue = claimHtlcTxs.keys.toList() + // We watch the commitment transaction: once confirmed, it invalidates other types of force-close. + val watchEventConfirmedList = watchConfirmedIfNeeded(nodeParams, channelId, listOf(commitTx), irrevocablySpent) + // We watch outputs of the commitment transaction that we may spend: every time we detect a spending transaction, + // we will watch for its confirmation. This ensures that we detect double-spends that could come from: + // - our own RBF attempts + // - remote transactions for outputs that both parties may spend (e.g. HTLCs) + val watchSpentQueue = listOfNotNull(localOutput) + htlcOutputs val watchEventSpentList = watchSpentIfNeeded(channelId, commitTx, watchSpentQueue, irrevocablySpent) - return buildList { addAll(publishList) addAll(watchEventConfirmedList) @@ -204,36 +200,39 @@ data class RemoteCommitPublished( } } +/** Transactions spending outputs of a revoked remote commitment transactions. */ +data class RevokedCommitSecondStageTransactions(val mainTx: Transactions.ClaimRemoteDelayedOutputTx?, val mainPenaltyTx: Transactions.MainPenaltyTx?, val htlcPenaltyTxs: List) + +/** Transactions spending outputs of confirmed remote HTLC transactions. */ +data class RevokedCommitThirdStageTransactions(val htlcDelayedPenaltyTxs: List) + /** * Details about a force-close where they published one of their revoked commitments. + * In that case, we're able to spend every output of the commitment transaction (if economical). * - * @param commitTx revoked commitment tx. - * @param claimMainOutputTx tx claiming our main output (if we have one). - * @param mainPenaltyTx penalty tx claiming their main output (if they have one). - * @param htlcPenaltyTxs penalty txs claiming every HTLC output. - * @param claimHtlcDelayedPenaltyTxs penalty txs claiming the output of their HTLC txs (if they managed to get them confirmed before our htlcPenaltyTxs). - * @param irrevocablySpent map of relevant outpoints that have been spent and the confirmed transaction that spends them. + * @param htlcDelayedOutputs if our peer manages to get some of their HTLC transactions confirmed before our penalty + * transactions, we must spend the output(s) of their HTLC transactions. */ data class RevokedCommitPublished( - val commitTx: Transaction, + override val commitTx: Transaction, val remotePerCommitmentSecret: PrivateKey, - val claimMainOutputTx: ClaimRemoteCommitMainOutputTx? = null, - val mainPenaltyTx: MainPenaltyTx? = null, - val htlcPenaltyTxs: List = emptyList(), - val claimHtlcDelayedPenaltyTxs: List = emptyList(), - val irrevocablySpent: Map = emptyMap() -) { - /** - * In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the - * revoked commit scenario and keep track of it. - * - * We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be - * spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't - * want to wait forever before declaring that the channel is CLOSED. - * - * @param tx a transaction that has been irrevocably confirmed - */ - fun update(tx: Transaction): RevokedCommitPublished { + override val localOutput: OutPoint?, + val remoteOutput: OutPoint?, + override val htlcOutputs: Set, + val htlcDelayedOutputs: Set, + override val irrevocablySpent: Map +) : CommitPublished() { + // We don't use the anchor output, we can CPFP the commitment with any other output. + override val anchorOutput: OutPoint? = null + override val isDone: Boolean = run { + val mainOutputSpent = localOutput?.let { irrevocablySpent.contains(it) } ?: true + val remoteOutputSpent = remoteOutput?.let { irrevocablySpent.contains(it) } ?: true + val allHtlcsSpent = (htlcOutputs - irrevocablySpent.keys).isEmpty() + val allHtlcTxsSpent = (htlcDelayedOutputs - irrevocablySpent.keys).isEmpty() + isConfirmed && mainOutputSpent && remoteOutputSpent && allHtlcsSpent && allHtlcTxsSpent + } + + override fun updateIrrevocablySpent(tx: Transaction): RevokedCommitPublished { // even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant val relevantOutpoints = tx.txIn.map { it.outPoint }.filter { outPoint -> @@ -243,80 +242,47 @@ data class RevokedCommitPublished( val spendsTheCommitTx = commitTx.txid == outPoint.txid // is the tx a 3rd stage txs? (a 3rd stage tx is a tx spending the output of an htlc tx, which // is itself spending the output of the commitment tx) - val is3rdStageDelayedTx = claimHtlcDelayedPenaltyTxs.map { it.input.outPoint }.contains(outPoint) + val is3rdStageDelayedTx = htlcDelayedOutputs.contains(outPoint) isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx } // then we add the relevant outpoints to the map keeping track of which txid spends which outpoint return this.copy(irrevocablySpent = irrevocablySpent + relevantOutpoints.associateWith { tx }.toMap()) } - /** - * A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed - * (even if the spending tx was not ours). - */ - fun isDone(): Boolean { - val confirmedTxs = irrevocablySpent.values.map { it.txid }.toSet() - // is the commitment tx buried? (we need to check this because we may not have any outputs) - val isCommitTxConfirmed = confirmedTxs.contains(commitTx.txid) - // are there remaining spendable outputs from the commitment tx? - val unspentCommitTxOutputs = run { - val commitOutputsSpendableByUs = (listOfNotNull(claimMainOutputTx) + listOfNotNull(mainPenaltyTx) + htlcPenaltyTxs).map { it.input.outPoint } - commitOutputsSpendableByUs.toSet() - irrevocablySpent.keys - } - // are all outputs from htlc txs spent? - val unconfirmedHtlcDelayedTxs = claimHtlcDelayedPenaltyTxs.map { it.input.outPoint } - // only the txs which parents are already confirmed may get confirmed (note that this also eliminates outputs that have been double-spent by a competing tx) - .filter { input -> confirmedTxs.contains(input.txid) } - // if one of the tx inputs has been spent, the tx has already been confirmed or a competing tx has been confirmed - .filterNot { input -> irrevocablySpent.contains(input) } - return isCommitTxConfirmed && unspentCommitTxOutputs.isEmpty() && unconfirmedHtlcDelayedTxs.isEmpty() - } - - fun isConfirmed(): Boolean { - return irrevocablySpent.values.any { it.txid == commitTx.txid } || irrevocablySpent.keys.any { it.txid == commitTx.txid } - } - - internal fun LoggingContext.doPublish(nodeParams: NodeParams, channelId: ByteVector32): List { + internal fun LoggingContext.doPublish(nodeParams: NodeParams, channelId: ByteVector32, txs: RevokedCommitSecondStageTransactions): List { val publishQueue = buildList { - claimMainOutputTx?.let { add(ChannelAction.Blockchain.PublishTx(it)) } - mainPenaltyTx?.let { add(ChannelAction.Blockchain.PublishTx(it)) } - addAll(htlcPenaltyTxs.map { ChannelAction.Blockchain.PublishTx(it) }) - addAll(claimHtlcDelayedPenaltyTxs.map { ChannelAction.Blockchain.PublishTx(it) }) + txs.mainTx?.let { add(ChannelAction.Blockchain.PublishTx(it)) } + txs.mainPenaltyTx?.let { add(ChannelAction.Blockchain.PublishTx(it)) } + addAll(txs.htlcPenaltyTxs.map { ChannelAction.Blockchain.PublishTx(it) }) } val publishList = publishIfNeeded(publishQueue, irrevocablySpent) - - // we watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = buildList { - add(commitTx) - claimMainOutputTx?.let { add(it.tx) } - } - val watchEventConfirmedList = watchConfirmedIfNeeded(nodeParams, channelId, watchConfirmedQueue, irrevocablySpent) - - // we watch outputs of the commitment tx that both parties may spend - val watchSpentQueue = buildList { - mainPenaltyTx?.let { add(it.input.outPoint) } - addAll(htlcPenaltyTxs.map { it.input.outPoint }) - } + // We watch the commitment transaction: once confirmed, it invalidates other types of force-close. + val watchEventConfirmedList = watchConfirmedIfNeeded(nodeParams, channelId, listOf(commitTx), irrevocablySpent) + // We watch outputs of the commitment tx that both parties may spend, or that we may RBF. + val watchSpentQueue = listOfNotNull(localOutput, remoteOutput) + htlcOutputs val watchEventSpentList = watchSpentIfNeeded(channelId, commitTx, watchSpentQueue, irrevocablySpent) - return buildList { addAll(publishList) addAll(watchEventConfirmedList) addAll(watchEventSpentList) } } + + internal fun LoggingContext.doPublish(channelId: ByteVector32, txs: RevokedCommitThirdStageTransactions): List { + val publishList = publishIfNeeded(txs.htlcDelayedPenaltyTxs.map { ChannelAction.Blockchain.PublishTx(it) }, irrevocablySpent) + // We watch the spent outputs to detect our RBF attempts. + val watchSpentList = txs.htlcDelayedPenaltyTxs.flatMap { tx -> watchSpentIfNeeded(channelId, tx.input, irrevocablySpent) } + return buildList { + addAll(publishList) + addAll(watchSpentList) + } + } } -data class LocalParams( +/** Local params that apply for the channel's lifetime. */ +data class LocalChannelParams( val nodeId: PublicKey, val fundingKeyPath: KeyPath, - val dustLimit: Satoshi, - val maxHtlcValueInFlightMsat: Long, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - val htlcMinimum: MilliSatoshi, - val toSelfDelay: CltvExpiryDelta, - val maxAcceptedHtlcs: Int, val isChannelOpener: Boolean, val paysCommitTxFees: Boolean, val defaultFinalScriptPubKey: ByteVector, @@ -325,11 +291,6 @@ data class LocalParams( constructor(nodeParams: NodeParams, isChannelOpener: Boolean, payCommitTxFees: Boolean) : this( nodeId = nodeParams.nodeId, 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, isChannelOpener = isChannelOpener, paysCommitTxFees = payCommitTxFees, defaultFinalScriptPubKey = nodeParams.keyManager.finalOnChainWallet.pubkeyScript(addressIndex = 0), // the default closing address is the same for all channels @@ -339,13 +300,9 @@ data class LocalParams( fun channelKeys(keyManager: KeyManager) = keyManager.channelKeys(fundingKeyPath) } -data class RemoteParams( +/** Remote params that apply for the channel's lifetime. */ +data class RemoteChannelParams( val nodeId: PublicKey, - val dustLimit: Satoshi, - val maxHtlcValueInFlightMsat: Long, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - val htlcMinimum: MilliSatoshi, - val toSelfDelay: CltvExpiryDelta, - val maxAcceptedHtlcs: Int, val revocationBasepoint: PublicKey, val paymentBasepoint: PublicKey, val delayedPaymentBasepoint: PublicKey, @@ -353,6 +310,15 @@ data class RemoteParams( val features: Features ) +/** Configuration parameters that apply to local or remote commitment transactions, and may be updated dynamically. */ +data class CommitParams( + val dustLimit: Satoshi, + val maxHtlcValueInFlightMsat: Long, + val htlcMinimum: MilliSatoshi, + val toSelfDelay: CltvExpiryDelta, + val maxAcceptedHtlcs: Int, +) + /** * 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. diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt index ed359c5c9..3e4ad727d 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt @@ -3,12 +3,12 @@ package fr.acinq.lightning.channel import fr.acinq.lightning.Feature import fr.acinq.lightning.FeatureSupport import fr.acinq.lightning.Features +import fr.acinq.lightning.transactions.Transactions import fr.acinq.secp256k1.Hex /** * Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel. - * Even if one of these features is later disabled at the connection level, it will still apply to the channel until the - * channel is upgraded or closed. + * Even if one of these features is later disabled at the connection level, it will still apply to the channel. */ data class ChannelFeatures(val features: Set) { @@ -18,7 +18,7 @@ data class ChannelFeatures(val features: Set) { */ constructor(channelType: ChannelType, localFeatures: Features, remoteFeatures: Features) : this( buildSet { - addAll(channelType.features) + addAll(channelType.permanentChannelFeatures) addAll(permanentChannelFeatures.filter { Features.canUseFeature(localFeatures, remoteFeatures, it) }) } ) @@ -42,6 +42,8 @@ sealed class ChannelType { abstract val name: String abstract val features: Set + abstract val permanentChannelFeatures: Set + abstract val commitmentFormat: Transactions.CommitmentFormat override fun toString(): String = name @@ -52,11 +54,15 @@ sealed class ChannelType { object AnchorOutputs : SupportedChannelType() { override val name: String get() = "anchor_outputs" override val features: Set get() = setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs) + override val permanentChannelFeatures: Set get() = setOf() + override val commitmentFormat: Transactions.CommitmentFormat get() = Transactions.CommitmentFormat.AnchorOutputs } object AnchorOutputsZeroReserve : SupportedChannelType() { override val name: String get() = "anchor_outputs_zero_reserve" override val features: Set get() = setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels) + override val permanentChannelFeatures: Set get() = setOf(Feature.ZeroReserveChannels) + override val commitmentFormat: Transactions.CommitmentFormat get() = Transactions.CommitmentFormat.AnchorOutputs } } @@ -64,6 +70,8 @@ sealed class ChannelType { data class UnsupportedChannelType(val featureBits: Features) : ChannelType() { override val name: String get() = "0x${Hex.encode(featureBits.toByteArray())}" override val features: Set get() = featureBits.activated.keys + override val permanentChannelFeatures: Set get() = setOf() + override val commitmentFormat: Transactions.CommitmentFormat get() = Transactions.CommitmentFormat.AnchorOutputs } companion object { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt index 38aef002f..c6b21fa19 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt @@ -3,7 +3,6 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Crypto.sha256 import fr.acinq.bitcoin.utils.Either -import fr.acinq.bitcoin.utils.Try import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Feature import fr.acinq.lightning.MilliSatoshi @@ -14,6 +13,7 @@ import fr.acinq.lightning.blockchain.fee.FeerateTolerance import fr.acinq.lightning.channel.states.Channel import fr.acinq.lightning.channel.states.ChannelContext import fr.acinq.lightning.crypto.ChannelKeys +import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.crypto.LocalCommitmentKeys import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.crypto.ShaChain @@ -21,8 +21,8 @@ import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.payment.OutgoingPaymentPacket import fr.acinq.lightning.transactions.CommitmentSpec import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.CommitTx -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx +import fr.acinq.lightning.transactions.Transactions.CommitTx +import fr.acinq.lightning.transactions.Transactions.HtlcTx import fr.acinq.lightning.transactions.Transactions.commitTxFee import fr.acinq.lightning.transactions.Transactions.commitTxFeeMsat import fr.acinq.lightning.transactions.Transactions.htlcOutputFee @@ -40,7 +40,8 @@ data class ChannelParams( val channelId: ByteVector32, val channelConfig: ChannelConfig, val channelFeatures: ChannelFeatures, - val localParams: LocalParams, val remoteParams: RemoteParams, + val localParams: LocalChannelParams, + val remoteParams: RemoteChannelParams, val channelFlags: ChannelFlags ) { init { @@ -93,87 +94,82 @@ data class CommitmentChanges(val localChanges: LocalChanges, val remoteChanges: } } -data class HtlcTxAndSigs(val txinfo: HtlcTx, val localSig: ByteVector64, val remoteSig: ByteVector64) -data class PublishableTxs(val commitTx: CommitTx, val htlcTxsAndSigs: List) +sealed class ChannelSpendSignature { + /** When using a 2-of-2 multisig, we need two individual ECDSA signatures. */ + data class IndividualSignature(val sig: ByteVector64) : ChannelSpendSignature() +} /** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */ -data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishableTxs: PublishableTxs) { +data class LocalCommit(val index: Long, val spec: CommitmentSpec, val txId: TxId, val remoteSig: ChannelSpendSignature, val htlcRemoteSigs: List) { companion object { fun fromCommitSig( channelParams: ChannelParams, + commitParams: CommitParams, commitKeys: LocalCommitmentKeys, fundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, commit: CommitSig, localCommitIndex: Long, + commitmentFormat: Transactions.CommitmentFormat, spec: CommitmentSpec, log: MDCLogger ): Either { val (localCommitTx, sortedHtlcTxs) = Commitments.makeLocalTxs( channelParams = channelParams, + commitParams = commitParams, commitKeys = commitKeys, commitTxNumber = localCommitIndex, localFundingKey = fundingKey, remoteFundingPubKey = remoteFundingPubKey, commitmentInput = commitInput, + commitmentFormat = commitmentFormat, spec = spec, ) - val sig = Transactions.sign(localCommitTx, fundingKey) - // no need to compute htlc sigs if commit sig doesn't check out - val signedCommitTx = Transactions.addSigs(localCommitTx, fundingKey.publicKey(), remoteFundingPubKey, sig, commit.signature) - when (val check = Transactions.checkSpendable(signedCommitTx)) { - is Try.Failure -> { - log.error(check.error) { "remote signature $commit is invalid" } - return Either.Left(InvalidCommitmentSignature(channelParams.channelId, signedCommitTx.tx.txid)) - } - else -> {} + if (!localCommitTx.checkRemoteSig(fundingKey.publicKey(), remoteFundingPubKey, commit.signature)) { + log.error { "remote signature $commit is invalid" } + return Either.Left(InvalidCommitmentSignature(channelParams.channelId, localCommitTx.tx.txid)) } if (commit.htlcSignatures.size != sortedHtlcTxs.size) { return Either.Left(HtlcSigCountMismatch(channelParams.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) } - val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, commitKeys.ourHtlcKey, SigHash.SIGHASH_ALL) } - // combine the sigs to make signed txs - val htlcTxsAndSigs = Triple(sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped().map { (htlcTx, localSig, remoteSig) -> - when (htlcTx) { - is HtlcTx.HtlcTimeoutTx -> { - if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isFailure) { - return Either.Left(InvalidHtlcSignature(channelParams.channelId, htlcTx.tx.txid)) - } - HtlcTxAndSigs(htlcTx, localSig, remoteSig) - } - is HtlcTx.HtlcSuccessTx -> { - // we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig - // which was created with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - if (!Transactions.checkSig(htlcTx, remoteSig, commitKeys.theirHtlcPublicKey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) { - return Either.Left(InvalidHtlcSignature(channelParams.channelId, htlcTx.tx.txid)) - } - HtlcTxAndSigs(htlcTx, localSig, remoteSig) - } + sortedHtlcTxs.zip(commit.htlcSignatures).forEach { (htlcTx, remoteSig) -> + if (!htlcTx.checkRemoteSig(commitKeys, remoteSig)) { + return Either.Left(InvalidHtlcSignature(channelParams.channelId, htlcTx.tx.txid)) } } - return Either.Right(LocalCommit(localCommitIndex, spec, PublishableTxs(signedCommitTx, htlcTxsAndSigs))) + return Either.Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commit.signature, commit.htlcSignatures)) } } } /** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxId, val remotePerCommitmentPoint: PublicKey) { - fun sign(channelParams: ChannelParams, channelKeys: ChannelKeys, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, batchSize: Int): CommitSig { + fun sign( + channelParams: ChannelParams, + commitParams: CommitParams, + channelKeys: ChannelKeys, + fundingTxIndex: Long, + remoteFundingPubKey: PublicKey, + commitInput: Transactions.InputInfo, + commitmentFormat: Transactions.CommitmentFormat, + batchSize: Int + ): CommitSig { val fundingKey = channelKeys.fundingKey(fundingTxIndex) val commitKeys = channelKeys.remoteCommitmentKeys(channelParams, remotePerCommitmentPoint) val (remoteCommitTx, sortedHtlcsTxs) = Commitments.makeRemoteTxs( channelParams = channelParams, + commitParams = commitParams, commitKeys = commitKeys, commitTxNumber = index, localFundingKey = fundingKey, remoteFundingPubKey = remoteFundingPubKey, commitmentInput = commitInput, + commitmentFormat = commitmentFormat, spec = spec ) - val sig = Transactions.sign(remoteCommitTx, fundingKey) - // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - val htlcSigs = sortedHtlcsTxs.map { Transactions.sign(it, commitKeys.ourHtlcKey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } + val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey) + val htlcSigs = sortedHtlcsTxs.map { it.localSig(commitKeys) } val tlvs = buildSet { if (batchSize > 1) add(CommitSigTlv.Batch(batchSize)) } @@ -181,26 +177,36 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI } fun sign(channelParams: ChannelParams, channelKeys: ChannelKeys, signingSession: InteractiveTxSigningSession): CommitSig { - return sign(channelParams, channelKeys, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubkey, signingSession.commitInput, batchSize = 1) + return sign( + channelParams, + signingSession.remoteCommitParams, + channelKeys, + signingSession.fundingTxIndex, + signingSession.fundingParams.remoteFundingPubkey, + signingSession.commitInput(channelKeys), + signingSession.fundingParams.commitmentFormat, + batchSize = 1 + ) } } -/** We have the next remote commit when we've sent our commit_sig but haven't yet received their revoke_and_ack. */ -data class NextRemoteCommit(val sig: CommitSig, val commit: RemoteCommit) - sealed class LocalFundingStatus { + /** While the transaction is unconfirmed, we keep the funding transaction (if available) to allow rebroadcasting. */ abstract val signedTx: Transaction? abstract val txId: TxId + abstract val txOut: TxOut abstract val fee: Satoshi data class UnconfirmedFundingTx(val sharedTx: SignedSharedTransaction, val fundingParams: InteractiveTxParams, val createdAt: Long) : LocalFundingStatus() { override val signedTx: Transaction? = sharedTx.signedTx override val txId: TxId = sharedTx.localSigs.txId + override val txOut: TxOut = TxOut(fundingParams.fundingAmount, sharedTx.tx.sharedOutput.pubkeyScript) override val fee: Satoshi = sharedTx.tx.fees } - data class ConfirmedFundingTx(override val signedTx: Transaction, override val fee: Satoshi, val localSigs: TxSignatures, val shortChannelId: ShortChannelId) : LocalFundingStatus() { - override val txId: TxId = signedTx.txid + data class ConfirmedFundingTx(val spentInputs: List, override val txOut: TxOut, override val fee: Satoshi, val localSigs: TxSignatures, val shortChannelId: ShortChannelId) : LocalFundingStatus() { + override val txId: TxId = localSigs.txId + override val signedTx: Transaction? = null } } @@ -212,29 +218,59 @@ sealed class RemoteFundingStatus { /** A minimal commitment for a given funding tx. */ data class Commitment( val fundingTxIndex: Long, + val fundingInput: OutPoint, + val fundingAmount: Satoshi, val remoteFundingPubkey: PublicKey, val localFundingStatus: LocalFundingStatus, val remoteFundingStatus: RemoteFundingStatus, + val commitmentFormat: Transactions.CommitmentFormat, + val localCommitParams: CommitParams, val localCommit: LocalCommit, + val remoteCommitParams: CommitParams, val remoteCommit: RemoteCommit, - val nextRemoteCommit: NextRemoteCommit? + val nextRemoteCommit: RemoteCommit? ) { - val commitInput = localCommit.publishableTxs.commitTx.input - val fundingTxId: TxId = commitInput.outPoint.txid + val fundingTxId: TxId = fundingInput.txid val shortChannelId: ShortChannelId? = when (localFundingStatus) { is LocalFundingStatus.ConfirmedFundingTx -> localFundingStatus.shortChannelId else -> null } - val fundingAmount: Satoshi = commitInput.txOut.amount + + fun localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex) + + fun commitInput(fundingKey: PrivateKey): Transactions.InputInfo = Transactions.makeFundingInputInfo(fundingInput.txid, fundingInput.index, fundingAmount, fundingKey.publicKey(), remoteFundingPubkey, commitmentFormat) + + fun commitInput(channelKeys: ChannelKeys): Transactions.InputInfo = commitInput(localFundingKey(channelKeys)) + + /** Return a fully signed commit tx, that can be published as-is. */ + fun fullySignedCommitTx(params: ChannelParams, channelKeys: ChannelKeys): Transaction { + val fundingKey = localFundingKey(channelKeys) + val commitKeys = channelKeys.localCommitmentKeys(params, localCommit.index) + val (unsignedCommitTx, _) = Commitments.makeLocalTxs(params, localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubkey, commitInput(channelKeys), commitmentFormat, localCommit.spec) + return when (val remoteSig = localCommit.remoteSig) { + is ChannelSpendSignature.IndividualSignature -> { + val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubkey) + unsignedCommitTx.aggregateSigs(fundingKey.publicKey(), remoteFundingPubkey, localSig, remoteSig) + } + } + } + + /** Return the HTLC transactions for our local commit and the corresponding remote signatures. */ + fun unsignedHtlcTxs(params: ChannelParams, channelKeys: ChannelKeys): List> { + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val commitKeys = channelKeys.localCommitmentKeys(params, localCommit.index) + val (_, htlcTxs) = Commitments.makeLocalTxs(params, localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubkey, commitInput(channelKeys), commitmentFormat, localCommit.spec) + return htlcTxs.sortedBy { it.input.outPoint.index }.zip(localCommit.htlcRemoteSigs) + } fun localChannelReserve(params: ChannelParams): Satoshi = when { params.channelFeatures.hasFeature(Feature.ZeroReserveChannels) -> 0.sat - else -> (fundingAmount / 100).max(params.remoteParams.dustLimit) + else -> (fundingAmount / 100).max(remoteCommitParams.dustLimit) } fun remoteChannelReserve(params: ChannelParams): Satoshi = when { params.channelFeatures.hasFeature(Feature.ZeroReserveChannels) -> 0.sat - else -> (fundingAmount / 100).max(params.localParams.dustLimit) + else -> (fundingAmount / 100).max(localCommitParams.dustLimit) } // NB: when computing availableBalanceForSend and availableBalanceForReceive, the initiator keeps an extra buffer on top @@ -265,23 +301,23 @@ data class Commitment( fun availableBalanceForSend(params: ChannelParams, changes: CommitmentChanges): MilliSatoshi { // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation - val remoteCommit1 = nextRemoteCommit?.commit ?: remoteCommit + val remoteCommit1 = nextRemoteCommit ?: 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.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) + val commitFees = commitTxFeeMsat(remoteCommitParams.dustLimit, reduced, commitmentFormat) // the initiator needs to keep a "initiator fee buffer" (see explanation above) - val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer = commitTxFeeMsat(remoteCommitParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), commitmentFormat) + htlcOutputFee(reduced.feerate * 2, commitmentFormat) val amountToReserve = commitFees.coerceAtLeast(initiatorFeeBuffer) - if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(params.remoteParams.dustLimit, reduced).toMilliSatoshi()) { + if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteCommitParams.dustLimit, reduced, commitmentFormat).toMilliSatoshi()) { // htlc will be trimmed (balanceNoFees - amountToReserve).coerceAtLeast(0.msat) } else { // htlc will have an output in the commitment tx, so there will be additional fees. - val commitFees1 = commitFees + htlcOutputFee(reduced.feerate) + val commitFees1 = commitFees + htlcOutputFee(reduced.feerate, commitmentFormat) // we take the additional fees for that htlc output into account in the fee buffer at a x2 feerate increase - val initiatorFeeBuffer1 = initiatorFeeBuffer + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer1 = initiatorFeeBuffer + htlcOutputFee(reduced.feerate * 2, commitmentFormat) val amountToReserve1 = commitFees1.coerceAtLeast(initiatorFeeBuffer1) (balanceNoFees - amountToReserve1).coerceAtLeast(0.msat) } @@ -299,18 +335,18 @@ data class Commitment( balanceNoFees } else { // The initiator always pays the on-chain fees, so we must subtract that from the amount we can receive. - val commitFees = commitTxFeeMsat(params.localParams.dustLimit, reduced) + val commitFees = commitTxFeeMsat(localCommitParams.dustLimit, reduced, commitmentFormat) // we expected the initiator to keep a "initiator fee buffer" (see explanation above) - val initiatorFeeBuffer = commitTxFeeMsat(params.localParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer = commitTxFeeMsat(localCommitParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), commitmentFormat) + htlcOutputFee(reduced.feerate * 2, commitmentFormat) val amountToReserve = commitFees.coerceAtLeast(initiatorFeeBuffer) - if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(params.localParams.dustLimit, reduced).toMilliSatoshi()) { + if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localCommitParams.dustLimit, reduced, commitmentFormat).toMilliSatoshi()) { // htlc will be trimmed (balanceNoFees - amountToReserve).coerceAtLeast(0.msat) } else { // htlc will have an output in the commitment tx, so there will be additional fees. - val commitFees1 = commitFees + htlcOutputFee(reduced.feerate) + val commitFees1 = commitFees + htlcOutputFee(reduced.feerate, commitmentFormat) // we take the additional fees for that htlc output into account in the fee buffer at a x2 feerate increase - val initiatorFeeBuffer1 = initiatorFeeBuffer + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer1 = initiatorFeeBuffer + htlcOutputFee(reduced.feerate * 2, commitmentFormat) val amountToReserve1 = commitFees1.coerceAtLeast(initiatorFeeBuffer1) (balanceNoFees - amountToReserve1).coerceAtLeast(0.msat) } @@ -330,7 +366,7 @@ data class Commitment( val thisCommitAdds = localCommit.spec.htlcs.outgoings().filter(::expired).toSet() + remoteCommit.spec.htlcs.incomings().filter(::expired).toSet() return when (nextRemoteCommit) { null -> thisCommitAdds - else -> thisCommitAdds + nextRemoteCommit.commit.spec.htlcs.incomings().filter(::expired).toSet() + else -> thisCommitAdds + nextRemoteCommit.spec.htlcs.incomings().filter(::expired).toSet() } } @@ -346,14 +382,14 @@ data class Commitment( } fun getOutgoingHtlcCrossSigned(htlcId: Long): UpdateAddHtlc? { - val localSigned = (nextRemoteCommit?.commit ?: remoteCommit).spec.findIncomingHtlcById(htlcId) ?: return null + val localSigned = (nextRemoteCommit ?: remoteCommit).spec.findIncomingHtlcById(htlcId) ?: return null val remoteSigned = localCommit.spec.findOutgoingHtlcById(htlcId) ?: return null require(localSigned.add == remoteSigned.add) return localSigned.add } fun getIncomingHtlcCrossSigned(htlcId: Long): UpdateAddHtlc? { - val localSigned = (nextRemoteCommit?.commit ?: remoteCommit).spec.findOutgoingHtlcById(htlcId) ?: return null + val localSigned = (nextRemoteCommit ?: remoteCommit).spec.findOutgoingHtlcById(htlcId) ?: return null val remoteSigned = localCommit.spec.findIncomingHtlcById(htlcId) ?: return null require(localSigned.add == remoteSigned.add) return localSigned.add @@ -361,16 +397,16 @@ data class Commitment( fun canSendAdd(amount: MilliSatoshi, params: ChannelParams, changes: CommitmentChanges): Either { // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation - val remoteCommit1 = nextRemoteCommit?.commit ?: remoteCommit + val remoteCommit1 = nextRemoteCommit ?: remoteCommit val reduced = CommitmentSpec.reduce(remoteCommit1.spec, changes.remoteChanges.acked, changes.localChanges.proposed) // the HTLC we are about to create is outgoing, but from their point of view it is incoming val outgoingHtlcs = reduced.htlcs.incomings() // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment - val fees = commitTxFee(params.remoteParams.dustLimit, reduced) + val fees = commitTxFee(remoteCommitParams.dustLimit, reduced, commitmentFormat) // the initiator needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid // getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728). - val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer = commitTxFeeMsat(remoteCommitParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), commitmentFormat) + htlcOutputFee(reduced.feerate * 2, commitmentFormat) // 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.paysCommitTxFees) fees.toMilliSatoshi().coerceAtLeast(initiatorFeeBuffer) else 0.msat) @@ -396,18 +432,18 @@ data class Commitment( // README: we check against our peer's max_htlc_value_in_flight_msat parameter, as per the BOLTS, but also against our own setting val htlcValueInFlight = outgoingHtlcs.map { it.amountMsat }.sum() - val maxHtlcValueInFlightMsat = min(params.remoteParams.maxHtlcValueInFlightMsat, params.localParams.maxHtlcValueInFlightMsat) + val maxHtlcValueInFlightMsat = min(remoteCommitParams.maxHtlcValueInFlightMsat, localCommitParams.maxHtlcValueInFlightMsat) if (htlcValueInFlight.toLong() > maxHtlcValueInFlightMsat) { return Either.Left(HtlcValueTooHighInFlight(params.channelId, maximum = maxHtlcValueInFlightMsat.toULong(), actual = htlcValueInFlight)) } - if (outgoingHtlcs.size > params.remoteParams.maxAcceptedHtlcs) { - return Either.Left(TooManyAcceptedHtlcs(params.channelId, maximum = params.remoteParams.maxAcceptedHtlcs.toLong())) + if (outgoingHtlcs.size > remoteCommitParams.maxAcceptedHtlcs) { + return Either.Left(TooManyAcceptedHtlcs(params.channelId, maximum = remoteCommitParams.maxAcceptedHtlcs.toLong())) } // README: this is not part of the LN Bolts: we also check against our own limit, to avoid creating commit txs that have too many outputs - if (outgoingHtlcs.size > params.localParams.maxAcceptedHtlcs) { - return Either.Left(TooManyOfferedHtlcs(params.channelId, maximum = params.localParams.maxAcceptedHtlcs.toLong())) + if (outgoingHtlcs.size > localCommitParams.maxAcceptedHtlcs) { + return Either.Left(TooManyOfferedHtlcs(params.channelId, maximum = localCommitParams.maxAcceptedHtlcs.toLong())) } return Either.Right(Unit) @@ -419,7 +455,7 @@ data class Commitment( val incomingHtlcs = reduced.htlcs.incomings() // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment - val fees = commitTxFee(params.localParams.dustLimit, reduced) + val fees = commitTxFee(localCommitParams.dustLimit, reduced, commitmentFormat) // 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.paysCommitTxFees) 0.sat else fees).toMilliSatoshi() @@ -437,12 +473,12 @@ data class Commitment( } val htlcValueInFlight = incomingHtlcs.map { it.amountMsat }.sum() - if (params.localParams.maxHtlcValueInFlightMsat < htlcValueInFlight.toLong()) { - return Either.Left(HtlcValueTooHighInFlight(params.channelId, maximum = params.localParams.maxHtlcValueInFlightMsat.toULong(), actual = htlcValueInFlight)) + if (localCommitParams.maxHtlcValueInFlightMsat < htlcValueInFlight.toLong()) { + return Either.Left(HtlcValueTooHighInFlight(params.channelId, maximum = localCommitParams.maxHtlcValueInFlightMsat.toULong(), actual = htlcValueInFlight)) } - if (incomingHtlcs.size > params.localParams.maxAcceptedHtlcs) { - return Either.Left(TooManyAcceptedHtlcs(params.channelId, maximum = params.localParams.maxAcceptedHtlcs.toLong())) + if (incomingHtlcs.size > localCommitParams.maxAcceptedHtlcs) { + return Either.Left(TooManyAcceptedHtlcs(params.channelId, maximum = localCommitParams.maxAcceptedHtlcs.toLong())) } return Either.Right(Unit) @@ -452,7 +488,7 @@ data class Commitment( val reduced = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is initiator remote doesn't pay the fees - val fees = commitTxFee(params.remoteParams.dustLimit, reduced) + val fees = commitTxFee(remoteCommitParams.dustLimit, reduced, commitmentFormat) val missing = reduced.toRemote.truncateToSatoshi() - localChannelReserve(params) - fees return if (missing < 0.sat) { Either.Left(CannotAffordFees(params.channelId, -missing, localChannelReserve(params), fees)) @@ -469,7 +505,7 @@ data class Commitment( // It is easier to do it here because under certain (race) conditions spec allows a lower-than-normal fee to be paid, // and it would be tricky to check if the conditions are met at signing // (it also means that we need to check the fee of the initial commitment tx somewhere) - val fees = commitTxFee(params.localParams.dustLimit, reduced) + val fees = commitTxFee(localCommitParams.dustLimit, reduced, commitmentFormat) val missing = reduced.toRemote.truncateToSatoshi() - remoteChannelReserve(params) - fees return if (missing < 0.sat) { Either.Left(CannotAffordFees(params.channelId, -missing, remoteChannelReserve(params), fees)) @@ -479,22 +515,22 @@ data class Commitment( } fun sendCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, log: MDCLogger): Pair { - val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val fundingKey = localFundingKey(channelKeys) // remote commitment will include all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val (remoteCommitTx, sortedHtlcTxs) = Commitments.makeRemoteTxs( channelParams = params, + commitParams = remoteCommitParams, commitKeys = commitKeys, commitTxNumber = remoteCommit.index + 1, localFundingKey = fundingKey, remoteFundingPubKey = remoteFundingPubkey, - commitmentInput = commitInput, + commitmentInput = commitInput(fundingKey), + commitmentFormat = commitmentFormat, spec = spec ) - val sig = Transactions.sign(remoteCommitTx, fundingKey) - - // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, commitKeys.ourHtlcKey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } + val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubkey) + val htlcSigs = sortedHtlcTxs.map { it.localSig(commitKeys) } // NB: IN/OUT htlcs are inverted because this is the remote commit log.info { @@ -509,14 +545,16 @@ data class Commitment( val alternativeSpec = spec.copy(feerate = feerate) val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( channelParams = params, + commitParams = remoteCommitParams, commitKeys = commitKeys, commitTxNumber = remoteCommit.index + 1, localFundingKey = fundingKey, remoteFundingPubKey = remoteFundingPubkey, - commitmentInput = commitInput, + commitmentInput = commitInput(fundingKey), + commitmentFormat = commitmentFormat, spec = alternativeSpec ) - val alternativeSig = Transactions.sign(alternativeRemoteCommitTx, fundingKey) + val alternativeSig = alternativeRemoteCommitTx.sign(fundingKey, remoteFundingPubkey).sig CommitSigTlv.AlternativeFeerateSig(feerate, alternativeSig) } add(CommitSigTlv.AlternativeFeerateSigs(alternativeSigs)) @@ -526,7 +564,7 @@ data class Commitment( } } val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), TlvStream(tlvs)) - val commitment1 = copy(nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint))) + val commitment1 = copy(nextRemoteCommit = RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)) return Pair(commitment1, commitSig) } @@ -539,13 +577,13 @@ data class Commitment( // ourCommit.index + 2 -> which is about to become our next revocation hash // we will reply to this sig with our old revocation hash preimage (at index) and our next revocation hash (at index + 1) // and will increment our index - val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val fundingKey = localFundingKey(channelKeys) val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) - return LocalCommit.fromCommitSig(params, commitKeys, fundingKey, remoteFundingPubkey, commitInput, commit, localCommit.index + 1, spec, log).map { localCommit1 -> + return LocalCommit.fromCommitSig(params, localCommitParams, commitKeys, fundingKey, remoteFundingPubkey, commitInput(fundingKey), commit, localCommit.index + 1, commitmentFormat, spec, log).map { localCommit1 -> log.info { val htlcsIn = spec.htlcs.incomings().map { it.id }.joinToString(",") val htlcsOut = spec.htlcs.outgoings().map { it.id }.joinToString(",") - "built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txid=${localCommit1.publishableTxs.commitTx.tx.txid} fundingTxId=$fundingTxId" + "built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txid=${localCommit1.txId} fundingTxId=$fundingTxId" } copy(localCommit = localCommit1) } @@ -553,31 +591,49 @@ data class Commitment( } /** Subset of Commitments when we want to work with a single, specific commitment. */ -data class FullCommitment( - val params: ChannelParams, val changes: CommitmentChanges, - val fundingTxIndex: Long, - val remoteFundingPubkey: PublicKey, - val localFundingStatus: LocalFundingStatus, val remoteFundingStatus: RemoteFundingStatus, - val localCommit: LocalCommit, val remoteCommit: RemoteCommit, val nextRemoteCommit: NextRemoteCommit? -) { - val channelId = params.channelId - val commitInput = localCommit.publishableTxs.commitTx.input - val fundingTxId: TxId = commitInput.outPoint.txid - val fundingAmount = commitInput.txOut.amount - val localChannelReserve = when { - params.channelFeatures.hasFeature(Feature.ZeroReserveChannels) -> 0.sat - else -> (fundingAmount / 100).max(params.remoteParams.dustLimit) +data class FullCommitment(val channelParams: ChannelParams, val changes: CommitmentChanges, val commitment: Commitment) { + val channelId: ByteVector32 = channelParams.channelId + val remoteFundingPubkey: PublicKey = commitment.remoteFundingPubkey + val fundingTxId: TxId = commitment.fundingTxId + val fundingAmount: Satoshi = commitment.fundingAmount + val fundingInput: OutPoint = commitment.fundingInput + val fundingTxIndex: Long = commitment.fundingTxIndex + val localFundingStatus: LocalFundingStatus = commitment.localFundingStatus + val remoteFundingStatus: RemoteFundingStatus = commitment.remoteFundingStatus + val commitmentFormat: Transactions.CommitmentFormat = commitment.commitmentFormat + val localChannelParams: LocalChannelParams = channelParams.localParams + val localCommitParams: CommitParams = commitment.localCommitParams + val localCommit: LocalCommit = commitment.localCommit + val remoteChannelParams: RemoteChannelParams = channelParams.remoteParams + val remoteCommitParams: CommitParams = commitment.remoteCommitParams + val remoteCommit: RemoteCommit = commitment.remoteCommit + val nextRemoteCommit: RemoteCommit? = commitment.nextRemoteCommit + val localChannelReserve: Satoshi = when { + channelParams.channelFeatures.hasFeature(Feature.ZeroReserveChannels) -> 0.sat + else -> (fundingAmount / 100).max(remoteCommitParams.dustLimit) } - val remoteChannelReserve = when { - params.channelFeatures.hasFeature(Feature.ZeroReserveChannels) -> 0.sat - else -> (fundingAmount / 100).max(params.localParams.dustLimit) + val remoteChannelReserve: Satoshi = when { + channelParams.channelFeatures.hasFeature(Feature.ZeroReserveChannels) -> 0.sat + else -> (fundingAmount / 100).max(localCommitParams.dustLimit) } + + fun localKeys(channelKeys: ChannelKeys): LocalCommitmentKeys = channelKeys.localCommitmentKeys(channelParams, localCommit.index) + + fun remoteKeys(channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = channelKeys.remoteCommitmentKeys(channelParams, remotePerCommitmentPoint) + + fun commitInput(channelKeys: ChannelKeys): Transactions.InputInfo = commitment.commitInput(channelKeys) + + /** Return a fully signed commit tx, that can be published as-is. */ + fun fullySignedCommitTx(channelKeys: ChannelKeys): Transaction = commitment.fullySignedCommitTx(channelParams, channelKeys) + + /** Return the HTLC transactions for our local commit and the corresponding remote signatures. */ + fun unsignedHtlcTxs(channelKeys: ChannelKeys): List> = commitment.unsignedHtlcTxs(channelParams, channelKeys) } data class WaitingForRevocation(val sentAfterLocalCommitIndex: Long) data class Commitments( - val params: ChannelParams, + val channelParams: ChannelParams, val changes: CommitmentChanges, val active: List, val inactive: List, @@ -589,20 +645,20 @@ data class Commitments( require(active.isNotEmpty()) { "there must be at least one active commitment" } } - val channelId: ByteVector32 = params.channelId - val localNodeId: PublicKey = params.localParams.nodeId - val remoteNodeId: PublicKey = params.remoteParams.nodeId + val channelId: ByteVector32 = channelParams.channelId + val localNodeId: PublicKey = channelParams.localParams.nodeId + val remoteNodeId: PublicKey = channelParams.remoteParams.nodeId // Commitment numbers are the same for all active commitments. val localCommitIndex = active.first().localCommit.index val remoteCommitIndex = active.first().remoteCommit.index val nextRemoteCommitIndex = remoteCommitIndex + 1 - fun availableBalanceForSend(): MilliSatoshi = active.minOf { it.availableBalanceForSend(params, changes) } - fun availableBalanceForReceive(): MilliSatoshi = active.minOf { it.availableBalanceForReceive(params, changes) } + fun availableBalanceForSend(): MilliSatoshi = active.minOf { it.availableBalanceForSend(channelParams, changes) } + fun availableBalanceForReceive(): MilliSatoshi = active.minOf { it.availableBalanceForReceive(channelParams, changes) } // We always use the last commitment that was created, to make sure we never go back in time. - val latest = active.first().let { c -> FullCommitment(params, changes, c.fundingTxIndex, c.remoteFundingPubkey, c.localFundingStatus, c.remoteFundingStatus, c.localCommit, c.remoteCommit, c.nextRemoteCommit) } + val latest = FullCommitment(channelParams, changes, active.first()) val all = buildList { addAll(active) @@ -614,6 +670,8 @@ data class Commitments( addAll(active) }) + fun channelKeys(keyManager: KeyManager): ChannelKeys = channelParams.localParams.channelKeys(keyManager) + fun isMoreRecent(other: Commitments): Boolean { return this.localCommitIndex > other.localCommitIndex || this.remoteCommitIndex > other.remoteCommitIndex || @@ -656,7 +714,7 @@ data class Commitments( } // even if remote advertises support for 0 msat htlc, we limit ourselves to values strictly positive, hence the max(1 msat) - val htlcMinimum = params.remoteParams.htlcMinimum.coerceAtLeast(1.msat) + val htlcMinimum = active.minOf { it.remoteCommitParams.htlcMinimum }.coerceAtLeast(1.msat) if (cmd.amount < htlcMinimum) { return Either.Left(HtlcValueTooSmall(channelId, minimum = htlcMinimum, actual = cmd.amount)) } @@ -666,7 +724,7 @@ data class Commitments( // we increment the local htlc index and add an entry to the origins map val changes1 = changes.addLocalProposal(add).copy(localNextHtlcId = changes.localNextHtlcId + 1) val payments1 = payments + mapOf(add.id to paymentId) - val failure = active.map { it.canSendAdd(cmd.amount, params, changes1).left }.firstOrNull() + val failure = active.map { it.canSendAdd(cmd.amount, channelParams, changes1).left }.firstOrNull() return failure?.let { Either.Left(it) } ?: Either.Right(Pair(copy(changes = changes1, payments = payments1), add)) } @@ -676,13 +734,13 @@ data class Commitments( } // we used to not enforce a strictly positive minimum, hence the max(1 msat) - val htlcMinimum = params.localParams.htlcMinimum.coerceAtLeast(1.msat) + val htlcMinimum = active.minOf { it.localCommitParams.htlcMinimum }.coerceAtLeast(1.msat) if (add.amountMsat < htlcMinimum) { return Either.Left(HtlcValueTooSmall(channelId, minimum = htlcMinimum, actual = add.amountMsat)) } val changes1 = changes.addRemoteProposal(add).copy(remoteNextHtlcId = changes.remoteNextHtlcId + 1) - val failure = active.map { it.canReceiveAdd(add.amountMsat, params, changes1).left }.firstOrNull() + val failure = active.map { it.canReceiveAdd(add.amountMsat, channelParams, changes1).left }.firstOrNull() return failure?.let { Either.Left(it) } ?: Either.Right(copy(changes = changes1)) } @@ -754,29 +812,29 @@ data class Commitments( } fun sendFee(cmd: ChannelCommand.Commitment.UpdateFee): Either> { - if (!params.localParams.paysCommitTxFees) return Either.Left(NonInitiatorCannotSendUpdateFee(channelId)) + if (!channelParams.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 val changes1 = changes.copy(localChanges = changes.localChanges.copy(proposed = changes.localChanges.proposed.filterNot { it is UpdateFee } + fee)) - val failure = active.map { it.canSendFee(params, changes1).left }.firstOrNull() + val failure = active.map { it.canSendFee(channelParams, changes1).left }.firstOrNull() return failure?.let { Either.Left(it) } ?: Either.Right(Pair(copy(changes = changes1), fee)) } fun receiveFee(fee: UpdateFee, feerateTolerance: FeerateTolerance): Either { - if (params.localParams.paysCommitTxFees) return Either.Left(NonInitiatorCannotSendUpdateFee(channelId)) + if (channelParams.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)) - val failure = active.map { it.canReceiveFee(params, changes1).left }.firstOrNull() + val failure = active.map { it.canReceiveFee(channelParams, changes1).left }.firstOrNull() return failure?.let { Either.Left(it) } ?: Either.Right(copy(changes = changes1)) } fun sendCommit(channelKeys: ChannelKeys, log: MDCLogger): Either> { val remoteNextPerCommitmentPoint = remoteNextCommitInfo.right ?: return Either.Left(CannotSignBeforeRevocation(channelId)) - val commitKeys = channelKeys.remoteCommitmentKeys(params, remoteNextPerCommitmentPoint) + val commitKeys = channelKeys.remoteCommitmentKeys(channelParams, remoteNextPerCommitmentPoint) if (!changes.localHasChanges()) return Either.Left(CannotSignWithoutChanges(channelId)) - val (active1, sigs) = active.map { it.sendCommit(params, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, log) }.unzip() + val (active1, sigs) = active.map { it.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, log) }.unzip() val commitments1 = copy( active = active1, remoteNextCommitInfo = Either.Left(WaitingForRevocation(localCommitIndex)), @@ -799,10 +857,10 @@ data class Commitments( if (sigs.size < active.size) { return Either.Left(CommitSigCountMismatch(channelId, active.size, sigs.size)) } - val commitKeys = channelKeys.localCommitmentKeys(params, localCommitIndex + 1) + val commitKeys = channelKeys.localCommitmentKeys(channelParams, localCommitIndex + 1) // Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments. val active1 = active.zip(sigs).map { - when (val commitment1 = it.first.receiveCommit(params, channelKeys, commitKeys, changes, it.second, log)) { + when (val commitment1 = it.first.receiveCommit(channelParams, channelKeys, commitKeys, changes, it.second, log)) { is Either.Left -> return Either.Left(commitment1.value) is Either.Right -> commitment1.value } @@ -861,7 +919,7 @@ data class Commitments( else -> Unit } } - val active1 = active.map { it.copy(remoteCommit = it.nextRemoteCommit!!.commit, nextRemoteCommit = null) } + val active1 = active.map { it.copy(remoteCommit = it.nextRemoteCommit!!, nextRemoteCommit = null) } val commitments1 = this.copy( active = active1, changes = changes.copy( @@ -910,11 +968,18 @@ data class Commitments( fun ChannelContext.updateLocalFundingConfirmed(fundingTx: Transaction, blockHeight: Int, txIndex: Int): Either> = updateFundingStatus(fundingTx.txid) { c: Commitment, _: Long -> if (c.fundingTxId == fundingTx.txid) { - val shortChannelId = ShortChannelId(blockHeight, txIndex, c.commitInput.outPoint.index.toInt()) + val shortChannelId = ShortChannelId(blockHeight, txIndex, c.fundingInput.index.toInt()) when (c.localFundingStatus) { is LocalFundingStatus.UnconfirmedFundingTx -> { logger.debug { "setting localFundingStatus confirmed for fundingTxId=${fundingTx.txid}" } - c.copy(localFundingStatus = LocalFundingStatus.ConfirmedFundingTx(fundingTx, c.localFundingStatus.sharedTx.tx.fees, c.localFundingStatus.sharedTx.localSigs, shortChannelId)) + val localFundingStatus1 = LocalFundingStatus.ConfirmedFundingTx( + fundingTx.txIn.map { it.outPoint }, + fundingTx.txOut[c.fundingInput.index.toInt()], + c.localFundingStatus.sharedTx.tx.fees, + c.localFundingStatus.sharedTx.localSigs, + shortChannelId + ) + c.copy(localFundingStatus = localFundingStatus1) } is LocalFundingStatus.ConfirmedFundingTx -> when (c.localFundingStatus.shortChannelId) { ShortChannelId(0) -> { @@ -1009,17 +1074,11 @@ data class Commitments( * @param spendingTx A transaction that may spend a current or former funding tx */ fun resolveCommitment(spendingTx: Transaction): Commitment? { - return all.find { commitment -> spendingTx.txIn.map { it.outPoint }.contains(commitment.commitInput.outPoint) } + return all.find { commitment -> spendingTx.txIn.map { it.outPoint }.contains(commitment.fundingInput) } } companion object { - val ANCHOR_AMOUNT = 330.sat - const val COMMIT_WEIGHT = 1124 - const val HTLC_OUTPUT_WEIGHT = 172 - const val HTLC_TIMEOUT_WEIGHT = 666 - const val HTLC_SUCCESS_WEIGHT = 706 - /** * Alternative feerates at which we will sign commitment transactions that have no pending HTLCs. * WARNING: never remove a feerate from this list, we can only add more, otherwise we will not be able to detect when our peer broadcasts the commit tx at the removed feerate. @@ -1032,16 +1091,27 @@ data class Commitments( */ fun alternativeFeerateCommits(commitments: Commitments, channelKeys: ChannelKeys): List { val localFundingKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex) + val commitParams = commitments.latest.remoteCommitParams return buildList { add(commitments.latest.remoteCommit) - commitments.latest.nextRemoteCommit?.let { add(it.commit) } + commitments.latest.nextRemoteCommit?.let { add(it) } }.filter { remoteCommit -> remoteCommit.spec.htlcs.isEmpty() }.flatMap { remoteCommit -> alternativeFeerates.map { feerate -> val alternativeSpec = remoteCommit.spec.copy(feerate = feerate) - val commitKeys = channelKeys.remoteCommitmentKeys(commitments.params, remoteCommit.remotePerCommitmentPoint) - val (alternativeRemoteCommitTx, _) = makeRemoteTxs(commitments.params, commitKeys, remoteCommit.index, localFundingKey, commitments.latest.remoteFundingPubkey, commitments.latest.commitInput, alternativeSpec) + val commitKeys = channelKeys.remoteCommitmentKeys(commitments.channelParams, remoteCommit.remotePerCommitmentPoint) + val (alternativeRemoteCommitTx, _) = makeRemoteTxs( + commitments.channelParams, + commitParams, + commitKeys, + remoteCommit.index, + localFundingKey, + commitments.latest.remoteFundingPubkey, + commitments.latest.commitInput(channelKeys), + commitments.latest.commitmentFormat, + alternativeSpec + ) RemoteCommit(remoteCommit.index, alternativeSpec, alternativeRemoteCommitTx.tx.txid, remoteCommit.remotePerCommitmentPoint) } } @@ -1049,11 +1119,13 @@ data class Commitments( fun makeLocalTxs( channelParams: ChannelParams, + commitParams: CommitParams, commitKeys: LocalCommitmentKeys, commitTxNumber: Long, localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitmentInput: Transactions.InputInfo, + commitmentFormat: Transactions.CommitmentFormat, spec: CommitmentSpec ): Pair> { val outputs = makeCommitTxOutputs( @@ -1061,22 +1133,25 @@ data class Commitments( remoteFundingPubkey = remoteFundingPubKey, commitKeys = commitKeys.publicKeys, payCommitTxFees = channelParams.localParams.paysCommitTxFees, - dustLimit = channelParams.localParams.dustLimit, - toSelfDelay = channelParams.remoteParams.toSelfDelay, + dustLimit = commitParams.dustLimit, + toSelfDelay = commitParams.toSelfDelay, + commitmentFormat = commitmentFormat, spec = spec ) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, commitKeys.ourPaymentBasePoint, channelParams.remoteParams.paymentBasepoint, channelParams.localParams.isChannelOpener, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, commitKeys.publicKeys, channelParams.localParams.dustLimit, channelParams.remoteParams.toSelfDelay, spec.feerate, outputs) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, outputs, commitmentFormat) return Pair(commitTx, htlcTxs) } fun makeRemoteTxs( channelParams: ChannelParams, + commitParams: CommitParams, commitKeys: RemoteCommitmentKeys, commitTxNumber: Long, localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitmentInput: Transactions.InputInfo, + commitmentFormat: Transactions.CommitmentFormat, spec: CommitmentSpec ): Pair> { val outputs = makeCommitTxOutputs( @@ -1084,13 +1159,14 @@ data class Commitments( remoteFundingPubkey = localFundingKey.publicKey(), commitKeys = commitKeys.publicKeys, payCommitTxFees = !channelParams.localParams.paysCommitTxFees, - dustLimit = channelParams.remoteParams.dustLimit, - toSelfDelay = channelParams.localParams.toSelfDelay, + dustLimit = commitParams.dustLimit, + toSelfDelay = commitParams.toSelfDelay, + commitmentFormat = commitmentFormat, spec = spec ) // NB: we are creating the remote commit tx, so local/remote parameters are inverted. val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, channelParams.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, !channelParams.localParams.isChannelOpener, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, commitKeys.publicKeys, channelParams.remoteParams.dustLimit, channelParams.localParams.toSelfDelay, spec.feerate, outputs) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, outputs, commitmentFormat) return Pair(commitTx, htlcTxs) } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt index 64732207c..9c5208603 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt @@ -1,12 +1,10 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* -import fr.acinq.bitcoin.Crypto.ripemd160 -import fr.acinq.bitcoin.Script.pay2wsh -import fr.acinq.bitcoin.Script.write import fr.acinq.bitcoin.utils.Either import fr.acinq.bitcoin.utils.Try import fr.acinq.bitcoin.utils.runTrying +import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.WatchConfirmed @@ -22,17 +20,10 @@ import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.logging.LoggingContext import fr.acinq.lightning.transactions.* -import fr.acinq.lightning.transactions.Scripts.multiSig2of2 -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx import fr.acinq.lightning.transactions.Transactions.commitTxFee import fr.acinq.lightning.transactions.Transactions.makeCommitTxOutputs import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.sum -import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* object Helpers { @@ -136,20 +127,18 @@ object Helpers { * this tells if we can use the channel to make a payment. */ fun aboveReserve(commitments: Commitments): Boolean = commitments.active.all { - val remoteCommit = it.nextRemoteCommit?.commit ?: it.remoteCommit + val remoteCommit = it.nextRemoteCommit ?: it.remoteCommit val toRemote = remoteCommit.spec.toRemote.truncateToSatoshi() // NB: this is an approximation (we don't take network fees into account) - toRemote > it.localChannelReserve(commitments.params) + toRemote > it.localChannelReserve(commitments.channelParams) } /** This helper method will publish txs only if they haven't yet reached minDepth. */ fun LoggingContext.publishIfNeeded(txs: List, irrevocablySpent: Map): List { val (skip, process) = txs.partition { it.tx.inputsAlreadySpent(irrevocablySpent) } skip.forEach { tx -> logger.info { "no need to republish txid=${tx.tx.txid}, it has already been confirmed" } } - return process.map { publish -> - logger.info(mapOf("txType" to publish.txType)) { "publishing txid=${publish.tx.txid}" } - publish - } + process.forEach { tx -> logger.info(mapOf("txType" to tx.txType)) { "publishing txid=${tx.tx.txid}" } } + return process } /** This helper method will watch txs only if they haven't yet reached minDepth. */ @@ -171,6 +160,18 @@ object Helpers { } } + /** This helper method will watch the given output only if it hasn't already been irrevocably spent. */ + fun LoggingContext.watchSpentIfNeeded(channelId: ByteVector32, input: Transactions.InputInfo, irrevocablySpent: Map): List { + val watch = WatchSpent(channelId, input.outPoint.txid, input.outPoint.index.toInt(), input.txOut.publicKeyScript, WatchSpent.ClosingOutputSpent(input.txOut.amount)) + return when (val spendingTx = irrevocablySpent[input.outPoint]) { + null -> listOf(ChannelAction.Blockchain.SendWatch(watch)) + else -> { + logger.info { "no need to watch output=${input.outPoint.txid}:${input.outPoint.index}, it has already been spent by txid=${spendingTx.txid}" } + listOf() + } + } + } + object Funding { /** Compute the channelId of a dual-funded channel. */ @@ -182,33 +183,13 @@ object Helpers { } } - fun makeFundingPubKeyScript(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey): ByteVector { - return write(pay2wsh(multiSig2of2(localFundingPubkey, remoteFundingPubkey))).toByteVector() - } - - fun makeFundingInputInfo( - fundingTxId: TxId, - fundingTxOutputIndex: Int, - fundingAmount: Satoshi, - fundingPubkey1: PublicKey, - fundingPubkey2: PublicKey - ): Transactions.InputInfo { - val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) - val fundingTxOut = TxOut(fundingAmount, pay2wsh(fundingScript)) - return Transactions.InputInfo( - OutPoint(fundingTxId, fundingTxOutputIndex.toLong()), - fundingTxOut, - ByteVector(write(fundingScript)) - ) - } - data class PairOfCommitTxs( val localSpec: CommitmentSpec, - val localCommitTx: Transactions.TransactionWithInputInfo.CommitTx, - val localHtlcTxs: List, + val localCommitTx: Transactions.CommitTx, + val localHtlcTxs: List, val remoteSpec: CommitmentSpec, - val remoteCommitTx: Transactions.TransactionWithInputInfo.CommitTx, - val remoteHtlcTxs: List + val remoteCommitTx: Transactions.CommitTx, + val remoteHtlcTxs: List ) /** @@ -218,6 +199,8 @@ object Helpers { */ fun makeCommitTxs( channelParams: ChannelParams, + localCommitParams: CommitParams, + remoteCommitParams: CommitParams, fundingAmount: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi, @@ -225,8 +208,9 @@ object Helpers { localCommitmentIndex: Long, remoteCommitmentIndex: Long, commitTxFeerate: FeeratePerKw, + commitmentFormat: Transactions.CommitmentFormat, fundingTxId: TxId, - fundingTxOutputIndex: Int, + fundingTxOutputIndex: Long, localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, localCommitKeys: LocalCommitmentKeys, @@ -240,30 +224,34 @@ object Helpers { // 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. - val fees = commitTxFee(channelParams.remoteParams.dustLimit, remoteSpec) + val fees = commitTxFee(remoteCommitParams.dustLimit, remoteSpec, commitmentFormat) val missing = fees - remoteSpec.toLocal.truncateToSatoshi() if (missing > 0.sat) { return Either.Left(CannotAffordFirstCommitFees(channelParams.channelId, missing = missing, fees = fees)) } } - val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, localFundingKey.publicKey(), remoteFundingPubkey) + val commitmentInput = Transactions.makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, localFundingKey.publicKey(), remoteFundingPubkey, commitmentFormat) val (localCommitTx, localHtlcTxs) = Commitments.makeLocalTxs( channelParams = channelParams, + commitParams = localCommitParams, commitKeys = localCommitKeys, commitTxNumber = localCommitmentIndex, localFundingKey = localFundingKey, remoteFundingPubKey = remoteFundingPubkey, commitmentInput = commitmentInput, + commitmentFormat = commitmentFormat, spec = localSpec ) val (remoteCommitTx, remoteHtlcTxs) = Commitments.makeRemoteTxs( channelParams = channelParams, + commitParams = remoteCommitParams, commitKeys = remoteCommitKeys, commitTxNumber = remoteCommitmentIndex, localFundingKey = localFundingKey, remoteFundingPubKey = remoteFundingPubkey, commitmentInput = commitmentInput, + commitmentFormat = commitmentFormat, spec = remoteSpec ) return Either.Right(PairOfCommitTxs(localSpec, localCommitTx, localHtlcTxs, remoteSpec, remoteCommitTx, remoteHtlcTxs)) @@ -303,28 +291,31 @@ object Helpers { feerate: FeeratePerKw, lockTime: Long, ): Either> { + val commitInput = commitment.commitInput(channelKeys) // We must convert the feerate to a fee: we must build dummy transactions to compute their weight. val closingFee = run { - val dummyClosingTxs = Transactions.makeClosingTxs(commitment.commitInput, commitment.localCommit.spec, Transactions.ClosingTxFee.PaidByUs(0.sat), lockTime, localScriptPubkey, remoteScriptPubkey) + val dummyClosingTxs = Transactions.makeClosingTxs(commitInput, commitment.localCommit.spec, Transactions.ClosingTxFee.PaidByUs(0.sat), lockTime, localScriptPubkey, remoteScriptPubkey) when (val dummyTx = dummyClosingTxs.preferred) { null -> return Either.Left(CannotGenerateClosingTx(commitment.channelId)) else -> { - val dummySignedTx = Transactions.addSigs(dummyTx, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig) - Transactions.ClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.tx.weight())) + val dummyPubkey = commitment.remoteFundingPubkey + val dummySig = ChannelSpendSignature.IndividualSignature(Transactions.PlaceHolderSig) + val dummySignedTx = dummyTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig) + Transactions.ClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight())) } } } // Now that we know the fee we're ready to pay, we can create our closing transactions. - val closingTxs = Transactions.makeClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, lockTime, localScriptPubkey, remoteScriptPubkey) + val closingTxs = Transactions.makeClosingTxs(commitInput, commitment.localCommit.spec, closingFee, lockTime, localScriptPubkey, remoteScriptPubkey) if (closingTxs.preferred == null || closingTxs.preferred.fee <= 0.sat) { return Either.Left(CannotGenerateClosingTx(commitment.channelId)) } val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val tlvs = TlvStream( setOfNotNull( - closingTxs.localAndRemote?.let { tx -> ClosingCompleteTlv.CloserAndCloseeOutputs(Transactions.sign(tx, localFundingKey)) }, - closingTxs.localOnly?.let { tx -> ClosingCompleteTlv.CloserOutputOnly(Transactions.sign(tx, localFundingKey)) }, - closingTxs.remoteOnly?.let { tx -> ClosingCompleteTlv.CloseeOutputOnly(Transactions.sign(tx, localFundingKey)) }, + closingTxs.localAndRemote?.let { tx -> ClosingCompleteTlv.CloserAndCloseeOutputs(tx.sign(localFundingKey, commitment.remoteFundingPubkey).sig) }, + closingTxs.localOnly?.let { tx -> ClosingCompleteTlv.CloserOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubkey).sig) }, + closingTxs.remoteOnly?.let { tx -> ClosingCompleteTlv.CloseeOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubkey).sig) }, ) ) val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, lockTime, tlvs) @@ -343,9 +334,9 @@ object Helpers { localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete - ): Either> { + ): Either> { val closingFee = Transactions.ClosingTxFee.PaidByThem(closingComplete.fees) - val closingTxs = Transactions.makeClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) + val closingTxs = Transactions.makeClosingTxs(commitment.commitInput(channelKeys), commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) // If our output isn't dust, they must provide a signature for a transaction that includes it. // Note that we're the closee, so we look for signatures including the closee output. if (closingTxs.localAndRemote != null && closingTxs.localOnly != null && closingComplete.closerAndCloseeOutputsSig == null && closingComplete.closeeOutputOnlySig == null) { @@ -358,7 +349,7 @@ object Helpers { return Either.Left(MissingCloseSignature(commitment.channelId)) } // We choose the closing signature that matches our preferred closing transaction. - val closingTxsWithSigs = listOfNotNull ClosingSigTlv>>( + val closingTxsWithSigs = listOfNotNull ClosingSigTlv>>( closingComplete.closerAndCloseeOutputsSig?.let { remoteSig -> closingTxs.localAndRemote?.let { tx -> Triple(tx, remoteSig) { localSig: ByteVector64 -> ClosingSigTlv.CloserAndCloseeOutputs(localSig) } } }, closingComplete.closeeOutputOnlySig?.let { remoteSig -> closingTxs.localOnly?.let { tx -> Triple(tx, remoteSig) { localSig -> ClosingSigTlv.CloseeOutputOnly(localSig) } } }, closingComplete.closerOutputOnlySig?.let { remoteSig -> closingTxs.remoteOnly?.let { tx -> Triple(tx, remoteSig) { localSig -> ClosingSigTlv.CloserOutputOnly(localSig) } } }, @@ -368,11 +359,13 @@ object Helpers { else -> { val (closingTx, remoteSig, sigToTlv) = preferred val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - val localSig = Transactions.sign(closingTx, localFundingKey) - val signedClosingTx = Transactions.addSigs(closingTx, localFundingKey.publicKey(), commitment.remoteFundingPubkey, localSig, remoteSig) - when (Transactions.checkSpendable(signedClosingTx)) { - is Try.Failure -> Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) - is Try.Success -> Either.Right(Pair(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig))))) + val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubkey) + val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey(), commitment.remoteFundingPubkey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig)) + val signedClosingTx = closingTx.copy(tx = signedTx) + if (!signedClosingTx.validate(extraUtxos = mapOf())) { + Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + } else { + Either.Right(Pair(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig.sig))))) } } } @@ -390,7 +383,7 @@ object Helpers { commitment: FullCommitment, closingTxs: Transactions.ClosingTxs, closingSig: ClosingSig - ): Either { + ): Either { val closingTxsWithSig = listOfNotNull( closingSig.closerAndCloseeOutputsSig?.let { sig -> closingTxs.localAndRemote?.let { tx -> Pair(tx, sig) } }, closingSig.closerOutputOnlySig?.let { sig -> closingTxs.localOnly?.let { tx -> Pair(tx, sig) } }, @@ -401,406 +394,634 @@ object Helpers { else -> { val (closingTx, remoteSig) = preferred val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - val localSig = Transactions.sign(closingTx, localFundingKey) - val signedClosingTx = Transactions.addSigs(closingTx, localFundingKey.publicKey(), commitment.remoteFundingPubkey, localSig, remoteSig) - when (Transactions.checkSpendable(signedClosingTx)) { - is Try.Failure -> Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) - is Try.Success -> Either.Right(signedClosingTx) + val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubkey) + val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey(), commitment.remoteFundingPubkey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig)) + val signedClosingTx = closingTx.copy(tx = signedTx) + if (!signedClosingTx.validate(extraUtxos = mapOf())) { + Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + } else { + Either.Right(signedClosingTx) } } } } - /** - * Claim all the outputs that we can from our current commit tx. - * - * @param commitment our commitment data, which includes payment preimages. - * @return a list of transactions (one per output that we can claim). - */ - fun LoggingContext.claimCurrentLocalCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, commitTx: Transaction, feerates: OnChainFeerates): LocalCommitPublished { - require(commitment.localCommit.publishableTxs.commitTx.tx.txid == commitTx.txid) { "txid mismatch, provided tx is not the current local commit tx" } - val commitKeys = channelKeys.localCommitmentKeys(commitment.params, commitment.localCommit.index) - val feerateDelayed = feerates.claimMainFeerate - - // first we will claim our main output as soon as the delay is over - val mainDelayedTx = generateTx("main-delayed-output") { - Transactions.makeClaimLocalDelayedOutputTx( - commitTx, - commitment.params.localParams.dustLimit, - commitKeys.revocationPublicKey, - commitment.params.remoteParams.toSelfDelay, - commitKeys.ourDelayedPaymentKey.publicKey(), - commitment.params.localParams.defaultFinalScriptPubKey.toByteArray(), - feerateDelayed + object LocalClose { + + /** Claim all the outputs that belong to us in our local commitment transaction. */ + fun LoggingContext.claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, commitTx: Transaction, feerates: OnChainFeerates): Pair { + require(commitment.localCommit.txId == commitTx.txid) { "txid mismatch, provided tx is not the current local commit tx" } + val commitKeys = channelKeys.localCommitmentKeys(commitment.channelParams, commitment.localCommit.index) + val feerateDelayed = feerates.claimMainFeerate + // first we will claim our main output as soon as the delay is over + val mainDelayedTx = generateTx("main-delayed-output") { + Transactions.ClaimLocalDelayedOutputTx.createUnsignedTx( + commitKeys, + commitTx, + commitment.localCommitParams.dustLimit, + commitment.localCommitParams.toSelfDelay, + commitment.channelParams.localParams.defaultFinalScriptPubKey, + feerateDelayed, + commitment.commitmentFormat + ).map { it.sign() } + } + val unsignedHtlcTxs = commitment.unsignedHtlcTxs(channelKeys) + val (incomingHtlcs, htlcSuccessTxs) = claimIncomingHtlcOutputs(commitKeys, commitment.changes, unsignedHtlcTxs) + val (outgoingHtlcs, htlcTimeoutTxs) = claimOutgoingHtlcOutputs(commitKeys, unsignedHtlcTxs) + val lcp = LocalCommitPublished( + commitTx = commitTx, + localOutput = mainDelayedTx?.input?.outPoint, + anchorOutput = null, + incomingHtlcs = incomingHtlcs, + outgoingHtlcs = outgoingHtlcs, + htlcDelayedOutputs = setOf(), // we will add these once the htlc txs are confirmed + irrevocablySpent = mapOf() + ) + val txs = LocalCommitSecondStageTransactions(mainDelayedTx, htlcSuccessTxs + htlcTimeoutTxs) + return Pair(lcp, txs) + } + + /** Create outputs of the local commitment transaction, allowing us for example to identify HTLC outputs. */ + fun makeLocalCommitTxOutputs(channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, commitment: FullCommitment): List { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + return makeCommitTxOutputs( + fundingKey.publicKey(), + commitment.remoteFundingPubkey, + commitKeys.publicKeys, + commitment.localChannelParams.paysCommitTxFees, + commitment.localCommitParams.dustLimit, + commitment.localCommitParams.toSelfDelay, + commitment.commitmentFormat, + commitment.localCommit.spec ) - }?.let { - val sig = Transactions.sign(it, commitKeys.ourDelayedPaymentKey, SigHash.SIGHASH_ALL) - Transactions.addSigs(it, sig) - } - - // We collect all the preimages we wanted to reveal to our peer. - val hash2Preimage = commitment.changes.localChanges.all.filterIsInstance().map { it.paymentPreimage }.associateBy { r -> r.sha256() } - // We collect incoming HTLCs that we started failing but didn't cross-sign. - val failedIncomingHtlcs = commitment.changes.localChanges.all.mapNotNull { - when (it) { - is UpdateFailHtlc -> it.id - is UpdateFailMalformedHtlc -> it.id - else -> null + } + + /** + * Claim the outputs of a local commit tx corresponding to incoming HTLCs. If we don't have the preimage for an + * incoming HTLC, we still include an entry in the map because we may receive that preimage later. + */ + private fun LoggingContext.claimIncomingHtlcOutputs( + commitKeys: LocalCommitmentKeys, + changes: CommitmentChanges, + unsignedHtlcTxs: List> + ): Pair, List> { + // We collect all the preimages available. + val preimages = (changes.localChanges.all + changes.remoteChanges.all).filterIsInstance().map { it.paymentPreimage }.associateBy { r -> r.sha256() } + // We collect incoming HTLCs that we started failing but didn't cross-sign. + val failedIncomingHtlcs = changes.localChanges.all.mapNotNull { + when (it) { + is UpdateFailHtlc -> it.id + is UpdateFailMalformedHtlc -> it.id + else -> null + } + }.toSet() + val incomingHtlcs = unsignedHtlcTxs.mapNotNull { + when (val htlcTx = it.first) { + is Transactions.HtlcSuccessTx -> when { + // We immediately spend incoming htlcs for which we have the preimage. + preimages.contains(htlcTx.paymentHash) -> { + val preimage = preimages.getValue(htlcTx.paymentHash) + val signedHtlcTx = generateTx("htlc-success") { + Either.Right(htlcTx.sign(commitKeys, it.second, preimage)) + } + Triple(htlcTx.input.outPoint, htlcTx.htlcId, signedHtlcTx) + } + // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. + // We don't track those outputs because we want to move to the CLOSED state even if our peer never claims them. + failedIncomingHtlcs.contains(htlcTx.htlcId) -> null + // For all other incoming htlcs, we may reveal the preimage later if it matches one of our unpaid invoices. + // We thus want to track the corresponding outputs to ensure we don't forget the channel until they've been spent, + // either by us if we accept the payment, or by our peer after the timeout. + else -> Triple(htlcTx.input.outPoint, htlcTx.htlcId, null) + } + is Transactions.HtlcTimeoutTx -> null + } } - }.toSet() + val htlcOutputs = incomingHtlcs.associate { (outpoint, htlcId, _) -> outpoint to htlcId } + val htlcTxs = incomingHtlcs.mapNotNull { (_, _, signedHtlcTx) -> signedHtlcTx } + return Pair(htlcOutputs, htlcTxs) + } - val htlcTxs = commitment.localCommit.publishableTxs.htlcTxsAndSigs.mapNotNull { - when (it.txinfo) { - is HtlcSuccessTx -> when { - // We immediately spend incoming htlcs for which we have the preimage. - hash2Preimage.containsKey(it.txinfo.paymentHash) -> Pair(it.txinfo.input.outPoint, Transactions.addSigs(it.txinfo, it.localSig, it.remoteSig, hash2Preimage[it.txinfo.paymentHash]!!)) - // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. - // We don't track those outputs because we want to forget the channel even if our peer never claims them. - failedIncomingHtlcs.contains(it.txinfo.htlcId) -> null - // For all other incoming htlcs, we may reveal the preimage later if it matches one of our unpaid invoices. - // We thus want to track the corresponding outputs to ensure we don't forget the channel until they've been spent, - // either by us if we accept the payment, or by our peer after the timeout. - else -> Pair(it.txinfo.input.outPoint, null) + /** + * Claim the outputs of a local commit tx corresponding to outgoing HTLCs, after their timeout. + */ + private fun LoggingContext.claimOutgoingHtlcOutputs( + commitKeys: LocalCommitmentKeys, + unsignedHtlcTxs: List> + ): Pair, List> { + val outgoingHtlcs = unsignedHtlcTxs.mapNotNull { + when (val htlcTx = it.first) { + // We track all outputs that belong to outgoing htlcs. Our peer may or may not have the preimage: if they + // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds + // back after the timeout. + is Transactions.HtlcTimeoutTx -> { + val signedHtlcTx = generateTx("htlc-timeout") { + Either.Right(htlcTx.sign(commitKeys, it.second)) + } + Triple(htlcTx.input.outPoint, htlcTx.htlcId, signedHtlcTx) + } + is Transactions.HtlcSuccessTx -> null } - // We track all outputs that belong to outgoing htlcs. Our peer may or may not have the preimage: if they - // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds - // back after the timeout. - is HtlcTimeoutTx -> Pair(it.txinfo.input.outPoint, Transactions.addSigs(it.txinfo, it.localSig, it.remoteSig)) } - }.toMap() - - // All htlc output to us are delayed, so we need to claim them as soon as the delay is over. - val htlcDelayedTxs = htlcTxs.values.filterNotNull().mapNotNull { txInfo -> - generateTx("claim-htlc-delayed") { - Transactions.makeClaimLocalDelayedOutputTx( - txInfo.tx, - commitment.params.localParams.dustLimit, - commitKeys.revocationPublicKey, - commitment.params.remoteParams.toSelfDelay, - commitKeys.ourDelayedPaymentKey.publicKey(), - commitment.params.localParams.defaultFinalScriptPubKey.toByteArray(), - feerateDelayed - ) - }?.let { - val sig = Transactions.sign(it, commitKeys.ourDelayedPaymentKey, SigHash.SIGHASH_ALL) - Transactions.addSigs(it, sig) + val htlcOutputs = outgoingHtlcs.associate { (outpoint, htlcId, _) -> outpoint to htlcId } + val htlcTxs = outgoingHtlcs.mapNotNull { (_, _, signedHtlcTx) -> signedHtlcTx } + return Pair(htlcOutputs, htlcTxs) + } + + /** Claim the outputs of incoming HTLCs for the payment_hash matching the preimage provided. */ + fun LoggingContext.claimHtlcsWithPreimage(channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, commitment: FullCommitment, preimage: ByteVector32): List { + return commitment.unsignedHtlcTxs(channelKeys).mapNotNull { (htlcTx, remoteSig) -> + when { + htlcTx is Transactions.HtlcSuccessTx && htlcTx.paymentHash == preimage.sha256() -> generateTx("htlc-success") { Either.Right(htlcTx.sign(commitKeys, remoteSig, preimage)) } + else -> null + } } } - return LocalCommitPublished( - commitTx = commitTx, - claimMainDelayedOutputTx = mainDelayedTx, - htlcTxs = htlcTxs, - claimHtlcDelayedTxs = htlcDelayedTxs, - claimAnchorTxs = emptyList(), - irrevocablySpent = emptyMap() - ) + /** + * An incoming HTLC that we've received has been failed by us: if the channel wasn't closing we would relay + * that failure. Since the channel is closing, our peer should claim the HTLC on-chain after the timeout. + * We stop tracking the corresponding output because we want to move to the CLOSED state even if our peer never + * claims it (which may happen if the HTLC amount is low and on-chain fees are high). + */ + fun ignoreFailedIncomingHtlc(htlcId: Long, localCommitPublished: LocalCommitPublished, commitment: FullCommitment): LocalCommitPublished { + // If we have the preimage (e.g. for partially fulfilled multi-part payments), we keep the HTLC-success tx. + val preimages = (commitment.changes.localChanges.all + commitment.changes.remoteChanges.all).filterIsInstance().map { it.paymentPreimage }.associateBy { r -> r.sha256() } + val htlcsWithPreimage = commitment.localCommit.spec.htlcs.mapNotNull { htlc -> + when { + htlc is IncomingHtlc && preimages.contains(htlc.add.paymentHash) -> htlc.add.id + else -> null + } + } + val outpoints = localCommitPublished.incomingHtlcs.mapNotNull { (outpoint, id) -> + when { + id == htlcId && !htlcsWithPreimage.contains(id) -> outpoint + else -> null + } + }.toSet() + return localCommitPublished.copy(incomingHtlcs = localCommitPublished.incomingHtlcs - outpoints) + } + + /** + * Claim the output of a 2nd-stage HTLC transaction. If the provided transaction isn't an htlc, this will be a no-op. + * + * NB: with anchor outputs, it's possible to have transactions that spend *many* HTLC outputs at once, but we're not + * doing that because it introduces a lot of subtle edge cases. + */ + fun LoggingContext.claimHtlcDelayedOutput( + localCommitPublished: LocalCommitPublished, + channelKeys: ChannelKeys, + commitment: FullCommitment, + tx: Transaction, + feerates: OnChainFeerates + ): Pair { + return if (tx.txIn.any { txIn -> localCommitPublished.htlcOutputs.contains(txIn.outPoint) }) { + val feerateDelayed = feerates.claimMainFeerate + val commitKeys = commitment.localKeys(channelKeys) + // Note that this will return null if the transaction wasn't one of our HTLC transactions, which may happen + // if our peer was able to claim the HTLC output before us (race condition between success and timeout). + val htlcDelayedTx = generateTx("htlc-delayed") { + Transactions.HtlcDelayedTx.createUnsignedTx( + commitKeys, + tx, + commitment.localCommitParams.dustLimit, + commitment.localCommitParams.toSelfDelay, + commitment.channelParams.localParams.defaultFinalScriptPubKey, + feerateDelayed, + commitment.commitmentFormat + ).map { it.sign() } + } + val localCommitPublished1 = localCommitPublished.copy(htlcDelayedOutputs = localCommitPublished.htlcDelayedOutputs + setOfNotNull(htlcDelayedTx?.input?.outPoint)) + Pair(localCommitPublished1, LocalCommitThirdStageTransactions(listOfNotNull(htlcDelayedTx))) + } else { + Pair(localCommitPublished, LocalCommitThirdStageTransactions(listOf())) + } + } + + /** + * Claim the outputs of all 2nd-stage HTLC transactions that have been confirmed. + */ + fun LoggingContext.claimHtlcDelayedOutputs(localCommitPublished: LocalCommitPublished, channelKeys: ChannelKeys, commitment: FullCommitment, feerates: OnChainFeerates): LocalCommitThirdStageTransactions { + val confirmedHtlcTxs = localCommitPublished.htlcOutputs.mapNotNull { htlcOutput -> localCommitPublished.irrevocablySpent[htlcOutput] } + val htlcDelayedTxs = confirmedHtlcTxs.flatMap { tx -> claimHtlcDelayedOutput(localCommitPublished, channelKeys, commitment, tx, feerates).second.htlcDelayedTxs } + return LocalCommitThirdStageTransactions(htlcDelayedTxs) + } + } - /** - * Claim all the outputs that we can from their current commit tx. - * - * @param commitment our commitment data, which includes payment preimages. - * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment). - * @param commitTx the remote commitment transaction that has just been published. - * @return a list of transactions (one per output that we can claim). - */ - fun LoggingContext.claimRemoteCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, commitTx: Transaction, feerates: OnChainFeerates): RemoteCommitPublished { - val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - val commitKeys = channelKeys.remoteCommitmentKeys(commitment.params, remoteCommit.remotePerCommitmentPoint) - val (remoteCommitTx, _) = Commitments.makeRemoteTxs( - channelParams = commitment.params, - commitKeys = commitKeys, - commitTxNumber = remoteCommit.index, - localFundingKey = fundingKey, - remoteFundingPubKey = commitment.remoteFundingPubkey, - commitmentInput = commitment.commitInput, - spec = remoteCommit.spec - ) - require(remoteCommitTx.tx.txid == commitTx.txid) { "txid mismatch, provided tx is not the current remote commit tx" } - - val outputs = makeCommitTxOutputs( - localFundingPubkey = fundingKey.publicKey(), - remoteFundingPubkey = commitment.remoteFundingPubkey, - commitKeys = commitKeys.publicKeys, - payCommitTxFees = !commitment.params.localParams.paysCommitTxFees, - dustLimit = commitment.params.remoteParams.dustLimit, - toSelfDelay = commitment.params.localParams.toSelfDelay, - spec = remoteCommit.spec - ) + object RemoteClose { + + /** Claim all the outputs that belong to us in the remote commitment transaction (which can be either their current or next commitment). */ + fun LoggingContext.claimCommitTxOutputs( + channelKeys: ChannelKeys, + commitment: FullCommitment, + remoteCommit: RemoteCommit, + commitTx: Transaction, + feerates: OnChainFeerates + ): Pair { + require(remoteCommit.txid == commitTx.txid) { "txid mismatch, provided tx is not the current remote commit tx" } + val commitKeys = channelKeys.remoteCommitmentKeys(commitment.channelParams, remoteCommit.remotePerCommitmentPoint) + val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) + val mainTx = claimMainOutput(commitKeys, commitTx, commitment.localCommitParams.dustLimit, commitment.commitmentFormat, commitment.localChannelParams.defaultFinalScriptPubKey, feerates.claimMainFeerate) + // We need to use a rather high fee for htlc-claim because we compete with the counterparty. + val feerateClaimHtlc = feerates.fastFeerate + val (incomingHtlcs, htlcSuccessTxs) = claimIncomingHtlcOutputs(commitKeys, commitTx, outputs, commitment, remoteCommit, feerateClaimHtlc) + val (outgoingHtlcs, htlcTimeoutTxs) = claimOutgoingHtlcOutputs(commitKeys, commitTx, outputs, commitment, remoteCommit, feerateClaimHtlc) + val rcp = RemoteCommitPublished( + commitTx = commitTx, + localOutput = mainTx?.input?.outPoint, + anchorOutput = null, + incomingHtlcs = incomingHtlcs, + outgoingHtlcs = outgoingHtlcs, + irrevocablySpent = mapOf() + ) + val txs = RemoteCommitSecondStageTransactions(mainTx, htlcSuccessTxs + htlcTimeoutTxs) + return Pair(rcp, txs) + } - // We need to use a rather high fee for htlc-claim because we compete with the counterparty. - val feerateClaimHtlc = feerates.fastFeerate - - // We collect all the preimages we wanted to reveal to our peer. - val hash2Preimage = commitment.changes.localChanges.all.filterIsInstance().map { it.paymentPreimage }.associateBy { r -> r.sha256() } - // We collect incoming HTLCs that we started failing but didn't cross-sign. - val failedIncomingHtlcs = commitment.changes.localChanges.all.mapNotNull { - when (it) { - is UpdateFailHtlc -> it.id - is UpdateFailMalformedHtlc -> it.id - else -> null + /** Create outputs of the remote commitment transaction, allowing us for example to identify HTLC outputs. */ + fun makeRemoteCommitTxOutputs(channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment, remoteCommit: RemoteCommit): List { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + return makeCommitTxOutputs( + commitment.remoteFundingPubkey, + fundingKey.publicKey(), + commitKeys.publicKeys, + !commitment.localChannelParams.paysCommitTxFees, + commitment.remoteCommitParams.dustLimit, + commitment.remoteCommitParams.toSelfDelay, + commitment.commitmentFormat, + remoteCommit.spec + ) + } + + /** Claim our main output from the remote commitment transaction, if available. */ + internal fun LoggingContext.claimMainOutput( + commitKeys: RemoteCommitmentKeys, + commitTx: Transaction, + dustLimit: Satoshi, + commitmentFormat: Transactions.CommitmentFormat, + finalScriptPubKey: ByteVector, + claimMainFeerate: FeeratePerKw + ): Transactions.ClaimRemoteDelayedOutputTx? { + return generateTx("claim-remote-delayed-output") { + Transactions.ClaimRemoteDelayedOutputTx.createUnsignedTx( + commitKeys, + commitTx, + dustLimit, + finalScriptPubKey, + claimMainFeerate, + commitmentFormat, + ).map { it.sign() } } - }.toSet() + } - // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. - val claimHtlcTxs = remoteCommit.spec.htlcs.mapNotNull { htlc -> - when (htlc) { - is OutgoingHtlc -> { - generateTx("claim-htlc-success") { - Transactions.makeClaimHtlcSuccessTx( - commitTx = remoteCommitTx.tx, - outputs = outputs, - localDustLimit = commitment.params.localParams.dustLimit, - localHtlcPubkey = commitKeys.ourHtlcKey.publicKey(), - remoteHtlcPubkey = commitKeys.theirHtlcPublicKey, - remoteRevocationPubkey = commitKeys.revocationPublicKey, - localFinalScriptPubKey = commitment.params.localParams.defaultFinalScriptPubKey.toByteArray(), - htlc = htlc.add, - feerate = feerateClaimHtlc - ) - }?.let { claimHtlcTx -> - when { - // We immediately spend incoming htlcs for which we have the preimage. - hash2Preimage.containsKey(htlc.add.paymentHash) -> { - val sig = Transactions.sign(claimHtlcTx, commitKeys.ourHtlcKey, SigHash.SIGHASH_ALL) - Pair(claimHtlcTx.input.outPoint, Transactions.addSigs(claimHtlcTx, sig, hash2Preimage[htlc.add.paymentHash]!!)) + /** + * Claim the outputs of a remote commit tx corresponding to incoming HTLCs. If we don't have the preimage for an + * incoming HTLC, we still include an entry in the map because we may receive that preimage later. + */ + private fun LoggingContext.claimIncomingHtlcOutputs( + commitKeys: RemoteCommitmentKeys, + commitTx: Transaction, + outputs: List, + commitment: FullCommitment, + remoteCommit: RemoteCommit, + feerate: FeeratePerKw + ): Pair, List> { + // We collect all the preimages available. + val preimages = (commitment.changes.localChanges.all + commitment.changes.remoteChanges.all).filterIsInstance().map { it.paymentPreimage }.associateBy { r -> r.sha256() } + // We collect incoming HTLCs that we started failing but didn't cross-sign. + val failedIncomingHtlcs = commitment.changes.localChanges.all.mapNotNull { + when (it) { + is UpdateFailHtlc -> it.id + is UpdateFailMalformedHtlc -> it.id + else -> null + } + }.toSet() + // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. + val incomingHtlcs = remoteCommit.spec.htlcs.mapNotNull { htlc -> + when (htlc) { + is OutgoingHtlc -> when { + // We immediately spend incoming htlcs for which we have the preimage. + preimages.contains(htlc.add.paymentHash) -> { + val preimage = preimages.getValue(htlc.add.paymentHash) + val signedHtlcTx = generateTx("claim-htlc-success") { + Transactions.ClaimHtlcSuccessTx.createUnsignedTx( + commitKeys, + commitTx, + commitment.localCommitParams.dustLimit, + outputs, + commitment.localChannelParams.defaultFinalScriptPubKey, + htlc.add, + preimage, + feerate, + commitment.commitmentFormat + ).map { it.sign() } } - // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. - // We don't track those outputs because we want to forget the channel even if our peer never claims them. - failedIncomingHtlcs.contains(htlc.add.id) -> null - // For all other incoming htlcs, we may reveal the preimage later if it matches one of our unpaid invoices. - // We thus want to track the corresponding outputs to ensure we don't forget the channel until they've been spent, - // either by us if we accept the payment, or by our peer after the timeout. - else -> Pair(claimHtlcTx.input.outPoint, null) + signedHtlcTx?.let { Triple(it.input.outPoint, htlc.add.id, it) } + } + // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. + // We don't track those outputs because we want to move to the CLOSED state even if our peer never claims them. + failedIncomingHtlcs.contains(htlc.add.id) -> null + // For all other incoming htlcs, we may reveal the preimage later if it matches one of our unpaid invoices. + // We thus want to track the corresponding outputs to ensure we don't forget the channel until they've been spent, + // either by us if we accept the payment, or by our peer after the timeout. + else -> { + Transactions.ClaimHtlcSuccessTx.findInput(commitTx, outputs, htlc.add)?.let { Triple(it.outPoint, htlc.add.id, null) } } } + is IncomingHtlc -> null } - is IncomingHtlc -> { - // We track all outputs that belong to outgoing htlcs. Our peer may or may not have the preimage: if they - // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds - // back after the timeout. - generateTx("claim-htlc-timeout") { - Transactions.makeClaimHtlcTimeoutTx( - commitTx = remoteCommitTx.tx, - outputs = outputs, - localDustLimit = commitment.params.localParams.dustLimit, - localHtlcPubkey = commitKeys.ourHtlcKey.publicKey(), - remoteHtlcPubkey = commitKeys.theirHtlcPublicKey, - remoteRevocationPubkey = commitKeys.revocationPublicKey, - localFinalScriptPubKey = commitment.params.localParams.defaultFinalScriptPubKey.toByteArray(), - htlc = htlc.add, - feerate = feerateClaimHtlc - ) - }?.let { claimHtlcTx -> - val sig = Transactions.sign(claimHtlcTx, commitKeys.ourHtlcKey, SigHash.SIGHASH_ALL) - Pair(claimHtlcTx.input.outPoint, Transactions.addSigs(claimHtlcTx, sig)) + } + val htlcOutputs = incomingHtlcs.associate { (outpoint, htlcId, _) -> outpoint to htlcId } + val htlcTxs = incomingHtlcs.mapNotNull { (_, _, signedHtlcTx) -> signedHtlcTx } + return Pair(htlcOutputs, htlcTxs) + } + + /** + * Claim the outputs of a remote commit tx corresponding to outgoing HTLCs, after their timeout. + */ + private fun LoggingContext.claimOutgoingHtlcOutputs( + commitKeys: RemoteCommitmentKeys, + commitTx: Transaction, + outputs: List, + commitment: FullCommitment, + remoteCommit: RemoteCommit, + feerate: FeeratePerKw + ): Pair, List> { + // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. + val outgoingHtlcs = remoteCommit.spec.htlcs.mapNotNull { htlc -> + when (htlc) { + is IncomingHtlc -> { + // We track all outputs that belong to outgoing htlcs. Our peer may or may not have the preimage: if they + // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds + // back after the timeout. + generateTx("claim-htlc-timeout") { + Transactions.ClaimHtlcTimeoutTx.createUnsignedTx( + commitKeys, + commitTx, + commitment.localCommitParams.dustLimit, + outputs, + commitment.localChannelParams.defaultFinalScriptPubKey, + htlc.add, + feerate, + commitment.commitmentFormat + ).map { it.sign() } + }?.let { Triple(it.input.outPoint, htlc.add.id, it) } } + is OutgoingHtlc -> null } } - }.toMap() + val htlcOutputs = outgoingHtlcs.associate { (outpoint, htlcId, _) -> outpoint to htlcId } + val htlcTxs = outgoingHtlcs.map { (_, _, signedHtlcTx) -> signedHtlcTx } + return Pair(htlcOutputs, htlcTxs) + } - // We claim our output and add the htlc txs we just created. - return claimRemoteCommitMainOutput(commitKeys, commitTx, commitment.params.localParams.dustLimit, commitment.params.localParams.defaultFinalScriptPubKey, feerates.claimMainFeerate).copy(claimHtlcTxs = claimHtlcTxs) - } + /** Claim the outputs of incoming HTLCs for the payment_hash matching the preimage provided. */ + fun LoggingContext.claimHtlcsWithPreimage( + channelKeys: ChannelKeys, + remoteCommitPublished: RemoteCommitPublished, + commitment: FullCommitment, + remoteCommit: RemoteCommit, + preimage: ByteVector32, + feerates: OnChainFeerates + ): List { + val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) + val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) + val feerate = feerates.fastFeerate + return remoteCommit.spec.htlcs.mapNotNull { htlc -> + // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. + when { + htlc is OutgoingHtlc && htlc.add.paymentHash == preimage.sha256() -> generateTx("claim-htlc-success") { + Transactions.ClaimHtlcSuccessTx.createUnsignedTx( + commitKeys, + remoteCommitPublished.commitTx, + commitment.localCommitParams.dustLimit, + outputs, + commitment.localChannelParams.defaultFinalScriptPubKey, + htlc.add, + preimage, + feerate, + commitment.commitmentFormat + ).map { it.sign() } + } + else -> null + } + } + } - /** - * Claim our main output only from their commit tx. - * - * @param commitTx the remote commitment transaction that has just been published. - * @return a transaction to claim our main output. - */ - internal fun LoggingContext.claimRemoteCommitMainOutput(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, dustLimit: Satoshi, finalScriptPubKey: ByteVector, claimMainFeerate: FeeratePerKw): RemoteCommitPublished { - val mainTx = generateTx("claim-remote-delayed-output") { - Transactions.makeClaimRemoteDelayedOutputTx( - commitTx = commitTx, - localDustLimit = dustLimit, - localPaymentPubkey = commitKeys.ourPaymentKey.publicKey(), - localFinalScriptPubKey = finalScriptPubKey, - feerate = claimMainFeerate - ) - }?.let { - val sig = Transactions.sign(it, commitKeys.ourPaymentKey) - Transactions.addSigs(it, sig) + /** + * An incoming HTLC that we've received has been failed by us: if the channel wasn't closing we would relay + * that failure. Since the channel is closing, our peer should claim the HTLC on-chain after the timeout. + * We stop tracking the corresponding output because we want to move to the CLOSED state even if our peer never + * claims it (which may happen if the HTLC amount is low and on-chain fees are high). + */ + fun ignoreFailedIncomingHtlc(htlcId: Long, remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment, remoteCommit: RemoteCommit): RemoteCommitPublished { + val preimages = (commitment.changes.localChanges.all + commitment.changes.remoteChanges.all).filterIsInstance().map { it.paymentPreimage }.associateBy { r -> r.sha256() } + // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. + val htlcsWithPreimage = remoteCommit.spec.htlcs.mapNotNull { htlc -> + when { + htlc is OutgoingHtlc && preimages.contains(htlc.add.paymentHash) -> htlc.add.id + else -> null + } + } + val outpoints = remoteCommitPublished.incomingHtlcs.mapNotNull { (outpoint, id) -> + when { + id == htlcId && !htlcsWithPreimage.contains(id) -> outpoint + else -> null + } + }.toSet() + return remoteCommitPublished.copy(incomingHtlcs = remoteCommitPublished.incomingHtlcs - outpoints) } - return RemoteCommitPublished(commitTx = commitTx, claimMainOutputTx = mainTx) - } - /** - * When an unexpected transaction spending the funding tx is detected, we must be in one of the following scenarios: - * - * - it is a revoked commitment: we then extract the remote per-commitment secret and publish penalty transactions - * - it is a future commitment: if we lost future state, our peer could publish a future commitment (which may be - * revoked, but we won't be able to know because we lost the corresponding state) - * - it is not a valid commitment transaction: if our peer was able to steal our funding private key, they can - * spend the funding transaction however they want, and we won't be able to do anything about it - * - * This function returns the per-commitment secret in the first case, and null in the other cases. - */ - fun getRemotePerCommitmentSecret(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecrets: ShaChain, commitTx: Transaction): Pair? { - // a valid tx will always have at least one input, but this ensures we don't throw in tests - val sequence = commitTx.txIn.first().sequence - val obscuredTxNumber = Transactions.decodeTxNumber(sequence, commitTx.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.isChannelOpener, params.remoteParams.paymentBasepoint, localPaymentPoint) - if (commitmentNumber > 0xffffffffffffL) { - // txNumber must be lesser than 48 bits long - return null - } - // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain - val hash = remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - commitmentNumber) ?: return null - return Pair(PrivateKey(hash), commitmentNumber) } - /** - * When a revoked commitment transaction spending the funding tx is detected, we build a set of transactions that - * will punish our peer by stealing all their funds. - */ - fun LoggingContext.claimRevokedRemoteCommitTxOutputs(params: ChannelParams, channelKeys: ChannelKeys, commitTx: Transaction, remotePerCommitmentSecret: PrivateKey, feerates: OnChainFeerates): RevokedCommitPublished { - val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey() - val commitKeys = channelKeys.remoteCommitmentKeys(params, remotePerCommitmentPoint) - val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret) - val feerateMain = feerates.claimMainFeerate - // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty - val feeratePenalty = feerates.fastFeerate - - // first we will claim our main output right away - val mainTx = generateTx("claim-remote-delayed-output") { - Transactions.makeClaimRemoteDelayedOutputTx( - commitTx, - params.localParams.dustLimit, - commitKeys.ourPaymentKey.publicKey(), - params.localParams.defaultFinalScriptPubKey, - feerateMain - ) - }?.let { - val sig = Transactions.sign(it, commitKeys.ourPaymentKey) - Transactions.addSigs(it, sig) - } - - // then we punish them by stealing their main output - val mainPenaltyTx = generateTx("main-penalty") { - Transactions.makeMainPenaltyTx( - commitTx, - params.localParams.dustLimit, - commitKeys.revocationPublicKey, - params.localParams.defaultFinalScriptPubKey.toByteArray(), - params.localParams.toSelfDelay, - commitKeys.theirDelayedPaymentPublicKey, - feeratePenalty + object RevokedClose { + + /** + * When an unexpected transaction spending the funding tx is detected, we must be in one of the following scenarios: + * + * - it is a revoked commitment: we then extract the remote per-commitment secret and publish penalty transactions + * - it is a future commitment: if we lost future state, our peer could publish a future commitment (which may be + * revoked, but we won't be able to know because we lost the corresponding state) + * - it is not a valid commitment transaction: if our peer was able to steal our funding private key, they can + * spend the funding transaction however they want, and we won't be able to do anything about it + * + * This function returns the per-commitment secret in the first case, and null in the other cases. + */ + fun getRemotePerCommitmentSecret(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecrets: ShaChain, commitTx: Transaction): Pair? { + // This transaction has been published by the remote, so we need to invert local/remote params. + val commitmentNumber = Transactions.getCommitTxNumber( + commitTx = commitTx, + isInitiator = !params.localParams.isChannelOpener, + localPaymentBasePoint = params.remoteParams.paymentBasepoint, + remotePaymentBasePoint = channelKeys.paymentBasePoint ) - }?.let { - val sig = Transactions.sign(it, revocationKey) - Transactions.addSigs(it, sig) + if (commitmentNumber > 0xffffffffffffL) { + // The commitment number must be lesser than 48 bits long, otherwise this isn't a commitment transaction. + return null + } + // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain + val hash = remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - commitmentNumber) ?: return null + return Pair(PrivateKey(hash), commitmentNumber) } - return RevokedCommitPublished(commitTx = commitTx, remotePerCommitmentSecret = remotePerCommitmentSecret, claimMainOutputTx = mainTx, mainPenaltyTx = mainPenaltyTx) - } - - /** - * Once we've fetched htlc information for a revoked commitment from the DB, we create penalty transactions to claim all htlc outputs. - */ - fun LoggingContext.claimRevokedRemoteCommitTxHtlcOutputs( - params: ChannelParams, - channelKeys: ChannelKeys, - revokedCommitPublished: RevokedCommitPublished, - feerates: OnChainFeerates, - htlcInfos: List - ): RevokedCommitPublished { - // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty - val feeratePenalty = feerates.fastFeerate - val remotePerCommitmentPoint = revokedCommitPublished.remotePerCommitmentSecret.publicKey() - val commitKeys = channelKeys.remoteCommitmentKeys(params, remotePerCommitmentPoint) - val revocationKey = channelKeys.revocationKey(revokedCommitPublished.remotePerCommitmentSecret) - // we retrieve the information needed to rebuild htlc scripts - logger.info { "found ${htlcInfos.size} htlcs for txid=${revokedCommitPublished.commitTx.txid}" } - val htlcsRedeemScripts = htlcInfos.flatMap { htlcInfo -> - val htlcReceived = Scripts.htlcReceived(commitKeys.theirHtlcPublicKey, commitKeys.ourHtlcKey.publicKey(), commitKeys.revocationPublicKey, ripemd160(htlcInfo.paymentHash), htlcInfo.cltvExpiry) - val htlcOffered = Scripts.htlcOffered(commitKeys.theirHtlcPublicKey, commitKeys.ourHtlcKey.publicKey(), commitKeys.revocationPublicKey, ripemd160(htlcInfo.paymentHash)) - listOf(htlcReceived, htlcOffered) - }.associate { redeemScript -> write(pay2wsh(redeemScript)).toByteVector() to write(redeemScript).toByteVector() } - // and finally we steal the htlc outputs - val htlcPenaltyTxs = revokedCommitPublished.commitTx.txOut.mapIndexedNotNull { outputIndex, txOut -> - htlcsRedeemScripts[txOut.publicKeyScript]?.let { redeemScript -> - generateTx("htlc-penalty") { - Transactions.makeHtlcPenaltyTx( - revokedCommitPublished.commitTx, - outputIndex, - redeemScript.toByteArray(), - params.localParams.dustLimit, - params.localParams.defaultFinalScriptPubKey.toByteArray(), - feeratePenalty - ) - }?.let { htlcPenaltyTx -> - val sig = Transactions.sign(htlcPenaltyTx, revocationKey) - Transactions.addSigs(htlcPenaltyTx, sig, commitKeys.revocationPublicKey) - } + /** + * When a revoked commitment transaction spending the funding tx is detected, we build a set of transactions that + * will punish our peer by stealing all their funds. + */ + fun LoggingContext.claimCommitTxOutputs( + params: ChannelParams, + channelKeys: ChannelKeys, + commitTx: Transaction, + remotePerCommitmentSecret: PrivateKey, + dustLimit: Satoshi, + toSelfDelay: CltvExpiryDelta, + commitmentFormat: Transactions.CommitmentFormat, + feerates: OnChainFeerates + ): Pair { + val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey() + val commitKeys = channelKeys.remoteCommitmentKeys(params, remotePerCommitmentPoint) + val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret) + val feerateMain = feerates.claimMainFeerate + // We need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty. + val feeratePenalty = feerates.fastFeerate + // First we will claim our main output right away. + val mainTx = generateTx("claim-remote-delayed-output") { + Transactions.ClaimRemoteDelayedOutputTx.createUnsignedTx( + commitKeys, + commitTx, + dustLimit, + params.localParams.defaultFinalScriptPubKey, + feerateMain, + commitmentFormat + ).map { it.sign() } + } + // Then we punish them by stealing their main output. + val mainPenaltyTx = generateTx("main-penalty") { + Transactions.MainPenaltyTx.createUnsignedTx( + commitKeys, + revocationKey, + commitTx, + dustLimit, + params.localParams.defaultFinalScriptPubKey, + toSelfDelay, + feeratePenalty, + commitmentFormat + ).map { it.sign() } } + val rvk = RevokedCommitPublished( + commitTx = commitTx, + remotePerCommitmentSecret = remotePerCommitmentSecret, + localOutput = mainTx?.input?.outPoint, + remoteOutput = mainPenaltyTx?.input?.outPoint, + htlcOutputs = setOf(), // we will get HTLC data from our DB and fill this in [claimRevokedRemoteCommitTxHtlcOutputs] + htlcDelayedOutputs = setOf(), + irrevocablySpent = mapOf() + ) + val txs = RevokedCommitSecondStageTransactions(mainTx, mainPenaltyTx, htlcPenaltyTxs = listOf()) + return Pair(rvk, txs) } - return revokedCommitPublished.copy(htlcPenaltyTxs = htlcPenaltyTxs) - } - /** - * Claims the output of an [[HtlcSuccessTx]] or [[HtlcTimeoutTx]] transaction using a revocation key. - * - * In case a revoked commitment with pending HTLCs is published, there are two ways the HTLC outputs can be taken as punishment: - * - by spending the corresponding output of the commitment tx, using [[ClaimHtlcDelayedOutputPenaltyTx]] that we generate as soon as we detect that a revoked commit - * has been spent; note that those transactions will compete with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] published by the counterparty. - * - by spending the delayed output of [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] if those get confirmed; because the output of these txs is protected by - * an OP_CSV delay, we will have time to spend them with a revocation key. In that case, we generate the spending transactions "on demand", - * this is the purpose of this method. - * - * NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same - * lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs. - */ - fun LoggingContext.claimRevokedHtlcTxOutputs( - params: ChannelParams, - channelKeys: ChannelKeys, - revokedCommitPublished: RevokedCommitPublished, - htlcTx: Transaction, - feerates: OnChainFeerates - ): Pair> { - // We published HTLC-penalty transactions for every HTLC output: this transaction may be ours, or it may be one - // of their HTLC transactions that confirmed before our HTLC-penalty transaction. If it is spending an HTLC - // output, we assume that it's an HTLC transaction published by our peer and try to create penalty transactions - // that spend it, which will automatically be skipped if this was instead one of our HTLC-penalty transactions. - val htlcOutputs = revokedCommitPublished.htlcPenaltyTxs.map { it.input.outPoint }.toSet() - val spendsHtlcOutput = htlcTx.txIn.any { htlcOutputs.contains(it.outPoint) } - if (spendsHtlcOutput) { + /** + * Once we've fetched htlc information for a revoked commitment from the DB, we create penalty transactions to claim all htlc outputs. + */ + fun LoggingContext.claimHtlcOutputs( + params: ChannelParams, + channelKeys: ChannelKeys, + revokedCommitPublished: RevokedCommitPublished, + dustLimit: Satoshi, + commitmentFormat: Transactions.CommitmentFormat, + feerates: OnChainFeerates, + htlcInfos: List + ): List { + // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty + val feeratePenalty = feerates.fastFeerate val remotePerCommitmentPoint = revokedCommitPublished.remotePerCommitmentSecret.publicKey() val commitKeys = channelKeys.remoteCommitmentKeys(params, remotePerCommitmentPoint) val revocationKey = channelKeys.revocationKey(revokedCommitPublished.remotePerCommitmentSecret) - // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty - val feeratePenalty = feerates.fastFeerate - val penaltyTxs = Transactions.makeClaimDelayedOutputPenaltyTxs( - htlcTx, - params.localParams.dustLimit, - commitKeys.revocationPublicKey, - params.localParams.toSelfDelay, - commitKeys.theirDelayedPaymentPublicKey, - params.localParams.defaultFinalScriptPubKey.toByteArray(), - feeratePenalty - ).mapNotNull { claimDelayedOutputPenaltyTx -> - generateTx("claim-htlc-delayed-penalty") { - claimDelayedOutputPenaltyTx - }?.let { - val sig = Transactions.sign(it, revocationKey) - val signedTx = Transactions.addSigs(it, sig) - // we need to make sure that the tx is indeed valid - when (runTrying { signedTx.tx.correctlySpends(listOf(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) { - is Try.Success -> { - logger.info { "txId=${htlcTx.txid} is a 2nd level htlc tx spending revoked commit txId=${revokedCommitPublished.commitTx.txid}: publishing htlc-penalty txId=${signedTx.tx.txid}" } - signedTx - } - is Try.Failure -> null - } - } + return Transactions.HtlcPenaltyTx.createUnsignedTxs( + commitKeys, + revocationKey, + revokedCommitPublished.commitTx, + htlcInfos.map { Pair(it.paymentHash, it.cltvExpiry) }, + dustLimit, + params.localParams.defaultFinalScriptPubKey, + feeratePenalty, + commitmentFormat + ).mapNotNull { tx -> generateTx("htlc-penalty") { tx.map { it.sign() } } } + } + + /** + * Claims the output of an [Transactions.HtlcSuccessTx] or [Transactions.HtlcTimeoutTx] transaction using a revocation key. + * + * In case a revoked commitment with pending HTLCs is published, there are two ways the HTLC outputs can be taken as punishment: + * - by spending the corresponding output of the commitment tx, using [Transactions.ClaimHtlcDelayedOutputPenaltyTx] that we generate as soon as we detect that a revoked commit + * has been spent; note that those transactions will compete with [Transactions.HtlcSuccessTx] and [Transactions.HtlcTimeoutTx] published by the counterparty. + * - by spending the delayed output of [Transactions.HtlcSuccessTx] and [Transactions.HtlcTimeoutTx] if those get confirmed; because the output of these txs is protected by + * an OP_CSV delay, we will have time to spend them with a revocation key. In that case, we generate the spending transactions "on demand", + * this is the purpose of this method. + * + * NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same + * lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs. + */ + fun LoggingContext.claimHtlcTxOutputs( + params: ChannelParams, + channelKeys: ChannelKeys, + revokedCommitPublished: RevokedCommitPublished, + htlcTx: Transaction, + dustLimit: Satoshi, + toSelfDelay: CltvExpiryDelta, + commitmentFormat: Transactions.CommitmentFormat, + feerates: OnChainFeerates + ): Pair { + // We published HTLC-penalty transactions for every HTLC output: this transaction may be ours, or it may be one + // of their HTLC transactions that confirmed before our HTLC-penalty transaction. If it is spending an HTLC + // output, we assume that it's an HTLC transaction published by our peer and try to create penalty transactions + // that spend it, which will automatically be skipped if this was instead one of our HTLC-penalty transactions. + val spendsHtlcOutput = htlcTx.txIn.any { revokedCommitPublished.htlcOutputs.contains(it.outPoint) } + return if (spendsHtlcOutput) { + val remotePerCommitmentPoint = revokedCommitPublished.remotePerCommitmentSecret.publicKey() + val commitKeys = channelKeys.remoteCommitmentKeys(params, remotePerCommitmentPoint) + val revocationKey = channelKeys.revocationKey(revokedCommitPublished.remotePerCommitmentSecret) + // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty + val feeratePenalty = feerates.fastFeerate + val penaltyTxs = Transactions.ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs( + commitKeys, + revocationKey, + htlcTx, + dustLimit, + toSelfDelay, + params.localParams.defaultFinalScriptPubKey, + feeratePenalty, + commitmentFormat + ).mapNotNull { tx -> generateTx("htlc-delayed-penalty") { tx.map { it.sign() } } } + val revokedCommitPublished1 = revokedCommitPublished.copy(htlcDelayedOutputs = revokedCommitPublished.htlcDelayedOutputs + penaltyTxs.map { it.input.outPoint }) + val txs = RevokedCommitThirdStageTransactions(penaltyTxs) + Pair(revokedCommitPublished1, txs) + } else { + Pair(revokedCommitPublished, RevokedCommitThirdStageTransactions(listOf())) } - return revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs + penaltyTxs) to penaltyTxs - } else { - return revokedCommitPublished to listOf() } + + /** + * Claim the outputs of all 2nd-stage HTLC transactions that have been confirmed. + */ + fun LoggingContext.claimHtlcTxsOutputs( + params: ChannelParams, + channelKeys: ChannelKeys, + revokedCommitPublished: RevokedCommitPublished, + dustLimit: Satoshi, + toSelfDelay: CltvExpiryDelta, + commitmentFormat: Transactions.CommitmentFormat, + feerates: OnChainFeerates + ): RevokedCommitThirdStageTransactions { + val confirmedHtlcTxs = revokedCommitPublished.htlcOutputs.mapNotNull { htlcOutput -> revokedCommitPublished.irrevocablySpent[htlcOutput] } + val penaltyTxs = confirmedHtlcTxs.flatMap { htlcTx -> + claimHtlcTxOutputs(params, channelKeys, revokedCommitPublished, htlcTx, dustLimit, toSelfDelay, commitmentFormat, feerates).second.htlcDelayedPenaltyTxs + } + return RevokedCommitThirdStageTransactions(penaltyTxs) + } + } /** @@ -832,7 +1053,7 @@ object Helpers { val fromRemote = commitment.remoteCommit.spec.htlcs .filter { it is IncomingHtlc && it.add.paymentHash == paymentHash } .map { it.add to paymentPreimage } - val fromNextRemote = commitment.nextRemoteCommit?.commit?.spec?.htlcs.orEmpty() + val fromNextRemote = commitment.nextRemoteCommit?.spec?.htlcs.orEmpty() .filter { it is IncomingHtlc && it.add.paymentHash == paymentHash } .map { it.add to paymentPreimage } fromLocal + fromRemote + fromNextRemote @@ -844,43 +1065,36 @@ object Helpers { * more htlcs have timed out and need to be considered failed. Trimmed htlcs can be failed as soon as the commitment * tx has been confirmed. * - * @param tx a tx that has reached min_depth * @return a set of outgoing htlcs that can be considered failed */ - fun LoggingContext.trimmedOrTimedOutHtlcs(localCommit: LocalCommit, localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction): Set { - val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec).map { it.add } - return when { - tx.txid == localCommit.publishableTxs.commitTx.tx.txid -> { - // The commitment tx is confirmed: we can immediately fail all dust htlcs (they don't have an output in the tx). - (localCommit.spec.htlcs.outgoings() - untrimmedHtlcs.toSet()).toSet() - } - else -> { - // Maybe this is a timeout tx: in that case we can resolve and fail the corresponding htlc. - tx.txIn.mapNotNull { txIn -> - when (val htlcTx = localCommitPublished.htlcTxs[txIn.outPoint]) { - is HtlcTimeoutTx -> { - val htlc = untrimmedHtlcs.find { it.id == htlcTx.htlcId } - when { - // This may also be our peer claiming the HTLC by revealing the preimage: in that case we have already - // extracted the preimage with [extractPreimages] and relayed it to the payment handler. - Scripts.extractPreimagesFromClaimHtlcSuccess(tx).isNotEmpty() -> { - logger.info { "htlc-timeout double-spent by claim-htlc-success txId=${tx.txid} (tx=$tx)" } - null - } - htlc != null -> { - logger.info { "htlc-timeout tx for htlc #${htlc.id} paymentHash=${htlc.paymentHash} expiry=${tx.lockTime} has been confirmed (tx=$tx)" } - htlc - } - else -> { - logger.error { "could not find htlc #${htlcTx.htlcId} for htlc-timeout tx=$tx" } - null - } - } + fun LoggingContext.trimmedOrTimedOutHtlcs(channelKeys: ChannelKeys, commitment: FullCommitment, localCommit: LocalCommit, confirmedTx: Transaction): Set { + return if (confirmedTx.txid == localCommit.txId) { + // The commitment tx is confirmed: we can immediately fail all dust htlcs (they don't have an output in the tx). + val untrimmedHtlcs = Transactions.trimOfferedHtlcs(commitment.localCommitParams.dustLimit, localCommit.spec, commitment.commitmentFormat).map { it.add } + (localCommit.spec.htlcs.outgoings() - untrimmedHtlcs.toSet()).toSet() + } else if (confirmedTx.txIn.any { it.outPoint.txid == localCommit.txId }) { + // The transaction spends the commitment tx: maybe it is a timeout tx, in which case we can resolve and fail the + // corresponding htlc. + val commitKeys = commitment.localKeys(channelKeys) + val outputs = LocalClose.makeLocalCommitTxOutputs(channelKeys, commitKeys, commitment) + confirmedTx.txIn.filter { it.outPoint.txid == localCommit.txId }.mapNotNull { txIn -> + when (val output = outputs[txIn.outPoint.index.toInt()]) { + // This may also be our peer claiming the HTLC by revealing the preimage: in that case we have already + // extracted the preimage with [extractPreimages] and relayed it to the payment handler. + is CommitmentOutput.OutHtlc -> { + if (Scripts.extractPreimagesFromClaimHtlcSuccess(confirmedTx).isEmpty()) { + logger.info { "htlc-timeout tx for htlc #${output.htlc.add.id} paymentHash=${output.htlc.add.paymentHash} expiry=${output.htlc.add.cltvExpiry} has been confirmed (txId=${confirmedTx.txid})" } + output.htlc.add + } else { + logger.info { "htlc-timeout tx for htlc #${output.htlc.add.id} paymentHash=${output.htlc.add.paymentHash} expiry=${output.htlc.add.cltvExpiry} double-spent by claim-htlc-success txId=${confirmedTx.txid}" } + null } - else -> null } - }.toSet() - } + else -> null + } + }.toSet() + } else { + setOf() } } @@ -889,43 +1103,36 @@ object Helpers { * more htlcs have timed out and need to be considered failed. Trimmed htlcs can be failed as soon as the commitment * tx has been confirmed. * - * @param tx a tx that has reached min_depth * @return a set of htlcs that need to be failed upstream */ - fun LoggingContext.trimmedOrTimedOutHtlcs(remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished, remoteDustLimit: Satoshi, tx: Transaction): Set { - val untrimmedHtlcs = Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec).map { it.add } - return when { - tx.txid == remoteCommit.txid -> { - // The commitment tx is confirmed: we can immediately fail all dust htlcs (they don't have an output in the tx). - (remoteCommit.spec.htlcs.incomings() - untrimmedHtlcs.toSet()).toSet() - } - else -> { - // Maybe this is a timeout tx: in that case we can resolve and fail the corresponding htlc. - tx.txIn.mapNotNull { txIn -> - when (val htlcTx = remoteCommitPublished.claimHtlcTxs[txIn.outPoint]) { - is ClaimHtlcTimeoutTx -> { - val htlc = untrimmedHtlcs.find { it.id == htlcTx.htlcId } - when { - // This may also be our peer claiming the HTLC by revealing the preimage: in that case we have already - // extracted the preimage with [extractPreimages] and relayed it upstream. - Scripts.extractPreimagesFromHtlcSuccess(tx).isNotEmpty() -> { - logger.info { "claim-htlc-timeout double-spent by htlc-success txId=${tx.txid} (tx=$tx)" } - null - } - htlc != null -> { - logger.info { "claim-htlc-timeout tx for htlc #${htlc.id} paymentHash=${htlc.paymentHash} expiry=${tx.lockTime} has been confirmed (tx=$tx)" } - htlc - } - else -> { - logger.error { "could not find htlc #${htlcTx.htlcId} for claim-htlc-timeout tx=$tx" } - null - } - } + fun LoggingContext.trimmedOrTimedOutHtlcs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, confirmedTx: Transaction): Set { + return if (confirmedTx.txid == remoteCommit.txid) { + // The commitment tx is confirmed: we can immediately fail all dust htlcs (they don't have an output in the tx). + val untrimmedHtlcs = Transactions.trimReceivedHtlcs(commitment.remoteCommitParams.dustLimit, remoteCommit.spec, commitment.commitmentFormat).map { it.add } + (remoteCommit.spec.htlcs.incomings() - untrimmedHtlcs.toSet()).toSet() + } else if (confirmedTx.txIn.any { it.outPoint.txid == remoteCommit.txid }) { + // The transaction spends the commitment tx: maybe it is a timeout tx, in which case we can resolve and fail the + // corresponding htlc. + val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) + val outputs = RemoteClose.makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) + confirmedTx.txIn.filter { it.outPoint.txid == remoteCommit.txid }.mapNotNull { txIn -> + when (val output = outputs[txIn.outPoint.index.toInt()]) { + // This may also be our peer claiming the HTLC by revealing the preimage: in that case we have already + // extracted the preimage with [extractPreimages] and relayed it to the payment handler. + is CommitmentOutput.InHtlc -> { + if (Scripts.extractPreimagesFromHtlcSuccess(confirmedTx).isEmpty()) { + logger.info { "claim-htlc-timeout tx for htlc #${output.htlc.add.id} paymentHash=${output.htlc.add.paymentHash} expiry=${output.htlc.add.cltvExpiry} has been confirmed (txId=${confirmedTx.txid})" } + output.htlc.add + } else { + logger.info { "claim-htlc-timeout tx for htlc #${output.htlc.add.id} paymentHash=${output.htlc.add.paymentHash} expiry=${output.htlc.add.cltvExpiry} double-spent by htlc-success txId=${confirmedTx.txid}" } + null } - else -> null } - }.toSet() - } + else -> null + } + }.toSet() + } else { + setOf() } } @@ -936,7 +1143,7 @@ object Helpers { * @param tx a transaction that is sufficiently buried in the blockchain */ fun onChainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit: RemoteCommit?, tx: Transaction): Set = when { - localCommit.publishableTxs.commitTx.tx.txid == tx.txid -> localCommit.spec.htlcs.outgoings().toSet() + localCommit.txId == tx.txid -> localCommit.spec.htlcs.outgoings().toSet() remoteCommit.txid == tx.txid -> remoteCommit.spec.htlcs.incomings().toSet() nextRemoteCommit?.txid == tx.txid -> nextRemoteCommit.spec.htlcs.incomings().toSet() else -> emptySet() @@ -952,7 +1159,7 @@ object Helpers { val outgoingHtlcs = (localCommit.spec.htlcs.outgoings() + remoteCommit.spec.htlcs.incomings() + nextRemoteCommit?.spec?.htlcs.orEmpty().incomings()).toSet() return when { // Our commit got confirmed: any htlc that is *not* in our commit will never reach the chain. - localCommit.publishableTxs.commitTx.tx.txid == tx.txid -> outgoingHtlcs - localCommit.spec.htlcs.outgoings().toSet() + localCommit.txId == tx.txid -> outgoingHtlcs - localCommit.spec.htlcs.outgoings().toSet() // A revoked commitment got confirmed: we will claim its outputs, but we also need to resolve htlcs. // We consider *all* outgoing htlcs failed: our peer may reveal the preimage with an HTLC-success transaction, // but it's more likely that our penalty transaction will confirm first. In any case, since we will get those @@ -987,15 +1194,15 @@ object Helpers { /** * Wraps transaction generation in a Try and filters failures to avoid one transaction negatively impacting a whole commitment. */ - private fun LoggingContext.generateTx(desc: String, attempt: () -> Transactions.TxResult): T? = + private fun LoggingContext.generateTx(desc: String, attempt: () -> Either): T? = when (val result = runTrying { attempt() }) { is Try.Success -> when (val txResult = result.get()) { - is Transactions.TxResult.Success -> { - logger.info { "tx generation success: desc=$desc txid=${txResult.result.tx.txid} amount=${txResult.result.tx.txOut.map { it.amount }.sum()} tx=${txResult.result.tx}" } - txResult.result + is Either.Right -> { + logger.info { "tx generation success: desc=$desc txid=${txResult.value.tx.txid} amount=${txResult.value.tx.txOut.map { it.amount }.sum()} tx=${txResult.value.tx}" } + txResult.value } - is Transactions.TxResult.Skipped -> { - logger.info { "tx generation skipped: desc=$desc reason: ${txResult.why}" } + is Either.Left -> { + logger.info { "tx generation skipped: desc=$desc reason: ${txResult.value}" } null } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index d25bce87c..cbf9fb342 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -27,30 +27,24 @@ import kotlinx.coroutines.CompletableDeferred */ /** An input that is already shared between participants (e.g. the current funding output when doing a splice). */ -sealed class SharedFundingInput { - abstract val info: Transactions.InputInfo - abstract val weight: Int - abstract fun sign(channelKeys: ChannelKeys, tx: Transaction): ByteVector64 - - data class Multisig2of2(override val info: Transactions.InputInfo, val fundingTxIndex: Long, val remoteFundingPubkey: PublicKey) : SharedFundingInput() { - - constructor(commitment: Commitment) : this( - info = commitment.commitInput, - fundingTxIndex = commitment.fundingTxIndex, - remoteFundingPubkey = commitment.remoteFundingPubkey - ) +data class SharedFundingInput( + val info: Transactions.InputInfo, + val fundingTxIndex: Long, + val remoteFundingPubkey: PublicKey, + val commitmentFormat: Transactions.CommitmentFormat, +) { + constructor(channelKeys: ChannelKeys, commitment: Commitment) : this( + info = commitment.commitInput(channelKeys), + fundingTxIndex = commitment.fundingTxIndex, + remoteFundingPubkey = commitment.remoteFundingPubkey, + commitmentFormat = commitment.commitmentFormat, + ) - // This value was computed assuming 73 bytes signatures (worst-case scenario). - override val weight: Int = Multisig2of2.weight + val weight: Int = commitmentFormat.fundingInputWeight - override fun sign(channelKeys: ChannelKeys, tx: Transaction): ByteVector64 { - val fundingKey = channelKeys.fundingKey(fundingTxIndex) - return Transactions.sign(Transactions.TransactionWithInputInfo.SpliceTx(info, tx), fundingKey) - } - - companion object { - const val weight: Int = 388 - } + fun sign(channelKeys: ChannelKeys, tx: Transaction, spentUtxos: Map): ChannelSpendSignature.IndividualSignature { + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + return Transactions.SpliceTx(info, tx).sign(fundingKey, remoteFundingPubkey, spentUtxos) } } @@ -79,12 +73,22 @@ data class InteractiveTxParams( val sharedInput: SharedFundingInput?, val remoteFundingPubkey: PublicKey, val localOutputs: List, + val commitmentFormat: Transactions.CommitmentFormat, val lockTime: Long, val dustLimit: Satoshi, val targetFeerate: FeeratePerKw ) { - constructor(channelId: ByteVector32, isInitiator: Boolean, localContribution: Satoshi, remoteContribution: Satoshi, remoteFundingPubKey: PublicKey, lockTime: Long, dustLimit: Satoshi, targetFeerate: FeeratePerKw) : - this(channelId, isInitiator, localContribution, remoteContribution, null, remoteFundingPubKey, listOf(), lockTime, dustLimit, targetFeerate) + constructor( + channelId: ByteVector32, + isInitiator: Boolean, + localContribution: Satoshi, + remoteContribution: Satoshi, + remoteFundingPubKey: PublicKey, + lockTime: Long, + dustLimit: Satoshi, + commitmentFormat: Transactions.CommitmentFormat, + targetFeerate: FeeratePerKw + ) : this(channelId, isInitiator, localContribution, remoteContribution, null, remoteFundingPubKey, listOf(), commitmentFormat, lockTime, dustLimit, targetFeerate) /** Amount of the new funding output, which is the sum of the shared input, if any, and both sides' contributions. */ val fundingAmount: Satoshi = (sharedInput?.info?.txOut?.amount ?: 0.sat) + localContribution + remoteContribution @@ -96,8 +100,8 @@ data class InteractiveTxParams( val serialIdParity = if (isInitiator) 0 else 1 fun fundingPubkeyScript(channelKeys: ChannelKeys): ByteVector { - val fundingTxIndex = (sharedInput as? SharedFundingInput.Multisig2of2)?.let { it.fundingTxIndex + 1 } ?: 0 - return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingKey(fundingTxIndex).publicKey(), remoteFundingPubkey) + val fundingTxIndex = sharedInput?.let { it.fundingTxIndex + 1 } ?: 0 + return Transactions.makeFundingScript(channelKeys.fundingKey(fundingTxIndex).publicKey(), remoteFundingPubkey, commitmentFormat).pubkeyScript } fun liquidityFees(purchase: LiquidityAds.Purchase?): MilliSatoshi = purchase?.let { l -> @@ -250,8 +254,16 @@ sealed class FundingContributionFailure { data class FundingContributions(val inputs: List, val outputs: List) { companion object { /** Compute our local splice contribution using all the funds available in our wallet. */ - fun computeSpliceContribution(isInitiator: Boolean, commitment: Commitment, walletInputs: List, localOutputs: List, isLiquidityPurchase: Boolean, targetFeerate: FeeratePerKw): Satoshi { - val weight = computeWeightPaid(isInitiator, commitment, walletInputs, localOutputs) + fun computeSpliceContribution( + channelKeys: ChannelKeys, + isInitiator: Boolean, + commitment: Commitment, + walletInputs: List, + localOutputs: List, + isLiquidityPurchase: Boolean, + targetFeerate: FeeratePerKw + ): Satoshi { + val weight = computeWeightPaid(channelKeys, isInitiator, commitment, walletInputs, localOutputs) val fees = Transactions.weight2fee(targetFeerate, weight) return when { // When buying inbound liquidity, we may not have enough funds in our current balance to pay on-chain fees. @@ -321,7 +333,7 @@ data class FundingContributions(val inputs: List, v val changeOutput = when (changePubKey) { null -> listOf() else -> { - val weightWithChange = computeWeightPaid(params.isInitiator, sharedUtxo?.first, fundingPubkeyScript, walletInputs, localOutputs + listOf(TxOut(0.sat, Script.pay2wpkh(Transactions.PlaceHolderPubKey)))) + val weightWithChange = computeWeightPaid(params.isInitiator, sharedUtxo?.first, fundingPubkeyScript, walletInputs, localOutputs + listOf(TxOut(0.sat, Script.pay2wpkh(changePubKey)))) val changeAmount = totalAmountIn - totalAmountOut - Transactions.weight2fee(params.targetFeerate, weightWithChange) if (params.dustLimit <= changeAmount) { listOf(InteractiveTxOutput.Local.Change(0, changeAmount, Script.write(Script.pay2wpkh(changePubKey)).byteVector())) @@ -378,11 +390,11 @@ data class FundingContributions(val inputs: List, v } } - fun computeWeightPaid(isInitiator: Boolean, commitment: Commitment, walletInputs: List, localOutputs: List): Int = + fun computeWeightPaid(channelKeys: ChannelKeys, isInitiator: Boolean, commitment: Commitment, walletInputs: List, localOutputs: List): Int = computeWeightPaid( isInitiator, - SharedFundingInput.Multisig2of2(commitment.commitInput, commitment.fundingTxIndex, Transactions.PlaceHolderPubKey), - commitment.commitInput.txOut.publicKeyScript, + SharedFundingInput(channelKeys, commitment), + commitment.commitInput(channelKeys).txOut.publicKeyScript, walletInputs, localOutputs ) @@ -459,10 +471,10 @@ data class SharedTransaction( return Transaction(2, inputs, outputs, lockTime) } - fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { + fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalChannelParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { val unsignedTx = buildUnsignedTx() val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) - val sharedSig = fundingParams.sharedInput?.sign(channelKeys, unsignedTx) + val sharedSig = fundingParams.sharedInput?.sign(channelKeys, unsignedTx, spentOutputs) // NB: the order in this list must match the order of the transaction's inputs. val previousOutputs = unsignedTx.txIn.map { spentOutputs[it.outPoint]!! } @@ -524,7 +536,7 @@ data class SharedTransaction( } }.filterNotNull() - return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) + return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig?.sig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) } } @@ -550,14 +562,12 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, if (remoteSigs.witnesses.size != tx.remoteOnlyInputs().size) return null if (remoteSigs.txId != localSigs.txId) return null val sharedSigs = fundingParams.sharedInput?.let { - when (it) { - is SharedFundingInput.Multisig2of2 -> Scripts.witness2of2( - localSigs.previousFundingTxSig ?: return null, - remoteSigs.previousFundingTxSig ?: return null, - channelKeys.fundingKey(it.fundingTxIndex).publicKey(), - it.remoteFundingPubkey, - ) - } + Scripts.witness2of2( + localSigs.previousFundingTxSig ?: return null, + remoteSigs.previousFundingTxSig ?: return null, + channelKeys.fundingKey(it.fundingTxIndex).publicKey(), + it.remoteFundingPubkey, + ) } val fullySignedTx = FullySignedSharedTransaction(tx, localSigs, remoteSigs, sharedSigs) return when (runTrying { fullySignedTx.signedTx.correctlySpends(tx.spentOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) { @@ -1014,7 +1024,9 @@ data class InteractiveTxSigningSession( val fundingParams: InteractiveTxParams, val fundingTxIndex: Long, val fundingTx: PartiallySignedSharedTransaction, + val localCommitParams: CommitParams, val localCommit: Either, + val remoteCommitParams: CommitParams, val remoteCommit: RemoteCommit, ) { @@ -1026,47 +1038,60 @@ data class InteractiveTxSigningSession( // | |<------- tx_signatures ------| | // +-------+ +-------+ - val commitInput: Transactions.InputInfo = when (localCommit) { - is Either.Left -> localCommit.value.commitTx.input - is Either.Right -> localCommit.value.publishableTxs.commitTx.input - } // This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not. val reconnectNextLocalCommitmentNumber = when (localCommit) { is Either.Left -> localCommit.value.index is Either.Right -> localCommit.value.index + 1 } + fun localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex) + + fun commitInput(fundingKey: PrivateKey): Transactions.InputInfo { + val fundingScript = Transactions.makeFundingScript(fundingKey.publicKey(), fundingParams.remoteFundingPubkey, fundingParams.commitmentFormat).pubkeyScript + val fundingOutput = OutPoint(fundingTx.txId, fundingTx.tx.buildUnsignedTx().txOut.indexOfFirst { it.amount == fundingParams.fundingAmount && it.publicKeyScript == fundingScript }.toLong()) + return Transactions.InputInfo(fundingOutput, TxOut(fundingParams.fundingAmount, fundingScript)) + } + + fun commitInput(channelKeys: ChannelKeys): Transactions.InputInfo = commitInput(localFundingKey(channelKeys)) + fun receiveCommitSig(channelKeys: ChannelKeys, channelParams: ChannelParams, remoteCommitSig: CommitSig, currentBlockHeight: Long, logger: MDCLogger): Pair { return when (localCommit) { is Either.Left -> { val localCommitIndex = localCommit.value.index + val fundingKey = localFundingKey(channelKeys) val commitKeys = channelKeys.localCommitmentKeys(channelParams, localCommitIndex) + val fundingInput = commitInput(fundingKey) when (val signedLocalCommit = LocalCommit.fromCommitSig( channelParams = channelParams, + commitParams = localCommitParams, commitKeys = commitKeys, - fundingKey = channelKeys.fundingKey(fundingTxIndex), + fundingKey = fundingKey, remoteFundingPubKey = fundingParams.remoteFundingPubkey, - commitInput = commitInput, + commitInput = fundingInput, commit = remoteCommitSig, localCommitIndex = localCommitIndex, + commitmentFormat = fundingParams.commitmentFormat, spec = localCommit.value.spec, log = logger )) { - is Either.Left -> { - val fundingKey = channelKeys.fundingKey(fundingTxIndex) - val localSigOfLocalTx = Transactions.sign(localCommit.value.commitTx, fundingKey) - val signedLocalCommitTx = Transactions.addSigs(localCommit.value.commitTx, fundingKey.publicKey(), fundingParams.remoteFundingPubkey, localSigOfLocalTx, remoteCommitSig.signature) - logger.info { "interactiveTxSession=$this" } - logger.info { "channelParams=$channelParams" } - logger.info { "fundingKey=${fundingKey.publicKey()}" } - logger.info { "localSigOfLocalTx=$localSigOfLocalTx" } - logger.info { "signedLocalCommitTx=$signedLocalCommitTx" } - Pair(this, InteractiveTxSigningSessionAction.AbortFundingAttempt(signedLocalCommit.value)) - } + is Either.Left -> Pair(this, InteractiveTxSigningSessionAction.AbortFundingAttempt(signedLocalCommit.value)) is Either.Right -> { if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) { val fundingStatus = LocalFundingStatus.UnconfirmedFundingTx(fundingTx, fundingParams, currentBlockHeight) - val commitment = Commitment(fundingTxIndex, fundingParams.remoteFundingPubkey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit.value, remoteCommit, nextRemoteCommit = null) + val commitment = Commitment( + fundingTxIndex, + fundingInput.outPoint, + fundingParams.fundingAmount, + fundingParams.remoteFundingPubkey, + fundingStatus, + RemoteFundingStatus.NotLocked, + fundingParams.commitmentFormat, + localCommitParams, + signedLocalCommit.value, + remoteCommitParams, + remoteCommit, + nextRemoteCommit = null + ) val action = InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs) Pair(this.copy(localCommit = Either.Right(signedLocalCommit.value)), action) } else { @@ -1085,8 +1110,22 @@ data class InteractiveTxSigningSession( is Either.Right -> when (val fullySignedTx = fundingTx.addRemoteSigs(channelKeys, fundingParams, remoteTxSigs)) { null -> Either.Left(InteractiveTxSigningSessionAction.AbortFundingAttempt(InvalidFundingSignature(fundingParams.channelId, fundingTx.txId))) else -> { + val fundingInput = commitInput(channelKeys) val fundingStatus = LocalFundingStatus.UnconfirmedFundingTx(fullySignedTx, fundingParams, currentBlockHeight) - val commitment = Commitment(fundingTxIndex, fundingParams.remoteFundingPubkey, fundingStatus, RemoteFundingStatus.NotLocked, localCommit.value, remoteCommit, nextRemoteCommit = null) + val commitment = Commitment( + fundingTxIndex, + fundingInput.outPoint, + fundingParams.fundingAmount, + fundingParams.remoteFundingPubkey, + fundingStatus, + RemoteFundingStatus.NotLocked, + fundingParams.commitmentFormat, + localCommitParams, + localCommit.value, + remoteCommitParams, + remoteCommit, + nextRemoteCommit = null + ) Either.Right(InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs)) } } @@ -1095,12 +1134,14 @@ data class InteractiveTxSigningSession( companion object { /** A local commitment for which we haven't received our peer's signatures. */ - data class UnsignedLocalCommit(val index: Long, val spec: CommitmentSpec, val commitTx: Transactions.TransactionWithInputInfo.CommitTx, val htlcTxs: List) + data class UnsignedLocalCommit(val index: Long, val spec: CommitmentSpec, val txId: TxId) fun create( session: InteractiveTxSession, keyManager: KeyManager, channelParams: ChannelParams, + localCommitParams: CommitParams, + remoteCommitParams: CommitParams, fundingParams: InteractiveTxParams, fundingTxIndex: Long, sharedTx: SharedTransaction, @@ -1116,10 +1157,12 @@ data class InteractiveTxSigningSession( val localCommitKeys = channelKeys.localCommitmentKeys(channelParams, localCommitmentIndex) val remoteCommitKeys = channelKeys.remoteCommitmentKeys(channelParams, remotePerCommitmentPoint) val unsignedTx = sharedTx.buildUnsignedTx() - val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) } + val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) }.toLong() val liquidityFees = fundingParams.liquidityFees(liquidityPurchase) return Helpers.Funding.makeCommitTxs( channelParams = channelParams, + localCommitParams = localCommitParams, + remoteCommitParams = remoteCommitParams, fundingAmount = sharedTx.sharedOutput.amount, toLocal = sharedTx.sharedOutput.localAmount - liquidityFees, toRemote = sharedTx.sharedOutput.remoteAmount + liquidityFees, @@ -1127,6 +1170,7 @@ data class InteractiveTxSigningSession( localCommitmentIndex = localCommitmentIndex, remoteCommitmentIndex = remoteCommitmentIndex, commitTxFeerate = commitTxFeerate, + commitmentFormat = fundingParams.commitmentFormat, fundingTxId = unsignedTx.txid, fundingTxOutputIndex = sharedOutputIndex, localFundingKey = fundingKey, @@ -1134,21 +1178,23 @@ data class InteractiveTxSigningSession( localCommitKeys = localCommitKeys, remoteCommitKeys = remoteCommitKeys, ).map { firstCommitTx -> - val localSigOfRemoteCommitTx = Transactions.sign(firstCommitTx.remoteCommitTx, fundingKey) - val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { Transactions.sign(it, remoteCommitKeys.ourHtlcKey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } + val localSigOfRemoteCommitTx = firstCommitTx.remoteCommitTx.sign(fundingKey, fundingParams.remoteFundingPubkey) + val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { it.localSig(remoteCommitKeys) } val alternativeSigs = if (firstCommitTx.remoteHtlcTxs.isEmpty()) { val commitSigTlvs = Commitments.alternativeFeerates.map { feerate -> val alternativeSpec = firstCommitTx.remoteSpec.copy(feerate = feerate) val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( channelParams = channelParams, + commitParams = remoteCommitParams, commitKeys = remoteCommitKeys, commitTxNumber = remoteCommitmentIndex, localFundingKey = fundingKey, remoteFundingPubKey = fundingParams.remoteFundingPubkey, commitmentInput = firstCommitTx.remoteCommitTx.input, + commitmentFormat = fundingParams.commitmentFormat, spec = alternativeSpec ) - val sig = Transactions.sign(alternativeRemoteCommitTx, fundingKey) + val sig = alternativeRemoteCommitTx.sign(fundingKey, fundingParams.remoteFundingPubkey).sig CommitSigTlv.AlternativeFeerateSig(feerate, sig) } TlvStream(CommitSigTlv.AlternativeFeerateSigs(commitSigTlvs) as CommitSigTlv) @@ -1157,10 +1203,10 @@ data class InteractiveTxSigningSession( } val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, alternativeSigs) // We haven't received the remote commit_sig: we don't have local htlc txs yet. - val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf()) + val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx.tx.txid) val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint) val signedFundingTx = sharedTx.sign(session, keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) - Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) + Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, localCommitParams, Either.Left(unsignedLocalCommit), remoteCommitParams, remoteCommit), commitSig) } } @@ -1193,6 +1239,7 @@ sealed class QuiescenceNegotiation : SpliceStatus() { abstract class Initiator : QuiescenceNegotiation() { abstract val command: ChannelCommand.Commitment.Splice.Request } + abstract class NonInitiator : QuiescenceNegotiation() } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt index b841a53da..dc50a9637 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt @@ -11,17 +11,12 @@ import fr.acinq.lightning.blockchain.WatchSpent import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.channel.* -import fr.acinq.lightning.channel.Helpers.Closing.claimCurrentLocalCommitTxOutputs -import fr.acinq.lightning.channel.Helpers.Closing.claimRemoteCommitMainOutput -import fr.acinq.lightning.channel.Helpers.Closing.claimRemoteCommitTxOutputs -import fr.acinq.lightning.channel.Helpers.Closing.claimRevokedRemoteCommitTxOutputs -import fr.acinq.lightning.channel.Helpers.Closing.getRemotePerCommitmentSecret import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.db.ChannelCloseOutgoingPayment.ChannelClosingType import fr.acinq.lightning.logging.LoggingContext import fr.acinq.lightning.logging.MDCLogger -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.* @@ -119,7 +114,7 @@ sealed class ChannelState { } } - private fun ChannelContext.emitMutualCloseEvents(state: ChannelStateWithCommitments, mutualCloseTx: ClosingTx): List { + private fun ChannelContext.emitMutualCloseEvents(state: ChannelStateWithCommitments, mutualCloseTx: Transactions.ClosingTx): List { val channelBalance = state.commitments.latest.localCommit.spec.toLocal val finalAmount = mutualCloseTx.toLocalOutput?.amount ?: 0.sat val address = mutualCloseTx.toLocalOutput?.publicKeyScript?.let { Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, it.toByteArray()).right } ?: "unknown" @@ -131,7 +126,7 @@ sealed class ChannelState { miningFee = channelBalance.truncateToSatoshi() - finalAmount, address = address, txId = mutualCloseTx.tx.txid, - isSentToDefaultAddress = mutualCloseTx.toLocalOutput?.publicKeyScript == state.commitments.params.localParams.defaultFinalScriptPubKey, + isSentToDefaultAddress = mutualCloseTx.toLocalOutput?.publicKeyScript == state.commitments.channelParams.localParams.defaultFinalScriptPubKey, closingType = ChannelClosingType.Mutual ) ) @@ -143,7 +138,7 @@ sealed class ChannelState { val channelBalance = state.commitments.latest.localCommit.spec.toLocal val address = Bitcoin.addressFromPublicKeyScript( chainHash = staticParams.nodeParams.chainHash, - pubkeyScript = state.commitments.params.localParams.defaultFinalScriptPubKey.toByteArray() // force close always send to the default script + pubkeyScript = state.commitments.channelParams.localParams.defaultFinalScriptPubKey.toByteArray() // force close always send to the default script ).right ?: "unknown" return buildList { if (channelBalance > 0.msat) { @@ -181,7 +176,7 @@ sealed class ChannelState { } } - internal fun ChannelContext.doPublish(tx: ClosingTx, channelId: ByteVector32): List = listOf( + internal fun ChannelContext.doPublish(tx: Transactions.ClosingTx, channelId: ByteVector32): List = listOf( ChannelAction.Blockchain.PublishTx(tx), ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, tx.tx, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ClosingTxConfirmed)) ) @@ -289,9 +284,14 @@ sealed class ChannelState { sealed class PersistedChannelState : ChannelState() { abstract val channelId: ByteVector32 + fun ChannelContext.channelKeys(): ChannelKeys = when (val state = this@PersistedChannelState) { + is WaitForFundingSigned -> state.channelParams.localParams.channelKeys(keyManager) + is ChannelStateWithCommitments -> state.commitments.channelKeys(keyManager) + } + internal fun ChannelContext.createChannelReestablish(): ChannelReestablish = when (val state = this@PersistedChannelState) { is WaitForFundingSigned -> { - val myFirstPerCommitmentPoint = keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).commitmentPoint(0) + val myFirstPerCommitmentPoint = channelKeys().commitmentPoint(0) ChannelReestablish( channelId = channelId, nextLocalCommitmentNumber = state.signingSession.reconnectNextLocalCommitmentNumber, @@ -303,7 +303,7 @@ sealed class PersistedChannelState : ChannelState() { } is ChannelStateWithCommitments -> { val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes - val myCurrentPerCommitmentPoint = keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath).commitmentPoint(state.commitments.localCommitIndex) + val myCurrentPerCommitmentPoint = channelKeys().commitmentPoint(state.commitments.localCommitIndex) // If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig. val nextLocalCommitmentNumber = when (state) { is WaitForFundingConfirmed -> when (state.rbfStatus) { @@ -342,12 +342,10 @@ sealed class PersistedChannelState : ChannelState() { sealed class ChannelStateWithCommitments : PersistedChannelState() { abstract val commitments: Commitments override val channelId: ByteVector32 get() = commitments.channelId - val isChannelOpener: Boolean get() = commitments.params.localParams.isChannelOpener - val paysCommitTxFees: Boolean get() = commitments.params.localParams.paysCommitTxFees + val isChannelOpener: Boolean get() = commitments.channelParams.localParams.isChannelOpener + val paysCommitTxFees: Boolean get() = commitments.channelParams.localParams.paysCommitTxFees val remoteNodeId: PublicKey get() = commitments.remoteNodeId - fun ChannelContext.channelKeys(): ChannelKeys = commitments.params.localParams.channelKeys(keyManager) - abstract fun updateCommitments(input: Commitments): ChannelStateWithCommitments /** @@ -358,7 +356,8 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { logger.info { "funding txid=${w.tx.txid} was confirmed at blockHeight=${w.blockHeight} txIndex=${w.txIndex}" } return commitments.run { updateLocalFundingConfirmed(w.tx, w.blockHeight, w.txIndex).map { (commitments1, commitment) -> - val watchSpent = WatchSpent(channelId, commitment.fundingTxId, commitment.commitInput.outPoint.index.toInt(), commitment.commitInput.txOut.publicKeyScript, WatchSpent.ChannelSpent(commitment.fundingAmount)) + val commitInput = commitment.commitInput(channelKeys()) + val watchSpent = WatchSpent(channelId, commitment.fundingTxId, commitInput.outPoint.index.toInt(), commitInput.txOut.publicKeyScript, WatchSpent.ChannelSpent(commitment.fundingAmount)) val actions = buildList { newlyLocked(commitments, commitments1).forEach { add(ChannelAction.Storage.SetLocked(it.fundingTxId)) } add(ChannelAction.Blockchain.SendWatch(watchSpent)) @@ -439,10 +438,10 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { internal suspend fun ChannelContext.handlePotentialForceClose(w: WatchSpentTriggered): Pair> = when { w.event !is WatchSpent.ChannelSpent -> Pair(this@ChannelStateWithCommitments, listOf()) commitments.all.any { it.fundingTxId == w.spendingTx.txid } -> Pair(this@ChannelStateWithCommitments, listOf()) // if the spending tx is itself a funding tx, this is a splice and there is nothing to do - w.spendingTx.txid == commitments.latest.localCommit.publishableTxs.commitTx.tx.txid -> spendLocalCurrent() + w.spendingTx.txid == commitments.latest.localCommit.txId -> spendLocalCurrent() w.spendingTx.txid == commitments.latest.remoteCommit.txid -> handleRemoteSpentCurrent(w.spendingTx, commitments.latest) - w.spendingTx.txid == commitments.latest.nextRemoteCommit?.commit?.txid -> handleRemoteSpentNext(w.spendingTx, commitments.latest) - w.spendingTx.txIn.any { it.outPoint == commitments.latest.commitInput.outPoint } -> handleRemoteSpentOther(w.spendingTx) + w.spendingTx.txid == commitments.latest.nextRemoteCommit?.txid -> handleRemoteSpentNext(w.spendingTx, commitments.latest) + w.spendingTx.txIn.any { it.outPoint == commitments.latest.fundingInput } -> handleRemoteSpentOther(w.spendingTx) else -> when (val commitment = commitments.resolveCommitment(w.spendingTx)) { is Commitment -> { logger.warning { "a commit tx for an older commitment has been published fundingTxId=${commitment.fundingTxId} fundingTxIndex=${commitment.fundingTxIndex}" } @@ -467,9 +466,9 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { internal suspend fun ChannelContext.handleRemoteSpentCurrent(commitTx: Transaction, commitment: FullCommitment): Pair> { logger.warning { "they published their current commit in txid=${commitTx.txid}" } require(commitTx.txid == commitment.remoteCommit.txid) { "txid mismatch" } - - val remoteCommitPublished = claimRemoteCommitTxOutputs(channelKeys(), commitment, commitment.remoteCommit, commitTx, currentOnChainFeerates()) - + val (remoteCommitPublished, closingTxs) = Helpers.Closing.RemoteClose.run { + claimCommitTxOutputs(channelKeys(), commitment, commitment.remoteCommit, commitTx, currentOnChainFeerates()) + } val nextState = when (this@ChannelStateWithCommitments) { is Closing -> this@ChannelStateWithCommitments.copy(remoteCommitPublished = remoteCommitPublished) is Negotiating -> Closing( @@ -485,21 +484,20 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { remoteCommitPublished = remoteCommitPublished ) } - return Pair(nextState, buildList { add(ChannelAction.Storage.StoreState(nextState)) - addAll(remoteCommitPublished.run { doPublish(staticParams.nodeParams, channelId) }) + addAll(remoteCommitPublished.run { doPublish(staticParams.nodeParams, channelId, closingTxs) }) }) } internal suspend fun ChannelContext.handleRemoteSpentNext(commitTx: Transaction, commitment: FullCommitment): Pair> { logger.warning { "they published their next commit in txid=${commitTx.txid}" } require(commitment.nextRemoteCommit != null) { "next remote commit must be defined" } - val remoteCommit = commitment.nextRemoteCommit.commit + val remoteCommit = commitment.nextRemoteCommit require(commitTx.txid == remoteCommit.txid) { "txid mismatch" } - - val remoteCommitPublished = claimRemoteCommitTxOutputs(channelKeys(), commitment, remoteCommit, commitTx, currentOnChainFeerates()) - + val (remoteCommitPublished, closingTxs) = Helpers.Closing.RemoteClose.run { + claimCommitTxOutputs(channelKeys(), commitment, remoteCommit, commitTx, currentOnChainFeerates()) + } val nextState = when (this@ChannelStateWithCommitments) { is Closing -> copy(nextRemoteCommitPublished = remoteCommitPublished) is Negotiating -> Closing( @@ -515,18 +513,23 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { nextRemoteCommitPublished = remoteCommitPublished ) } - return Pair(nextState, buildList { add(ChannelAction.Storage.StoreState(nextState)) - addAll(remoteCommitPublished.run { doPublish(staticParams.nodeParams, channelId) }) + addAll(remoteCommitPublished.run { doPublish(staticParams.nodeParams, channelId, closingTxs) }) }) } internal suspend fun ChannelContext.handleRemoteSpentOther(tx: Transaction): Pair> { logger.warning { "funding tx spent in txid=${tx.txid}" } - return getRemotePerCommitmentSecret(commitments.params, channelKeys(), commitments.remotePerCommitmentSecrets, tx)?.let { (remotePerCommitmentSecret, commitmentNumber) -> - logger.warning { "txid=${tx.txid} was a revoked commitment, publishing the penalty tx" } - val revokedCommitPublished = claimRevokedRemoteCommitTxOutputs(commitments.params, channelKeys(), tx, remotePerCommitmentSecret, currentOnChainFeerates()) + return Helpers.Closing.RevokedClose.getRemotePerCommitmentSecret(commitments.channelParams, channelKeys(), commitments.remotePerCommitmentSecrets, tx)?.let { (remotePerCommitmentSecret, commitmentNumber) -> + logger.warning { "txId=${tx.txid} was a revoked commitment for commitmentNumber=$commitmentNumber, publishing penalty transactions" } + val (revokedCommitPublished, closingTxs) = Helpers.Closing.RevokedClose.run { + // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect. + val toSelfDelay = commitments.latest.remoteCommitParams.toSelfDelay + val commitmentFormat = commitments.latest.commitmentFormat + val dustLimit = commitments.latest.localCommitParams.dustLimit + claimCommitTxOutputs(commitments.channelParams, channelKeys(), tx, remotePerCommitmentSecret, dustLimit, toSelfDelay, commitmentFormat, currentOnChainFeerates()) + } val ex = FundingTxSpent(channelId, tx.txid) val error = Error(channelId, ex.message) val nextState = when (this@ChannelStateWithCommitments) { @@ -550,7 +553,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { } Pair(nextState, buildList { add(ChannelAction.Storage.StoreState(nextState)) - addAll(revokedCommitPublished.run { doPublish(staticParams.nodeParams, channelId) }) + addAll(revokedCommitPublished.run { doPublish(staticParams.nodeParams, channelId, closingTxs) }) add(ChannelAction.Message.Send(error)) add(ChannelAction.Storage.GetHtlcInfos(revokedCommitPublished.commitTx.txid, commitmentNumber)) }) @@ -558,16 +561,34 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { when (this@ChannelStateWithCommitments) { is WaitForRemotePublishFutureCommitment -> { logger.warning { "they published their future commit (because we asked them to) in txid=${tx.txid}" } - val commitKeys = channelKeys().remoteCommitmentKeys(commitments.params, remoteChannelReestablish.myCurrentPerCommitmentPoint) - val remoteCommitPublished = claimRemoteCommitMainOutput(commitKeys, tx, commitments.params.localParams.dustLimit, commitments.params.localParams.defaultFinalScriptPubKey, currentOnChainFeerates().claimMainFeerate) + val commitKeys = channelKeys().remoteCommitmentKeys(commitments.channelParams, remoteChannelReestablish.myCurrentPerCommitmentPoint) + val mainTx = Helpers.Closing.RemoteClose.run { + claimMainOutput( + commitKeys, + tx, + commitments.latest.localCommitParams.dustLimit, + commitments.latest.commitmentFormat, + commitments.channelParams.localParams.defaultFinalScriptPubKey, + currentOnChainFeerates().claimMainFeerate + ) + } + mainTx?.let { logger.warning { "our recovery transaction is tx=${it.tx}" } } + val remoteCommitPublished = RemoteCommitPublished( + commitTx = tx, + localOutput = mainTx?.input?.outPoint, + anchorOutput = null, + incomingHtlcs = mapOf(), + outgoingHtlcs = mapOf(), + irrevocablySpent = mapOf(), + ) val nextState = Closing( commitments = commitments, waitingSinceBlock = currentBlockHeight.toLong(), - futureRemoteCommitPublished = remoteCommitPublished + futureRemoteCommitPublished = remoteCommitPublished, ) Pair(nextState, buildList { add(ChannelAction.Storage.StoreState(nextState)) - addAll(remoteCommitPublished.run { doPublish(staticParams.nodeParams, channelId) }) + addAll(remoteCommitPublished.run { doPublish(staticParams.nodeParams, channelId, RemoteCommitSecondStageTransactions(mainTx, listOf())) }) }) } else -> { @@ -589,8 +610,26 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { } else -> { logger.warning { "they published an alternative commitment with feerate=${remoteCommit.spec.feerate} txid=${tx.txid}" } - val commitKeys = channelKeys().remoteCommitmentKeys(commitments.params, remoteCommit.remotePerCommitmentPoint) - val remoteCommitPublished = claimRemoteCommitMainOutput(commitKeys, tx, commitments.params.localParams.dustLimit, commitments.params.localParams.defaultFinalScriptPubKey, currentOnChainFeerates().claimMainFeerate) + // We only provide alternative feerate signatures when there are no pending HTLCs: we only need to claim our main output. + val commitKeys = channelKeys().remoteCommitmentKeys(commitments.channelParams, remoteCommit.remotePerCommitmentPoint) + val mainTx = Helpers.Closing.RemoteClose.run { + claimMainOutput( + commitKeys, + tx, + commitments.latest.localCommitParams.dustLimit, + commitments.latest.commitmentFormat, + commitments.channelParams.localParams.defaultFinalScriptPubKey, + currentOnChainFeerates().claimMainFeerate + ) + } + val remoteCommitPublished = RemoteCommitPublished( + commitTx = tx, + localOutput = mainTx?.input?.outPoint, + anchorOutput = null, + incomingHtlcs = mapOf(), + outgoingHtlcs = mapOf(), + irrevocablySpent = mapOf(), + ) val nextState = when (this@ChannelStateWithCommitments) { is Closing -> this@ChannelStateWithCommitments.copy(remoteCommitPublished = remoteCommitPublished) is Negotiating -> Closing(commitments, waitingSinceBlock, proposedClosingTxs.flatMap { it.all }, publishedClosingTxs, remoteCommitPublished = remoteCommitPublished) @@ -598,7 +637,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { } return Pair(nextState, buildList { add(ChannelAction.Storage.StoreState(nextState)) - addAll(remoteCommitPublished.run { doPublish(staticParams.nodeParams, channelId) }) + addAll(remoteCommitPublished.run { doPublish(staticParams.nodeParams, channelId, RemoteCommitSecondStageTransactions(mainTx, listOf())) }) }) } } @@ -613,18 +652,14 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { is Closing -> this@ChannelStateWithCommitments.futureRemoteCommitPublished != null else -> false } - return if (outdatedCommitment) { logger.warning { "we have an outdated commitment: will not publish our local tx" } Pair(this@ChannelStateWithCommitments, listOf()) } else { - val commitTx = commitments.latest.localCommit.publishableTxs.commitTx.tx - val localCommitPublished = claimCurrentLocalCommitTxOutputs( - channelKeys(), - commitments.latest, - commitTx, - currentOnChainFeerates() - ) + val commitTx = commitments.latest.fullySignedCommitTx(channelKeys()) + val (localCommitPublished, closingTxs) = Helpers.Closing.LocalClose.run { + claimCommitTxOutputs(channelKeys(), commitments.latest, commitTx, currentOnChainFeerates()) + } val nextState = when (this@ChannelStateWithCommitments) { is Closing -> copy(localCommitPublished = localCommitPublished) is Negotiating -> Closing( @@ -640,10 +675,9 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { localCommitPublished = localCommitPublished ) } - Pair(nextState, buildList { add(ChannelAction.Storage.StoreState(nextState)) - addAll(localCommitPublished.run { doPublish(staticParams.nodeParams, channelId) }) + addAll(localCommitPublished.run { doPublish(staticParams.nodeParams, channelId, closingTxs) }) }) } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt index b66db0103..47520064b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt @@ -8,15 +8,8 @@ import fr.acinq.lightning.blockchain.WatchConfirmedTriggered import fr.acinq.lightning.blockchain.WatchSpent import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.* -import fr.acinq.lightning.channel.Helpers.Closing.claimCurrentLocalCommitTxOutputs -import fr.acinq.lightning.channel.Helpers.Closing.claimRemoteCommitTxOutputs -import fr.acinq.lightning.channel.Helpers.Closing.claimRevokedHtlcTxOutputs -import fr.acinq.lightning.channel.Helpers.Closing.claimRevokedRemoteCommitTxHtlcOutputs -import fr.acinq.lightning.channel.Helpers.Closing.extractPreimages -import fr.acinq.lightning.channel.Helpers.Closing.onChainOutgoingHtlcs -import fr.acinq.lightning.channel.Helpers.Closing.overriddenOutgoingHtlcs -import fr.acinq.lightning.channel.Helpers.Closing.trimmedOrTimedOutHtlcs -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx +import fr.acinq.lightning.channel.Helpers.Closing +import fr.acinq.lightning.transactions.Transactions.ClosingTx import fr.acinq.lightning.transactions.incomings import fr.acinq.lightning.transactions.outgoings import fr.acinq.lightning.utils.getValue @@ -107,7 +100,7 @@ data class Closing( val newState = this@Closing.copy(commitments = commitments1) // This commitment may be revoked: we need to verify that its index matches our latest known index before overwriting our previous commitments. when { - watch.tx.txid == commitments1.latest.localCommit.publishableTxs.commitTx.tx.txid -> { + watch.tx.txid == commitments1.latest.localCommit.txId -> { // Our local commit has been published from the outside, it's unexpected but let's deal with it anyway. newState.run { spendLocalCurrent() } } @@ -115,7 +108,7 @@ data class Closing( // Our counterparty may attempt to spend its last commit tx at any time. newState.run { handleRemoteSpentCurrent(watch.tx, commitments1.latest) } } - watch.tx.txid == commitments1.latest.nextRemoteCommit?.commit?.txid && commitments1.remoteCommitIndex == commitments.remoteCommitIndex && commitments.remoteNextCommitInfo.isLeft -> { + watch.tx.txid == commitments1.latest.nextRemoteCommit?.txid && commitments1.remoteCommitIndex == commitments.remoteCommitIndex && commitments.remoteNextCommitInfo.isLeft -> { // Our counterparty may attempt to spend its next commit tx at any time. newState.run { handleRemoteSpentNext(watch.tx, commitments1.latest) } } @@ -131,7 +124,7 @@ data class Closing( // outgoing payment handler so the fail command will be a no-op. val outgoingHtlcs = commitments.latest.localCommit.spec.htlcs.outgoings().toSet() + commitments.latest.remoteCommit.spec.htlcs.incomings().toSet() + - commitments.latest.nextRemoteCommit?.commit?.spec?.htlcs.orEmpty().incomings().toSet() + commitments.latest.nextRemoteCommit?.spec?.htlcs.orEmpty().incomings().toSet() val htlcSettledActions = outgoingHtlcs.mapNotNull { add -> commitments.payments[add.id]?.let { paymentId -> logger.info { "failing htlc #${add.id} paymentHash=${add.paymentHash} paymentId=$paymentId: overridden by revoked remote commit" } @@ -150,19 +143,36 @@ data class Closing( } WatchConfirmed.ClosingTxConfirmed -> { logger.info { "txid=${watch.tx.txid} has reached mindepth, updating closing state" } - // first we check if this tx belongs to one of the current local/remote commits, update it and update the channel data + val channelKeys = channelKeys() + val onChainActions = mutableListOf() + // First we check if this tx belongs to one of the current local/remote commits and update it status. val closing1 = this@Closing.copy( - localCommitPublished = localCommitPublished?.update(watch.tx), - remoteCommitPublished = remoteCommitPublished?.update(watch.tx), - nextRemoteCommitPublished = nextRemoteCommitPublished?.update(watch.tx), - futureRemoteCommitPublished = futureRemoteCommitPublished?.update(watch.tx), - revokedCommitPublished = revokedCommitPublished.map { it.update(watch.tx) } + localCommitPublished = localCommitPublished?.let { lcp -> + // If the tx is one of our HTLC txs, we now publish a 3rd-stage transaction that claims its output. + val (lcp1, htlcDelayedTxs) = Closing.LocalClose.run { claimHtlcDelayedOutput(lcp, channelKeys, commitments.latest, watch.tx, currentOnChainFeerates()) } + onChainActions.addAll(lcp1.run { doPublish(channelId, htlcDelayedTxs) }) + lcp1.updateIrrevocablySpent(watch.tx) + }, + remoteCommitPublished = remoteCommitPublished?.updateIrrevocablySpent(watch.tx), + nextRemoteCommitPublished = nextRemoteCommitPublished?.updateIrrevocablySpent(watch.tx), + futureRemoteCommitPublished = futureRemoteCommitPublished?.updateIrrevocablySpent(watch.tx), + revokedCommitPublished = revokedCommitPublished.map { rvk -> + // If the tx is one of our peer's HTLC txs, they were able to claim the output before us. + // In that case, we immediately publish a penalty transaction spending their HTLC tx to steal their funds. + // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect. + val toSelfDelay = commitments.latest.remoteCommitParams.toSelfDelay + val commitmentFormat = commitments.latest.commitmentFormat + val dustLimit = commitments.latest.localCommitParams.dustLimit + val (rvk1, penaltyTxs) = Closing.RevokedClose.run { claimHtlcTxOutputs(commitments.channelParams, channelKeys, rvk, watch.tx, dustLimit, toSelfDelay, commitmentFormat, currentOnChainFeerates()) } + onChainActions.addAll(rvk1.run { doPublish(channelId, penaltyTxs) }) + rvk1.updateIrrevocablySpent(watch.tx) + } ) // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold val htlcSettledActions = mutableListOf() val timedOutHtlcs = when (val closingType = closing1.closingTypeAlreadyKnown()) { - is LocalClose -> trimmedOrTimedOutHtlcs(closingType.localCommit, closingType.localCommitPublished, commitments.params.localParams.dustLimit, watch.tx) - is RemoteClose -> trimmedOrTimedOutHtlcs(closingType.remoteCommit, closingType.remoteCommitPublished, commitments.params.remoteParams.dustLimit, watch.tx) + is LocalClose -> Closing.run { trimmedOrTimedOutHtlcs(channelKeys, commitments.latest, closingType.localCommit, watch.tx) } + is RemoteClose -> Closing.run { trimmedOrTimedOutHtlcs(channelKeys, commitments.latest, closingType.remoteCommit, watch.tx) } is RevokedClose -> setOf() // revoked commitments are handled using [overriddenOutgoingHtlcs] below is RecoveryClose -> setOf() // we lose htlc outputs in option_data_loss_protect scenarios (future remote commit) is MutualClose -> setOf() @@ -181,7 +191,7 @@ data class Closing( } } // we also need to fail outgoing htlcs that we know will never reach the blockchain - overriddenOutgoingHtlcs(commitments.latest.localCommit, commitments.latest.remoteCommit, commitments.latest.nextRemoteCommit?.commit, closing1.revokedCommitPublished, watch.tx).forEach { add -> + Closing.overriddenOutgoingHtlcs(commitments.latest.localCommit, commitments.latest.remoteCommit, commitments.latest.nextRemoteCommit, closing1.revokedCommitPublished, watch.tx).forEach { add -> when (val paymentId = commitments.payments[add.id]) { null -> { // same as for fulfilling the htlc (no big deal) @@ -190,7 +200,7 @@ data class Closing( else -> { logger.info { "failing htlc #${add.id} paymentHash=${add.paymentHash} paymentId=$paymentId: overridden by confirmed commit" } val failure = when { - watch.tx.txid == commitments.latest.localCommit.publishableTxs.commitTx.tx.txid -> HtlcOverriddenByLocalCommit(channelId, add) + watch.tx.txid == commitments.latest.localCommit.txId -> HtlcOverriddenByLocalCommit(channelId, add) else -> HtlcOverriddenByRemoteCommit(channelId, add) } htlcSettledActions += ChannelAction.ProcessCmdRes.AddSettledFail(paymentId, add, ChannelAction.HtlcResult.Fail.OnChainFail(failure)) @@ -198,7 +208,7 @@ data class Closing( } } // for our outgoing payments, let's log something if we know that they will settle on chain - onChainOutgoingHtlcs(commitments.latest.localCommit, commitments.latest.remoteCommit, commitments.latest.nextRemoteCommit?.commit, watch.tx).forEach { add -> + Closing.onChainOutgoingHtlcs(commitments.latest.localCommit, commitments.latest.remoteCommit, commitments.latest.nextRemoteCommit, watch.tx).forEach { add -> commitments.payments[add.id]?.let { paymentId -> logger.info { "paymentId=$paymentId will settle on-chain (htlc #${add.id} sending ${add.amountMsat})" } } } val (nextState, closedActions) = when (val closingType = closing1.isClosed(watch.tx)) { @@ -210,6 +220,7 @@ data class Closing( } val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) + addAll(onChainActions) addAll(htlcSettledActions) addAll(closedActions) } @@ -242,11 +253,11 @@ data class Closing( // counterparty may attempt to spend its last commit tx at any time handleRemoteSpentCurrent(watch.spendingTx, commitments.latest) } - watch.spendingTx.txid == commitments.latest.nextRemoteCommit?.commit?.txid -> { + watch.spendingTx.txid == commitments.latest.nextRemoteCommit?.txid -> { // counterparty may attempt to spend its next commit tx at any time handleRemoteSpentNext(watch.spendingTx, commitments.latest) } - watch.spendingTx.txIn.map { it.outPoint }.contains(commitments.latest.commitInput.outPoint) -> { + watch.spendingTx.txIn.map { it.outPoint }.contains(commitments.latest.fundingInput) -> { // counterparty may attempt to spend a revoked commit tx at any time handleRemoteSpentOther(watch.spendingTx) } @@ -262,12 +273,11 @@ data class Closing( } } is WatchSpent.ClosingOutputSpent -> { - // when a remote or local commitment tx containing outgoing htlcs is published on the network, - // we watch it in order to extract payment preimage if funds are pulled by the counterparty - // we can then use these preimages to fulfill payments + // One of the outputs of the local/remote/revoked commit transaction or of an HTLC transaction was spent. + // We put a watch to be notified when the transaction confirms: it may double-spend one of our transactions. logger.info { "processing spent closing output with txid=${watch.spendingTx.txid} tx=${watch.spendingTx}" } val htlcSettledActions = mutableListOf() - extractPreimages(commitments.latest, watch.spendingTx).forEach { (htlc, preimage) -> + Closing.run { extractPreimages(commitments.latest, watch.spendingTx) }.forEach { (htlc, preimage) -> when (val paymentId = commitments.payments[htlc.id]) { null -> { // if we don't have a reference to the payment, it means that we already have forwarded the fulfill so that's not a big deal. @@ -280,25 +290,11 @@ data class Closing( } } } - val revokedCommitPublishActions = mutableListOf() - val revokedCommitPublished1 = revokedCommitPublished.map { rev -> - val (newRevokedCommitPublished, penaltyTxs) = claimRevokedHtlcTxOutputs(commitments.params, channelKeys(), rev, watch.spendingTx, currentOnChainFeerates()) - penaltyTxs.forEach { - revokedCommitPublishActions += ChannelAction.Blockchain.PublishTx(it) - revokedCommitPublishActions += ChannelAction.Blockchain.SendWatch(WatchSpent(channelId, watch.spendingTx, it.input.outPoint.index.toInt(), WatchSpent.ClosingOutputSpent(it.amountIn))) - } - newRevokedCommitPublished - } - val nextState = copy(revokedCommitPublished = revokedCommitPublished1) val actions = buildList { - add(ChannelAction.Storage.StoreState(nextState)) - // one of the outputs of the local/remote/revoked commit was spent - // we just put a watch to be notified when it is confirmed add(ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, watch.spendingTx, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ClosingTxConfirmed))) - addAll(revokedCommitPublishActions) addAll(htlcSettledActions) } - Pair(nextState, actions) + Pair(this@Closing, actions) } } } @@ -306,11 +302,17 @@ data class Closing( is ChannelCommand.Closing.GetHtlcInfosResponse -> { val index = revokedCommitPublished.indexOfFirst { it.commitTx.txid == cmd.revokedCommitTxId } if (index >= 0) { - val revokedCommitPublished1 = claimRevokedRemoteCommitTxHtlcOutputs(commitments.params, channelKeys(), revokedCommitPublished[index], currentOnChainFeerates(), cmd.htlcInfos) + // TODO: once we allow changing the commitment format during a splice, the value below may be incorrect. + val commitmentFormat = commitments.latest.commitmentFormat + val dustLimit = commitments.latest.localCommitParams.dustLimit + val htlcPenaltyTxs = Closing.RevokedClose.run { + claimHtlcOutputs(commitments.channelParams, channelKeys(), revokedCommitPublished[index], dustLimit, commitmentFormat, currentOnChainFeerates(), cmd.htlcInfos) + } + val revokedCommitPublished1 = revokedCommitPublished[index].copy(htlcOutputs = htlcPenaltyTxs.map { it.input.outPoint }.toSet()) val nextState = copy(revokedCommitPublished = revokedCommitPublished.updated(index, revokedCommitPublished1)) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) - addAll(revokedCommitPublished1.run { doPublish(staticParams.nodeParams, channelId) }) + addAll(revokedCommitPublished1.run { doPublish(staticParams.nodeParams, channelId, RevokedCommitSecondStageTransactions(null, null, htlcPenaltyTxs)) }) } Pair(nextState, actions) } else { @@ -342,38 +344,54 @@ data class Closing( } is ChannelCommand.Htlc.Settlement.Fulfill -> when (val result = commitments.sendFulfill(cmd)) { is Either.Right -> { - logger.info { "got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain" } + logger.info { "htlc #${cmd.id} with payment_hash=${cmd.r.sha256()} is fulfilled, recalculating htlc-success transactions" } + // We may be able to publish HTLC-success transactions for which we didn't have the preimage. + // We are already watching the corresponding outputs: no need to set additional watches. val commitments1 = result.value.first - val localCommitPublished1 = localCommitPublished?.let { - claimCurrentLocalCommitTxOutputs(channelKeys(), commitments1.latest, it.commitTx, currentOnChainFeerates()) - } - val remoteCommitPublished1 = remoteCommitPublished?.let { - claimRemoteCommitTxOutputs(channelKeys(), commitments1.latest, commitments1.latest.remoteCommit, it.commitTx, currentOnChainFeerates()) + val channelKeys = channelKeys() + val publishActions = mutableListOf() + localCommitPublished?.let { + val commitKeys = commitments1.latest.localKeys(channelKeys) + val htlcTxs = Closing.LocalClose.run { claimHtlcsWithPreimage(channelKeys, commitKeys, commitments1.latest, cmd.r) } + publishActions.addAll(htlcTxs.map { tx -> ChannelAction.Blockchain.PublishTx(tx) }) } - val nextRemoteCommitPublished1 = nextRemoteCommitPublished?.let { - val remoteCommit = commitments1.latest.nextRemoteCommit?.commit ?: error("next remote commit must be defined") - claimRemoteCommitTxOutputs(channelKeys(), commitments1.latest, remoteCommit, it.commitTx, currentOnChainFeerates()) + remoteCommitPublished?.let { rcp -> + val remoteCommit = commitments1.latest.remoteCommit + val htlcTxs = Closing.RemoteClose.run { claimHtlcsWithPreimage(channelKeys, rcp, commitments1.latest, remoteCommit, cmd.r, currentOnChainFeerates()) } + publishActions.addAll(htlcTxs.map { tx -> ChannelAction.Blockchain.PublishTx(tx) }) } - val republishList = buildList { - localCommitPublished1?.run { addAll(doPublish(staticParams.nodeParams, channelId)) } - remoteCommitPublished1?.run { addAll(doPublish(staticParams.nodeParams, channelId)) } - nextRemoteCommitPublished1?.run { addAll(doPublish(staticParams.nodeParams, channelId)) } + nextRemoteCommitPublished?.let { rcp -> + val remoteCommit = commitments1.latest.nextRemoteCommit ?: error("next remote commit must be defined") + val htlcTxs = Closing.RemoteClose.run { claimHtlcsWithPreimage(channelKeys, rcp, commitments1.latest, remoteCommit, cmd.r, currentOnChainFeerates()) } + publishActions.addAll(htlcTxs.map { tx -> ChannelAction.Blockchain.PublishTx(tx) }) } - val nextState = copy( - commitments = commitments1, - localCommitPublished = localCommitPublished1, - remoteCommitPublished = remoteCommitPublished1, - nextRemoteCommitPublished = nextRemoteCommitPublished1 - ) + val nextState = copy(commitments = commitments1) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) - addAll(republishList) + addAll(publishActions) } Pair(nextState, actions) } is Either.Left -> handleCommandError(cmd, result.value) } - is ChannelCommand.Htlc.Settlement -> unhandled(cmd) + is ChannelCommand.Htlc.Settlement.Fail -> { + logger.info { "htlc #${cmd.id} was failed by us, recalculating watched htlc outputs" } + val nextState = copy( + localCommitPublished = localCommitPublished?.let { Closing.LocalClose.ignoreFailedIncomingHtlc(cmd.id, it, commitments.latest) }, + remoteCommitPublished = remoteCommitPublished?.let { Closing.RemoteClose.ignoreFailedIncomingHtlc(cmd.id, it, commitments.latest, commitments.latest.remoteCommit) }, + nextRemoteCommitPublished = nextRemoteCommitPublished?.let { Closing.RemoteClose.ignoreFailedIncomingHtlc(cmd.id, it, commitments.latest, commitments.latest.nextRemoteCommit!!) }, + ) + Pair(nextState, listOf(ChannelAction.Storage.StoreState(nextState))) + } + is ChannelCommand.Htlc.Settlement.FailMalformed -> { + logger.info { "htlc #${cmd.id} was failed by us, recalculating watched htlc outputs" } + val nextState = copy( + localCommitPublished = localCommitPublished?.let { Closing.LocalClose.ignoreFailedIncomingHtlc(cmd.id, it, commitments.latest) }, + remoteCommitPublished = remoteCommitPublished?.let { Closing.RemoteClose.ignoreFailedIncomingHtlc(cmd.id, it, commitments.latest, commitments.latest.remoteCommit) }, + nextRemoteCommitPublished = nextRemoteCommitPublished?.let { Closing.RemoteClose.ignoreFailedIncomingHtlc(cmd.id, it, commitments.latest, commitments.latest.nextRemoteCommit!!) }, + ) + Pair(nextState, listOf(ChannelAction.Storage.StoreState(nextState))) + } is ChannelCommand.Commitment.CheckHtlcTimeout -> checkHtlcTimeout() is ChannelCommand.Commitment -> unhandled(cmd) is ChannelCommand.Init -> unhandled(cmd) @@ -395,22 +413,22 @@ data class Closing( val closingTx = mutualClosePublished.first { it.tx.txid == additionalConfirmedTx.txid }.copy(tx = additionalConfirmedTx) MutualClose(closingTx) } - localCommitPublished?.isDone() == true -> LocalClose(commitments.latest.localCommit, localCommitPublished) - remoteCommitPublished?.isDone() == true -> CurrentRemoteClose(commitments.latest.remoteCommit, remoteCommitPublished) - nextRemoteCommitPublished?.isDone() == true -> NextRemoteClose(commitments.latest.nextRemoteCommit!!.commit, nextRemoteCommitPublished) - futureRemoteCommitPublished?.isDone() == true -> RecoveryClose(futureRemoteCommitPublished) - revokedCommitPublished.any { it.isDone() } -> RevokedClose(revokedCommitPublished.first { it.isDone() }) + localCommitPublished?.isDone == true -> LocalClose(commitments.latest.localCommit, localCommitPublished) + remoteCommitPublished?.isDone == true -> CurrentRemoteClose(commitments.latest.remoteCommit, remoteCommitPublished) + nextRemoteCommitPublished?.isDone == true -> NextRemoteClose(commitments.latest.nextRemoteCommit!!, nextRemoteCommitPublished) + futureRemoteCommitPublished?.isDone == true -> RecoveryClose(futureRemoteCommitPublished) + revokedCommitPublished.any { it.isDone } -> RevokedClose(revokedCommitPublished.first { it.isDone }) else -> null } } fun closingTypeAlreadyKnown(): ClosingType? { return when { - localCommitPublished?.isConfirmed() == true -> LocalClose(commitments.latest.localCommit, localCommitPublished) - remoteCommitPublished?.isConfirmed() == true -> CurrentRemoteClose(commitments.latest.remoteCommit, remoteCommitPublished) - nextRemoteCommitPublished?.isConfirmed() == true -> NextRemoteClose(commitments.latest.nextRemoteCommit!!.commit, nextRemoteCommitPublished) - futureRemoteCommitPublished?.isConfirmed() == true -> RecoveryClose(futureRemoteCommitPublished) - revokedCommitPublished.any { it.isConfirmed() } -> RevokedClose(revokedCommitPublished.first { it.isConfirmed() }) + localCommitPublished?.isConfirmed == true -> LocalClose(commitments.latest.localCommit, localCommitPublished) + remoteCommitPublished?.isConfirmed == true -> CurrentRemoteClose(commitments.latest.remoteCommit, remoteCommitPublished) + nextRemoteCommitPublished?.isConfirmed == true -> NextRemoteClose(commitments.latest.nextRemoteCommit!!, nextRemoteCommitPublished) + futureRemoteCommitPublished?.isConfirmed == true -> RecoveryClose(futureRemoteCommitPublished) + revokedCommitPublished.any { it.isConfirmed } -> RevokedClose(revokedCommitPublished.first { it.isConfirmed }) else -> null } } @@ -424,7 +442,7 @@ data class Closing( private fun setClosingStatus(closingType: ClosingType): ChannelAction.Storage.SetLocked { val txId = when (closingType) { is MutualClose -> closingType.tx.tx.txid - is LocalClose -> closingType.localCommit.publishableTxs.commitTx.tx.txid + is LocalClose -> closingType.localCommit.txId is RemoteClose -> closingType.remoteCommit.txid is RecoveryClose -> closingType.remoteCommitPublished.commitTx.txid is RevokedClose -> closingType.revokedCommitPublished.commitTx.txid diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt index d47769a33..af837b4a5 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt @@ -9,7 +9,6 @@ import fr.acinq.lightning.blockchain.WatchSpent import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.* import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx import fr.acinq.lightning.wire.* data class Negotiating( @@ -20,7 +19,7 @@ data class Negotiating( val proposedClosingTxs: List, // Closing transactions we published: this contains our local transactions for // which they sent a signature, and their closing transactions that we signed. - val publishedClosingTxs: List, + val publishedClosingTxs: List, val waitingSinceBlock: Long, // how many blocks since we initiated the closing val closeCommand: ChannelCommand.Close.MutualClose?, ) : ChannelStateWithCommitments() { @@ -170,13 +169,13 @@ data class Negotiating( } /** Return full information about a closing tx that we proposed and they then published. */ - internal fun getMutualClosePublished(tx: Transaction): ClosingTx { + internal fun getMutualClosePublished(tx: Transaction): Transactions.ClosingTx { // They can publish a closing tx with any sig we sent them, even if we are not done negotiating. // They added their signature, so we use their version of the transaction. return proposedClosingTxs.flatMap { it.all }.first { it.tx.txid == tx.txid }.copy(tx = tx) } - internal fun ChannelContext.completeMutualClose(signedClosingTx: ClosingTx): Pair> { + internal fun ChannelContext.completeMutualClose(signedClosingTx: Transactions.ClosingTx): Pair> { logger.info { "channel was closed with txId=${signedClosingTx.tx.txid}" } val nextState = Closed( Closing( diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 0c536a5e4..82daca9a8 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -1,7 +1,6 @@ package fr.acinq.lightning.channel.states 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.* @@ -9,7 +8,6 @@ import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchConfirmedTriggered import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.* -import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* @@ -68,11 +66,18 @@ data class Normal( is Either.Left -> handleCommandError(cmd, result.value, channelUpdate) is Either.Right -> { val commitments1 = result.value.first - val nextRemoteSpec = commitments1.latest.nextRemoteCommit!!.commit.spec + val nextRemoteSpec = commitments1.latest.nextRemoteCommit!!.spec // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(commitments.params.remoteParams.dustLimit, nextRemoteSpec) + Transactions.trimReceivedHtlcs(commitments.params.remoteParams.dustLimit, nextRemoteSpec) - val htlcInfos = trimmedHtlcs.map { it.add }.map { + val trimmedOfferedHtlcs = commitments.active + .flatMap { c -> Transactions.trimOfferedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteSpec, c.commitmentFormat) } + .map { it.add } + .toSet() + val trimmedReceivedHtlcs = commitments.active + .flatMap { c -> Transactions.trimReceivedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteSpec, c.commitmentFormat) } + .map { it.add } + .toSet() + val htlcInfos = (trimmedOfferedHtlcs + trimmedReceivedHtlcs).map { logger.info { "adding paymentHash=${it.paymentHash} cltvExpiry=${it.cltvExpiry} to htlcs db for commitNumber=${commitments1.nextRemoteCommitIndex}" } ChannelAction.Storage.HtlcInfo(channelId, commitments1.nextRemoteCommitIndex, it.paymentHash, it.cltvExpiry) } @@ -87,9 +92,9 @@ data class Normal( } } is ChannelCommand.Close.MutualClose -> { - val allowAnySegwit = Features.canUseFeature(commitments.params.localParams.features, commitments.params.remoteParams.features, Feature.ShutdownAnySegwit) - val allowOpReturn = Features.canUseFeature(commitments.params.localParams.features, commitments.params.remoteParams.features, Feature.SimpleClose) - val localScriptPubkey = cmd.scriptPubKey ?: commitments.params.localParams.defaultFinalScriptPubKey + val allowAnySegwit = Features.canUseFeature(commitments.channelParams.localParams.features, commitments.channelParams.remoteParams.features, Feature.ShutdownAnySegwit) + val allowOpReturn = Features.canUseFeature(commitments.channelParams.localParams.features, commitments.channelParams.remoteParams.features, Feature.SimpleClose) + val localScriptPubkey = cmd.scriptPubKey ?: commitments.channelParams.localParams.defaultFinalScriptPubKey when { localShutdown != null -> { cmd.replyTo.complete(ChannelCloseResponse.Failure.ClosingAlreadyInProgress) @@ -179,7 +184,7 @@ data class Normal( Pair(this@Normal, listOf()) } spliceStatus is SpliceStatus.WaitingForSigs && cmd.message is CommitSig -> { - val (signingSession1, action) = spliceStatus.session.receiveCommitSig(channelKeys(), commitments.params, cmd.message, currentBlockHeight.toLong(), logger) + val (signingSession1, action) = spliceStatus.session.receiveCommitSig(channelKeys(), commitments.channelParams, cmd.message, currentBlockHeight.toLong(), logger) when (action) { is InteractiveTxSigningSessionAction.AbortFundingAttempt -> { logger.warning { "splice attempt failed: ${action.reason.message} (fundingTxId=${spliceStatus.session.fundingTx.txId})" } @@ -193,13 +198,6 @@ data class Normal( is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityPurchase) } } - cmd.message is CommitSig && ignoreRetransmittedCommitSig(cmd.message) -> { - // We haven't received our peer's tx_signatures for the latest funding transaction and asked them to resend it on reconnection. - // They also resend their corresponding commit_sig, but we have already received it so we should ignore it. - // Note that the funding transaction may have confirmed while we were offline. - logger.info { "ignoring commit_sig, we're still waiting for tx_signatures" } - Pair(this@Normal, listOf()) - } // NB: in all other cases we process the commit_sig normally. We could do a full pattern matching on all splice statuses, but it would force us to handle // corner cases like race condition between splice_init and a non-splice commit_sig else -> when (val result = commitments.receiveCommit(cmd.message, channelKeys(), logger)) { @@ -239,7 +237,7 @@ data class Normal( } val nextState = if (remoteShutdown != null && !commitments1.changes.localHasUnsignedOutgoingHtlcs()) { // we were waiting for our pending htlcs to be signed before replying with our local shutdown - val localShutdown = Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey) + val localShutdown = Shutdown(channelId, commitments.channelParams.localParams.defaultFinalScriptPubKey) actions.add(ChannelAction.Message.Send(localShutdown)) if (commitments1.latest.remoteCommit.spec.htlcs.isNotEmpty()) { // we just signed htlcs that need to be resolved now @@ -264,8 +262,8 @@ data class Normal( } } is Shutdown -> { - val allowAnySegwit = Features.canUseFeature(commitments.params.localParams.features, commitments.params.remoteParams.features, Feature.ShutdownAnySegwit) - val allowOpReturn = Features.canUseFeature(commitments.params.localParams.features, commitments.params.remoteParams.features, Feature.SimpleClose) + val allowAnySegwit = Features.canUseFeature(commitments.channelParams.localParams.features, commitments.channelParams.remoteParams.features, Feature.ShutdownAnySegwit) + val allowOpReturn = Features.canUseFeature(commitments.channelParams.localParams.features, commitments.channelParams.remoteParams.features, Feature.SimpleClose) // they have pending unsigned htlcs => they violated the spec, close the channel // they don't have pending unsigned htlcs // we have pending unsigned htlcs @@ -302,7 +300,7 @@ data class Normal( else -> { // so we don't have any unsigned outgoing changes val actions = mutableListOf() - val localShutdown = this@Normal.localShutdown ?: Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey) + val localShutdown = this@Normal.localShutdown ?: Shutdown(channelId, commitments.channelParams.localParams.defaultFinalScriptPubKey) if (this@Normal.localShutdown == null) actions.add(ChannelAction.Message.Send(localShutdown)) when { commitments.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation(closeCommand, commitments, localShutdown, cmd.message, actions) @@ -346,11 +344,13 @@ data class Normal( Pair(this@Normal.copy(spliceStatus = SpliceStatus.ReceivedStfu(cmd.message)), emptyList()) } is SpliceStatus.InitiatorQuiescent -> { + val channelKeys = channelKeys() // if both sides send stfu at the same time, the quiescence initiator is the channel initiator if (!cmd.message.initiator || isChannelOpener) { if (commitments.isQuiescent()) { val parentCommitment = commitments.active.first() val fundingContribution = FundingContributions.computeSpliceContribution( + channelKeys = channelKeys, isInitiator = true, commitment = parentCommitment, walletInputs = spliceStatus.command.spliceIn?.walletInputs ?: emptyList(), @@ -359,7 +359,7 @@ data class Normal( targetFeerate = spliceStatus.command.feerate ) val commitTxFees = when { - paysCommitTxFees -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec) + paysCommitTxFees -> Transactions.commitTxFee(commitments.latest.remoteCommitParams.dustLimit, parentCommitment.remoteCommit.spec, commitments.latest.commitmentFormat) else -> 0.sat } val liquidityFees = when (val requestRemoteFunding = spliceStatus.command.requestRemoteFunding) { @@ -375,7 +375,7 @@ data class Normal( } val liquidityFeesOwed = (liquidityFees - spliceStatus.command.currentFeeCredit).max(0.msat) val balanceAfterFees = parentCommitment.localCommit.spec.toLocal + fundingContribution.toMilliSatoshi() - liquidityFeesOwed - if (balanceAfterFees < parentCommitment.localChannelReserve(commitments.params).max(commitTxFees)) { + if (balanceAfterFees < parentCommitment.localChannelReserve(commitments.channelParams).max(commitTxFees)) { logger.warning { "cannot do splice: insufficient funds (balanceAfterFees=$balanceAfterFees, liquidityFees=$liquidityFees, feeCredit=${spliceStatus.command.currentFeeCredit})" } spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.InsufficientFunds(balanceAfterFees, liquidityFees, spliceStatus.command.currentFeeCredit)) val action = listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidSpliceRequest(channelId).message))) @@ -391,7 +391,7 @@ data class Normal( fundingContribution = fundingContribution, lockTime = currentBlockHeight.toLong(), feerate = spliceStatus.command.feerate, - fundingPubkey = channelKeys().fundingKey(parentCommitment.fundingTxIndex + 1).publicKey(), + fundingPubkey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey(), requestFunding = spliceStatus.command.requestRemoteFunding, ) logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution}" } @@ -425,12 +425,13 @@ data class Normal( } is SpliceStatus.NonInitiatorQuiescent -> if (commitments.isQuiescent()) { + val channelKeys = channelKeys() logger.info { "accepting splice with remote.amount=${cmd.message.fundingContribution}" } val parentCommitment = commitments.active.first() val spliceAck = SpliceAck( channelId, fundingContribution = 0.sat, // only remote contributes to the splice - fundingPubkey = channelKeys().fundingKey(parentCommitment.fundingTxIndex + 1).publicKey(), + fundingPubkey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey(), willFund = null, ) val fundingParams = InteractiveTxParams( @@ -438,16 +439,17 @@ data class Normal( isInitiator = false, localContribution = spliceAck.fundingContribution, remoteContribution = cmd.message.fundingContribution, - sharedInput = SharedFundingInput.Multisig2of2(parentCommitment), + sharedInput = SharedFundingInput(channelKeys, parentCommitment), remoteFundingPubkey = cmd.message.fundingPubkey, localOutputs = emptyList(), + commitmentFormat = commitments.latest.commitmentFormat, lockTime = cmd.message.lockTime, - dustLimit = commitments.params.localParams.dustLimit.max(commitments.params.remoteParams.dustLimit), + dustLimit = commitments.latest.localCommitParams.dustLimit.max(commitments.latest.remoteCommitParams.dustLimit), targetFeerate = cmd.message.feerate ) val session = InteractiveTxSession( staticParams.remoteNodeId, - channelKeys(), + channelKeys, keyManager.swapInOnChainWallet, fundingParams, previousLocalBalance = parentCommitment.localCommit.spec.toLocal, @@ -485,7 +487,7 @@ data class Normal( spliceStatus.command.requestRemoteFunding, remoteNodeId, channelId, - Helpers.Funding.makeFundingPubKeyScript(spliceStatus.spliceInit.fundingPubkey, cmd.message.fundingPubkey), + Transactions.makeFundingScript(spliceStatus.spliceInit.fundingPubkey, cmd.message.fundingPubkey, commitments.latest.commitmentFormat).pubkeyScript, cmd.message.fundingContribution, spliceStatus.spliceInit.feerate, isChannelCreation = false, @@ -498,8 +500,9 @@ data class Normal( Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, liquidityPurchase.value.message)))) } is Either.Right -> { + val channelKeys = channelKeys() val parentCommitment = commitments.active.first() - val sharedInput = SharedFundingInput.Multisig2of2(parentCommitment) + val sharedInput = SharedFundingInput(channelKeys, parentCommitment) val fundingParams = InteractiveTxParams( channelId = channelId, isInitiator = true, @@ -508,12 +511,13 @@ data class Normal( sharedInput = sharedInput, remoteFundingPubkey = cmd.message.fundingPubkey, localOutputs = spliceStatus.command.spliceOutputs, + commitmentFormat = commitments.latest.commitmentFormat, lockTime = spliceStatus.spliceInit.lockTime, - dustLimit = commitments.params.localParams.dustLimit.max(commitments.params.remoteParams.dustLimit), + dustLimit = commitments.latest.localCommitParams.dustLimit.max(commitments.latest.remoteCommitParams.dustLimit), targetFeerate = spliceStatus.spliceInit.feerate ) when (val fundingContributions = FundingContributions.create( - channelKeys = channelKeys(), + channelKeys = channelKeys, swapInKeys = keyManager.swapInOnChainWallet, params = fundingParams, sharedUtxo = Pair( @@ -538,7 +542,7 @@ data class Normal( // The splice initiator always sends the first interactive-tx message. val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession( staticParams.remoteNodeId, - channelKeys(), + channelKeys, keyManager.swapInOnChainWallet, fundingParams, previousLocalBalance = parentCommitment.localCommit.spec.toLocal, @@ -585,7 +589,9 @@ data class Normal( val signingSession = InteractiveTxSigningSession.create( interactiveTxSession, keyManager, - commitments.params, + commitments.channelParams, + parentCommitment.localCommitParams, + parentCommitment.remoteCommitParams, spliceStatus.spliceSession.fundingParams, fundingTxIndex = parentCommitment.fundingTxIndex + 1, interactiveTxAction.sharedTx, @@ -832,7 +838,8 @@ data class Normal( ): Pair> { logger.info { "sending tx_sigs" } // We watch for confirmation in all cases, to allow pruning outdated commitments when transactions confirm. - val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, action.commitment.commitInput.txOut.publicKeyScript, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ChannelFundingDepthOk) + val fundingScript = action.commitment.commitInput(channelKeys()).txOut.publicKeyScript + val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, fundingScript, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ChannelFundingDepthOk) val commitments = commitments.add(action.commitment) val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None) val actions = buildList { @@ -887,15 +894,6 @@ data class Normal( return Pair(nextState, actions) } - /** This function should be used to ignore a commit_sig that we've already received. */ - private fun ignoreRetransmittedCommitSig(commit: CommitSig): Boolean { - // If we already have a signed commitment transaction containing their signature, we must have previously received that commit_sig. - val commitTx = commitments.latest.localCommit.publishableTxs.commitTx.tx - return commitments.params.channelFeatures.hasFeature(Feature.DualFunding) && - commit.batchSize == 1 && - commitTx.txIn.first().witness.stack.contains(Scripts.der(commit.signature, SigHash.SIGHASH_ALL)) - } - /** If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. */ fun getUnsignedFundingTxId(): TxId? = when { spliceStatus is SpliceStatus.WaitingForSigs -> spliceStatus.session.fundingTx.txId diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt index 860720ccb..bc64cc9fa 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt @@ -24,7 +24,7 @@ data class Offline(val state: PersistedChannelState) : ChannelState() { // there isn't much to do except asking them again to publish their current commitment by sending an error val exc = PleasePublishYourCommitment(channelId) val error = Error(channelId, exc.message) - val nextState = state.updateCommitments(state.commitments.copy(params = state.commitments.params.updateFeatures(cmd.localInit, cmd.remoteInit))) + val nextState = state.updateCommitments(state.commitments.copy(channelParams = state.commitments.channelParams.updateFeatures(cmd.localInit, cmd.remoteInit))) Pair(nextState, listOf(ChannelAction.Message.Send(error))) } is WaitForFundingSigned -> { @@ -48,7 +48,7 @@ data class Offline(val state: PersistedChannelState) : ChannelState() { add(ChannelAction.Message.Send(channelReestablish)) } } - val nextState = state.updateCommitments(state.commitments.copy(params = state.commitments.params.updateFeatures(cmd.localInit, cmd.remoteInit))) + val nextState = state.updateCommitments(state.commitments.copy(channelParams = state.commitments.channelParams.updateFeatures(cmd.localInit, cmd.remoteInit))) Pair(Syncing(nextState, channelReestablishSent = sendChannelReestablish), actions) } } @@ -66,9 +66,9 @@ data class Offline(val state: PersistedChannelState) : ChannelState() { val nextState = when (state) { is WaitForFundingConfirmed -> { logger.info { "was confirmed while offline at blockHeight=${watch.blockHeight} txIndex=${watch.txIndex} with funding txid=${watch.tx.txid}" } - val nextPerCommitmentPoint = commitments1.params.localParams.channelKeys(keyManager).commitmentPoint(1) + val nextPerCommitmentPoint = commitments1.channelParams.localParams.channelKeys(keyManager).commitmentPoint(1) val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) - val shortChannelId = ShortChannelId(watch.blockHeight, watch.txIndex, commitments1.latest.commitInput.outPoint.index.toInt()) + val shortChannelId = ShortChannelId(watch.blockHeight, watch.txIndex, commitments1.latest.fundingInput.index.toInt()) WaitForChannelReady(commitments1, shortChannelId, channelReady) } else -> state diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt index 0d10ab6fa..438dc6b77 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt @@ -113,11 +113,18 @@ data class ShuttingDown( is Either.Left -> handleCommandError(cmd, result.value) is Either.Right -> { val commitments1 = result.value.first - val nextRemoteSpec = commitments1.latest.nextRemoteCommit!!.commit.spec + val nextRemoteSpec = commitments1.latest.nextRemoteCommit!!.spec // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter - val trimmedHtlcs = Transactions.trimOfferedHtlcs(commitments.params.remoteParams.dustLimit, nextRemoteSpec) + Transactions.trimReceivedHtlcs(commitments.params.remoteParams.dustLimit, nextRemoteSpec) - val htlcInfos = trimmedHtlcs.map { it.add }.map { + val trimmedOfferedHtlcs = commitments.active + .flatMap { c -> Transactions.trimOfferedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteSpec, c.commitmentFormat) } + .map { it.add } + .toSet() + val trimmedReceivedHtlcs = commitments.active + .flatMap { c -> Transactions.trimReceivedHtlcs(c.remoteCommitParams.dustLimit, nextRemoteSpec, c.commitmentFormat) } + .map { it.add } + .toSet() + val htlcInfos = (trimmedOfferedHtlcs + trimmedReceivedHtlcs).map { logger.info { "adding paymentHash=${it.paymentHash} cltvExpiry=${it.cltvExpiry} to htlcs db for commitNumber=${commitments1.nextRemoteCommitIndex}" } ChannelAction.Storage.HtlcInfo(channelId, commitments1.nextRemoteCommitIndex, it.paymentHash, it.cltvExpiry) } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt index 412a2b909..65f3e0036 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt @@ -7,7 +7,6 @@ import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchConfirmedTriggered import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.* -import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* @@ -15,12 +14,11 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: val channelId = state.channelId - fun ChannelContext.channelKeys(): ChannelKeys = when (state) { - is WaitForFundingSigned -> state.channelParams.localParams.channelKeys(keyManager) - is ChannelStateWithCommitments -> state.commitments.params.localParams.channelKeys(keyManager) - } - override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> { + val channelKeys = when (state) { + is WaitForFundingSigned -> state.channelParams.localParams.channelKeys(keyManager) + is ChannelStateWithCommitments -> state.commitments.channelParams.localParams.channelKeys(keyManager) + } return when (cmd) { is ChannelCommand.MessageReceived -> when (cmd.message) { is ChannelReestablish -> { @@ -31,7 +29,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received // their commit_sig or their tx_signatures (depending on who must send tx_signatures first). logger.info { "re-sending commit_sig for channel creation with fundingTxId=${state.signingSession.fundingTx.txId}" } - val commitSig = state.signingSession.remoteCommit.sign(state.channelParams, channelKeys(), state.signingSession) + val commitSig = state.signingSession.remoteCommit.sign(state.channelParams, channelKeys, state.signingSession) add(ChannelAction.Message.Send(commitSig)) } } @@ -47,7 +45,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // They haven't received our commit_sig: we retransmit it. // We're waiting for signatures from them, and will send our tx_signatures once we receive them. logger.info { "re-sending commit_sig for rbf attempt with fundingTxId=${cmd.message.nextFundingTxId}" } - val commitSig = state.rbfStatus.session.remoteCommit.sign(state.commitments.params, channelKeys(), state.rbfStatus.session) + val commitSig = state.rbfStatus.session.remoteCommit.sign(state.commitments.channelParams, channelKeys, state.rbfStatus.session) add(ChannelAction.Message.Send(commitSig)) } } @@ -59,11 +57,13 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (cmd.message.nextLocalCommitmentNumber == 0L) { logger.info { "re-sending commit_sig for fundingTxId=${cmd.message.nextFundingTxId}" } val commitSig = state.commitments.latest.remoteCommit.sign( - state.commitments.params, - channelKeys(), - fundingTxIndex = 0, + state.commitments.channelParams, + state.commitments.latest.remoteCommitParams, + channelKeys, + fundingTxIndex = state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, - state.commitments.latest.commitInput, + state.commitments.latest.commitInput(channelKeys), + state.commitments.latest.commitmentFormat, batchSize = 1 ) add(ChannelAction.Message.Send(commitSig)) @@ -90,11 +90,13 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (cmd.message.nextLocalCommitmentNumber == 0L) { logger.info { "re-sending commit_sig for fundingTxId=${state.commitments.latest.fundingTxId}" } val commitSig = state.commitments.latest.remoteCommit.sign( - state.commitments.params, - channelKeys(), - fundingTxIndex = state.commitments.latest.fundingTxIndex, + state.commitments.channelParams, + state.commitments.latest.remoteCommitParams, + channelKeys, + state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, - state.commitments.latest.commitInput, + state.commitments.latest.commitInput(channelKeys), + state.commitments.latest.commitmentFormat, batchSize = 1 ) actions.add(ChannelAction.Message.Send(commitSig)) @@ -109,7 +111,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: } } logger.debug { "re-sending channel_ready" } - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) + val nextPerCommitmentPoint = channelKeys.commitmentPoint(1) val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint) actions.add(ChannelAction.Message.Send(channelReady)) Pair(state, actions) @@ -125,7 +127,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (state.commitments.latest.fundingTxIndex == 0L && cmd.message.nextLocalCommitmentNumber == 1L && state.commitments.localCommitIndex == 0L) { // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT logger.debug { "re-sending channel_ready" } - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) + val nextPerCommitmentPoint = channelKeys.commitmentPoint(1) val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint) actions.add(ChannelAction.Message.Send(channelReady)) } @@ -136,7 +138,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // They haven't received our commit_sig: we retransmit it. // We're waiting for signatures from them, and will send our tx_signatures once we receive them. logger.info { "re-sending commit_sig for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" } - val commitSig = state.spliceStatus.session.remoteCommit.sign(state.commitments.params, channelKeys(), state.spliceStatus.session) + val commitSig = state.spliceStatus.session.remoteCommit.sign(state.commitments.channelParams, channelKeys, state.spliceStatus.session) actions.add(ChannelAction.Message.Send(commitSig)) } state.spliceStatus @@ -148,11 +150,13 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (cmd.message.nextLocalCommitmentNumber == state.commitments.remoteCommitIndex) { logger.info { "re-sending commit_sig for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" } val commitSig = state.commitments.latest.remoteCommit.sign( - state.commitments.params, - channelKeys(), - fundingTxIndex = state.commitments.latest.fundingTxIndex, + state.commitments.channelParams, + state.commitments.latest.remoteCommitParams, + channelKeys, + state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, - state.commitments.latest.commitInput, + state.commitments.latest.commitInput(channelKeys), + state.commitments.latest.commitmentFormat, batchSize = 1 ) actions.add(ChannelAction.Message.Send(commitSig)) @@ -285,9 +289,9 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: val nextState = when (state) { is WaitForFundingConfirmed -> { logger.info { "was confirmed while syncing at blockHeight=${watch.blockHeight} txIndex=${watch.txIndex} with funding txid=${watch.tx.txid}" } - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) + val nextPerCommitmentPoint = channelKeys.commitmentPoint(1) val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) - val shortChannelId = ShortChannelId(watch.blockHeight, watch.txIndex, commitments1.latest.commitInput.outPoint.index.toInt()) + val shortChannelId = ShortChannelId(watch.blockHeight, watch.txIndex, commitments1.latest.fundingInput.index.toInt()) WaitForChannelReady(commitments1, shortChannelId, channelReady) } else -> state @@ -353,7 +357,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // there is no way to make sure that they are saying the truth, the best thing to do is ask them to publish their commitment right now // maybe they will publish their commitment, in that case we need to remember their commitment point in order to be able to claim our outputs // not that if they don't comply, we could publish our own commitment (it is not stale, otherwise we would be in the case above) - logger.warning { "counterparty says that they have a more recent commitment than the one we know of!!! ourCommitmentNumber=${commitments.latest.nextRemoteCommit?.commit?.index ?: commitments.latest.remoteCommit.index} theirCommitmentNumber=${remoteChannelReestablish.nextLocalCommitmentNumber}" } + logger.warning { "counterparty says that they have a more recent commitment than the one we know of!!! ourCommitmentNumber=${commitments.latest.nextRemoteCommit?.index ?: commitments.latest.remoteCommit.index} theirCommitmentNumber=${remoteChannelReestablish.nextLocalCommitmentNumber}" } handleOutdatedCommitment(remoteChannelReestablish, commitments) } is SyncResult.Failure.RemoteLying -> { @@ -386,7 +390,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: * Check whether we are in sync with our peer. */ fun ChannelContext.handleSync(commitments: Commitments, remoteChannelReestablish: ChannelReestablish): SyncResult { - val channelKeys = keyManager.channelKeys(commitments.params.localParams.fundingKeyPath) + val channelKeys = keyManager.channelKeys(commitments.channelParams.localParams.fundingKeyPath) // This is done in two steps: // - step 1: we check our local commitment // - step 2: we check the remote commitment @@ -401,14 +405,13 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // We just sent a new commit_sig but they didn't receive it: we resend the same updates and sign them again, // and preserve the same ordering of messages. val signedUpdates = commitments.changes.localChanges.signed - val channelParams = commitments.params val batchSize = commitments.active.size val commitSigs = CommitSigs.fromSigs(commitments.active.mapNotNull { c -> - val commitInput = c.commitInput + val commitInput = c.commitInput(channelKeys) // Note that we ignore errors and simply skip failures to sign: we've already signed those updates before // the disconnection, so we don't expect any error here unless our peer sends an invalid nonce. In that // case, we simply won't send back our commit_sig until they fix their node. - c.nextRemoteCommit?.commit?.sign(channelParams, channelKeys, c.fundingTxIndex, c.remoteFundingPubkey, commitInput, batchSize) + c.nextRemoteCommit?.sign(commitments.channelParams, c.remoteCommitParams, channelKeys, c.fundingTxIndex, c.remoteFundingPubkey, commitInput, c.commitmentFormat, batchSize) }) val retransmit = when (retransmitRevocation) { null -> buildList { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt index 5f57b4a7d..e2ee80093 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt @@ -32,14 +32,11 @@ data class WaitForAcceptChannel( when (val res = Helpers.validateParamsInitiator(staticParams.nodeParams, init, lastSent, accept)) { is Either.Right -> { val channelType = res.value - val channelFeatures = ChannelFeatures(channelType, localFeatures = init.localParams.features, remoteFeatures = init.remoteInit.features) - val remoteParams = RemoteParams( + val channelFeatures = ChannelFeatures(channelType, localFeatures = init.localChannelParams.features, remoteFeatures = init.remoteInit.features) + val localCommitParams = CommitParams(init.dustLimit, init.maxHtlcValueInFlightMsat, init.htlcMinimum, accept.toSelfDelay, init.maxAcceptedHtlcs) + val remoteCommitParams = CommitParams(accept.dustLimit, accept.maxHtlcValueInFlightMsat, accept.htlcMinimum, init.toRemoteDelay, accept.maxAcceptedHtlcs) + val remoteChannelParams = RemoteChannelParams( nodeId = staticParams.remoteNodeId, - dustLimit = accept.dustLimit, - maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, - htlcMinimum = accept.htlcMinimum, - toSelfDelay = accept.toSelfDelay, - maxAcceptedHtlcs = accept.maxAcceptedHtlcs, revocationBasepoint = accept.revocationBasepoint, paymentBasepoint = accept.paymentBasepoint, delayedPaymentBasepoint = accept.delayedPaymentBasepoint, @@ -47,10 +44,10 @@ data class WaitForAcceptChannel( features = init.remoteInit.features ) val channelId = computeChannelId(lastSent, accept) - val channelKeys = keyManager.channelKeys(init.localParams.fundingKeyPath) + val channelKeys = keyManager.channelKeys(init.localChannelParams.fundingKeyPath) val remoteFundingPubkey = accept.fundingPubkey - val dustLimit = accept.dustLimit.max(init.localParams.dustLimit) - val fundingParams = InteractiveTxParams(channelId, true, init.fundingAmount, accept.fundingAmount, remoteFundingPubkey, lastSent.lockTime, dustLimit, lastSent.fundingFeerate) + val dustLimit = localCommitParams.dustLimit.max(remoteCommitParams.dustLimit) + val fundingParams = InteractiveTxParams(channelId, true, init.fundingAmount, accept.fundingAmount, remoteFundingPubkey, lastSent.lockTime, dustLimit, channelType.commitmentFormat, lastSent.fundingFeerate) when (val liquidityPurchase = LiquidityAds.validateRemoteFunding( lastSent.requestFunding, staticParams.remoteNodeId, @@ -95,8 +92,10 @@ data class WaitForAcceptChannel( is InteractiveTxSessionAction.SendMessage -> { val nextState = WaitForFundingCreated( init.replyTo, - init.localParams, - remoteParams, + init.localChannelParams, + localCommitParams, + remoteChannelParams, + remoteCommitParams, interactiveTxSession, lastSent.commitmentFeerate, accept.firstPerCommitmentPoint, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt index 3cc891cdd..99ba39471 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt @@ -66,7 +66,7 @@ data class WaitForChannelReady( staticParams.remoteNodeId, shortChannelId, staticParams.nodeParams.expiryDeltaBlocks, - commitments.params.remoteParams.htlcMinimum, + commitments.latest.remoteCommitParams.htlcMinimum, staticParams.nodeParams.feeBase, staticParams.nodeParams.feeProportionalMillionths.toLong(), commitments.latest.fundingAmount.toMilliSatoshi(), diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt index d455c6674..99eecaecd 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -90,6 +90,7 @@ data class WaitForFundingConfirmed( latestFundingTx.fundingParams.remoteFundingPubkey, cmd.message.lockTime, latestFundingTx.fundingParams.dustLimit, + latestFundingTx.fundingParams.commitmentFormat, cmd.message.feerate ) val toSend = buildList> { @@ -132,6 +133,7 @@ data class WaitForFundingConfirmed( latestFundingTx.fundingParams.remoteFundingPubkey, rbfStatus.command.lockTime, latestFundingTx.fundingParams.dustLimit, + latestFundingTx.fundingParams.commitmentFormat, rbfStatus.command.targetFeerate ) when (val contributions = FundingContributions.create(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, rbfStatus.command.walletInputs, null)) { @@ -178,7 +180,9 @@ data class WaitForFundingConfirmed( val signingSession = InteractiveTxSigningSession.create( rbfSession1, keyManager, - commitments.params, + commitments.channelParams, + commitments.latest.localCommitParams, + commitments.latest.remoteCommitParams, rbfSession1.fundingParams, fundingTxIndex = replacedCommitment.fundingTxIndex, interactiveTxAction.sharedTx, @@ -219,7 +223,7 @@ data class WaitForFundingConfirmed( } is CommitSig -> when (rbfStatus) { is RbfStatus.WaitingForSigs -> { - val (signingSession1, action) = rbfStatus.session.receiveCommitSig(channelKeys(), commitments.params, cmd.message, currentBlockHeight.toLong(), logger) + val (signingSession1, action) = rbfStatus.session.receiveCommitSig(channelKeys(), commitments.channelParams, cmd.message, currentBlockHeight.toLong(), logger) when (action) { is InteractiveTxSigningSessionAction.AbortFundingAttempt -> { logger.warning { "rbf attempt failed: ${action.reason.message}" } @@ -264,7 +268,7 @@ data class WaitForFundingConfirmed( // this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel // as soon as it reaches NORMAL state, and before it is announced on the network // (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime) - val shortChannelId = ShortChannelId(cmd.watch.blockHeight, cmd.watch.txIndex, commitment.commitInput.outPoint.index.toInt()) + val shortChannelId = ShortChannelId(cmd.watch.blockHeight, cmd.watch.txIndex, commitment.fundingInput.index.toInt()) val nextState = WaitForChannelReady(commitments1, shortChannelId, channelReady) val actions1 = buildList { if (rbfStatus != RbfStatus.None) add(ChannelAction.Message.Send(TxAbort(channelId, InvalidRbfTxConfirmed(channelId, cmd.watch.tx.txid).message))) @@ -325,7 +329,8 @@ data class WaitForFundingConfirmed( private fun ChannelContext.sendRbfTxSigs(action: InteractiveTxSigningSessionAction.SendTxSigs): Pair> { logger.info { "rbf funding tx created with txId=${action.fundingTx.txId}, ${action.fundingTx.sharedTx.tx.localInputs.size} local inputs, ${action.fundingTx.sharedTx.tx.remoteInputs.size} remote inputs, ${action.fundingTx.sharedTx.tx.localOutputs.size} local outputs and ${action.fundingTx.sharedTx.tx.remoteOutputs.size} remote outputs" } logger.info { "will wait for ${staticParams.nodeParams.minDepthBlocks} confirmations" } - val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, action.commitment.commitInput.txOut.publicKeyScript, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ChannelFundingDepthOk) + val fundingScript = action.commitment.commitInput(channelKeys()).txOut.publicKeyScript + val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, fundingScript, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ChannelFundingDepthOk) val nextState = WaitForFundingConfirmed( commitments.add(action.commitment), waitingSinceBlock, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt index cc74b7c75..c1826304c 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -31,8 +31,10 @@ import kotlinx.coroutines.CompletableDeferred */ data class WaitForFundingCreated( val replyTo: CompletableDeferred, - val localParams: LocalParams, - val remoteParams: RemoteParams, + val localChannelParams: LocalChannelParams, + val localCommitParams: CommitParams, + val remoteChannelParams: RemoteChannelParams, + val remoteCommitParams: CommitParams, val interactiveTxSession: InteractiveTxSession, val commitTxFeerate: FeeratePerKw, val remoteFirstPerCommitmentPoint: PublicKey, @@ -53,11 +55,13 @@ data class WaitForFundingCreated( when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> Pair(this@WaitForFundingCreated.copy(interactiveTxSession = interactiveTxSession1), listOf(ChannelAction.Message.Send(interactiveTxAction.msg))) is InteractiveTxSessionAction.SignSharedTx -> { - val channelParams = ChannelParams(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags) + val channelParams = ChannelParams(channelId, channelConfig, channelFeatures, localChannelParams, remoteChannelParams, channelFlags) val signingSession = InteractiveTxSigningSession.create( interactiveTxSession1, keyManager, channelParams, + localCommitParams, + remoteCommitParams, interactiveTxSession.fundingParams, fundingTxIndex = 0, interactiveTxAction.sharedTx, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt index 56ee87eb5..a071521ac 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt @@ -106,7 +106,9 @@ data class WaitForFundingSigned( private fun ChannelContext.sendTxSigs(action: InteractiveTxSigningSessionAction.SendTxSigs): Pair> { logger.info { "funding tx created with txId=${action.fundingTx.txId}, ${action.fundingTx.sharedTx.tx.localInputs.size} local inputs, ${action.fundingTx.sharedTx.tx.remoteInputs.size} remote inputs, ${action.fundingTx.sharedTx.tx.localOutputs.size} local outputs and ${action.fundingTx.sharedTx.tx.remoteOutputs.size} remote outputs" } // We watch for confirmation in all cases, to allow pruning outdated commitments when transactions confirm. - val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, action.commitment.commitInput.txOut.publicKeyScript, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ChannelFundingDepthOk) + val channelKeys = channelParams.localParams.channelKeys(keyManager) + val fundingInput = action.commitment.commitInput(channelKeys) + val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, fundingInput.txOut.publicKeyScript, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ChannelFundingDepthOk) val commitments = Commitments( channelParams, CommitmentChanges.init(), @@ -165,7 +167,7 @@ data class WaitForFundingSigned( // We use part of the funding txid to create a dummy short channel id. // This gives us a probability of collisions of 0.1% for 5 0-conf channels and 1% for 20 // Collisions mean that users may temporarily see incorrect numbers for their 0-conf channels (until they've been confirmed). - val shortChannelId = ShortChannelId(0, Pack.int32BE(action.commitment.fundingTxId.value.slice(0, 16).toByteArray()).absoluteValue, action.commitment.commitInput.outPoint.index.toInt()) + val shortChannelId = ShortChannelId(0, Pack.int32BE(action.commitment.fundingTxId.value.slice(0, 16).toByteArray()).absoluteValue, fundingInput.outPoint.index.toInt()) val nextState = WaitForChannelReady(commitments, shortChannelId, channelReady) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt index b7a22dc46..ff8b68b64 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt @@ -5,6 +5,7 @@ import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchSpent 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.wire.ChannelTlv import fr.acinq.lightning.wire.OpenDualFundedChannel @@ -15,30 +16,35 @@ data object WaitForInit : ChannelState() { return when (cmd) { is ChannelCommand.Init.NonInitiator -> { val nextState = WaitForOpenChannel( - cmd.replyTo, - cmd.temporaryChannelId, - cmd.fundingAmount, - cmd.walletInputs, - cmd.localParams, - cmd.channelConfig, - cmd.remoteInit, - cmd.fundingRates, + replyTo = cmd.replyTo, + temporaryChannelId = cmd.temporaryChannelId, + fundingAmount = cmd.fundingAmount, + walletInputs = cmd.walletInputs, + localChannelParams = cmd.localParams, + dustLimit = cmd.dustLimit, + htlcMinimum = cmd.htlcMinimum, + maxHtlcValueInFlightMsat = cmd.maxHtlcValueInFlightMsat, + maxAcceptedHtlcs = cmd.maxAcceptedHtlcs, + toRemoteDelay = cmd.toRemoteDelay, + channelConfig = cmd.channelConfig, + remoteInit = cmd.remoteInit, + fundingRates = cmd.fundingRates, ) Pair(nextState, listOf()) } is ChannelCommand.Init.Initiator -> { - val channelKeys = keyManager.channelKeys(cmd.localParams.fundingKeyPath) + val channelKeys = keyManager.channelKeys(cmd.localChannelParams.fundingKeyPath) val open = OpenDualFundedChannel( chainHash = staticParams.nodeParams.chainHash, temporaryChannelId = cmd.temporaryChannelId(channelKeys), fundingFeerate = cmd.fundingTxFeerate, commitmentFeerate = cmd.commitTxFeerate, fundingAmount = cmd.fundingAmount, - dustLimit = cmd.localParams.dustLimit, - maxHtlcValueInFlightMsat = cmd.localParams.maxHtlcValueInFlightMsat, - htlcMinimum = cmd.localParams.htlcMinimum, - toSelfDelay = cmd.localParams.toSelfDelay, - maxAcceptedHtlcs = cmd.localParams.maxAcceptedHtlcs, + dustLimit = cmd.dustLimit, + maxHtlcValueInFlightMsat = cmd.maxHtlcValueInFlightMsat, + htlcMinimum = cmd.htlcMinimum, + toSelfDelay = cmd.toRemoteDelay, + maxAcceptedHtlcs = cmd.maxAcceptedHtlcs, lockTime = currentBlockHeight.toLong(), fundingPubkey = channelKeys.fundingKey(0).publicKey(), revocationBasepoint = channelKeys.revocationBasePoint, @@ -60,6 +66,7 @@ data object WaitForInit : ChannelState() { } is ChannelCommand.Init.Restore -> { logger.info { "restoring channel ${cmd.state.channelId} to state ${cmd.state::class.simpleName}" } + val channelKeys = cmd.state.run { channelKeys() } // We republish unconfirmed transactions. val unconfirmedFundingTxs = when (cmd.state) { is ChannelStateWithCommitments -> cmd.state.commitments.active.mapNotNull { commitment -> @@ -75,12 +82,13 @@ data object WaitForInit : ChannelState() { val fundingTxWatches = when (cmd.state) { is ChannelStateWithCommitments -> cmd.state.commitments.active.map { commitment -> val fundingMinDepth = staticParams.nodeParams.minDepthBlocks + val commitInput = commitment.commitInput(channelKeys) when (commitment.localFundingStatus) { - is LocalFundingStatus.UnconfirmedFundingTx -> WatchConfirmed(cmd.state.channelId, commitment.fundingTxId, commitment.commitInput.txOut.publicKeyScript, fundingMinDepth, WatchConfirmed.ChannelFundingDepthOk) + is LocalFundingStatus.UnconfirmedFundingTx -> WatchConfirmed(cmd.state.channelId, commitment.fundingTxId, commitInput.txOut.publicKeyScript, fundingMinDepth, WatchConfirmed.ChannelFundingDepthOk) is LocalFundingStatus.ConfirmedFundingTx -> when (commitment.localFundingStatus.shortChannelId) { // If the short_channel_id isn't correctly set, we put a watch on the funding transaction to compute it. - ShortChannelId(0) -> WatchConfirmed(cmd.state.channelId, commitment.fundingTxId, commitment.commitInput.txOut.publicKeyScript, fundingMinDepth, WatchConfirmed.ChannelFundingDepthOk) - else -> WatchSpent(cmd.state.channelId, commitment.fundingTxId, commitment.commitInput.outPoint.index.toInt(), commitment.commitInput.txOut.publicKeyScript, WatchSpent.ChannelSpent(commitment.fundingAmount)) + ShortChannelId(0) -> WatchConfirmed(cmd.state.channelId, commitment.fundingTxId, commitInput.txOut.publicKeyScript, fundingMinDepth, WatchConfirmed.ChannelFundingDepthOk) + else -> WatchSpent(cmd.state.channelId, commitment.fundingTxId, commitInput.outPoint.index.toInt(), commitInput.txOut.publicKeyScript, WatchSpent.ChannelSpent(commitment.fundingAmount)) } } } @@ -90,40 +98,106 @@ data object WaitForInit : ChannelState() { is Closing -> { val closingType = cmd.state.closingTypeAlreadyKnown() logger.info { "channel is closing (closing type = ${closingType?.let { it::class } ?: "unknown yet"})" } - // if the closing type is known: - // - there is no need to watch funding txs because one has already been spent and the spending tx has already reached mindepth - // - there is no need to attempt to publish transactions for other type of closes + // If the closing type is known: + // - there is no need to attempt to publish transactions for other type of closes + // - there may be 3rd-stage transactions to publish + // - there is a single commitment, the others have all been invalidated + val commitment = cmd.state.commitments.latest + val feerates = currentOnChainFeerates() when (closingType) { - is MutualClose -> { - Pair(cmd.state, doPublish(closingType.tx, cmd.state.channelId)) - } + is MutualClose -> Pair(cmd.state, doPublish(closingType.tx, cmd.state.channelId)) is LocalClose -> { - val actions = closingType.localCommitPublished.run { doPublish(staticParams.nodeParams, cmd.state.channelId) } + val (_, secondStageTxs) = Helpers.Closing.LocalClose.run { + claimCommitTxOutputs(channelKeys, commitment, closingType.localCommitPublished.commitTx, feerates) + } + val thirdStageTxs = Helpers.Closing.LocalClose.run { + claimHtlcDelayedOutputs(closingType.localCommitPublished, channelKeys, commitment, feerates) + } + val actions = buildList { + addAll(closingType.localCommitPublished.run { doPublish(staticParams.nodeParams, cmd.state.channelId, secondStageTxs) }) + addAll(closingType.localCommitPublished.run { doPublish(cmd.state.channelId, thirdStageTxs) }) + } Pair(cmd.state, actions) } is RemoteClose -> { - val actions = closingType.remoteCommitPublished.run { doPublish(staticParams.nodeParams, cmd.state.channelId) } + val (_, secondStageTxs) = Helpers.Closing.RemoteClose.run { + claimCommitTxOutputs(channelKeys, commitment, closingType.remoteCommit, closingType.remoteCommitPublished.commitTx, feerates) + } + val actions = closingType.remoteCommitPublished.run { doPublish(staticParams.nodeParams, cmd.state.channelId, secondStageTxs) } Pair(cmd.state, actions) } is RevokedClose -> { - val actions = closingType.revokedCommitPublished.run { doPublish(staticParams.nodeParams, cmd.state.channelId) } + val commitTx = closingType.revokedCommitPublished.commitTx + val remotePerCommitmentSecret = closingType.revokedCommitPublished.remotePerCommitmentSecret + // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect. + val toSelfDelay = cmd.state.commitments.latest.remoteCommitParams.toSelfDelay + val commitmentFormat = cmd.state.commitments.latest.commitmentFormat + val dustLimit = cmd.state.commitments.latest.localCommitParams.dustLimit + val actions = buildList { + val secondStageTxs = Helpers.Closing.RevokedClose.run { + claimCommitTxOutputs(cmd.state.commitments.channelParams, channelKeys, commitTx, remotePerCommitmentSecret, dustLimit, toSelfDelay, commitmentFormat, feerates).second + } + val thirdStageTxs = Helpers.Closing.RevokedClose.run { + claimHtlcTxsOutputs(cmd.state.commitments.channelParams, channelKeys, closingType.revokedCommitPublished, dustLimit, toSelfDelay, commitmentFormat, feerates) + } + closingType.revokedCommitPublished.run { addAll(doPublish(staticParams.nodeParams, cmd.state.channelId, secondStageTxs)) } + closingType.revokedCommitPublished.run { addAll(doPublish(cmd.state.channelId, thirdStageTxs)) } + // We fetch HTLC details to republish HTLC-penalty transactions. + Helpers.Closing.RevokedClose.getRemotePerCommitmentSecret(cmd.state.commitments.channelParams, channelKeys, cmd.state.commitments.remotePerCommitmentSecrets, commitTx)?.let { + add(ChannelAction.Storage.GetHtlcInfos(commitTx.txid, it.second)) + } + } Pair(cmd.state, actions) } is RecoveryClose -> { - val actions = closingType.remoteCommitPublished.run { doPublish(staticParams.nodeParams, cmd.state.channelId) } + // We cannot do anything in that case: we've already published our recovery transaction before restarting, + // and must wait for it to confirm. + val rcp = closingType.remoteCommitPublished + val actions = Helpers.run { watchSpentIfNeeded(cmd.state.channelId, rcp.commitTx, listOfNotNull(rcp.localOutput), rcp.irrevocablySpent) } Pair(cmd.state, actions) } null -> { - // in all other cases we need to be ready for any type of closing + // The closing type isn't known yet: + // - we publish transactions for all types of closes that we detected + // - there may be other commitments, but we'll adapt if we receive WatchAlternativeCommitTxConfirmedTriggered + // - there cannot be 3rd-stage transactions yet, no need to re-compute them val actions = buildList { addAll(unconfirmedFundingTxs.map { ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx) }) addAll(fundingTxWatches.map { ChannelAction.Blockchain.SendWatch(it) }) cmd.state.mutualClosePublished.forEach { addAll(doPublish(it, cmd.state.channelId)) } - cmd.state.localCommitPublished?.run { addAll(doPublish(staticParams.nodeParams, cmd.state.channelId)) } - cmd.state.remoteCommitPublished?.run { addAll(doPublish(staticParams.nodeParams, cmd.state.channelId)) } - cmd.state.nextRemoteCommitPublished?.run { addAll(doPublish(staticParams.nodeParams, cmd.state.channelId)) } - cmd.state.revokedCommitPublished.forEach { it.run { addAll(doPublish(staticParams.nodeParams, cmd.state.channelId)) } } - cmd.state.futureRemoteCommitPublished?.run { addAll(doPublish(staticParams.nodeParams, cmd.state.channelId)) } + cmd.state.localCommitPublished?.let { lcp -> + val (_, txs) = Helpers.Closing.LocalClose.run { claimCommitTxOutputs(channelKeys, commitment, lcp.commitTx, feerates) } + addAll(lcp.run { doPublish(staticParams.nodeParams, cmd.state.channelId, txs) }) + } + cmd.state.remoteCommitPublished?.let { rcp -> + val (_, txs) = Helpers.Closing.RemoteClose.run { claimCommitTxOutputs(channelKeys, commitment, commitment.remoteCommit, rcp.commitTx, feerates) } + addAll(rcp.run { doPublish(staticParams.nodeParams, cmd.state.channelId, txs) }) + } + cmd.state.nextRemoteCommitPublished?.let { rcp -> + val remoteCommit = commitment.nextRemoteCommit!! + val (_, txs) = Helpers.Closing.RemoteClose.run { claimCommitTxOutputs(channelKeys, commitment, remoteCommit, rcp.commitTx, feerates) } + addAll(rcp.run { doPublish(staticParams.nodeParams, cmd.state.channelId, txs) }) + } + cmd.state.futureRemoteCommitPublished?.let { rcp -> + val watchConfirmed = Helpers.run { watchConfirmedIfNeeded(staticParams.nodeParams, cmd.state.channelId, listOf(rcp.commitTx), rcp.irrevocablySpent) } + addAll(watchConfirmed) + val watchSpent = Helpers.run { watchSpentIfNeeded(cmd.state.channelId, rcp.commitTx, listOfNotNull(rcp.localOutput), rcp.irrevocablySpent) } + addAll(watchSpent) + } + cmd.state.revokedCommitPublished.forEach { rvk -> + // TODO: once we allow changing the commitment format or to_self_delay during a splice, those values may be incorrect. + val toSelfDelay = cmd.state.commitments.latest.remoteCommitParams.toSelfDelay + val commitmentFormat = cmd.state.commitments.latest.commitmentFormat + val dustLimit = cmd.state.commitments.latest.localCommitParams.dustLimit + val (_, txs) = Helpers.Closing.RevokedClose.run { + claimCommitTxOutputs(cmd.state.commitments.channelParams, channelKeys, rvk.commitTx, rvk.remotePerCommitmentSecret, dustLimit, toSelfDelay, commitmentFormat, feerates) + } + rvk.run { addAll(doPublish(staticParams.nodeParams, cmd.state.channelId, txs)) } + // We fetch HTLC details to republish HTLC-penalty transactions. + Helpers.Closing.RevokedClose.getRemotePerCommitmentSecret(cmd.state.commitments.channelParams, channelKeys, cmd.state.commitments.remotePerCommitmentSecrets, rvk.commitTx)?.let { + add(ChannelAction.Storage.GetHtlcInfos(rvk.commitTx.txid, it.second)) + } + } } Pair(cmd.state, actions) } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt index d6a2eab13..f567b582d 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt @@ -4,9 +4,12 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Satoshi import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ChannelEvents +import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.Helpers.Funding.computeChannelId +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.msat import fr.acinq.lightning.wire.* import kotlinx.coroutines.CompletableDeferred @@ -25,7 +28,12 @@ data class WaitForOpenChannel( val temporaryChannelId: ByteVector32, val fundingAmount: Satoshi, val walletInputs: List, - val localParams: LocalParams, + val localChannelParams: LocalChannelParams, + val dustLimit: Satoshi, + val htlcMinimum: MilliSatoshi, + val maxHtlcValueInFlightMsat: Long, + val maxAcceptedHtlcs: Int, + val toRemoteDelay: CltvExpiryDelta, val channelConfig: ChannelConfig, val remoteInit: Init, val fundingRates: LiquidityAds.WillFundRates? @@ -38,11 +46,11 @@ data class WaitForOpenChannel( when (val res = Helpers.validateParamsNonInitiator(staticParams.nodeParams, open)) { is Either.Right -> { val channelType = res.value - val channelFeatures = ChannelFeatures(channelType, localFeatures = localParams.features, remoteFeatures = remoteInit.features) + val channelFeatures = ChannelFeatures(channelType, localFeatures = localChannelParams.features, remoteFeatures = remoteInit.features) val minimumDepth = if (staticParams.useZeroConf) 0 else staticParams.nodeParams.minDepthBlocks - val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) + val channelKeys = keyManager.channelKeys(localChannelParams.fundingKeyPath) val localFundingPubkey = channelKeys.fundingKey(0).publicKey() - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey) + val fundingScript = Transactions.makeFundingScript(localFundingPubkey, open.fundingPubkey, channelType.commitmentFormat).pubkeyScript val requestFunding = open.requestFunding val willFund = when { fundingRates == null -> null @@ -53,12 +61,12 @@ data class WaitForOpenChannel( val accept = AcceptDualFundedChannel( temporaryChannelId = open.temporaryChannelId, fundingAmount = fundingAmount, - dustLimit = localParams.dustLimit, - maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - htlcMinimum = localParams.htlcMinimum, + dustLimit = dustLimit, + maxHtlcValueInFlightMsat = maxHtlcValueInFlightMsat, + htlcMinimum = htlcMinimum, minimumDepth = minimumDepth.toLong(), - toSelfDelay = localParams.toSelfDelay, - maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, + toSelfDelay = toRemoteDelay, + maxAcceptedHtlcs = maxAcceptedHtlcs, fundingPubkey = localFundingPubkey, revocationBasepoint = channelKeys.revocationBasePoint, paymentBasepoint = channelKeys.paymentBasePoint, @@ -73,13 +81,10 @@ data class WaitForOpenChannel( } ), ) - val remoteParams = RemoteParams( + val localCommitParams = CommitParams(dustLimit, maxHtlcValueInFlightMsat, htlcMinimum, open.toSelfDelay, maxAcceptedHtlcs) + val remoteCommitParams = CommitParams(open.dustLimit, open.maxHtlcValueInFlightMsat, open.htlcMinimum, toRemoteDelay, open.maxAcceptedHtlcs) + val remoteChannelParams = RemoteChannelParams( nodeId = staticParams.remoteNodeId, - dustLimit = open.dustLimit, - maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, - htlcMinimum = open.htlcMinimum, - toSelfDelay = open.toSelfDelay, - maxAcceptedHtlcs = open.maxAcceptedHtlcs, revocationBasepoint = open.revocationBasepoint, paymentBasepoint = open.paymentBasepoint, delayedPaymentBasepoint = open.delayedPaymentBasepoint, @@ -88,8 +93,8 @@ data class WaitForOpenChannel( ) val channelId = computeChannelId(open, accept) val remoteFundingPubkey = open.fundingPubkey - val dustLimit = open.dustLimit.max(localParams.dustLimit) - val fundingParams = InteractiveTxParams(channelId, false, fundingAmount, open.fundingAmount, remoteFundingPubkey, open.lockTime, dustLimit, open.fundingFeerate) + val dustLimit = localCommitParams.dustLimit.max(remoteCommitParams.dustLimit) + val fundingParams = InteractiveTxParams(channelId, false, fundingAmount, open.fundingAmount, remoteFundingPubkey, open.lockTime, dustLimit, channelType.commitmentFormat, open.fundingFeerate) when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, walletInputs, null)) { is Either.Left -> { logger.error { "could not fund channel: ${fundingContributions.value}" } @@ -101,8 +106,10 @@ data class WaitForOpenChannel( val nextState = WaitForFundingCreated( replyTo, // If our peer asks us to pay the commit tx fees, we accept (only used in tests, as we're otherwise always the channel opener). - localParams.copy(paysCommitTxFees = open.channelFlags.nonInitiatorPaysCommitFees), - remoteParams, + localChannelParams.copy(paysCommitTxFees = open.channelFlags.nonInitiatorPaysCommitFees), + localCommitParams, + remoteChannelParams, + remoteCommitParams, interactiveTxSession, open.commitmentFeerate, open.firstPerCommitmentPoint, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 4b104b083..8a1aab1e7 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.io import fr.acinq.bitcoin.* import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* +import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.IClient import fr.acinq.lightning.blockchain.IWatcher import fr.acinq.lightning.blockchain.WatchTriggered @@ -611,7 +612,13 @@ class Peer( .filterIsInstance() .firstOrNull { it.commitments.availableBalanceForSend() >= amount } ?.let { channel -> - val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = channel.commitments.active.first(), walletInputs = emptyList(), localOutputs = listOf(TxOut(amount, scriptPubKey))) + val weight = FundingContributions.computeWeightPaid( + isInitiator = true, + commitment = channel.commitments.active.first(), + walletInputs = emptyList(), + localOutputs = listOf(TxOut(amount, scriptPubKey)), + channelKeys = channel.commitments.channelKeys(nodeParams.keyManager), + ) val (actualFeerate, miningFee) = client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) Pair(actualFeerate, ChannelManagementFees(miningFee, 0.sat)) } @@ -630,7 +637,13 @@ class Peer( .filterIsInstance() .find { it.channelId == channelId } ?.let { channel -> - val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = channel.commitments.active.first(), walletInputs = emptyList(), localOutputs = emptyList()) + val weight = FundingContributions.computeWeightPaid( + isInitiator = true, + commitment = channel.commitments.active.first(), + walletInputs = emptyList(), + localOutputs = emptyList(), + channelKeys = channel.commitments.channelKeys(nodeParams.keyManager), + ) val (actualFeerate, miningFee) = client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) Pair(actualFeerate, ChannelManagementFees(miningFee, 0.sat)) } @@ -645,7 +658,13 @@ class Peer( .filterIsInstance() .firstOrNull() ?.let { channel -> - val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = channel.commitments.active.first(), walletInputs = emptyList(), localOutputs = emptyList()) + fundingRate.fundingWeight + val weight = fundingRate.fundingWeight + FundingContributions.computeWeightPaid( + isInitiator = true, + commitment = channel.commitments.active.first(), + walletInputs = emptyList(), + localOutputs = emptyList(), + channelKeys = channel.commitments.channelKeys(nodeParams.keyManager), + ) // The mining fee below pays for the entirety of the splice transaction, including inputs and outputs from the liquidity provider. val (actualFeerate, miningFee) = client.computeSpliceCpfpFeerate(channel.commitments, targetFeerate, spliceWeight = weight, logger) // The mining fee below only covers the remote node's inputs and outputs, which are already included in the mining fee above. @@ -665,10 +684,10 @@ class Peer( ?.let { channel -> // We cannot be sure of the scripts that will end up being used, but that shouldn't change the fee too much. Helpers.Closing.makeClosingTxs( - nodeParams.keyManager.channelKeys(channel.commitments.params.localParams.fundingKeyPath), + channel.commitments.channelKeys(nodeParams.keyManager), channel.commitments.latest, - channel.commitments.params.localParams.defaultFinalScriptPubKey, - channel.commitments.params.localParams.defaultFinalScriptPubKey, + channel.commitments.channelParams.localParams.defaultFinalScriptPubKey, + channel.commitments.channelParams.localParams.defaultFinalScriptPubKey, targetFeerate, 0 ).map { ChannelManagementFees(miningFee = it.second.fees, serviceFee = 0.sat) }.right @@ -1222,7 +1241,7 @@ class Peer( } else if (_channels.containsKey(msg.temporaryChannelId)) { logger.warning { "ignoring open_channel with duplicate temporaryChannelId=${msg.temporaryChannelId}" } } else { - val localParams = LocalParams(nodeParams, isChannelOpener = false, payCommitTxFees = msg.channelFlags.nonInitiatorPaysCommitFees) + val localParams = LocalChannelParams(nodeParams, isChannelOpener = false, payCommitTxFees = msg.channelFlags.nonInitiatorPaysCommitFees) val state = WaitForInit val channelConfig = ChannelConfig.standard val initCommand = ChannelCommand.Init.NonInitiator( @@ -1231,6 +1250,11 @@ class Peer( fundingAmount = 0.sat, walletInputs = listOf(), localParams = localParams, + dustLimit = nodeParams.dustLimit, + htlcMinimum = nodeParams.htlcMinimum, + maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, + maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, + toRemoteDelay = nodeParams.toRemoteDelayBlocks, channelConfig = channelConfig, remoteInit = theirInit!!, fundingRates = null @@ -1373,7 +1397,7 @@ class Peer( } } is OpenChannel -> { - val localParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = true) + val localParams = LocalChannelParams(nodeParams, isChannelOpener = true, payCommitTxFees = true) val state = WaitForInit val (state1, actions1) = state.process( ChannelCommand.Init.Initiator( @@ -1382,7 +1406,12 @@ class Peer( walletInputs = cmd.walletInputs, commitTxFeerate = cmd.commitTxFeerate, fundingTxFeerate = cmd.fundingTxFeerate, - localParams = localParams, + localChannelParams = localParams, + dustLimit = nodeParams.dustLimit, + htlcMinimum = nodeParams.htlcMinimum, + maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, + maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, + toRemoteDelay = nodeParams.toRemoteDelayBlocks, remoteInit = theirInit!!, channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), channelConfig = ChannelConfig.standard, @@ -1400,7 +1429,13 @@ class Peer( 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 weight = FundingContributions.computeWeightPaid( + isInitiator = true, + commitment = available.channel.commitments.active.first(), + walletInputs = cmd.walletInputs, + localOutputs = emptyList(), + channelKeys = available.channel.commitments.channelKeys(nodeParams.keyManager), + ) 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(), ChannelManagementFees(miningFee = fee, serviceFee = 0.sat), LiquidityEvents.Source.OnChainWallet, logger)) { @@ -1456,7 +1491,7 @@ class Peer( // 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 dummyFundingScript = Script.write(Scripts.multiSig2of2(randomKey().publicKey(), randomKey().publicKey())).byteVector() val localMiningFee = Transactions.weight2fee(currentFeerates.fundingFeerate, FundingContributions.computeWeightPaid(isInitiator = true, null, dummyFundingScript, cmd.walletInputs, emptyList())) val localFundingAmount = cmd.totalAmount - localMiningFee val fundingFees = requestRemoteFunding.fees(currentFeerates.fundingFeerate, isChannelCreation = true) @@ -1478,7 +1513,7 @@ class Peer( } else -> { // We ask our peer to pay the commit tx fees. - val localParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = false) + val localParams = LocalChannelParams(nodeParams, isChannelOpener = true, payCommitTxFees = false) val channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = true) val initCommand = ChannelCommand.Init.Initiator( replyTo = CompletableDeferred(), @@ -1486,7 +1521,12 @@ class Peer( walletInputs = cmd.walletInputs, commitTxFeerate = currentFeerates.commitmentFeerate, fundingTxFeerate = currentFeerates.fundingFeerate, - localParams = localParams, + localChannelParams = localParams, + dustLimit = nodeParams.dustLimit, + htlcMinimum = nodeParams.htlcMinimum, + maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, + maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, + toRemoteDelay = nodeParams.toRemoteDelayBlocks, remoteInit = theirInit!!, channelFlags = channelFlags, channelConfig = ChannelConfig.standard, @@ -1522,7 +1562,13 @@ class Peer( // We don't contribute any input or output, but we must pay on-chain fees for the shared input and output. // We pay those on-chain fees using our current channel balance. val localBalance = available.channel.commitments.active.first().localCommit.spec.toLocal - val spliceWeight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = available.channel.commitments.active.first(), walletInputs = listOf(), localOutputs = listOf()) + val spliceWeight = FundingContributions.computeWeightPaid( + isInitiator = true, + commitment = available.channel.commitments.active.first(), + walletInputs = listOf(), + localOutputs = listOf(), + channelKeys = available.channel.commitments.channelKeys(nodeParams.keyManager), + ) val (fundingFeerate, localMiningFee) = client.computeSpliceCpfpFeerate(available.channel.commitments, currentFeerates.fundingFeerate, spliceWeight, logger) val (targetFeerate, paymentDetails) = when { localBalance + currentFeeCredit >= localMiningFee + cmd.fees(fundingFeerate, isChannelCreation = false).total -> { @@ -1573,7 +1619,7 @@ class Peer( } SelectChannelResult.None -> { // We ask our peer to pay the commit tx fees. - val localParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = false) + val localParams = LocalChannelParams(nodeParams, isChannelOpener = true, payCommitTxFees = false) val channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = true) // Since we don't have inputs to contribute, we're unable to pay on-chain fees for the shared output. // We target a higher feerate so that the effective feerate isn't too low compared to our target. @@ -1602,7 +1648,12 @@ class Peer( walletInputs = listOf(), commitTxFeerate = currentFeerates.commitmentFeerate, fundingTxFeerate = fundingFeerate, - localParams = localParams, + localChannelParams = localParams, + dustLimit = nodeParams.dustLimit, + htlcMinimum = nodeParams.htlcMinimum, + maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, + maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, + toRemoteDelay = nodeParams.toRemoteDelayBlocks, remoteInit = theirInit!!, channelFlags = channelFlags, channelConfig = ChannelConfig.standard, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt index efc1e504d..f8ecdf04b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt @@ -7,12 +7,12 @@ // serialization code in this file. JsonSerializers.CommitmentSerializer::class, JsonSerializers.CommitmentsSerializer::class, - JsonSerializers.LocalParamsSerializer::class, - JsonSerializers.RemoteParamsSerializer::class, + JsonSerializers.LocalChannelParamsSerializer::class, + JsonSerializers.RemoteChannelParamsSerializer::class, + JsonSerializers.CommitParamsSerializer::class, JsonSerializers.LocalCommitSerializer::class, JsonSerializers.UnsignedLocalCommitSerializer::class, JsonSerializers.RemoteCommitSerializer::class, - JsonSerializers.NextRemoteCommitSerializer::class, JsonSerializers.LocalChangesSerializer::class, JsonSerializers.RemoteChangesSerializer::class, JsonSerializers.EitherSerializer::class, @@ -31,8 +31,6 @@ JsonSerializers.CltvExpiryDeltaSerializer::class, JsonSerializers.FeeratePerKwSerializer::class, JsonSerializers.CommitmentSpecSerializer::class, - JsonSerializers.PublishableTxsSerializer::class, - JsonSerializers.HtlcTxAndSigsSerializer::class, JsonSerializers.ChannelConfigSerializer::class, JsonSerializers.ChannelFeaturesSerializer::class, JsonSerializers.FeaturesSerializer::class, @@ -69,8 +67,14 @@ JsonSerializers.ChannelParamsSerializer::class, JsonSerializers.ChannelOriginSerializer::class, JsonSerializers.CommitmentChangesSerializer::class, + JsonSerializers.CommitmentFormatSerializer::class, + JsonSerializers.InputInfoSerializer::class, + JsonSerializers.IndividualSignatureSerializer::class, + JsonSerializers.ChannelSpendSignatureSerializer::class, JsonSerializers.LocalFundingStatusSerializer::class, JsonSerializers.RemoteFundingStatusSerializer::class, + JsonSerializers.ClosingTxSerializer::class, + JsonSerializers.ClosingTxsSerializer::class, JsonSerializers.CloseCommandSerializer::class, JsonSerializers.ShutdownSerializer::class, JsonSerializers.ClosingCompleteSerializer::class, @@ -120,6 +124,7 @@ import fr.acinq.lightning.payment.Bolt11Invoice.TaggedField import fr.acinq.lightning.transactions.CommitmentSpec import fr.acinq.lightning.transactions.IncomingHtlc import fr.acinq.lightning.transactions.OutgoingHtlc +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.wire.* import fr.acinq.lightning.wire.OfferTypes.OfferChains @@ -203,16 +208,6 @@ object JsonSerializers { subclass(CommitSigTlv.Batch::class, CommitSigTlvBatchSerializer) subclass(UpdateAddHtlcTlv.PathKey::class, UpdateAddHtlcTlvPathKeySerializer) } - // TODO The following declarations are required because serializers for [TransactionWithInputInfo] - // depend themselves on @Contextual serializers. Once we get rid of v2/v3 serialization and we - // define our own context-less serializers in this file, we will be able to clean up - // those declarations. - contextual(OutPointSerializer) - contextual(TxOutSerializer) - contextual(TransactionSerializer) - contextual(ByteVectorSerializer) - contextual(ByteVector32Serializer) - contextual(Bolt11InvoiceSerializer) contextual(OfferSerializer) } @@ -261,11 +256,7 @@ object JsonSerializers { @Serializable data class SharedFundingInputSurrogate(val outPoint: OutPoint, val amount: Satoshi) object SharedFundingInputSerializer : SurrogateSerializer( - transform = { i -> - when (i) { - is SharedFundingInput.Multisig2of2 -> SharedFundingInputSurrogate(i.info.outPoint, i.info.txOut.amount) - } - }, + transform = { i -> SharedFundingInputSurrogate(i.info.outPoint, i.info.txOut.amount) }, delegateSerializer = SharedFundingInputSurrogate.serializer() ) @@ -323,6 +314,34 @@ object JsonSerializers { @Serializer(forClass = CommitmentChanges::class) object CommitmentChangesSerializer + @Serializable + data class CommitmentFormatSurrogate(val rfcName: String) + object CommitmentFormatSerializer : SurrogateSerializer( + transform = { f -> + when (f) { + Transactions.CommitmentFormat.AnchorOutputs -> CommitmentFormatSurrogate("anchor_outputs") + } + }, + delegateSerializer = CommitmentFormatSurrogate.serializer() + ) + + @Serializer(forClass = Transactions.InputInfo::class) + object InputInfoSerializer + + @Serializer(forClass = ChannelSpendSignature.IndividualSignature::class) + object IndividualSignatureSerializer + + @Serializable + data class ChannelSpendSignatureSurrogate(val sig: ByteVector64) + object ChannelSpendSignatureSerializer : SurrogateSerializer( + transform = { s -> + when (s) { + is ChannelSpendSignature.IndividualSignature -> ChannelSpendSignatureSurrogate(s.sig) + } + }, + delegateSerializer = ChannelSpendSignatureSurrogate.serializer() + ) + @Serializable data class LocalFundingStatusSurrogate(val status: String, val txId: TxId) object LocalFundingStatusSerializer : SurrogateSerializer( @@ -347,17 +366,26 @@ object JsonSerializers { delegateSerializer = RemoteFundingStatusSurrogate.serializer() ) + @Serializer(forClass = Transactions.ClosingTx::class) + object ClosingTxSerializer + + @Serializer(forClass = Transactions.ClosingTxs::class) + object ClosingTxsSerializer + @Serializer(forClass = Commitment::class) object CommitmentSerializer @Serializer(forClass = Commitments::class) object CommitmentsSerializer - @Serializer(forClass = LocalParams::class) - object LocalParamsSerializer + @Serializer(forClass = LocalChannelParams::class) + object LocalChannelParamsSerializer + + @Serializer(forClass = RemoteChannelParams::class) + object RemoteChannelParamsSerializer - @Serializer(forClass = RemoteParams::class) - object RemoteParamsSerializer + @Serializer(forClass = CommitParams::class) + object CommitParamsSerializer @Serializer(forClass = LocalCommit::class) object LocalCommitSerializer @@ -368,9 +396,6 @@ object JsonSerializers { @Serializer(forClass = RemoteCommit::class) object RemoteCommitSerializer - @Serializer(forClass = NextRemoteCommit::class) - object NextRemoteCommitSerializer - @Serializer(forClass = LocalChanges::class) object LocalChangesSerializer @@ -405,9 +430,6 @@ object JsonSerializers { object OutPointSerializer : StringSerializer({ "${it.txid}:${it.index}" }) object TransactionSerializer : StringSerializer() - @Serializer(forClass = PublishableTxs::class) - object PublishableTxsSerializer - @Serializable data class CommitmentsSpecSurrogate(val htlcsIn: List, val htlcsOut: List, val feerate: FeeratePerKw, val toLocal: MilliSatoshi, val toRemote: MilliSatoshi) object CommitmentSpecSerializer : SurrogateSerializer( @@ -421,9 +443,6 @@ object JsonSerializers { delegateSerializer = CommitmentsSpecSurrogate.serializer() ) - @Serializer(forClass = HtlcTxAndSigs::class) - object HtlcTxAndSigsSerializer - object ChannelConfigSerializer : SurrogateSerializer>( transform = { o -> o.options.map { it.name } }, delegateSerializer = ListSerializer(String.serializer()) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Serialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Serialization.kt index fdd4daa54..7336d7db4 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Serialization.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Serialization.kt @@ -6,16 +6,17 @@ import fr.acinq.lightning.channel.states.PersistedChannelState object Serialization { fun serialize(state: PersistedChannelState): ByteArray { - return fr.acinq.lightning.serialization.channel.v4.Serialization.serialize(state) + return fr.acinq.lightning.serialization.channel.v5.Serialization.serialize(state) } fun serializePeerStorage(states: List): Pair { - return Pair(4, fr.acinq.lightning.serialization.channel.v4.Serialization.serializePeerStorage(states)) + return Pair(5, fr.acinq.lightning.serialization.channel.v5.Serialization.serializePeerStorage(states)) } fun deserialize(bin: ByteArray): DeserializationResult { return when { - // v4 uses a 1-byte version discriminator + // We started using a 1-byte version discriminator in v4 + bin[0].toInt() == 5 -> DeserializationResult.Success(fr.acinq.lightning.serialization.channel.v5.Deserialization.deserialize(bin)) bin[0].toInt() == 4 -> DeserializationResult.Success(fr.acinq.lightning.serialization.channel.v4.Deserialization.deserialize(bin)) // v2/v3 used a 4-bytes version discriminator and are now unsupported Pack.int32BE(bin) == 3 -> DeserializationResult.UnknownVersion(3) @@ -26,6 +27,7 @@ object Serialization { fun deserializePeerStorage(versionByte: Byte, bin: ByteArray): PeerStorageDeserializationResult { return when(versionByte.toInt()) { + 5 -> PeerStorageDeserializationResult.Success(fr.acinq.lightning.serialization.channel.v5.Deserialization.deserializePeerStorage(bin)) 4 -> PeerStorageDeserializationResult.Success(fr.acinq.lightning.serialization.channel.v4.Deserialization.deserializePeerStorage(bin)) else -> PeerStorageDeserializationResult.UnknownVersion(versionByte.toInt()) } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Utils.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Utils.kt index fa312ee8b..105631994 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Utils.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Utils.kt @@ -13,12 +13,12 @@ internal val Commitments.allHtlcs: Set get() = this.run { // All active commitments have the same htlc set, so we only consider the first one addAll(active.first().localCommit.spec.htlcs) addAll(active.first().remoteCommit.spec.htlcs.map { htlc -> htlc.opposite() }) - active.first().nextRemoteCommit?.let { addAll(it.commit.spec.htlcs.map { htlc -> htlc.opposite() }) } + active.first().nextRemoteCommit?.let { addAll(it.spec.htlcs.map { htlc -> htlc.opposite() }) } // Each inactive commitment may have a distinct htlc set inactive.forEach { c -> addAll(c.localCommit.spec.htlcs) addAll(c.remoteCommit.spec.htlcs.map { htlc -> htlc.opposite() }) - c.nextRemoteCommit?.let { addAll(it.commit.spec.htlcs.map { htlc -> htlc.opposite() }) } + c.nextRemoteCommit?.let { addAll(it.spec.htlcs.map { htlc -> htlc.opposite() }) } } } } \ No newline at end of file diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Deserialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Deserialization.kt index 426e23210..5bfa7430c 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Deserialization.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Deserialization.kt @@ -27,7 +27,6 @@ import fr.acinq.lightning.serialization.InputExtensions.readTxId import fr.acinq.lightning.serialization.channel.allHtlcs import fr.acinq.lightning.serialization.common.liquidityads.Deserialization.readLiquidityPurchase import fr.acinq.lightning.transactions.* -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat @@ -37,10 +36,12 @@ import kotlinx.coroutines.CompletableDeferred object Deserialization { + const val VERSION_MAGIC = 4 + fun deserialize(bin: ByteArray): PersistedChannelState { val input = ByteArrayInput(bin) val version = input.read() - require(version == Serialization.VERSION_MAGIC) { "incorrect version $version, expected ${Serialization.VERSION_MAGIC}" } + require(version == VERSION_MAGIC) { "incorrect version $version, expected $VERSION_MAGIC" } return input.readPersistedChannelState() } @@ -69,17 +70,18 @@ object Deserialization { else -> error("unknown discriminator $discriminator for class ${PersistedChannelState::class}") } - private fun Input.readWaitForFundingSigned() = WaitForFundingSigned( - channelParams = readChannelParams(), - signingSession = readInteractiveTxSigningSession(emptySet()), - remoteSecondPerCommitmentPoint = readPublicKey(), - liquidityPurchase = readNullable { readLiquidityPurchase() }, - channelOrigin = readNullable { readChannelOrigin() } - ) + private fun Input.readWaitForFundingSigned(): WaitForFundingSigned { + val (channelParams, localCommitParams, remoteCommitParams) = readChannelParams() + val signingSession = readInteractiveTxSigningSession(emptySet(), localCommitParams, remoteCommitParams) + val remoteSecondPerCommitmentPoint = readPublicKey() + val liquidityPurchase = readNullable { readLiquidityPurchase() } + val channelOrigin = readNullable { readChannelOrigin() } + return WaitForFundingSigned(channelParams, signingSession, remoteSecondPerCommitmentPoint, liquidityPurchase, channelOrigin) + } private fun Input.readWaitForFundingSignedWithPushAmount(): WaitForFundingSigned { - val channelParams = readChannelParams() - val signingSession = readInteractiveTxSigningSession(emptySet()) + val (channelParams, localCommitParams, remoteCommitParams) = readChannelParams() + val signingSession = readInteractiveTxSigningSession(emptySet(), localCommitParams, remoteCommitParams) // We previously included a local_push_amount and a remote_push_amount. readNumber() readNumber() @@ -90,8 +92,8 @@ object Deserialization { } private fun Input.readWaitForFundingSignedLegacy(): WaitForFundingSigned { - val channelParams = readChannelParams() - val signingSession = readInteractiveTxSigningSession(emptySet()) + val (channelParams, localCommitParams, remoteCommitParams) = readChannelParams() + val signingSession = readInteractiveTxSigningSession(emptySet(), localCommitParams, remoteCommitParams) // We previously included a local_push_amount and a remote_push_amount. readNumber() readNumber() @@ -102,6 +104,8 @@ object Deserialization { private fun Input.readWaitForFundingConfirmedWithPushAmount(): WaitForFundingConfirmed { val commitments = readCommitments() + val localCommitParams = commitments.latest.localCommitParams + val remoteCommitParams = commitments.latest.remoteCommitParams // We previously included a local_push_amount and a remote_push_amount. readNumber() readNumber() @@ -109,22 +113,25 @@ object Deserialization { val deferred = readNullable { readLightningMessage() as ChannelReady } val rbfStatus = when (val discriminator = read()) { 0x00 -> RbfStatus.None - 0x01 -> RbfStatus.WaitingForSigs(readInteractiveTxSigningSession(emptySet())) + 0x01 -> RbfStatus.WaitingForSigs(readInteractiveTxSigningSession(emptySet(), localCommitParams, remoteCommitParams)) else -> error("unknown discriminator $discriminator for class ${RbfStatus::class}") } return WaitForFundingConfirmed(commitments, waitingSinceBlock, deferred, rbfStatus) } - private fun Input.readWaitForFundingConfirmed() = WaitForFundingConfirmed( - commitments = readCommitments(), - waitingSinceBlock = readNumber(), - deferred = readNullable { readLightningMessage() as ChannelReady }, - rbfStatus = when (val discriminator = read()) { + private fun Input.readWaitForFundingConfirmed(): WaitForFundingConfirmed { + val commitments = readCommitments() + val localCommitParams = commitments.latest.localCommitParams + val remoteCommitParams = commitments.latest.remoteCommitParams + val waitingSinceBlock = readNumber() + val deferred = readNullable { readLightningMessage() as ChannelReady } + val rbfStatus = when (val discriminator = read()) { 0x00 -> RbfStatus.None - 0x01 -> RbfStatus.WaitingForSigs(readInteractiveTxSigningSession(emptySet())) + 0x01 -> RbfStatus.WaitingForSigs(readInteractiveTxSigningSession(emptySet(), localCommitParams, remoteCommitParams)) else -> error("unknown discriminator $discriminator for class ${RbfStatus::class}") } - ) + return WaitForFundingConfirmed(commitments, waitingSinceBlock, deferred, rbfStatus) + } private fun Input.readWaitForChannelReady() = WaitForChannelReady( commitments = readCommitments(), @@ -134,6 +141,8 @@ object Deserialization { private fun Input.readNormal(): Normal { val commitments = readCommitments() + val localCommitParams = commitments.latest.localCommitParams + val remoteCommitParams = commitments.latest.remoteCommitParams return Normal( commitments = commitments, shortChannelId = ShortChannelId(readNumber()), @@ -141,7 +150,7 @@ object Deserialization { remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate }, spliceStatus = when (val discriminator = read()) { 0x00 -> SpliceStatus.None - 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(commitments.allHtlcs), readNullable { readLiquidityPurchase() }, readCollection { readChannelOrigin() }.toList()) + 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(commitments.allHtlcs, localCommitParams, remoteCommitParams), readNullable { readLiquidityPurchase() }, readCollection { readChannelOrigin() }.toList()) else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") }, localShutdown = readNullable { readLightningMessage() as Shutdown }, @@ -152,6 +161,8 @@ object Deserialization { private fun Input.readNormalBeforeSimpleClose(): Normal { val commitments = readCommitments() + val localCommitParams = commitments.latest.localCommitParams + val remoteCommitParams = commitments.latest.remoteCommitParams val shortChannelId = ShortChannelId(readNumber()) val channelUpdate = readLightningMessage() as ChannelUpdate val remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate } @@ -166,7 +177,7 @@ object Deserialization { } val spliceStatus = when (val discriminator = read()) { 0x00 -> SpliceStatus.None - 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(commitments.allHtlcs), readNullable { readLiquidityPurchase() }, readCollection { readChannelOrigin() }.toList()) + 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(commitments.allHtlcs, localCommitParams, remoteCommitParams), readNullable { readLiquidityPurchase() }, readCollection { readChannelOrigin() }.toList()) else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") } return Normal(commitments, shortChannelId, channelUpdate, remoteChannelUpdate, spliceStatus, localShutdown, remoteShutdown, closeCommand) @@ -174,6 +185,8 @@ object Deserialization { private fun Input.readNormalLegacy(): Normal { val commitments = readCommitments() + val localCommitParams = commitments.latest.localCommitParams + val remoteCommitParams = commitments.latest.remoteCommitParams val shortChannelId = ShortChannelId(readNumber()) val channelUpdate = readLightningMessage() as ChannelUpdate val remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate } @@ -188,7 +201,7 @@ object Deserialization { } val spliceStatus = when (val discriminator = read()) { 0x00 -> SpliceStatus.None - 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(commitments.allHtlcs), null, readCollection { readChannelOrigin() }.toList()) + 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(commitments.allHtlcs, localCommitParams, remoteCommitParams), null, readCollection { readChannelOrigin() }.toList()) else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") } return Normal(commitments, shortChannelId, channelUpdate, remoteChannelUpdate, spliceStatus, localShutdown, remoteShutdown, closeCommand) @@ -223,11 +236,11 @@ object Deserialization { // We simply ignore them, which will lead to a force-close if one of the proposed transactions is published. readCollection { readCollection { - readTransactionWithInputInfo() // unsigned closing tx + readClosingTx() // unsigned closing tx readDelimitedByteArray() // closing_signed message }.toList() }.toList() - val bestUnpublishedClosingTx = readNullable { readTransactionWithInputInfo() as ClosingTx } + val bestUnpublishedClosingTx = readNullable { readClosingTx() } val closeCommand = readNullable { // We used to store three closing feerates for fee range negotiation. val preferred = FeeratePerKw(readNumber().sat) @@ -244,12 +257,12 @@ object Deserialization { remoteScript = readDelimitedByteArray().byteVector(), proposedClosingTxs = readCollection { Transactions.ClosingTxs( - readNullable { readTransactionWithInputInfo() as ClosingTx }, - readNullable { readTransactionWithInputInfo() as ClosingTx }, - readNullable { readTransactionWithInputInfo() as ClosingTx }, + readNullable { readClosingTx() }, + readNullable { readClosingTx() }, + readNullable { readClosingTx() }, ) }.toList(), - publishedClosingTxs = readCollection { readTransactionWithInputInfo() as ClosingTx }.toList(), + publishedClosingTxs = readCollection { readClosingTx() }.toList(), waitingSinceBlock = readNumber(), closeCommand = readNullable { readCloseCommand() }, ) @@ -257,8 +270,8 @@ object Deserialization { private fun Input.readClosing(): Closing = Closing( commitments = readCommitments(), waitingSinceBlock = readNumber(), - mutualCloseProposed = readCollection { readTransactionWithInputInfo() as ClosingTx }.toList(), - mutualClosePublished = readCollection { readTransactionWithInputInfo() as ClosingTx }.toList(), + mutualCloseProposed = readCollection { readClosingTx() }.toList(), + mutualClosePublished = readCollection { readClosingTx() }.toList(), localCommitPublished = readNullable { readLocalCommitPublished() }, remoteCommitPublished = readNullable { readRemoteCommitPublished() }, nextRemoteCommitPublished = readNullable { readRemoteCommitPublished() }, @@ -266,32 +279,58 @@ object Deserialization { revokedCommitPublished = readCollection { readRevokedCommitPublished() }.toList() ) - private fun Input.readLocalCommitPublished(): LocalCommitPublished = LocalCommitPublished( - commitTx = readTransaction(), - claimMainDelayedOutputTx = readNullable { readTransactionWithInputInfo() as ClaimLocalDelayedOutputTx }, - htlcTxs = readCollection { readOutPoint() to readNullable { readTransactionWithInputInfo() as HtlcTx } }.toMap(), - claimHtlcDelayedTxs = readCollection { readTransactionWithInputInfo() as ClaimLocalDelayedOutputTx }.toList(), - claimAnchorTxs = readCollection { readTransactionWithInputInfo() as ClaimAnchorOutputTx }.toList(), - irrevocablySpent = readIrrevocablySpent() - ) + private fun Input.readLocalCommitPublished(): LocalCommitPublished { + val commitTx = readTransaction() + val localOutput = readNullable { readForceCloseTransactionInputInfo().outPoint } + val (incomingHtlcs, outgoingHtlcs) = readLocalHtlcTransactions() + val htlcDelayedOutputs = readCollection { readForceCloseTransactionInputInfo().outPoint }.toSet() + val anchorOutput = readCollection { readForceCloseTransactionInputInfo().outPoint }.firstOrNull() + val irrevocablySpent = readIrrevocablySpent() + return LocalCommitPublished( + commitTx = commitTx, + localOutput = localOutput, + anchorOutput = anchorOutput, + incomingHtlcs = incomingHtlcs, + outgoingHtlcs = outgoingHtlcs, + htlcDelayedOutputs = htlcDelayedOutputs, + irrevocablySpent = irrevocablySpent + ) + } - private fun Input.readRemoteCommitPublished(): RemoteCommitPublished = RemoteCommitPublished( - commitTx = readTransaction(), - claimMainOutputTx = readNullable { readTransactionWithInputInfo() as ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx }, - claimHtlcTxs = readCollection { readOutPoint() to readNullable { readTransactionWithInputInfo() as ClaimHtlcTx } }.toMap(), - claimAnchorTxs = readCollection { readTransactionWithInputInfo() as ClaimAnchorOutputTx }.toList(), - irrevocablySpent = readIrrevocablySpent() - ) + private fun Input.readRemoteCommitPublished(): RemoteCommitPublished { + val commitTx = readTransaction() + val localOutput = readNullable { readForceCloseTransactionInputInfo().outPoint } + val (incomingHtlcs, outgoingHtlcs) = readRemoteHtlcTransactions() + val anchorOutput = readCollection { readForceCloseTransactionInputInfo().outPoint }.firstOrNull() + val irrevocablySpent = readIrrevocablySpent() + return RemoteCommitPublished( + commitTx = commitTx, + localOutput = localOutput, + anchorOutput = anchorOutput, + incomingHtlcs = incomingHtlcs, + outgoingHtlcs = outgoingHtlcs, + irrevocablySpent = irrevocablySpent + ) + } - private fun Input.readRevokedCommitPublished(): RevokedCommitPublished = RevokedCommitPublished( - commitTx = readTransaction(), - remotePerCommitmentSecret = PrivateKey(readByteVector32()), - claimMainOutputTx = readNullable { readTransactionWithInputInfo() as ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx }, - mainPenaltyTx = readNullable { readTransactionWithInputInfo() as MainPenaltyTx }, - htlcPenaltyTxs = readCollection { readTransactionWithInputInfo() as HtlcPenaltyTx }.toList(), - claimHtlcDelayedPenaltyTxs = readCollection { readTransactionWithInputInfo() as ClaimHtlcDelayedOutputPenaltyTx }.toList(), - irrevocablySpent = readIrrevocablySpent() - ) + private fun Input.readRevokedCommitPublished(): RevokedCommitPublished { + val commitTx = readTransaction() + val remotePerCommitmentSecret = PrivateKey(readByteVector32()) + val localOutput = readNullable { readForceCloseTransactionInputInfo().outPoint } + val remoteOutput = readNullable { readForceCloseTransactionInputInfo().outPoint } + val htlcOutputs = readCollection { readForceCloseTransactionInputInfo().outPoint }.toSet() + val htlcDelayedOutputs = readCollection { readForceCloseTransactionInputInfo().outPoint }.toSet() + val irrevocablySpent = readIrrevocablySpent() + return RevokedCommitPublished( + commitTx = commitTx, + remotePerCommitmentSecret = remotePerCommitmentSecret, + localOutput = localOutput, + remoteOutput = remoteOutput, + htlcOutputs = htlcOutputs, + htlcDelayedOutputs = htlcDelayedOutputs, + irrevocablySpent = irrevocablySpent + ) + } private fun Input.readIrrevocablySpent(): Map = readCollection { readOutPoint() to readTransaction() @@ -307,10 +346,11 @@ object Deserialization { ) private fun Input.readSharedFundingInput(): SharedFundingInput = when (val discriminator = read()) { - 0x01 -> SharedFundingInput.Multisig2of2( + 0x01 -> SharedFundingInput( info = readInputInfo(), fundingTxIndex = readNumber(), - remoteFundingPubkey = readPublicKey() + remoteFundingPubkey = readPublicKey(), + commitmentFormat = Transactions.CommitmentFormat.AnchorOutputs, ) else -> error("unknown discriminator $discriminator for class ${SharedFundingInput::class}") } @@ -323,6 +363,7 @@ object Deserialization { sharedInput = readNullable { readSharedFundingInput() }, remoteFundingPubkey = readPublicKey(), localOutputs = readCollection { TxOut.read(readDelimitedByteArray()) }.toList(), + commitmentFormat = Transactions.CommitmentFormat.AnchorOutputs, lockTime = readNumber(), dustLimit = readNumber().sat, targetFeerate = FeeratePerKw(readNumber().sat) @@ -485,49 +526,84 @@ object Deserialization { else -> error("unknown discriminator $discriminator for class ${SignedSharedTransaction::class}") } - private fun Input.readUnsignedLocalCommitWithHtlcs(): InteractiveTxSigningSession.Companion.UnsignedLocalCommit = InteractiveTxSigningSession.Companion.UnsignedLocalCommit( - index = readNumber(), - spec = readCommitmentSpecWithHtlcs(), - commitTx = readTransactionWithInputInfo() as CommitTx, - htlcTxs = readCollection { readTransactionWithInputInfo() as HtlcTx }.toList(), - ) + private fun Input.readUnsignedLocalCommitWithHtlcs(): InteractiveTxSigningSession.Companion.UnsignedLocalCommit { + val index = readNumber() + val spec = readCommitmentSpecWithHtlcs() + val commitTxId = readCommitTxId() + readCollection { skipHtlcTx() } // htlc transactions + return InteractiveTxSigningSession.Companion.UnsignedLocalCommit(index, spec, commitTxId) + } - private fun Input.readUnsignedLocalCommitWithoutHtlcs(htlcs: Set): InteractiveTxSigningSession.Companion.UnsignedLocalCommit = InteractiveTxSigningSession.Companion.UnsignedLocalCommit( - index = readNumber(), - spec = readCommitmentSpecWithoutHtlcs(htlcs), - commitTx = readTransactionWithInputInfo() as CommitTx, - htlcTxs = readCollection { readTransactionWithInputInfo() as HtlcTx }.toList(), - ) + private fun Input.readUnsignedLocalCommitWithoutHtlcs(htlcs: Set): InteractiveTxSigningSession.Companion.UnsignedLocalCommit { + val index = readNumber() + val spec = readCommitmentSpecWithoutHtlcs(htlcs) + val commitTxId = readCommitTxId() + readCollection { skipHtlcTx() } // htlc transactions + return InteractiveTxSigningSession.Companion.UnsignedLocalCommit(index, spec, commitTxId) + } - private fun Input.readLocalCommitWithHtlcs(): LocalCommit = LocalCommit( - index = readNumber(), - spec = readCommitmentSpecWithHtlcs(), - publishableTxs = PublishableTxs( - commitTx = readTransactionWithInputInfo() as CommitTx, - htlcTxsAndSigs = readCollection { - HtlcTxAndSigs( - txinfo = readTransactionWithInputInfo() as HtlcTx, - localSig = readByteVector64(), - remoteSig = readByteVector64() - ) - }.toList() + private fun Input.readCommitTxId(): TxId { + require(read() == 0x00) // legacy discriminator for commit tx + readInputInfoWithRedeemScript() + val commitTx = readTransaction() + return commitTx.txid + } + + private fun Input.readLocalCommitWithHtlcs(remoteFundingPubKey: PublicKey): LocalCommit { + val index = readNumber() + val spec = readCommitmentSpecWithHtlcs() + val (_, commitTxId, remoteSig) = readRemoteCommitSig(remoteFundingPubKey) + val htlcSigs = readHtlcRemoteSigs() + return LocalCommit( + index = index, + spec = spec, + txId = commitTxId, + remoteSig = ChannelSpendSignature.IndividualSignature(remoteSig), + htlcRemoteSigs = htlcSigs, ) - ) + } - private fun Input.readLocalCommitWithoutHtlcs(htlcs: Set): LocalCommit = LocalCommit( - index = readNumber(), - spec = readCommitmentSpecWithoutHtlcs(htlcs), - publishableTxs = PublishableTxs( - commitTx = readTransactionWithInputInfo() as CommitTx, - htlcTxsAndSigs = readCollection { - HtlcTxAndSigs( - txinfo = readTransactionWithInputInfo() as HtlcTx, - localSig = readByteVector64(), - remoteSig = readByteVector64() - ) - }.toList() + private fun Input.readLocalCommitWithoutHtlcs(htlcs: Set, remoteFundingPubKey: PublicKey): Pair { + val index = readNumber() + val spec = readCommitmentSpecWithoutHtlcs(htlcs) + val (commitInput, commitTxId, remoteSig) = readRemoteCommitSig(remoteFundingPubKey) + val htlcSigs = readHtlcRemoteSigs() + val localCommit = LocalCommit( + index = index, + spec = spec, + txId = commitTxId, + remoteSig = ChannelSpendSignature.IndividualSignature(remoteSig), + htlcRemoteSigs = htlcSigs, ) - ) + return Pair(commitInput, localCommit) + } + + private fun Input.readRemoteCommitSig(remoteFundingPubKey: PublicKey): Triple { + require(read() == 0x00) // legacy discriminator for commit tx + val (commitInput, redeemScript) = readInputInfoWithRedeemScript() + val commitTx = readTransaction() + val remoteSig = extractRemoteCommitSig(commitTx, redeemScript, remoteFundingPubKey) + return Triple(commitInput, commitTx.txid, remoteSig) + } + + private fun Input.readHtlcRemoteSigs(): List { + return readCollection { + skipHtlcTx() // we previously stored the fully signed HTLC transaction, which we now ignore + readByteVector64() // local_sig + readByteVector64() // remote_sig + }.toList() + } + + // We previously stored the signed commit tx: we need to extract the remote sig from its witness. + private fun extractRemoteCommitSig(commitTx: Transaction, redeemScript: ByteVector, remoteFundingPubKey: PublicKey): ByteVector64 { + val script = Script.parse(redeemScript) + val pubkey1 = PublicKey((script[1] as OP_PUSHDATA).data) + val witness = commitTx.txIn.first().witness + return when { + remoteFundingPubKey == pubkey1 -> Crypto.der2compact(witness.stack[1].toByteArray()) + else -> Crypto.der2compact(witness.stack[2].toByteArray()) + } + } private fun Input.readRemoteCommitWithHtlcs(): RemoteCommit = RemoteCommit( index = readNumber(), @@ -555,26 +631,26 @@ object Deserialization { readNumber() // maximum base relay fee } - private fun Input.readInteractiveTxSigningSession(htlcs: Set): InteractiveTxSigningSession { + private fun Input.readInteractiveTxSigningSession(htlcs: Set, localCommitParams: CommitParams, remoteCommitParams: CommitParams): InteractiveTxSigningSession { val fundingParams = readInteractiveTxParams() val fundingTxIndex = readNumber() val fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction val (localCommit, remoteCommit) = when (val discriminator = read()) { 0 -> Pair(Either.Left(readUnsignedLocalCommitWithHtlcs()), readRemoteCommitWithHtlcs()) - 1 -> Pair(Either.Right(readLocalCommitWithHtlcs()), readRemoteCommitWithHtlcs()) + 1 -> Pair(Either.Right(readLocalCommitWithHtlcs(fundingParams.remoteFundingPubkey)), readRemoteCommitWithHtlcs()) 2 -> { skipLegacyLiquidityLease() Pair(Either.Left(readUnsignedLocalCommitWithHtlcs()), readRemoteCommitWithHtlcs()) } 3 -> { skipLegacyLiquidityLease() - Pair(Either.Right(readLocalCommitWithHtlcs()), readRemoteCommitWithHtlcs()) + Pair(Either.Right(readLocalCommitWithHtlcs(fundingParams.remoteFundingPubkey)), readRemoteCommitWithHtlcs()) } 4 -> Pair(Either.Left(readUnsignedLocalCommitWithoutHtlcs(htlcs)), readRemoteCommitWithoutHtlcs(htlcs)) - 5 -> Pair(Either.Right(readLocalCommitWithoutHtlcs(htlcs)), readRemoteCommitWithoutHtlcs(htlcs)) + 5 -> Pair(Either.Right(readLocalCommitWithoutHtlcs(htlcs, fundingParams.remoteFundingPubkey).second), readRemoteCommitWithoutHtlcs(htlcs)) else -> error("unknown discriminator $discriminator for class ${InteractiveTxSigningSession::class}") } - return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, localCommit, remoteCommit) + return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, localCommitParams, localCommit, remoteCommitParams, remoteCommit) } private fun Input.readChannelOrigin(): Origin = when (val discriminator = read()) { @@ -606,7 +682,7 @@ object Deserialization { else -> error("unknown discriminator $discriminator for class ${Origin::class}") } - private fun Input.readLocalParams(): LocalParams { + private fun Input.readLocalParams(): Pair { val nodeId = readPublicKey() val fundingKeyPath = KeyPath(readCollection { readNumber() }.toList()) val dustLimit = readNumber().sat @@ -619,36 +695,48 @@ object Deserialization { 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) + val channelParams = LocalChannelParams(nodeId, fundingKeyPath, isChannelOpener, payCommitTxFees, defaultFinalScriptPubKey, features) + val commitParams = CommitParams(dustLimit, maxHtlcValueInFlightMsat, htlcMinimum, toSelfDelay, maxAcceptedHtlcs) + return Pair(channelParams, commitParams) } - 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.readRemoteParams(): Pair { + val nodeId = readPublicKey() + val dustLimit = readNumber().sat + val maxHtlcValueInFlightMsat = readNumber() + val htlcMinimum = readNumber().msat + val toSelfDelay = CltvExpiryDelta(readNumber().toInt()) + val maxAcceptedHtlcs = readNumber().toInt() + val revocationBasepoint = readPublicKey() + val paymentBasepoint = readPublicKey() + val delayedPaymentBasepoint = readPublicKey() + val htlcBasepoint = readPublicKey() + val features = Features(readDelimitedByteArray().toByteVector()) + val channelParams = RemoteChannelParams(nodeId, revocationBasepoint, paymentBasepoint, delayedPaymentBasepoint, htlcBasepoint, features) + val commitParams = CommitParams(dustLimit, maxHtlcValueInFlightMsat, htlcMinimum, toSelfDelay, maxAcceptedHtlcs) + return Pair(channelParams, commitParams) + } 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 = readLocalParams(), - remoteParams = readRemoteParams(), - channelFlags = readChannelFlags(), - ) + private fun Input.readChannelParams(): Triple { + val channelId = readByteVector32() + val channelConfig = ChannelConfig(readDelimitedByteArray()) + val channelFeatures = ChannelFeatures(Features(readDelimitedByteArray()).activated.keys) + val (localChannelParams, localCommitParams) = readLocalParams() + val (remoteChannelParams, remoteCommitParams) = readRemoteParams() + val channelFlags = readChannelFlags() + val channelParams = ChannelParams(channelId, channelConfig, channelFeatures, localChannelParams, remoteChannelParams, channelFlags) + // We need to use the remote to_self_delay for our commitment, and vice-versa. + val localToRemoteDelay = localCommitParams.toSelfDelay + val remoteToRemoteDelay = remoteCommitParams.toSelfDelay + val localCommitParams1 = localCommitParams.copy(toSelfDelay = remoteToRemoteDelay) + val remoteCommitParams1 = remoteCommitParams.copy(toSelfDelay = localToRemoteDelay) + return Triple(channelParams, localCommitParams1, remoteCommitParams1) + } private fun Input.readCommitmentChanges(): CommitmentChanges = CommitmentChanges( localChanges = LocalChanges( @@ -665,65 +753,88 @@ object Deserialization { remoteNextHtlcId = readNumber(), ) - private fun Input.readCommitment(htlcs: Set): Commitment = Commitment( - fundingTxIndex = readNumber(), - remoteFundingPubkey = readPublicKey(), - localFundingStatus = when (val discriminator = read()) { + private fun Input.readCommitment(htlcs: Set, localCommitParams: CommitParams, remoteCommitParams: CommitParams): Commitment { + val fundingTxIndex = readNumber() + val remoteFundingPubkey = readPublicKey() + val localFundingStatus = when (val discriminator = read()) { 0x00 -> LocalFundingStatus.UnconfirmedFundingTx( sharedTx = readSignedSharedTransaction(), fundingParams = readInteractiveTxParams(), createdAt = readNumber() ) - 0x01 -> LocalFundingStatus.ConfirmedFundingTx( - signedTx = readTransaction(), - fee = readNumber().sat, + 0x01 -> { + val signedTx = readTransaction() + val fee = readNumber().sat // We previously didn't store the tx_signatures after the transaction was confirmed. // It is only used to be retransmitted on reconnection if our peer had not received it. // This happens very rarely in practice, so putting dummy values here shouldn't be an issue. - localSigs = TxSignatures(ByteVector32.Zeroes, TxId(ByteVector32.Zeroes), listOf()), + val localSigs = TxSignatures(ByteVector32.Zeroes, TxId(ByteVector32.Zeroes), listOf()) // We previously didn't store the short_channel_id in the commitment object. // We will fetch the funding transaction on restart to set it to the correct value. - shortChannelId = ShortChannelId(0), - ) - 0x02 -> LocalFundingStatus.ConfirmedFundingTx( - signedTx = readTransaction(), - fee = readNumber().sat, - localSigs = readLightningMessage() as TxSignatures, + val shortChannelId = ShortChannelId(0) + // We don't know yet which output of the funding transaction is the channel output: this will be fixed below. + LocalFundingStatus.ConfirmedFundingTx(signedTx.txIn.map { it.outPoint }, signedTx.txOut.first(), fee, localSigs, shortChannelId) + } + 0x02 -> { + val signedTx = readTransaction() + val fee = readNumber().sat + val localSigs = readLightningMessage() as TxSignatures // We previously didn't store the short_channel_id in the commitment object. // We will fetch the funding transaction on restart to set it to the correct value. - shortChannelId = ShortChannelId(0), - ) - 0x03 -> LocalFundingStatus.ConfirmedFundingTx( - signedTx = readTransaction(), - fee = readNumber().sat, - localSigs = readLightningMessage() as TxSignatures, - shortChannelId = ShortChannelId(readNumber()) - ) + val shortChannelId = ShortChannelId(0) + // We don't know yet which output of the funding transaction is the channel output: this will be fixed below. + LocalFundingStatus.ConfirmedFundingTx(signedTx.txIn.map { it.outPoint }, signedTx.txOut.first(), fee, localSigs, shortChannelId) + } + 0x03 -> { + val signedTx = readTransaction() + val fee = readNumber().sat + val localSigs = readLightningMessage() as TxSignatures + val shortChannelId = ShortChannelId(readNumber()) + // We don't know yet which output of the funding transaction is the channel output: this will be fixed below. + LocalFundingStatus.ConfirmedFundingTx(signedTx.txIn.map { it.outPoint }, signedTx.txOut.first(), fee, localSigs, shortChannelId) + } else -> error("unknown discriminator $discriminator for class ${LocalFundingStatus::class}") - }, - remoteFundingStatus = when (val discriminator = read()) { + } + val remoteFundingStatus = when (val discriminator = read()) { 0x00 -> RemoteFundingStatus.NotLocked 0x01 -> RemoteFundingStatus.Locked else -> error("unknown discriminator $discriminator for class ${RemoteFundingStatus::class}") - }, - localCommit = readLocalCommitWithoutHtlcs(htlcs), - remoteCommit = readRemoteCommitWithoutHtlcs(htlcs), - nextRemoteCommit = readNullable { - NextRemoteCommit( - sig = readLightningMessage() as CommitSig, - commit = readRemoteCommitWithoutHtlcs(htlcs) - ) } - ) + val (commitInput, localCommit) = readLocalCommitWithoutHtlcs(htlcs, remoteFundingPubkey) + val remoteCommit = readRemoteCommitWithoutHtlcs(htlcs) + val nextRemoteCommit = readNullable { + readLightningMessage() as CommitSig // we included our previously sent commit_sig, which we now recompute + readRemoteCommitWithoutHtlcs(htlcs) + } + // Now that we have extracted the funding output from the local commit, we make sure our localFundingStatus uses the right output. + val localFundingStatus1 = when (localFundingStatus) { + is LocalFundingStatus.ConfirmedFundingTx -> localFundingStatus.copy(txOut = commitInput.txOut) + is LocalFundingStatus.UnconfirmedFundingTx -> localFundingStatus + } + return Commitment( + fundingTxIndex = fundingTxIndex, + fundingInput = commitInput.outPoint, + fundingAmount = commitInput.txOut.amount, + remoteFundingPubkey = remoteFundingPubkey, + localFundingStatus = localFundingStatus1, + remoteFundingStatus = remoteFundingStatus, + commitmentFormat = Transactions.CommitmentFormat.AnchorOutputs, + localCommitParams = localCommitParams, + localCommit = localCommit, + remoteCommitParams = remoteCommitParams, + remoteCommit = remoteCommit, + nextRemoteCommit = nextRemoteCommit + ) + } private fun Input.readCommitments(): Commitments { - val params = readChannelParams() + val (channelParams, localCommitParams, remoteCommitParams) = readChannelParams() val changes = readCommitmentChanges() // When multiple commitments are active, htlcs are shared between all of these commitments, so we serialize them separately. // The direction we use is from our local point of view: we use sets, which deduplicates htlcs that are in both local and remote commitments. val htlcs = readCollection { readDirectedHtlc() }.toSet() - val active = readCollection { readCommitment(htlcs) }.toList() - val inactive = readCollection { readCommitment(htlcs) }.toList() + val active = readCollection { readCommitment(htlcs, localCommitParams, remoteCommitParams) }.toList() + val inactive = readCollection { readCommitment(htlcs, localCommitParams, remoteCommitParams) }.toList() val payments = readCollection { readNumber() to UUID.fromString(readString()) }.toMap() @@ -738,7 +849,7 @@ object Deserialization { lastIndex = readNullable { readNumber() } ) readDelimitedByteArray() // ignored legacy remoteChannelData - return Commitments(params, changes, active, inactive, payments, remoteNextCommitInfo, remotePerCommitmentSecrets) + return Commitments(channelParams, changes, active, inactive, payments, remoteNextCommitInfo, remotePerCommitmentSecrets) } private fun Input.readDirectedHtlc(): DirectedHtlc = when (val discriminator = read()) { @@ -773,33 +884,101 @@ object Deserialization { toRemote = readNumber().msat ) - private fun Input.readInputInfo(): Transactions.InputInfo = Transactions.InputInfo( - outPoint = readOutPoint(), - txOut = TxOut.read(readDelimitedByteArray()), - redeemScript = readDelimitedByteArray().toByteVector() - ) + private fun Input.readInputInfo(): Transactions.InputInfo = readInputInfoWithRedeemScript().first + + private fun Input.readInputInfoWithRedeemScript(): Pair { + val outPoint = readOutPoint() + val txOut = TxOut.read(readDelimitedByteArray()) + val redeemScript = readDelimitedByteArray().toByteVector() + return Pair(Transactions.InputInfo(outPoint, txOut), redeemScript) + } private fun Input.readOutPoint(): OutPoint = OutPoint.read(readDelimitedByteArray()) private fun Input.readTransaction(): Transaction = Transaction.read(readDelimitedByteArray()) - private fun Input.readTransactionWithInputInfo(): Transactions.TransactionWithInputInfo = when (val discriminator = read()) { - 0x00 -> CommitTx(input = readInputInfo(), tx = readTransaction()) - 0x01 -> HtlcTx.HtlcSuccessTx(input = readInputInfo(), tx = readTransaction(), paymentHash = readByteVector32(), htlcId = readNumber()) - 0x02 -> HtlcTx.HtlcTimeoutTx(input = readInputInfo(), tx = readTransaction(), htlcId = readNumber()) - 0x03 -> ClaimHtlcTx.ClaimHtlcSuccessTx(input = readInputInfo(), tx = readTransaction(), htlcId = readNumber()) - 0x04 -> ClaimHtlcTx.ClaimHtlcTimeoutTx(input = readInputInfo(), tx = readTransaction(), htlcId = readNumber()) - 0x05 -> ClaimAnchorOutputTx.ClaimLocalAnchorOutputTx(input = readInputInfo(), tx = readTransaction()) - 0x06 -> ClaimAnchorOutputTx.ClaimRemoteAnchorOutputTx(input = readInputInfo(), tx = readTransaction()) - 0x07 -> ClaimLocalDelayedOutputTx(input = readInputInfo(), tx = readTransaction()) - 0x09 -> ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx(input = readInputInfo(), tx = readTransaction()) - 0x10 -> ClaimLocalDelayedOutputTx(input = readInputInfo(), tx = readTransaction()) - 0x0a -> MainPenaltyTx(input = readInputInfo(), tx = readTransaction()) - 0x0b -> HtlcPenaltyTx(input = readInputInfo(), tx = readTransaction()) - 0x0c -> ClaimHtlcDelayedOutputPenaltyTx(input = readInputInfo(), tx = readTransaction()) - 0x0d -> ClosingTx(input = readInputInfo(), tx = readTransaction(), toLocalIndex = readNullable { readNumber().toInt() }) - 0x0e -> SpliceTx(input = readInputInfo(), tx = readTransaction()) - else -> error("unknown discriminator $discriminator for class ${Transactions.TransactionWithInputInfo::class}") + private fun Input.readLocalHtlcTransactions(): Pair, Map> { + val incomingHtlcs = mutableMapOf() + val outgoingHtlcs = mutableMapOf() + readCollection { + val outpoint = readOutPoint() + readNullable { + when (val discriminator = read()) { + 0x01 -> { + readInputInfo() + readTransaction() // htlc-success transaction + readByteVector32() // payment_hash + val htlcId = readNumber() + incomingHtlcs[outpoint] = htlcId + } + 0x02 -> { + readInputInfo() + readTransaction() // htlc-timeout + val htlcId = readNumber() + outgoingHtlcs[outpoint] = htlcId + } + else -> error("unknown discriminator $discriminator for legacy HTLC transactions") + } + } + } + return Pair(incomingHtlcs, outgoingHtlcs) + } + + private fun Input.readRemoteHtlcTransactions(): Pair, Map> { + val incomingHtlcs = mutableMapOf() + val outgoingHtlcs = mutableMapOf() + readCollection { + val outpoint = readOutPoint() + readNullable { + when (val discriminator = read()) { + 0x03 -> { + readInputInfo() + readTransaction() // claim-htlc-success transaction + val htlcId = readNumber() + incomingHtlcs[outpoint] = htlcId + } + 0x04 -> { + readInputInfo() + readTransaction() // claim-htlc-timeout transaction + val htlcId = readNumber() + outgoingHtlcs[outpoint] = htlcId + } + else -> error("unknown discriminator $discriminator for legacy Claim-HTLC transactions") + } + } + } + return Pair(incomingHtlcs, outgoingHtlcs) + } + + private fun Input.skipHtlcTx() { + when (val discriminator = read()) { + 0x01 -> { + readInputInfo() // input + readTransaction() // htlc-success tx + readByteVector32() // payment_hash + readNumber() // htlc_id + } + 0x02 -> { + readInputInfo() // input + readTransaction() // htlc-timeout tx + readNumber() // htlc_id + } + else -> error("unknown discriminator $discriminator for ignored HTLC transactions") + } + } + + private fun Input.readForceCloseTransactionInputInfo(): Transactions.InputInfo = when (val discriminator = read()) { + 0x00, 0x05, 0x06, 0x07, 0x09, 0x10, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e -> { + val input = readInputInfo() + readTransaction() // we ignore the serialized transaction + input + } + else -> error("unknown discriminator $discriminator for legacy force-close transactions") + } + + private fun Input.readClosingTx(): Transactions.ClosingTx = when (val discriminator = read()) { + 0x0d -> Transactions.ClosingTx(input = readInputInfo(), tx = readTransaction(), toLocalOutputIndex = readNullable { readNumber().toInt() }) + else -> error("unknown discriminator $discriminator for class ${Transactions.ClosingTx::class}") } private fun Input.readCloseCommand(): ChannelCommand.Close.MutualClose = ChannelCommand.Close.MutualClose( diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Deserialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Deserialization.kt new file mode 100644 index 000000000..41ee57607 --- /dev/null +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Deserialization.kt @@ -0,0 +1,561 @@ +package fr.acinq.lightning.serialization.channel.v5 + +import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.io.ByteArrayInput +import fr.acinq.bitcoin.io.Input +import fr.acinq.bitcoin.utils.Either +import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Features +import fr.acinq.lightning.ShortChannelId +import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.* +import fr.acinq.lightning.channel.states.* +import fr.acinq.lightning.crypto.ShaChain +import fr.acinq.lightning.serialization.InputExtensions.readBoolean +import fr.acinq.lightning.serialization.InputExtensions.readByteVector32 +import fr.acinq.lightning.serialization.InputExtensions.readByteVector64 +import fr.acinq.lightning.serialization.InputExtensions.readCollection +import fr.acinq.lightning.serialization.InputExtensions.readDelimitedByteArray +import fr.acinq.lightning.serialization.InputExtensions.readEither +import fr.acinq.lightning.serialization.InputExtensions.readLightningMessage +import fr.acinq.lightning.serialization.InputExtensions.readNullable +import fr.acinq.lightning.serialization.InputExtensions.readNumber +import fr.acinq.lightning.serialization.InputExtensions.readPublicKey +import fr.acinq.lightning.serialization.InputExtensions.readString +import fr.acinq.lightning.serialization.InputExtensions.readTxId +import fr.acinq.lightning.serialization.channel.allHtlcs +import fr.acinq.lightning.serialization.common.liquidityads.Deserialization.readLiquidityPurchase +import fr.acinq.lightning.transactions.* +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.* +import kotlinx.coroutines.CompletableDeferred + +object Deserialization { + + fun deserialize(bin: ByteArray): PersistedChannelState { + val input = ByteArrayInput(bin) + val version = input.read() + require(version == Serialization.VERSION_MAGIC) { "incorrect version $version, expected ${Serialization.VERSION_MAGIC}" } + return input.readPersistedChannelState() + } + + fun deserializePeerStorage(bin: ByteArray): List { + val input = ByteArrayInput(bin) + return input.readCollection { input.readPersistedChannelState() }.toList() + } + + private fun Input.readPersistedChannelState(): PersistedChannelState = when (val discriminator = read()) { + 0x00 -> readWaitForFundingSigned() + 0x10 -> readWaitForFundingConfirmed() + 0x20 -> readWaitForChannelReady() + 0x30 -> readNormal() + 0x40 -> readShuttingDown() + 0x50 -> readNegotiating() + 0x60 -> readClosing() + 0x70 -> readWaitForRemotePublishFutureCommitment() + 0x80 -> readClosed() + else -> error("unknown discriminator $discriminator for class ${PersistedChannelState::class}") + } + + private fun Input.readWaitForFundingSigned() = WaitForFundingSigned( + channelParams = readChannelParams(), + signingSession = readInteractiveTxSigningSession(emptySet()), + remoteSecondPerCommitmentPoint = readPublicKey(), + liquidityPurchase = readNullable { readLiquidityPurchase() }, + channelOrigin = readNullable { readChannelOrigin() } + ) + + private fun Input.readWaitForFundingConfirmed() = WaitForFundingConfirmed( + commitments = readCommitments(), + waitingSinceBlock = readNumber(), + deferred = readNullable { readLightningMessage() as ChannelReady }, + rbfStatus = when (val discriminator = read()) { + 0x00 -> RbfStatus.None + 0x01 -> RbfStatus.WaitingForSigs(readInteractiveTxSigningSession(emptySet())) + else -> error("unknown discriminator $discriminator for class ${RbfStatus::class}") + } + ) + + private fun Input.readWaitForChannelReady() = WaitForChannelReady( + commitments = readCommitments(), + shortChannelId = ShortChannelId(readNumber()), + lastSent = readLightningMessage() as ChannelReady + ) + + private fun Input.readNormal(): Normal { + val commitments = readCommitments() + return Normal( + commitments = commitments, + shortChannelId = ShortChannelId(readNumber()), + channelUpdate = readLightningMessage() as ChannelUpdate, + remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate }, + spliceStatus = when (val discriminator = read()) { + 0x00 -> SpliceStatus.None + 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(commitments.allHtlcs), readNullable { readLiquidityPurchase() }, readCollection { readChannelOrigin() }.toList()) + else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") + }, + localShutdown = readNullable { readLightningMessage() as Shutdown }, + remoteShutdown = readNullable { readLightningMessage() as Shutdown }, + closeCommand = readNullable { readCloseCommand() }, + ) + } + + private fun Input.readShuttingDown(): ShuttingDown = ShuttingDown( + commitments = readCommitments(), + localShutdown = readLightningMessage() as Shutdown, + remoteShutdown = readLightningMessage() as Shutdown, + closeCommand = readNullable { readCloseCommand() }, + ) + + private fun Input.readNegotiating(): Negotiating = Negotiating( + commitments = readCommitments(), + localScript = readDelimitedByteArray().byteVector(), + remoteScript = readDelimitedByteArray().byteVector(), + proposedClosingTxs = readCollection { + Transactions.ClosingTxs( + readNullable { readClosingTx() }, + readNullable { readClosingTx() }, + readNullable { readClosingTx() }, + ) + }.toList(), + publishedClosingTxs = readCollection { readClosingTx() }.toList(), + waitingSinceBlock = readNumber(), + closeCommand = readNullable { readCloseCommand() }, + ) + + private fun Input.readClosing(): Closing = Closing( + commitments = readCommitments(), + waitingSinceBlock = readNumber(), + mutualCloseProposed = readCollection { readClosingTx() }.toList(), + mutualClosePublished = readCollection { readClosingTx() }.toList(), + localCommitPublished = readNullable { readLocalCommitPublished() }, + remoteCommitPublished = readNullable { readRemoteCommitPublished() }, + nextRemoteCommitPublished = readNullable { readRemoteCommitPublished() }, + futureRemoteCommitPublished = readNullable { readRemoteCommitPublished() }, + revokedCommitPublished = readCollection { readRevokedCommitPublished() }.toList() + ) + + private fun Input.readClosingTx(): Transactions.ClosingTx = when (val discriminator = read()) { + 0x01 -> Transactions.ClosingTx( + input = readInputInfo(), + tx = readTransaction(), + toLocalOutputIndex = readNullable { readNumber().toInt() }, + ) + else -> error("unknown discriminator $discriminator for class ${Transactions.ClosingTx::class}") + } + + private fun Input.readLocalCommitPublished(): LocalCommitPublished = LocalCommitPublished( + commitTx = readTransaction(), + localOutput = readNullable { readOutPoint() }, + anchorOutput = readNullable { readOutPoint() }, + incomingHtlcs = readCollection { readOutPoint() to readNumber() }.toMap(), + outgoingHtlcs = readCollection { readOutPoint() to readNumber() }.toMap(), + htlcDelayedOutputs = readCollection { readOutPoint() }.toSet(), + irrevocablySpent = readIrrevocablySpent() + ) + + private fun Input.readRemoteCommitPublished(): RemoteCommitPublished = RemoteCommitPublished( + commitTx = readTransaction(), + localOutput = readNullable { readOutPoint() }, + anchorOutput = readNullable { readOutPoint() }, + incomingHtlcs = readCollection { readOutPoint() to readNumber() }.toMap(), + outgoingHtlcs = readCollection { readOutPoint() to readNumber() }.toMap(), + irrevocablySpent = readIrrevocablySpent() + ) + + private fun Input.readRevokedCommitPublished(): RevokedCommitPublished = RevokedCommitPublished( + commitTx = readTransaction(), + remotePerCommitmentSecret = PrivateKey(readByteVector32()), + localOutput = readNullable { readOutPoint() }, + remoteOutput = readNullable { readOutPoint() }, + htlcOutputs = readCollection { readOutPoint() }.toSet(), + htlcDelayedOutputs = readCollection { readOutPoint() }.toSet(), + irrevocablySpent = readIrrevocablySpent() + ) + + private fun Input.readIrrevocablySpent(): Map = readCollection { + readOutPoint() to readTransaction() + }.toMap() + + private fun Input.readWaitForRemotePublishFutureCommitment(): WaitForRemotePublishFutureCommitment = WaitForRemotePublishFutureCommitment( + commitments = readCommitments(), + remoteChannelReestablish = readLightningMessage() as ChannelReestablish + ) + + private fun Input.readClosed(): Closed = Closed( + state = readClosing() + ) + + private fun Input.readSharedFundingInput(): SharedFundingInput = when (val discriminator = read()) { + 0x01 -> SharedFundingInput( + info = readInputInfo(), + fundingTxIndex = readNumber(), + remoteFundingPubkey = readPublicKey(), + commitmentFormat = readCommitmentFormat(), + ) + else -> error("unknown discriminator $discriminator for class ${SharedFundingInput::class}") + } + + private fun Input.readInteractiveTxParams() = InteractiveTxParams( + channelId = readByteVector32(), + isInitiator = readBoolean(), + localContribution = readNumber().sat, + remoteContribution = readNumber().sat, + sharedInput = readNullable { readSharedFundingInput() }, + remoteFundingPubkey = readPublicKey(), + localOutputs = readCollection { TxOut.read(readDelimitedByteArray()) }.toList(), + commitmentFormat = readCommitmentFormat(), + lockTime = readNumber(), + dustLimit = readNumber().sat, + targetFeerate = FeeratePerKw(readNumber().sat) + ) + + private fun Input.readSharedInteractiveTxInput() = when (val discriminator = read()) { + 0x01 -> InteractiveTxInput.Shared( + serialId = readNumber(), + outPoint = readOutPoint(), + publicKeyScript = readDelimitedByteArray().byteVector(), + sequence = readNumber().toUInt(), + localAmount = readNumber().msat, + remoteAmount = readNumber().msat, + htlcAmount = readNumber().msat + ) + else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Shared::class}") + } + + private fun Input.readLocalInteractiveTxInput() = when (val discriminator = read()) { + 0x01 -> InteractiveTxInput.LocalOnly( + serialId = readNumber(), + previousTx = readTransaction(), + previousTxOutput = readNumber(), + sequence = readNumber().toUInt(), + ) + 0x02 -> InteractiveTxInput.LocalLegacySwapIn( + serialId = readNumber(), + previousTx = readTransaction(), + previousTxOutput = readNumber(), + sequence = readNumber().toUInt(), + userKey = readPublicKey(), + serverKey = readPublicKey(), + refundDelay = readNumber().toInt(), + ) + 0x03 -> InteractiveTxInput.LocalSwapIn( + serialId = readNumber(), + previousTx = readTransaction(), + previousTxOutput = readNumber(), + sequence = readNumber().toUInt(), + addressIndex = readNumber().toInt(), + userKey = readPublicKey(), + serverKey = readPublicKey(), + userRefundKey = readPublicKey(), + refundDelay = readNumber().toInt(), + ) + else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Local::class}") + } + + private fun Input.readRemoteInteractiveTxInput() = when (val discriminator = read()) { + 0x01 -> InteractiveTxInput.RemoteOnly( + serialId = readNumber(), + outPoint = readOutPoint(), + txOut = TxOut.read(readDelimitedByteArray()), + sequence = readNumber().toUInt(), + ) + 0x02 -> InteractiveTxInput.RemoteLegacySwapIn( + serialId = readNumber(), + outPoint = readOutPoint(), + txOut = TxOut.read(readDelimitedByteArray()), + sequence = readNumber().toUInt(), + userKey = readPublicKey(), + serverKey = readPublicKey(), + refundDelay = readNumber().toInt() + ) + 0x03 -> InteractiveTxInput.RemoteSwapIn( + serialId = readNumber(), + outPoint = readOutPoint(), + txOut = TxOut.read(readDelimitedByteArray()), + sequence = readNumber().toUInt(), + userKey = readPublicKey(), + serverKey = readPublicKey(), + userRefundKey = readPublicKey(), + refundDelay = readNumber().toInt() + ) + else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::class}") + } + + private fun Input.readSharedInteractiveTxOutput() = when (val discriminator = read()) { + 0x01 -> InteractiveTxOutput.Shared( + serialId = readNumber(), + pubkeyScript = readDelimitedByteArray().toByteVector(), + localAmount = readNumber().msat, + remoteAmount = readNumber().msat, + htlcAmount = readNumber().msat + ) + else -> error("unknown discriminator $discriminator for class ${InteractiveTxOutput.Shared::class}") + } + + private fun Input.readLocalInteractiveTxOutput() = when (val discriminator = read()) { + 0x01 -> InteractiveTxOutput.Local.Change( + serialId = readNumber(), + amount = readNumber().sat, + pubkeyScript = readDelimitedByteArray().toByteVector(), + ) + 0x02 -> InteractiveTxOutput.Local.NonChange( + serialId = readNumber(), + amount = readNumber().sat, + pubkeyScript = readDelimitedByteArray().toByteVector(), + ) + else -> error("unknown discriminator $discriminator for class ${InteractiveTxOutput.Local::class}") + } + + private fun Input.readRemoteInteractiveTxOutput() = when (val discriminator = read()) { + 0x01 -> InteractiveTxOutput.Remote( + serialId = readNumber(), + amount = readNumber().sat, + pubkeyScript = readDelimitedByteArray().toByteVector(), + ) + else -> error("unknown discriminator $discriminator for class ${InteractiveTxOutput.Remote::class}") + } + + private fun Input.readSharedTransaction() = SharedTransaction( + sharedInput = readNullable { readSharedInteractiveTxInput() }, + sharedOutput = readSharedInteractiveTxOutput(), + localInputs = readCollection { readLocalInteractiveTxInput() }.toList(), + remoteInputs = readCollection { readRemoteInteractiveTxInput() }.toList(), + localOutputs = readCollection { readLocalInteractiveTxOutput() }.toList(), + remoteOutputs = readCollection { readRemoteInteractiveTxOutput() }.toList(), + lockTime = readNumber(), + ) + + private fun Input.readScriptWitness() = ScriptWitness(readCollection { readDelimitedByteArray().toByteVector() }.toList()) + + private fun Input.readSignedSharedTransaction() = when (val discriminator = read()) { + 0x01 -> PartiallySignedSharedTransaction( + tx = readSharedTransaction(), + localSigs = readLightningMessage() as TxSignatures + ) + 0x02 -> FullySignedSharedTransaction( + tx = readSharedTransaction(), + localSigs = readLightningMessage() as TxSignatures, + remoteSigs = readLightningMessage() as TxSignatures, + sharedSigs = readNullable { readScriptWitness() }, + ) + else -> error("unknown discriminator $discriminator for class ${SignedSharedTransaction::class}") + } + + private fun Input.readUnsignedLocalCommitWithoutHtlcs(htlcs: Set): InteractiveTxSigningSession.Companion.UnsignedLocalCommit = InteractiveTxSigningSession.Companion.UnsignedLocalCommit( + index = readNumber(), + spec = readCommitmentSpecWithoutHtlcs(htlcs), + txId = readTxId(), + ) + + private fun Input.readLocalCommitWithoutHtlcs(htlcs: Set): LocalCommit = LocalCommit( + index = readNumber(), + spec = readCommitmentSpecWithoutHtlcs(htlcs), + txId = readTxId(), + remoteSig = readChannelSpendSignature(), + htlcRemoteSigs = readCollection { readByteVector64() }.toList(), + ) + + private fun Input.readRemoteCommitWithoutHtlcs(htlcs: Set): RemoteCommit = RemoteCommit( + index = readNumber(), + spec = readCommitmentSpecWithoutHtlcs(htlcs.map { it.opposite() }.toSet()), + txid = readTxId(), + remotePerCommitmentPoint = readPublicKey() + ) + + private fun Input.readInteractiveTxSigningSession(htlcs: Set): InteractiveTxSigningSession = InteractiveTxSigningSession( + fundingParams = readInteractiveTxParams(), + fundingTxIndex = readNumber(), + fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction, + localCommitParams = readCommitParams(), + localCommit = when (val discriminator = read()) { + 0x01 -> Either.Left(readUnsignedLocalCommitWithoutHtlcs(htlcs)) + 0x02 -> Either.Right(readLocalCommitWithoutHtlcs(htlcs)) + else -> error("unknown discriminator $discriminator for class ${InteractiveTxSigningSession::class}") + }, + remoteCommitParams = readCommitParams(), + remoteCommit = readRemoteCommitWithoutHtlcs(htlcs) + ) + + private fun Input.readChannelOrigin(): Origin = when (val discriminator = read()) { + 0x01 -> Origin.OffChainPayment( + paymentPreimage = readByteVector32(), + amountBeforeFees = readNumber().msat, + fees = ChannelManagementFees(miningFee = readNumber().sat, serviceFee = readNumber().sat), + ) + 0x02 -> Origin.OnChainWallet( + inputs = readCollection { readOutPoint() }.toSet(), + amountBeforeFees = readNumber().msat, + fees = ChannelManagementFees(miningFee = readNumber().sat, serviceFee = readNumber().sat), + ) + else -> error("unknown discriminator $discriminator for class ${Origin::class}") + } + + private fun Input.readLocalChannelParams(): LocalChannelParams { + val nodeId = readPublicKey() + val fundingKeyPath = KeyPath(readCollection { readNumber() }.toList()) + 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 LocalChannelParams(nodeId, fundingKeyPath, isChannelOpener, payCommitTxFees, defaultFinalScriptPubKey, features) + } + + private fun Input.readRemoteChannelParams(): RemoteChannelParams = RemoteChannelParams( + nodeId = readPublicKey(), + revocationBasepoint = readPublicKey(), + paymentBasepoint = readPublicKey(), + delayedPaymentBasepoint = readPublicKey(), + htlcBasepoint = readPublicKey(), + features = Features(readDelimitedByteArray().toByteVector()) + ) + + private fun Input.readCommitParams(): CommitParams = CommitParams( + dustLimit = readNumber().sat, + maxHtlcValueInFlightMsat = readNumber(), + htlcMinimum = readNumber().msat, + toSelfDelay = CltvExpiryDelta(readNumber().toInt()), + maxAcceptedHtlcs = readNumber().toInt(), + ) + + 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 = readLocalChannelParams(), + remoteParams = readRemoteChannelParams(), + channelFlags = readChannelFlags(), + ) + + private fun Input.readCommitmentChanges(): CommitmentChanges = CommitmentChanges( + localChanges = LocalChanges( + proposed = readCollection { readLightningMessage() as UpdateMessage }.toList(), + signed = readCollection { readLightningMessage() as UpdateMessage }.toList(), + acked = readCollection { readLightningMessage() as UpdateMessage }.toList(), + ), + remoteChanges = RemoteChanges( + proposed = readCollection { readLightningMessage() as UpdateMessage }.toList(), + acked = readCollection { readLightningMessage() as UpdateMessage }.toList(), + signed = readCollection { readLightningMessage() as UpdateMessage }.toList(), + ), + localNextHtlcId = readNumber(), + remoteNextHtlcId = readNumber(), + ) + + private fun Input.readCommitment(htlcs: Set): Commitment = Commitment( + fundingTxIndex = readNumber(), + fundingInput = readOutPoint(), + fundingAmount = readNumber().sat, + remoteFundingPubkey = readPublicKey(), + localFundingStatus = when (val discriminator = read()) { + 0x00 -> LocalFundingStatus.UnconfirmedFundingTx( + sharedTx = readSignedSharedTransaction(), + fundingParams = readInteractiveTxParams(), + createdAt = readNumber() + ) + 0x01 -> LocalFundingStatus.ConfirmedFundingTx( + spentInputs = readCollection { readOutPoint() }.toList(), + txOut = TxOut.read(readDelimitedByteArray()), + fee = readNumber().sat, + localSigs = readLightningMessage() as TxSignatures, + shortChannelId = ShortChannelId(readNumber()) + ) + else -> error("unknown discriminator $discriminator for class ${LocalFundingStatus::class}") + }, + remoteFundingStatus = when (val discriminator = read()) { + 0x00 -> RemoteFundingStatus.NotLocked + 0x01 -> RemoteFundingStatus.Locked + else -> error("unknown discriminator $discriminator for class ${RemoteFundingStatus::class}") + }, + commitmentFormat = readCommitmentFormat(), + localCommitParams = readCommitParams(), + localCommit = readLocalCommitWithoutHtlcs(htlcs), + remoteCommitParams = readCommitParams(), + remoteCommit = readRemoteCommitWithoutHtlcs(htlcs), + nextRemoteCommit = readNullable { readRemoteCommitWithoutHtlcs(htlcs) } + ) + + private fun Input.readCommitments(): Commitments { + val params = readChannelParams() + val changes = readCommitmentChanges() + // When multiple commitments are active, htlcs are shared between all of these commitments, so we serialize them separately. + // The direction we use is from our local point of view: we use sets, which deduplicates htlcs that are in both local and remote commitments. + val htlcs = readCollection { readDirectedHtlc() }.toSet() + val active = readCollection { readCommitment(htlcs) }.toList() + val inactive = readCollection { readCommitment(htlcs) }.toList() + val payments = readCollection { + readNumber() to UUID.fromString(readString()) + }.toMap() + val remoteNextCommitInfo = readEither( + readLeft = { WaitingForRevocation(sentAfterLocalCommitIndex = readNumber()) }, + readRight = { readPublicKey() }, + ) + val remotePerCommitmentSecrets = ShaChain( + knownHashes = readCollection { + readCollection { readBoolean() }.toList() to readByteVector32() + }.toMap(), + lastIndex = readNullable { readNumber() } + ) + return Commitments(params, changes, active, inactive, payments, remoteNextCommitInfo, remotePerCommitmentSecrets) + } + + private fun Input.readDirectedHtlc(): DirectedHtlc = when (val discriminator = read()) { + 0 -> IncomingHtlc(readLightningMessage() as UpdateAddHtlc) + 1 -> OutgoingHtlc(readLightningMessage() as UpdateAddHtlc) + else -> error("invalid discriminator $discriminator for class ${DirectedHtlc::class}") + } + + private fun Input.readCommitmentSpecWithoutHtlcs(htlcs: Set): CommitmentSpec = CommitmentSpec( + htlcs = readCollection { + when (val discriminator = read()) { + 0 -> { + val htlcId = readNumber() + htlcs.first { it is IncomingHtlc && it.add.id == htlcId } + } + 1 -> { + val htlcId = readNumber() + htlcs.first { it is OutgoingHtlc && it.add.id == htlcId } + } + else -> error("invalid discriminator $discriminator for class ${DirectedHtlc::class}") + } + }.toSet(), + feerate = FeeratePerKw(readNumber().sat), + toLocal = readNumber().msat, + toRemote = readNumber().msat + ) + + private fun Input.readInputInfo(): Transactions.InputInfo = Transactions.InputInfo( + outPoint = readOutPoint(), + txOut = TxOut.read(readDelimitedByteArray()), + ) + + private fun Input.readOutPoint(): OutPoint = OutPoint.read(readDelimitedByteArray()) + + private fun Input.readTransaction(): Transaction = Transaction.read(readDelimitedByteArray()) + + private fun Input.readCommitmentFormat(): Transactions.CommitmentFormat = when (val discriminator = read()) { + 0x00 -> Transactions.CommitmentFormat.AnchorOutputs + else -> error("invalid discriminator $discriminator for class ${Transactions.CommitmentFormat::class}") + } + + private fun Input.readChannelSpendSignature(): ChannelSpendSignature = when (val discriminator = read()) { + 0x00 -> ChannelSpendSignature.IndividualSignature(readByteVector64()) + else -> error("invalid discriminator $discriminator for class ${ChannelSpendSignature::class}") + } + + private fun Input.readCloseCommand(): ChannelCommand.Close.MutualClose = ChannelCommand.Close.MutualClose( + replyTo = CompletableDeferred(), + scriptPubKey = readNullable { readDelimitedByteArray().toByteVector() }, + feerate = FeeratePerKw(readNumber().sat), + ) + +} \ No newline at end of file diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Serialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Serialization.kt similarity index 76% rename from modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Serialization.kt rename to modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Serialization.kt index da9c60052..f5a34ab62 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Serialization.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Serialization.kt @@ -1,4 +1,4 @@ -package fr.acinq.lightning.serialization.channel.v4 +package fr.acinq.lightning.serialization.channel.v5 import fr.acinq.bitcoin.BtcSerializer import fr.acinq.bitcoin.OutPoint @@ -27,7 +27,6 @@ import fr.acinq.lightning.serialization.OutputExtensions.writeTxId import fr.acinq.lightning.serialization.channel.allHtlcs import fr.acinq.lightning.serialization.common.liquidityads.Serialization.writeLiquidityPurchase import fr.acinq.lightning.transactions.* -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* import fr.acinq.lightning.wire.LightningCodecs /** @@ -49,7 +48,7 @@ import fr.acinq.lightning.wire.LightningCodecs */ object Serialization { - const val VERSION_MAGIC = 4 + const val VERSION_MAGIC = 5 fun serialize(o: PersistedChannelState): ByteArray { val out = ByteArrayOutput() @@ -65,32 +64,32 @@ object Serialization { } private fun Output.writePersistedChannelState(o: PersistedChannelState) = when (o) { + is WaitForFundingSigned -> { + write(0x00); writeWaitForFundingSigned(o) + } + is WaitForFundingConfirmed -> { + write(0x10); writeWaitForFundingConfirmed(o) + } is WaitForChannelReady -> { - write(0x01); writeWaitForChannelReady(o) + write(0x20); writeWaitForChannelReady(o) } is Normal -> { - write(0x0f); writeNormal(o) + write(0x30); writeNormal(o) } is ShuttingDown -> { - write(0x10); writeShuttingDown(o) + write(0x40); writeShuttingDown(o) } is Negotiating -> { - write(0x11); writeNegotiating(o) + write(0x50); writeNegotiating(o) } is Closing -> { - write(0x05); writeClosing(o) + write(0x60); writeClosing(o) } is WaitForRemotePublishFutureCommitment -> { - write(0x06); writeWaitForRemotePublishFutureCommitment(o) + write(0x70); writeWaitForRemotePublishFutureCommitment(o) } is Closed -> { - write(0x07); writeClosed(o) - } - is WaitForFundingSigned -> { - write(0x0d); writeWaitForFundingSigned(o) - } - is WaitForFundingConfirmed -> { - write(0x0e); writeWaitForFundingConfirmed(o) + write(0x80); writeClosed(o) } } @@ -156,11 +155,11 @@ object Serialization { writeDelimited(localScript.toByteArray()) writeDelimited(remoteScript.toByteArray()) writeCollection(proposedClosingTxs) { - writeNullable(it.localAndRemote) { tx -> writeTransactionWithInputInfo(tx) } - writeNullable(it.localOnly) { tx -> writeTransactionWithInputInfo(tx) } - writeNullable(it.remoteOnly) { tx -> writeTransactionWithInputInfo(tx) } + writeNullable(it.localAndRemote) { tx -> writeClosingTx(tx) } + writeNullable(it.localOnly) { tx -> writeClosingTx(tx) } + writeNullable(it.remoteOnly) { tx -> writeClosingTx(tx) } } - writeCollection(publishedClosingTxs) { writeTransactionWithInputInfo(it) } + writeCollection(publishedClosingTxs) { writeClosingTx(it) } writeNumber(waitingSinceBlock) writeNullable(closeCommand) { writeCloseCommand(it) } } @@ -168,8 +167,8 @@ object Serialization { private fun Output.writeClosing(o: Closing) = o.run { writeCommitments(commitments) writeNumber(waitingSinceBlock) - writeCollection(mutualCloseProposed) { writeTransactionWithInputInfo(it) } - writeCollection(mutualClosePublished) { writeTransactionWithInputInfo(it) } + writeCollection(mutualCloseProposed) { writeClosingTx(it) } + writeCollection(mutualClosePublished) { writeClosingTx(it) } writeNullable(localCommitPublished) { writeLocalCommitPublished(it) } writeNullable(remoteCommitPublished) { writeRemoteCommitPublished(it) } writeNullable(nextRemoteCommitPublished) { writeRemoteCommitPublished(it) } @@ -177,36 +176,51 @@ object Serialization { writeCollection(revokedCommitPublished) { writeRevokedCommitPublished(it) } } + private fun Output.writeClosingTx(o: Transactions.ClosingTx) = o.run { + write(0x01) + writeInputInfo(input) + writeBtcObject(tx) + writeNullable(toLocalOutputIndex) { writeNumber(it) } + } + private fun Output.writeLocalCommitPublished(o: LocalCommitPublished) = o.run { writeBtcObject(commitTx) - writeNullable(claimMainDelayedOutputTx) { writeTransactionWithInputInfo(it) } - writeCollection(htlcTxs.entries) { + writeNullable(localOutput) { writeBtcObject(it) } + writeNullable(anchorOutput) { writeBtcObject(it) } + writeCollection(incomingHtlcs.entries) { + writeBtcObject(it.key) + writeNumber(it.value) + } + writeCollection(outgoingHtlcs.entries) { writeBtcObject(it.key) - writeNullable(it.value) { htlcTx -> writeTransactionWithInputInfo(htlcTx) } + writeNumber(it.value) } - writeCollection(claimHtlcDelayedTxs) { writeTransactionWithInputInfo(it) } - writeCollection(claimAnchorTxs) { writeTransactionWithInputInfo(it) } + writeCollection(htlcDelayedOutputs) { writeBtcObject(it) } writeIrrevocablySpent(irrevocablySpent) } private fun Output.writeRemoteCommitPublished(o: RemoteCommitPublished) = o.run { writeBtcObject(commitTx) - writeNullable(claimMainOutputTx) { writeTransactionWithInputInfo(it) } - writeCollection(claimHtlcTxs.entries) { + writeNullable(localOutput) { writeBtcObject(it) } + writeNullable(anchorOutput) { writeBtcObject(it) } + writeCollection(incomingHtlcs.entries) { writeBtcObject(it.key) - writeNullable(it.value) { claimHtlcTx -> writeTransactionWithInputInfo(claimHtlcTx) } + writeNumber(it.value) + } + writeCollection(outgoingHtlcs.entries) { + writeBtcObject(it.key) + writeNumber(it.value) } - writeCollection(claimAnchorTxs) { writeTransactionWithInputInfo(it) } writeIrrevocablySpent(irrevocablySpent) } private fun Output.writeRevokedCommitPublished(o: RevokedCommitPublished) = o.run { writeBtcObject(commitTx) writeByteVector32(remotePerCommitmentSecret.value) - writeNullable(claimMainOutputTx) { writeTransactionWithInputInfo(it) } - writeNullable(mainPenaltyTx) { writeTransactionWithInputInfo(it) } - writeCollection(htlcPenaltyTxs) { writeTransactionWithInputInfo(it) } - writeCollection(claimHtlcDelayedPenaltyTxs) { writeTransactionWithInputInfo(it) } + writeNullable(localOutput) { writeBtcObject(it) } + writeNullable(remoteOutput) { writeBtcObject(it) } + writeCollection(htlcOutputs) { writeBtcObject(it) } + writeCollection(htlcDelayedOutputs) { writeBtcObject(it) } writeIrrevocablySpent(irrevocablySpent) } @@ -224,13 +238,12 @@ object Serialization { writeClosing(state) } - private fun Output.writeSharedFundingInput(i: SharedFundingInput) = when (i) { - is SharedFundingInput.Multisig2of2 -> { - write(0x01) - writeInputInfo(i.info) - writeNumber(i.fundingTxIndex) - writePublicKey(i.remoteFundingPubkey) - } + private fun Output.writeSharedFundingInput(i: SharedFundingInput) = i.run { + write(0x01) + writeInputInfo(info) + writeNumber(fundingTxIndex) + writePublicKey(remoteFundingPubkey) + writeCommitmentFormat(commitmentFormat) } private fun Output.writeInteractiveTxParams(o: InteractiveTxParams) = o.run { @@ -241,13 +254,14 @@ object Serialization { writeNullable(sharedInput) { writeSharedFundingInput(it) } writePublicKey(remoteFundingPubkey) writeCollection(localOutputs) { writeBtcObject(it) } + writeCommitmentFormat(commitmentFormat) writeNumber(lockTime) writeNumber(dustLimit.toLong()) writeNumber(targetFeerate.toLong()) } private fun Output.writeSharedInteractiveTxInput(i: InteractiveTxInput.Shared) = i.run { - write(0x03) + write(0x01) writeNumber(serialId) writeBtcObject(outPoint) writeDelimited(publicKeyScript.toByteArray()) @@ -321,7 +335,7 @@ object Serialization { } private fun Output.writeSharedInteractiveTxOutput(o: InteractiveTxOutput.Shared) = o.run { - write(0x02) + write(0x01) writeNumber(serialId) writeDelimited(pubkeyScript.toByteArray()) writeNumber(localAmount.toLong()) @@ -381,21 +395,15 @@ object Serialization { private fun Output.writeUnsignedLocalCommitWithoutHtlcs(localCommit: InteractiveTxSigningSession.Companion.UnsignedLocalCommit) { writeNumber(localCommit.index) writeCommitmentSpecWithoutHtlcs(localCommit.spec) - writeTransactionWithInputInfo(localCommit.commitTx) - writeCollection(localCommit.htlcTxs) { writeTransactionWithInputInfo(it) } + writeTxId(localCommit.txId) } private fun Output.writeLocalCommitWithoutHtlcs(localCommit: LocalCommit) = localCommit.run { writeNumber(index) writeCommitmentSpecWithoutHtlcs(spec) - publishableTxs.run { - writeTransactionWithInputInfo(commitTx) - writeCollection(htlcTxsAndSigs) { htlc -> - writeTransactionWithInputInfo(htlc.txinfo) - writeByteVector64(htlc.localSig) - writeByteVector64(htlc.remoteSig) - } - } + writeTxId(txId) + writeChannelSpendSignature(remoteSig) + writeCollection(htlcRemoteSigs) { writeByteVector64(it) } } private fun Output.writeRemoteCommitWithoutHtlcs(remoteCommit: RemoteCommit) = remoteCommit.run { @@ -409,30 +417,31 @@ object Serialization { writeInteractiveTxParams(fundingParams) writeNumber(s.fundingTxIndex) writeSignedSharedTransaction(fundingTx) - when(localCommit) { + writeCommitParams(localCommitParams) + when (localCommit) { is Either.Left -> { - write(4) + write(0x01) writeUnsignedLocalCommitWithoutHtlcs(localCommit.value) - writeRemoteCommitWithoutHtlcs(remoteCommit) } is Either.Right -> { - write(5) + write(0x02) writeLocalCommitWithoutHtlcs(localCommit.value) - writeRemoteCommitWithoutHtlcs(remoteCommit) } } + writeCommitParams(remoteCommitParams) + writeRemoteCommitWithoutHtlcs(remoteCommit) } private fun Output.writeChannelOrigin(o: Origin) = when (o) { is Origin.OffChainPayment -> { - write(0x03) + write(0x01) writeByteVector32(o.paymentPreimage) writeNumber(o.amountBeforeFees.toLong()) writeNumber(o.fees.miningFee.toLong()) writeNumber(o.fees.serviceFee.toLong()) } is Origin.OnChainWallet -> { - write(0x04) + write(0x02) writeCollection(o.inputs) { writeBtcObject(it) } writeNumber(o.amountBeforeFees.toLong()) writeNumber(o.fees.miningFee.toLong()) @@ -447,11 +456,6 @@ object Serialization { localParams.run { writePublicKey(nodeId) writeCollection(fundingKeyPath.path) { writeNumber(it) } - writeNumber(dustLimit.toLong()) - writeNumber(maxHtlcValueInFlightMsat) - writeNumber(htlcMinimum.toLong()) - writeNumber(toSelfDelay.toLong()) - writeNumber(maxAcceptedHtlcs) // We encode those two booleans in the same byte. val isOpenerFlag = if (isChannelOpener) 1 else 0 val payCommitTxFeesFlag = if (paysCommitTxFees) 2 else 0 @@ -461,11 +465,6 @@ object Serialization { } remoteParams.run { writePublicKey(nodeId) - writeNumber(dustLimit.toLong()) - writeNumber(maxHtlcValueInFlightMsat) - writeNumber(htlcMinimum.toLong()) - writeNumber(toSelfDelay.toLong()) - writeNumber(maxAcceptedHtlcs) writePublicKey(revocationBasepoint) writePublicKey(paymentBasepoint) writePublicKey(delayedPaymentBasepoint) @@ -478,6 +477,14 @@ object Serialization { writeNumber(announceChannelFlag + nonInitiatorPaysCommitFeesFlag) } + private fun Output.writeCommitParams(o: CommitParams) = o.run { + writeNumber(dustLimit.toLong()) + writeNumber(maxHtlcValueInFlightMsat) + writeNumber(htlcMinimum.toLong()) + writeNumber(toSelfDelay.toLong()) + writeNumber(maxAcceptedHtlcs) + } + private fun Output.writeCommitmentChanges(o: CommitmentChanges) = o.run { localChanges.run { writeCollection(proposed) { writeLightningMessage(it) } @@ -495,6 +502,8 @@ object Serialization { private fun Output.writeCommitment(o: Commitment) = o.run { writeNumber(fundingTxIndex) + writeBtcObject(fundingInput) + writeNumber(fundingAmount.toLong()) writePublicKey(remoteFundingPubkey) when (localFundingStatus) { is LocalFundingStatus.UnconfirmedFundingTx -> { @@ -504,8 +513,9 @@ object Serialization { writeNumber(localFundingStatus.createdAt) } is LocalFundingStatus.ConfirmedFundingTx -> { - write(0x03) - writeBtcObject(localFundingStatus.signedTx) + write(0x01) + writeCollection(localFundingStatus.spentInputs) { writeBtcObject(it) } + writeBtcObject(localFundingStatus.txOut) writeNumber(localFundingStatus.fee.toLong()) writeLightningMessage(localFundingStatus.localSigs) writeNumber(localFundingStatus.shortChannelId.toLong()) @@ -515,19 +525,16 @@ object Serialization { is RemoteFundingStatus.NotLocked -> write(0x00) is RemoteFundingStatus.Locked -> write(0x01) } + writeCommitmentFormat(commitmentFormat) + writeCommitParams(localCommitParams) writeLocalCommitWithoutHtlcs(localCommit) + writeCommitParams(remoteCommitParams) writeRemoteCommitWithoutHtlcs(remoteCommit) - writeNullable(nextRemoteCommit) { - writeLightningMessage(it.sig) - writeNumber(it.commit.index) - writeCommitmentSpecWithoutHtlcs(it.commit.spec) - writeTxId(it.commit.txid) - writePublicKey(it.commit.remotePerCommitmentPoint) - } + writeNullable(nextRemoteCommit) { writeRemoteCommitWithoutHtlcs(it) } } private fun Output.writeCommitments(o: Commitments) = o.run { - writeChannelParams(params) + writeChannelParams(channelParams) writeCommitmentChanges(changes) // When multiple commitments are active, htlcs are shared between all of these commitments, so we serialize the htlcs separately to save space. // The direction we use is from our local point of view: we use sets, which deduplicates htlcs that are in both local and remote commitments. @@ -550,7 +557,6 @@ object Serialization { } writeNullable(lastIndex) { writeNumber(it) } } - writeNumber(0) // ignored legacy remoteChannelData } private fun Output.writeDirectedHtlc(htlc: DirectedHtlc) = htlc.run { @@ -578,53 +584,16 @@ object Serialization { private fun Output.writeInputInfo(o: Transactions.InputInfo): Unit = o.run { writeBtcObject(outPoint) writeBtcObject(txOut) - writeDelimited(redeemScript.toByteArray()) } - private fun Output.writeTransactionWithInputInfo(o: Transactions.TransactionWithInputInfo) { - when (o) { - is CommitTx -> { - write(0x00); writeInputInfo(o.input); writeBtcObject(o.tx) - } - is HtlcTx.HtlcSuccessTx -> { - write(0x01); writeInputInfo(o.input); writeBtcObject(o.tx); writeByteVector32(o.paymentHash); writeNumber(o.htlcId) - } - is HtlcTx.HtlcTimeoutTx -> { - write(0x02); writeInputInfo(o.input); writeBtcObject(o.tx); writeNumber(o.htlcId) - } - is ClaimHtlcTx.ClaimHtlcSuccessTx -> { - write(0x03); writeInputInfo(o.input); writeBtcObject(o.tx); writeNumber(o.htlcId) - } - is ClaimHtlcTx.ClaimHtlcTimeoutTx -> { - write(0x04); writeInputInfo(o.input); writeBtcObject(o.tx); writeNumber(o.htlcId) - } - is ClaimAnchorOutputTx.ClaimLocalAnchorOutputTx -> { - write(0x05); writeInputInfo(o.input); writeBtcObject(o.tx) - } - is ClaimAnchorOutputTx.ClaimRemoteAnchorOutputTx -> { - write(0x06); writeInputInfo(o.input); writeBtcObject(o.tx) - } - is ClaimLocalDelayedOutputTx -> { - write(0x07); writeInputInfo(o.input); writeBtcObject(o.tx) - } - is ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx -> { - write(0x09); writeInputInfo(o.input); writeBtcObject(o.tx) - } - is MainPenaltyTx -> { - write(0x0a); writeInputInfo(o.input); writeBtcObject(o.tx) - } - is HtlcPenaltyTx -> { - write(0x0b); writeInputInfo(o.input); writeBtcObject(o.tx) - } - is ClaimHtlcDelayedOutputPenaltyTx -> { - write(0x0c); writeInputInfo(o.input); writeBtcObject(o.tx) - } - is ClosingTx -> { - write(0x0d); writeInputInfo(o.input); writeBtcObject(o.tx); writeNullable(o.toLocalIndex) { writeNumber(it) } - } - is SpliceTx -> { - write(0x0e); writeInputInfo(o.input); writeBtcObject(o.tx) - } + private fun Output.writeCommitmentFormat(o: Transactions.CommitmentFormat) = when (o) { + Transactions.CommitmentFormat.AnchorOutputs -> write(0x00) + } + + private fun Output.writeChannelSpendSignature(sig: ChannelSpendSignature) = when (sig) { + is ChannelSpendSignature.IndividualSignature -> { + write(0x00) + writeByteVector64(sig.sig) } } @@ -632,4 +601,5 @@ object Serialization { writeNullable(scriptPubKey) { writeDelimited(it.toByteArray()) } writeNumber(feerate.toLong()) } + } \ No newline at end of file diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/CommitmentSpec.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/CommitmentSpec.kt index 07428e24d..d1dfda205 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/CommitmentSpec.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/CommitmentSpec.kt @@ -1,20 +1,38 @@ package fr.acinq.lightning.transactions -import fr.acinq.bitcoin.PublicKey +import fr.acinq.bitcoin.LexicographicalOrdering +import fr.acinq.bitcoin.TxOut import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.wire.* -import kotlinx.serialization.Transient -sealed class CommitmentOutput { - data object ToLocal : CommitmentOutput() - data object ToRemote : CommitmentOutput() - - data class ToLocalAnchor(val pub: PublicKey) : CommitmentOutput() - data class ToRemoteAnchor(val pub: PublicKey) : CommitmentOutput() - - data class InHtlc(val incomingHtlc: IncomingHtlc) : CommitmentOutput() - data class OutHtlc(val outgoingHtlc: OutgoingHtlc) : CommitmentOutput() +sealed class CommitmentOutput : Comparable { + abstract val txOut: TxOut + + data class ToLocal(override val txOut: TxOut) : CommitmentOutput() + data class ToRemote(override val txOut: TxOut) : CommitmentOutput() + data class ToLocalAnchor(override val txOut: TxOut) : CommitmentOutput() + data class ToRemoteAnchor(override val txOut: TxOut) : CommitmentOutput() + data class InHtlc(val htlc: IncomingHtlc, override val txOut: TxOut, val htlcDelayedOutput: TxOut) : CommitmentOutput() + data class OutHtlc(val htlc: OutgoingHtlc, override val txOut: TxOut, val htlcDelayedOutput: TxOut) : CommitmentOutput() + + override fun compareTo(other: CommitmentOutput): Int { + return when { + // Outgoing HTLCs that have the same payment_hash will have the same script. If they also have the same amount, they + // will produce exactly the same output: in that case, we must sort them using their expiry (see Bolt 3). + // If they also have the same expiry, it doesn't really matter how we sort them, but in order to provide a fully + // deterministic ordering (which is useful for tests), we sort them by htlc_id, which cannot be equal. + this is OutHtlc && other is OutHtlc && this.txOut == other.txOut && this.htlc.add.cltvExpiry == other.htlc.add.cltvExpiry -> this.htlc.add.id.compareTo(other.htlc.add.id) + this is OutHtlc && other is OutHtlc && this.txOut == other.txOut -> this.htlc.add.cltvExpiry.compareTo(other.htlc.add.cltvExpiry) + // Incoming HTLCs that have the same payment_hash *and* expiry will have the same script. If they also have the same + // amount, they will produce exactly the same output: just like offered HTLCs, it doesn't really matter how we sort + // them, but we use the htlc_id to provide a fully deterministic ordering. Note that the expiry is included in the + // script, so HTLCs with different expiries will have different scripts, and will thus be sorted by script as required + // by Bolt 3. + this is InHtlc && other is InHtlc && this.txOut == other.txOut -> this.htlc.add.id.compareTo(other.htlc.add.id) + else -> LexicographicalOrdering.compare(this.txOut, other.txOut) + } + } } sealed class DirectedHtlc { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt index 80db01998..76854eed0 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt @@ -4,6 +4,8 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.ScriptEltMapping.code2elt import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.crypto.CommitmentPublicKeys +import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.transactions.Scripts.htlcOffered import fr.acinq.lightning.transactions.Scripts.htlcReceived import fr.acinq.lightning.transactions.Scripts.toLocalDelayed @@ -81,65 +83,61 @@ object Scripts { return if (tx.version < 2) 0L else tx.txIn.map { it.sequence }.maxOf { sequenceToBlockHeight(it) } } - fun toAnchor(fundingPubkey: PublicKey): List = + fun toAnchor(anchorKey: PublicKey): List = listOf( // @formatter:off - listOf( - OP_PUSHDATA(fundingPubkey), - OP_CHECKSIG, - OP_IFDUP, - OP_NOTIF, - OP_16, OP_CHECKSEQUENCEVERIFY, - OP_ENDIF - ) + OP_PUSHDATA(anchorKey), OP_CHECKSIG, OP_IFDUP, + OP_NOTIF, + OP_16, OP_CHECKSEQUENCEVERIFY, + OP_ENDIF // @formatter:on + ) - fun toLocalDelayed(revocationPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey): List = + fun toLocalDelayed(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): List = listOf( // @formatter:off - listOf( - OP_IF, - OP_PUSHDATA(revocationPubkey), - OP_ELSE, - encodeNumber(toSelfDelay.toLong()), OP_CHECKSEQUENCEVERIFY, OP_DROP, - OP_PUSHDATA(localDelayedPaymentPubkey), - OP_ENDIF, - OP_CHECKSIG - ) + OP_IF, + OP_PUSHDATA(keys.revocationPublicKey), + OP_ELSE, + encodeNumber(toSelfDelay.toLong()), OP_CHECKSEQUENCEVERIFY, OP_DROP, + OP_PUSHDATA(keys.localDelayedPaymentPublicKey), + OP_ENDIF, + OP_CHECKSIG // @formatter:on + ) - fun toRemoteDelayed(pub: PublicKey): List = listOf(OP_PUSHDATA(pub), OP_CHECKSIGVERIFY, OP_1, OP_CHECKSEQUENCEVERIFY) + fun toRemoteDelayed(keys: CommitmentPublicKeys): List = listOf(OP_PUSHDATA(keys.remotePaymentPublicKey), OP_CHECKSIGVERIFY, OP_1, OP_CHECKSEQUENCEVERIFY) /** - * This witness script spends a [[toLocalDelayed]] output using a local sig after a delay + * This witness script spends a [toLocalDelayed] output using a local sig after a delay */ fun witnessToRemoteDelayedAfterDelay(localSig: ByteVector64, toRemoteDelayedScript: ByteVector) = ScriptWitness(listOf(der(localSig, SigHash.SIGHASH_ALL), toRemoteDelayedScript)) /** - * This witness script spends a [[toLocalDelayed]] output using a local sig after a delay + * This witness script spends a [toLocalDelayed] output using a local sig after a delay */ fun witnessToLocalDelayedAfterDelay(localSig: ByteVector64, toLocalDelayedScript: ByteVector) = ScriptWitness(listOf(der(localSig, SigHash.SIGHASH_ALL), ByteVector.empty, toLocalDelayedScript)) /** - * This witness script spends (steals) a [[toLocalDelayed]] output using a revocation key as a punishment + * This witness script spends (steals) a [toLocalDelayed] output using a revocation key as a punishment * for having published a revoked transaction */ fun witnessToLocalDelayedWithRevocationSig(revocationSig: ByteVector64, toLocalScript: ByteVector) = ScriptWitness(listOf(der(revocationSig, SigHash.SIGHASH_ALL), ByteVector(byteArrayOf(1)), toLocalScript)) - fun htlcOffered(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: ByteArray): List = listOf( + fun htlcOffered(keys: CommitmentPublicKeys, paymentHash: ByteVector32): List = listOf( // @formatter:off // To you with revocation key - OP_DUP, OP_HASH160, OP_PUSHDATA(revocationPubKey.hash160()), OP_EQUAL, + OP_DUP, OP_HASH160, OP_PUSHDATA(keys.revocationPublicKey.hash160()), OP_EQUAL, OP_IF, OP_CHECKSIG, OP_ELSE, - OP_PUSHDATA(remoteHtlcPubkey), OP_SWAP, OP_SIZE, encodeNumber(32), OP_EQUAL, + OP_PUSHDATA(keys.remoteHtlcPublicKey), OP_SWAP, OP_SIZE, encodeNumber(32), OP_EQUAL, OP_NOTIF, // To me via HTLC-timeout transaction (timelocked). - OP_DROP, OP_2, OP_SWAP, OP_PUSHDATA(localHtlcPubkey), OP_2, OP_CHECKMULTISIG, + OP_DROP, OP_2, OP_SWAP, OP_PUSHDATA(keys.localHtlcPublicKey), OP_2, OP_CHECKMULTISIG, OP_ELSE, - OP_HASH160, OP_PUSHDATA(paymentHash), OP_EQUALVERIFY, + OP_HASH160, OP_PUSHDATA(Crypto.ripemd160(paymentHash)), OP_EQUALVERIFY, OP_CHECKSIG, OP_ENDIF, OP_1, OP_CHECKSEQUENCEVERIFY, OP_DROP, @@ -152,8 +150,8 @@ object Scripts { * local signature is created with SIGHASH_ALL flag * remote signature is created with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY */ - fun witnessHtlcSuccess(localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, htlcOfferedScript: ByteVector) = - ScriptWitness(listOf(ByteVector.empty, der(remoteSig, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY), der(localSig, SigHash.SIGHASH_ALL), paymentPreimage, htlcOfferedScript)) + fun witnessHtlcSuccess(localSig: ByteVector64, remoteSig: ByteVector64, preimage: ByteVector32, htlcOfferedScript: ByteVector) = + ScriptWitness(listOf(ByteVector.empty, der(remoteSig, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY), der(localSig, SigHash.SIGHASH_ALL), preimage, htlcOfferedScript)) /** Extract payment preimages from a 2nd-stage HTLC Success transaction's witness script. */ fun extractPreimagesFromHtlcSuccess(tx: Transaction): Set { @@ -171,8 +169,8 @@ object Scripts { * If remote publishes its commit tx where there was a remote->local htlc, then local uses this script to * claim its funds using a payment preimage (consumes htlcOffered script from commit tx) */ - fun witnessClaimHtlcSuccessFromCommitTx(localSig: ByteVector64, paymentPreimage: ByteVector32, htlcOffered: ByteVector) = - ScriptWitness(listOf(der(localSig, SigHash.SIGHASH_ALL), paymentPreimage, htlcOffered)) + fun witnessClaimHtlcSuccessFromCommitTx(localSig: ByteVector64, preimage: ByteVector32, htlcOffered: ByteVector) = + ScriptWitness(listOf(der(localSig, SigHash.SIGHASH_ALL), preimage, htlcOffered)) /** Extract payment preimages from a claim-htlc transaction. */ fun extractPreimagesFromClaimHtlcSuccess(tx: Transaction): Set { @@ -185,18 +183,18 @@ object Scripts { }.toSet() } - fun htlcReceived(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: ByteArray, lockTime: CltvExpiry) = listOf( + fun htlcReceived(keys: CommitmentPublicKeys, paymentHash: ByteVector32, lockTime: CltvExpiry) = listOf( // @formatter:off // To you with revocation key - OP_DUP, OP_HASH160, OP_PUSHDATA(revocationPubKey.hash160()), OP_EQUAL, + OP_DUP, OP_HASH160, OP_PUSHDATA(keys.revocationPublicKey.hash160()), OP_EQUAL, OP_IF, OP_CHECKSIG, OP_ELSE, - OP_PUSHDATA(remoteHtlcPubkey), OP_SWAP, OP_SIZE, encodeNumber(32), OP_EQUAL, + OP_PUSHDATA(keys.remoteHtlcPublicKey), OP_SWAP, OP_SIZE, encodeNumber(32), OP_EQUAL, OP_IF, // To me via HTLC-success transaction. - OP_HASH160, OP_PUSHDATA(paymentHash), OP_EQUALVERIFY, - OP_2, OP_SWAP, OP_PUSHDATA(localHtlcPubkey), OP_2, OP_CHECKMULTISIG, + OP_HASH160, OP_PUSHDATA(Crypto.ripemd160(paymentHash)), OP_EQUALVERIFY, + OP_2, OP_SWAP, OP_PUSHDATA(keys.localHtlcPublicKey), OP_2, OP_CHECKMULTISIG, OP_ELSE, // To you after timeout. OP_DROP, encodeNumber(lockTime.toLong()), OP_CHECKLOCKTIMEVERIFY, OP_DROP, @@ -226,7 +224,7 @@ object Scripts { * This witness script spends (steals) a [[htlcOffered]] or [[htlcReceived]] output using a revocation key as a punishment * for having published a revoked transaction */ - fun witnessHtlcWithRevocationSig(revocationSig: ByteVector64, revocationPubkey: PublicKey, htlcScript: ByteVector) = - ScriptWitness(listOf(der(revocationSig, SigHash.SIGHASH_ALL), revocationPubkey.value, htlcScript)) + fun witnessHtlcWithRevocationSig(commitKeys: RemoteCommitmentKeys, revocationSig: ByteVector64, htlcScript: ByteVector) = + ScriptWitness(listOf(der(revocationSig, SigHash.SIGHASH_ALL), commitKeys.revocationPublicKey.value, htlcScript)) } \ No newline at end of file diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index 3fc751630..4749f7056 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -111,11 +111,13 @@ data class SwapInProtocolLegacy(val userPublicKey: PublicKey, val serverPublicKe fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOut: TxOut, userKey: PrivateKey): ByteVector64 { require(userKey.publicKey() == userPublicKey) { "user private key does not match expected public key: are you using the refund key instead of the user key?" } - return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, userKey) + val sigDER = fundingTx.signInput(index, redeemScript, SigHash.SIGHASH_ALL, parentTxOut.amount, SigVersion.SIGVERSION_WITNESS_V0, userKey) + return Crypto.der2compact(sigDER) } fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, serverKey: PrivateKey): ByteVector64 { - return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, serverKey) + val sigDER = fundingTx.signInput(index, redeemScript, SigHash.SIGHASH_ALL, parentTxOut.amount, SigVersion.SIGVERSION_WITNESS_V0, serverKey) + return Crypto.der2compact(sigDER) } companion object { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index cc97133dc..507713351 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -18,22 +18,21 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.Pack -import fr.acinq.bitcoin.utils.Try +import fr.acinq.bitcoin.utils.Either +import fr.acinq.bitcoin.utils.flatMap import fr.acinq.bitcoin.utils.runTrying +import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.Commitments +import fr.acinq.lightning.channel.ChannelSpendSignature import fr.acinq.lightning.crypto.CommitmentPublicKeys +import fr.acinq.lightning.crypto.LocalCommitmentKeys +import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.transactions.CommitmentOutput.InHtlc import fr.acinq.lightning.transactions.CommitmentOutput.OutHtlc import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.UpdateAddHtlc -import kotlinx.serialization.Contextual -import kotlinx.serialization.Serializable - -/** Type alias for a collection of commitment output links */ -typealias TransactionsCommitmentOutputs = List> /** * Created by PM on 15/12/2016. @@ -41,156 +40,823 @@ typealias TransactionsCommitmentOutputs = List) : this(outPoint, txOut, ByteVector(Script.write(redeemScript))) + sealed class CommitmentFormat { + /** Weight of a fully signed channel output, when spent by a [ChannelSpendTransaction]. */ + abstract val fundingInputWeight: Int + /** Weight of a fully signed [CommitTx] transaction without any HTLCs. */ + abstract val commitWeight: Int + /** Weight of an additional HTLC output added to a [CommitTx]. */ + abstract val htlcOutputWeight: Int + /** Weight of a fully signed [HtlcTimeoutTx] transaction without additional wallet inputs. */ + abstract val htlcTimeoutWeight: Int + /** Weight of a fully signed [HtlcSuccessTx] transaction without additional wallet inputs. */ + abstract val htlcSuccessWeight: Int + /** Weight of a fully signed [ClaimHtlcSuccessTx] transaction. */ + abstract val claimHtlcSuccessWeight: Int + /** Weight of a fully signed [ClaimHtlcTimeoutTx] transaction. */ + abstract val claimHtlcTimeoutWeight: Int + /** Weight of a fully signed [ClaimLocalDelayedOutputTx] transaction. */ + abstract val toLocalDelayedWeight: Int + /** Weight of a fully signed [ClaimRemoteDelayedOutputTx] transaction. */ + abstract val toRemoteWeight: Int + /** Weight of a fully signed [HtlcDelayedTx] 3rd-stage transaction (spending the output of an [HtlcTx]). */ + abstract val htlcDelayedWeight: Int + /** Weight of a fully signed [MainPenaltyTx] transaction. */ + abstract val mainPenaltyWeight: Int + /** Weight of a fully signed [HtlcPenaltyTx] transaction for an offered HTLC. */ + abstract val htlcOfferedPenaltyWeight: Int + /** Weight of a fully signed [HtlcPenaltyTx] transaction for a received HTLC. */ + abstract val htlcReceivedPenaltyWeight: Int + /** Weight of a fully signed [ClaimHtlcDelayedOutputPenaltyTx] transaction. */ + abstract val claimHtlcPenaltyWeight: Int + /** Amount of the anchor outputs of the [CommitTx]. */ + abstract val anchorAmount: Satoshi + + object AnchorOutputs : CommitmentFormat() { + override val fundingInputWeight: Int = 384 + override val commitWeight: Int = 1124 + override val htlcOutputWeight: Int = 172 + override val htlcTimeoutWeight: Int = 666 + override val htlcSuccessWeight: Int = 706 + override val claimHtlcSuccessWeight: Int = 574 + override val claimHtlcTimeoutWeight: Int = 547 + override val toLocalDelayedWeight: Int = 483 + override val toRemoteWeight: Int = 442 + override val htlcDelayedWeight: Int = 483 + override val mainPenaltyWeight: Int = 483 + override val htlcOfferedPenaltyWeight: Int = 575 + override val htlcReceivedPenaltyWeight: Int = 580 + override val claimHtlcPenaltyWeight: Int = 483 + override val anchorAmount: Satoshi = 330.sat + } + } + + data class InputInfo(val outPoint: OutPoint, val txOut: TxOut) + + /** This trait contains redeem information necessary to spend different types of segwit inputs. */ + sealed class RedeemInfo { + abstract val pubkeyScript: ByteVector + + /** @param redeemScript the actual script must be known to redeem pay2wsh inputs. */ + data class P2wsh(val redeemScript: ByteVector) : RedeemInfo() { + constructor(redeemScript: List) : this(Script.write(redeemScript).byteVector()) + + override val pubkeyScript: ByteVector = Script.write(Script.pay2wsh(redeemScript)).byteVector() + } } - @Serializable sealed class TransactionWithInputInfo { abstract val input: InputInfo abstract val tx: Transaction val amountIn: Satoshi get() = input.txOut.amount val fee: Satoshi get() = input.txOut.amount - tx.txOut.map { it.amount }.sum() + val inputIndex: Int get() = tx.txIn.indexOfFirst { it.outPoint == input.outPoint } + + fun sign(key: PrivateKey, sigHash: Int, redeemInfo: RedeemInfo, extraUtxos: Map): ByteVector64 { + // Note that we only need to provide details about all transaction inputs when using taproot, but we want to + // test that we're always correctly providing all inputs in all code paths to benefit from our existing test coverage. + val inputsMap = extraUtxos + (input.outPoint to input.txOut) + tx.txIn.forEach { require(inputsMap.contains(it.outPoint)) { "cannot sign txId=${tx.txid}: missing input details for ${it.outPoint}" } } + return when (redeemInfo) { + is RedeemInfo.P2wsh -> { + val sigDER = tx.signInput(inputIndex, redeemInfo.redeemScript, sigHash, amountIn, SigVersion.SIGVERSION_WITNESS_V0, key) + Crypto.der2compact(sigDER) + } + } + } - @Serializable - data class SpliceTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + fun checkSig(sig: ByteVector64, publicKey: PublicKey, sigHash: Int, redeemInfo: RedeemInfo): Boolean { + return if (inputIndex >= 0) { + when (redeemInfo) { + is RedeemInfo.P2wsh -> { + val redeemScript = redeemInfo.redeemScript.toByteArray() + val data = tx.hashForSigning(inputIndex, redeemScript, sigHash, amountIn, SigVersion.SIGVERSION_WITNESS_V0) + Crypto.verifySignature(data, sig, publicKey) + } + } + } else { + false + } + } - @Serializable - data class CommitTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + /** Check that this transaction is correctly signed. */ + fun validate(extraUtxos: Map): Boolean { + val inputsMap = extraUtxos + (input.outPoint to input.txOut) + val allInputsProvided = tx.txIn.all { inputsMap.contains(it.outPoint) } + val witnessesOk = runTrying { Transaction.correctlySpends(tx, inputsMap, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }.isSuccess + return allInputsProvided && witnessesOk + } + } - @Serializable - sealed class HtlcTx : TransactionWithInputInfo() { - abstract val htlcId: Long + /** + * Transactions spending the channel funding output: [CommitTx], [SpliceTx] and [ClosingTx]. + * Those transactions always require two signatures, one from each channel participant. + */ + sealed class ChannelSpendTransaction : TransactionWithInputInfo() { + /** Sign the channel's 2-of-2 funding output when using [CommitmentFormat.AnchorOutputs]. */ + fun sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, extraUtxos: Map): ChannelSpendSignature.IndividualSignature { + val redeemScript = Script.write(Scripts.multiSig2of2(localFundingKey.publicKey(), remoteFundingPubkey)).byteVector() + val sig = sign(localFundingKey, SigHash.SIGHASH_ALL, RedeemInfo.P2wsh(redeemScript), extraUtxos) + return ChannelSpendSignature.IndividualSignature(sig) + } - @Serializable - data class HtlcSuccessTx( - override val input: InputInfo, - @Contextual override val tx: Transaction, - @Contextual val paymentHash: ByteVector32, - override val htlcId: Long - ) : HtlcTx() + /** Aggregate local and remote channel spending signatures when using [CommitmentFormat.AnchorOutputs]. */ + fun aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ChannelSpendSignature.IndividualSignature, remoteSig: ChannelSpendSignature.IndividualSignature): Transaction { + val witness = Scripts.witness2of2(localSig.sig, remoteSig.sig, localFundingPubkey, remoteFundingPubkey) + return tx.updateWitness(inputIndex, witness) + } - @Serializable - data class HtlcTimeoutTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : HtlcTx() + /** Verify a signature received from the remote channel participant. */ + fun checkRemoteSig(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, remoteSig: ChannelSpendSignature.IndividualSignature): Boolean { + val redeemScript = Script.write(Scripts.multiSig2of2(localFundingPubkey, remoteFundingPubkey)).byteVector() + return checkSig(remoteSig.sig, remoteFundingPubkey, SigHash.SIGHASH_ALL, RedeemInfo.P2wsh(redeemScript)) } + } + + /** This transaction collaboratively spends the channel funding output to change its capacity. */ + data class SpliceTx(override val input: InputInfo, override val tx: Transaction) : ChannelSpendTransaction() - @Serializable - sealed class ClaimHtlcTx : TransactionWithInputInfo() { - abstract val htlcId: Long + /** This transaction unilaterally spends the channel funding output (force-close). */ + data class CommitTx(override val input: InputInfo, override val tx: Transaction) : ChannelSpendTransaction() { + fun sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey): ChannelSpendSignature.IndividualSignature = sign(localFundingKey, remoteFundingPubkey, mapOf()) + } + + /** This transaction collaboratively spends the channel funding output (mutual-close). */ + data class ClosingTx(override val input: InputInfo, override val tx: Transaction, val toLocalOutputIndex: Int?) : ChannelSpendTransaction() { + val toLocalOutput: TxOut? get() = toLocalOutputIndex?.let { tx.txOut[it] } - @Serializable - data class ClaimHtlcSuccessTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : ClaimHtlcTx() + fun sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey): ChannelSpendSignature.IndividualSignature = sign(localFundingKey, remoteFundingPubkey, mapOf()) + } - @Serializable - data class ClaimHtlcTimeoutTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : ClaimHtlcTx() + /** Transactions spending a [CommitTx] or one of its descendants. */ + sealed class ForceCloseTransaction : TransactionWithInputInfo() { + abstract val commitmentFormat: CommitmentFormat + abstract val expectedWeight: Int + + /** Sighash flags to use when signing the transaction. */ + val sigHash: Int get() = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> SigHash.SIGHASH_ALL } - @Serializable - sealed class ClaimAnchorOutputTx : TransactionWithInputInfo() { - @Serializable - data class ClaimLocalAnchorOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimAnchorOutputTx() + abstract fun sign(): ForceCloseTransaction + } + + /** + * Transactions spending a local [CommitTx] or one of its descendants: + * - [ClaimLocalDelayedOutputTx] spends the to-local output of [CommitTx] after a delay + * - [HtlcSuccessTx] spends received htlc outputs of [CommitTx] for which we have the preimage + * - [HtlcDelayedTx] spends [HtlcSuccessTx] after a delay + * - [[tlcTimeoutTx] spends sent htlc outputs of [CommitTx] after a timeout + * - [HtlcDelayedTx] spends [HtlcTimeoutTx] after a delay + */ + sealed class LocalCommitForceCloseTransaction : ForceCloseTransaction() { + abstract val commitKeys: LocalCommitmentKeys + } + + /** + * Transactions spending a remote [CommitTx] or one of its descendants. + * + * When a current remote [CommitTx] is published: + * - [ClaimRemoteDelayedOutputTx] spends the to-local output of [CommitTx] + * - [ClaimHtlcSuccessTx] spends received htlc outputs of [CommitTx] for which we have the preimage + * - [ClaimHtlcTimeoutTx] spends sent htlc outputs of [CommitTx] after a timeout + * + * When a revoked remote [CommitTx] is published: + * - [ClaimRemoteDelayedOutputTx] spends the to-local output of [CommitTx] + * - [MainPenaltyTx] spends the remote main output using the revocation secret + * - [HtlcPenaltyTx] spends all htlc outputs using the revocation secret (and competes with [HtlcSuccessTx] and [HtlcTimeoutTx] published by the remote node) + * - [ClaimHtlcDelayedOutputPenaltyTx] spends [HtlcSuccessTx] transactions published by the remote node using the revocation secret + * - [ClaimHtlcDelayedOutputPenaltyTx] spends [HtlcTimeoutTx] transactions published by the remote node using the revocation secret + */ + sealed class RemoteCommitForceCloseTransaction : ForceCloseTransaction() { + abstract val commitKeys: RemoteCommitmentKeys + } - @Serializable - data class ClaimRemoteAnchorOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimAnchorOutputTx() + /** Owner of a given HTLC transaction (local/remote). */ + sealed class TxOwner { + object Local : TxOwner() + object Remote : TxOwner() + } + + /** + * HTLC transactions require local and remote signatures and can be spent using two distinct script paths: + * - the success path by revealing the payment preimage + * - the timeout path after a predefined block height + * + * The success path must be used before the timeout is reached, otherwise there is a race where both channel + * participants may claim the output. + * + * We first create unsigned HTLC transactions based on the [CommitTx]: this lets us produce our local signature, + * which we need to send to our peer for their commitment. + * + * Once confirmed, HTLC transactions need to be spent by an [HtlcDelayedTx] after a relative delay to get the funds + * back into our on-chain wallet. + */ + sealed class HtlcTx : TransactionWithInputInfo() { + abstract val htlcId: Long + abstract val paymentHash: ByteVector32 + abstract val htlcExpiry: CltvExpiry + abstract val commitmentFormat: CommitmentFormat + + /** Create redeem information for this HTLC transaction, based on the commitment format used. */ + abstract fun redeemInfo(commitKeys: CommitmentPublicKeys): RedeemInfo + + fun sigHash(txOwner: TxOwner): Int = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> when (txOwner) { + TxOwner.Local -> SigHash.SIGHASH_ALL + TxOwner.Remote -> SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY + } + } + + /** Sign an HTLC transaction for the remote commitment. */ + fun localSig(commitKeys: RemoteCommitmentKeys): ByteVector64 { + return sign(commitKeys.ourHtlcKey, sigHash(TxOwner.Remote), redeemInfo(commitKeys.publicKeys), extraUtxos = mapOf()) } - @Serializable - data class ClaimLocalDelayedOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + fun checkRemoteSig(commitKeys: LocalCommitmentKeys, remoteSig: ByteVector64): Boolean { + // The transaction was signed by our remote for us: from their point of view, we're a remote owner. + val remoteSighash = sigHash(TxOwner.Remote) + return checkSig(remoteSig, commitKeys.theirHtlcPublicKey, remoteSighash, redeemInfo(commitKeys.publicKeys)) + } + } - @Serializable - sealed class ClaimRemoteCommitMainOutputTx : TransactionWithInputInfo() { - // TODO: once we deprecate v2/v3 serialization, we can remove the class nesting. - @Serializable - data class ClaimRemoteDelayedOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimRemoteCommitMainOutputTx() + /** This transaction spends a received (incoming) HTLC from a local or remote commitment by revealing the payment preimage. */ + data class HtlcSuccessTx( + override val input: InputInfo, + override val tx: Transaction, + override val paymentHash: ByteVector32, + override val htlcId: Long, + override val htlcExpiry: CltvExpiry, + override val commitmentFormat: CommitmentFormat + ) : HtlcTx() { + override fun redeemInfo(commitKeys: CommitmentPublicKeys): RedeemInfo = redeemInfo(commitKeys, paymentHash, htlcExpiry, commitmentFormat) + + fun sign(commitKeys: LocalCommitmentKeys, remoteSig: ByteVector64, preimage: ByteVector32): HtlcSuccessTx { + val redeemInfo = redeemInfo(commitKeys.publicKeys) + val localSig = sign(commitKeys.ourHtlcKey, sigHash(TxOwner.Local), redeemInfo, extraUtxos = mapOf()) + val witness = when (redeemInfo) { + is RedeemInfo.P2wsh -> Scripts.witnessHtlcSuccess(localSig, remoteSig, preimage, redeemInfo.redeemScript) + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) } - @Serializable - data class MainPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + companion object { + fun redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat): RedeemInfo { + return when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.htlcReceived(commitKeys, paymentHash, htlcExpiry)) + } + } + + fun createUnsignedTx(commitTx: Transaction, output: InHtlc, outputIndex: Int, commitmentFormat: CommitmentFormat): HtlcSuccessTx { + val htlc = output.htlc.add + val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex]) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1)), + txOut = listOf(output.htlcDelayedOutput), + lockTime = 0 + ) + return HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, htlc.cltvExpiry, commitmentFormat) + } + } + } - @Serializable - data class HtlcPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + /** This transaction spends an offered (outgoing) HTLC from a local or remote commitment after its expiry. */ + data class HtlcTimeoutTx( + override val input: InputInfo, + override val tx: Transaction, + override val paymentHash: ByteVector32, + override val htlcId: Long, + override val htlcExpiry: CltvExpiry, + override val commitmentFormat: CommitmentFormat + ) : HtlcTx() { + override fun redeemInfo(commitKeys: CommitmentPublicKeys): RedeemInfo = redeemInfo(commitKeys, paymentHash, commitmentFormat) + + fun sign(commitKeys: LocalCommitmentKeys, remoteSig: ByteVector64): HtlcTimeoutTx { + val redeemInfo = redeemInfo(commitKeys.publicKeys) + val localSig = sign(commitKeys.ourHtlcKey, sigHash(TxOwner.Local), redeemInfo, extraUtxos = mapOf()) + val witness = when (redeemInfo) { + is RedeemInfo.P2wsh -> Scripts.witnessHtlcTimeout(localSig, remoteSig, redeemInfo.redeemScript) + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) + } - @Serializable - data class ClaimHtlcDelayedOutputPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + companion object { + fun redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, commitmentFormat: CommitmentFormat): RedeemInfo { + return when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.htlcOffered(commitKeys, paymentHash)) + } + } - @Serializable - data class ClosingTx(override val input: InputInfo, @Contextual override val tx: Transaction, val toLocalIndex: Int?) : TransactionWithInputInfo() { - val toLocalOutput: TxOut? get() = toLocalIndex?.let { tx.txOut[it] } + fun createUnsignedTx(commitTx: Transaction, output: OutHtlc, outputIndex: Int, commitmentFormat: CommitmentFormat): HtlcTimeoutTx { + val htlc = output.htlc.add + val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex]) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1)), + txOut = listOf(output.htlcDelayedOutput), + lockTime = htlc.cltvExpiry.toLong() + ) + return HtlcTimeoutTx(input, tx, htlc.paymentHash, htlc.id, htlc.cltvExpiry, commitmentFormat) + } } } - sealed class TxGenerationSkipped { - object OutputNotFound : TxGenerationSkipped() { - override fun toString() = "output not found (probably trimmed)" + /** This transaction spends the output of a local [HtlcTx] after a to_self_delay relative delay. */ + data class HtlcDelayedTx( + override val commitKeys: LocalCommitmentKeys, + override val input: InputInfo, + override val tx: Transaction, + val toLocalDelay: CltvExpiryDelta, + override val commitmentFormat: CommitmentFormat + ) : LocalCommitForceCloseTransaction() { + override val expectedWeight: Int = commitmentFormat.htlcDelayedWeight + + override fun sign(): HtlcDelayedTx { + val witness = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> { + val redeemScript = Script.write(Scripts.toLocalDelayed(commitKeys.publicKeys, toLocalDelay)).byteVector() + val sig = sign(commitKeys.ourDelayedPaymentKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) + Scripts.witnessToLocalDelayedAfterDelay(sig, redeemScript) + } + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) + } + + companion object { + fun redeemInfo(commitKeys: CommitmentPublicKeys, toLocalDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat): RedeemInfo { + return when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys, toLocalDelay)) + } + } + + fun createUnsignedTx( + commitKeys: LocalCommitmentKeys, + htlcTx: Transaction, + localDustLimit: Satoshi, + toLocalDelay: CltvExpiryDelta, + localFinalScriptPubKey: ByteVector, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat + ): Either { + val pubkeyScript = redeemInfo(commitKeys.publicKeys, toLocalDelay, commitmentFormat).pubkeyScript + return findPubKeyScriptIndex(htlcTx, pubkeyScript).flatMap { outputIndex -> + val input = InputInfo(OutPoint(htlcTx, outputIndex.toLong()), htlcTx.txOut[outputIndex]) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.htlcDelayedWeight) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toLong())), + txOut = listOf(TxOut(amount, localFinalScriptPubKey)), + lockTime = 0 + ) + val unsignedTx = HtlcDelayedTx(commitKeys, input, tx, toLocalDelay, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } } + } - object AmountBelowDustLimit : TxGenerationSkipped() { - override fun toString() = "amount is below dust limit" + sealed class ClaimHtlcTx : RemoteCommitForceCloseTransaction() { + abstract val htlcId: Long + abstract val paymentHash: ByteVector32 + abstract val htlcExpiry: CltvExpiry + } + + /** This transaction spends an HTLC we received by revealing the payment preimage, from the remote commitment. */ + data class ClaimHtlcSuccessTx( + override val commitKeys: RemoteCommitmentKeys, + override val input: InputInfo, + override val tx: Transaction, + override val htlcId: Long, + val preimage: ByteVector32, + override val htlcExpiry: CltvExpiry, + override val commitmentFormat: CommitmentFormat + ) : ClaimHtlcTx() { + override val paymentHash: ByteVector32 = Crypto.sha256(preimage.toByteArray()).byteVector32() + override val expectedWeight: Int = commitmentFormat.claimHtlcSuccessWeight + + override fun sign(): ClaimHtlcSuccessTx { + // Note that in/out HTLCs are inverted in the remote commitment: from their point of view it's an offered (outgoing) HTLC. + val witness = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> { + val redeemScript = Script.write(Scripts.htlcOffered(commitKeys.publicKeys, paymentHash)).byteVector() + val sig = sign(commitKeys.ourHtlcKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) + Scripts.witnessClaimHtlcSuccessFromCommitTx(sig, preimage, redeemScript) + } + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) + } + + companion object { + /** + * Find the output of the commitment transaction matching this HTLC. + * Note that we match on a specific HTLC, because we may have multiple HTLCs with the same payment_hash, expiry + * and amount and thus the same pubkeyScript, and we must make sure we claim them all. + */ + fun findInput(commitTx: Transaction, outputs: List, htlc: UpdateAddHtlc): InputInfo? { + return outputs.withIndex() + .firstOrNull { (it.value as? OutHtlc)?.htlc?.add?.id == htlc.id } + ?.let { InputInfo(OutPoint(commitTx, it.index.toLong()), commitTx.txOut[it.index]) } + } + + fun createUnsignedTx( + commitKeys: RemoteCommitmentKeys, + commitTx: Transaction, + dustLimit: Satoshi, + outputs: List, + localFinalScriptPubKey: ByteVector, + htlc: UpdateAddHtlc, + preimage: ByteVector32, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat + ): Either { + return when (val input = findInput(commitTx, outputs, htlc)) { + null -> Either.Left(TxGenerationSkipped.OutputNotFound) + else -> { + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.claimHtlcSuccessWeight) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1)), + txOut = listOf(TxOut(amount, localFinalScriptPubKey)), + lockTime = 0 + ) + val unsignedTx = ClaimHtlcSuccessTx(commitKeys, input, tx, htlc.id, preimage, htlc.cltvExpiry, commitmentFormat) + skipTxIfBelowDust(unsignedTx, dustLimit) + } + } + } } } - sealed class ClosingTxFee { - data class PaidByUs(val fee: Satoshi) : ClosingTxFee() - data class PaidByThem(val fee: Satoshi) : ClosingTxFee() + /** This transaction spends an HTLC we sent after its expiry, from the remote commitment. */ + data class ClaimHtlcTimeoutTx( + override val commitKeys: RemoteCommitmentKeys, + override val input: InputInfo, + override val tx: Transaction, + override val htlcId: Long, + override val paymentHash: ByteVector32, + override val htlcExpiry: CltvExpiry, + override val commitmentFormat: CommitmentFormat + ) : ClaimHtlcTx() { + override val expectedWeight: Int = commitmentFormat.claimHtlcTimeoutWeight + + override fun sign(): ClaimHtlcTimeoutTx { + // Note that in/out HTLCs are inverted in the remote commitment: from their point of view it's an offered (outgoing) HTLC. + val witness = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> { + val redeemScript = Script.write(Scripts.htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry)).byteVector() + val sig = sign(commitKeys.ourHtlcKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) + Scripts.witnessClaimHtlcTimeoutFromCommitTx(sig, redeemScript) + } + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) + } + + companion object { + /** + * Find the output of the commitment transaction matching this HTLC. + * Note that we match on a specific HTLC, because we may have multiple HTLCs with the same payment_hash, expiry + * and amount and thus the same pubkeyScript, and we must make sure we claim them all. + */ + fun findInput(commitTx: Transaction, outputs: List, htlc: UpdateAddHtlc): InputInfo? { + return outputs.withIndex() + .firstOrNull { (it.value as? InHtlc)?.htlc?.add?.id == htlc.id } + ?.let { InputInfo(OutPoint(commitTx, it.index.toLong()), commitTx.txOut[it.index]) } + } + + fun createUnsignedTx( + commitKeys: RemoteCommitmentKeys, + commitTx: Transaction, + dustLimit: Satoshi, + outputs: List, + localFinalScriptPubKey: ByteVector, + htlc: UpdateAddHtlc, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat + ): Either { + return when (val input = findInput(commitTx, outputs, htlc)) { + null -> Either.Left(TxGenerationSkipped.OutputNotFound) + else -> { + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.claimHtlcTimeoutWeight) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1)), + txOut = listOf(TxOut(amount, localFinalScriptPubKey)), + lockTime = htlc.cltvExpiry.toLong() + ) + val unsignedTx = ClaimHtlcTimeoutTx(commitKeys, input, tx, htlc.id, htlc.paymentHash, htlc.cltvExpiry, commitmentFormat) + skipTxIfBelowDust(unsignedTx, dustLimit) + } + } + } + } } - @Serializable - data class ClosingTxs(val localAndRemote: TransactionWithInputInfo.ClosingTx?, val localOnly: TransactionWithInputInfo.ClosingTx?, val remoteOnly: TransactionWithInputInfo.ClosingTx?) { - val preferred: TransactionWithInputInfo.ClosingTx? = localAndRemote ?: localOnly ?: remoteOnly - val all: List = listOfNotNull(localAndRemote, localOnly, remoteOnly) + /** This transaction spends our main balance from our commitment after a to_self_delay relative delay. */ + data class ClaimLocalDelayedOutputTx( + override val commitKeys: LocalCommitmentKeys, + override val input: InputInfo, + override val tx: Transaction, + val toLocalDelay: CltvExpiryDelta, + override val commitmentFormat: CommitmentFormat, + ) : LocalCommitForceCloseTransaction() { + override val expectedWeight: Int = commitmentFormat.toLocalDelayedWeight + + override fun sign(): ClaimLocalDelayedOutputTx { + val witness = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> { + val redeemScript = Script.write(Scripts.toLocalDelayed(commitKeys.publicKeys, toLocalDelay)).byteVector() + val sig = sign(commitKeys.ourDelayedPaymentKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) + Scripts.witnessToLocalDelayedAfterDelay(sig, redeemScript) + } + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) + } - override fun toString(): String = "localAndRemote=${localAndRemote?.tx?.toString()}, localOnly=${localOnly?.tx?.toString()}, remoteOnly=${remoteOnly?.tx?.toString()}" + companion object { + fun createUnsignedTx( + commitKeys: LocalCommitmentKeys, + commitTx: Transaction, + localDustLimit: Satoshi, + toLocalDelay: CltvExpiryDelta, + localFinalScriptPubKey: ByteVector, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat + ): Either { + val redeemInfo = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys.publicKeys, toLocalDelay)) + } + return findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript).flatMap { outputIndex -> + val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex]) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.toLocalDelayedWeight) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toLong())), + txOut = listOf(TxOut(amount, localFinalScriptPubKey)), + lockTime = 0 + ) + val unsignedTx = ClaimLocalDelayedOutputTx(commitKeys, input, tx, toLocalDelay, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + } } - /** - * When *local* *current* [TransactionWithInputInfo.CommitTx] is published: - * - [TransactionWithInputInfo.ClaimLocalDelayedOutputTx] spends to-local output of [TransactionWithInputInfo.CommitTx] after a delay - * - [TransactionWithInputInfo.HtlcTx.HtlcSuccessTx] spends htlc-received outputs of [TransactionWithInputInfo.CommitTx] for which we have the preimage - * - [TransactionWithInputInfo.ClaimLocalDelayedOutputTx] spends [TransactionWithInputInfo.HtlcTx.HtlcSuccessTx] after a delay - * - [TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx] spends htlc-sent outputs of [TransactionWithInputInfo.CommitTx] after a timeout - * - [TransactionWithInputInfo.ClaimLocalDelayedOutputTx] spends [TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx] after a delay - * - * When *remote* *current* [TransactionWithInputInfo.CommitTx] is published: - * - [TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx] spends to-local output of [TransactionWithInputInfo.CommitTx] - * - [TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx] spends htlc-received outputs of [TransactionWithInputInfo.CommitTx] for which we have the preimage - * - [TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx] spends htlc-sent outputs of [TransactionWithInputInfo.CommitTx] after a timeout - * - * When *remote* *revoked* [TransactionWithInputInfo.CommitTx] is published: - * - [TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx] spends to-local output of [TransactionWithInputInfo.CommitTx] - * - [TransactionWithInputInfo.MainPenaltyTx] spends remote main output using the per-commitment secret - * - [TransactionWithInputInfo.HtlcTx.HtlcSuccessTx] spends htlc-sent outputs of [TransactionWithInputInfo.CommitTx] for which they have the preimage (published by remote) - * - [TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx] spends [TransactionWithInputInfo.HtlcTx.HtlcSuccessTx] using the revocation secret (published by local) - * - [TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx] spends htlc-received outputs of [TransactionWithInputInfo.CommitTx] after a timeout (published by remote) - * - [TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx] spends [TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx] using the revocation secret (published by local) - * - [TransactionWithInputInfo.HtlcPenaltyTx] spends competes with [TransactionWithInputInfo.HtlcTx.HtlcSuccessTx] and [TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx] for the same outputs (published by local) - */ - // legacy swap-in. witness is 2 signatures (73 bytes) + redeem script (77 bytes) - const val swapInputWeightLegacy = 392 - // musig2 swap-in. witness is a single Schnorr signature (64 bytes) - const val swapInputWeight = 233 + /** This transaction spends our main balance from the remote commitment with a 1-block relative delay. */ + data class ClaimRemoteDelayedOutputTx( + override val commitKeys: RemoteCommitmentKeys, + override val input: InputInfo, + override val tx: Transaction, + override val commitmentFormat: CommitmentFormat, + ) : RemoteCommitForceCloseTransaction() { + override val expectedWeight: Int = commitmentFormat.toRemoteWeight + + override fun sign(): ClaimRemoteDelayedOutputTx { + val witness = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> { + val redeemScript = Script.write(Scripts.toRemoteDelayed(commitKeys.publicKeys)).byteVector() + val sig = sign(commitKeys.ourPaymentKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) + Scripts.witnessToRemoteDelayedAfterDelay(sig, redeemScript) + } + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) + } + + companion object { + fun createUnsignedTx( + commitKeys: RemoteCommitmentKeys, + commitTx: Transaction, + localDustLimit: Satoshi, + localFinalScriptPubKey: ByteVector, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat + ): Either { + val redeemInfo = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toRemoteDelayed(commitKeys.publicKeys)) + } + return findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript).flatMap { outputIndex -> + val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex]) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.toRemoteWeight) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1)), + txOut = listOf(TxOut(amount, localFinalScriptPubKey)), + lockTime = 0 + ) + val unsignedTx = ClaimRemoteDelayedOutputTx(commitKeys, input, tx, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + } + } + + /** This transaction spends the remote main balance from one of their revoked commitments. */ + data class MainPenaltyTx( + override val commitKeys: RemoteCommitmentKeys, + val revocationKey: PrivateKey, + override val input: InputInfo, + override val tx: Transaction, + val toRemoteDelay: CltvExpiryDelta, + override val commitmentFormat: CommitmentFormat, + ) : RemoteCommitForceCloseTransaction() { + override val expectedWeight: Int = commitmentFormat.mainPenaltyWeight + + override fun sign(): MainPenaltyTx { + val witness = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> { + val redeemScript = Script.write(Scripts.toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)).byteVector() + val sig = sign(revocationKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) + Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript) + } + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) + } + + companion object { + fun createUnsignedTx( + commitKeys: RemoteCommitmentKeys, + revocationKey: PrivateKey, + commitTx: Transaction, + localDustLimit: Satoshi, + localFinalScriptPubKey: ByteVector, + toRemoteDelay: CltvExpiryDelta, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat + ): Either { + val redeemInfo = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) + } + return findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript).flatMap { outputIndex -> + val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex]) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.mainPenaltyWeight) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), + txOut = listOf(TxOut(amount, localFinalScriptPubKey)), + lockTime = 0 + ) + val unsignedTx = MainPenaltyTx(commitKeys, revocationKey, input, tx, toRemoteDelay, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + } + } + + private data class HtlcPenaltyRedeemDetails(val redeemInfo: RedeemInfo, val paymentHash: ByteVector32, val htlcExpiry: CltvExpiry, val weight: Int) + + /** This transaction spends an HTLC output from one of the remote revoked commitments. */ + data class HtlcPenaltyTx( + override val commitKeys: RemoteCommitmentKeys, + val revocationKey: PrivateKey, + val redeemInfo: RedeemInfo, + override val input: InputInfo, + override val tx: Transaction, + val paymentHash: ByteVector32, + val htlcExpiry: CltvExpiry, + override val commitmentFormat: CommitmentFormat, + ) : RemoteCommitForceCloseTransaction() { + // We don't know if this is an incoming or outgoing HTLC, so we just use the bigger one (they are very close anyway). + override val expectedWeight: Int = maxOf(commitmentFormat.htlcOfferedPenaltyWeight, commitmentFormat.htlcReceivedPenaltyWeight) + + override fun sign(): HtlcPenaltyTx { + val sig = sign(revocationKey, sigHash, redeemInfo, extraUtxos = mapOf()) + val witness = when (redeemInfo) { + is RedeemInfo.P2wsh -> Scripts.witnessHtlcWithRevocationSig(commitKeys, sig, redeemInfo.redeemScript) + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) + } + + companion object { + fun createUnsignedTxs( + commitKeys: RemoteCommitmentKeys, + revocationKey: PrivateKey, + commitTx: Transaction, + htlcs: List>, + localDustLimit: Satoshi, + localFinalScriptPubKey: ByteVector, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat + ): List> { + // We create the output scripts for the corresponding HTLCs. + val redeemInfos = htlcs.flatMap { (paymentHash, htlcExpiry) -> + // We don't know if this was an incoming or outgoing HTLC, so we try both cases. + val (offered, received) = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> { + val offered = RedeemInfo.P2wsh(Scripts.htlcOffered(commitKeys.publicKeys, paymentHash)) + val received = RedeemInfo.P2wsh(Scripts.htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry)) + Pair(offered, received) + } + } + listOf( + offered.pubkeyScript to HtlcPenaltyRedeemDetails(offered, paymentHash, htlcExpiry, commitmentFormat.htlcOfferedPenaltyWeight), + received.pubkeyScript to HtlcPenaltyRedeemDetails(received, paymentHash, htlcExpiry, commitmentFormat.htlcReceivedPenaltyWeight), + ) + }.toMap() + // We check every output of the commitment transaction, and create an HTLC-penalty transaction if it is an HTLC output. + return commitTx.txOut.withIndex() + .mapNotNull { (outputIndex, txOut) -> redeemInfos[txOut.publicKeyScript]?.let { Pair(outputIndex, it) } } + .map { (idx, details) -> createUnsignedTx(commitKeys, revocationKey, commitTx, idx, details, localDustLimit, localFinalScriptPubKey, feerate, commitmentFormat) } + } + + private fun createUnsignedTx( + commitKeys: RemoteCommitmentKeys, + revocationKey: PrivateKey, + commitTx: Transaction, + htlcOutputIndex: Int, + redeemDetails: HtlcPenaltyRedeemDetails, + localDustLimit: Satoshi, + localFinalScriptPubKey: ByteVector, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat + ): Either { + val input = InputInfo(OutPoint(commitTx, htlcOutputIndex.toLong()), commitTx.txOut[htlcOutputIndex]) + val amount = input.txOut.amount - weight2fee(feerate, redeemDetails.weight) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), + txOut = listOf(TxOut(amount, localFinalScriptPubKey)), + lockTime = 0 + ) + val unsignedTx = HtlcPenaltyTx(commitKeys, revocationKey, redeemDetails.redeemInfo, input, tx, redeemDetails.paymentHash, redeemDetails.htlcExpiry, commitmentFormat) + return skipTxIfBelowDust(unsignedTx, localDustLimit) + } + } + } + + /** This transaction spends a remote [HtlcTx] from one of their revoked commitments. */ + data class ClaimHtlcDelayedOutputPenaltyTx( + override val commitKeys: RemoteCommitmentKeys, + val revocationKey: PrivateKey, + override val input: InputInfo, + override val tx: Transaction, + val toRemoteDelay: CltvExpiryDelta, + override val commitmentFormat: CommitmentFormat, + ) : RemoteCommitForceCloseTransaction() { + override val expectedWeight: Int = commitmentFormat.claimHtlcPenaltyWeight + + override fun sign(): ClaimHtlcDelayedOutputPenaltyTx { + val witness = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> { + val redeemScript = Script.write(Scripts.toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)).byteVector() + val sig = sign(revocationKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) + Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript) + } + } + return this.copy(tx = tx.updateWitness(inputIndex, witness)) + } + + companion object { + fun createUnsignedTxs( + commitKeys: RemoteCommitmentKeys, + revocationKey: PrivateKey, + htlcTx: Transaction, + localDustLimit: Satoshi, + toRemoteDelay: CltvExpiryDelta, + localFinalScriptPubKey: ByteVector, + feerate: FeeratePerKw, + commitmentFormat: CommitmentFormat + ): List> { + val redeemInfo = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) + } + // Note that we check *all* outputs of the tx, because it could spend a batch of HTLC outputs from the commit tx. + return htlcTx.txOut.withIndex().mapNotNull { (outputIndex, txOut) -> + when { + txOut.publicKeyScript == redeemInfo.pubkeyScript -> { + val input = InputInfo(OutPoint(htlcTx, outputIndex.toLong()), htlcTx.txOut[outputIndex]) + val amount = input.txOut.amount - weight2fee(feerate, commitmentFormat.claimHtlcPenaltyWeight) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), + txOut = listOf(TxOut(amount, localFinalScriptPubKey)), + lockTime = 0 + ) + val unsignedTx = ClaimHtlcDelayedOutputPenaltyTx(commitKeys, revocationKey, input, tx, toRemoteDelay, commitmentFormat) + skipTxIfBelowDust(unsignedTx, localDustLimit) + } + else -> null + } + } + } + } + } - // The following values are specific to lightning and used to estimate fees. - const val claimHtlcDelayedWeight = 483 - const val claimHtlcSuccessWeight = 574 - const val claimHtlcTimeoutWeight = 548 - const val mainPenaltyWeight = 484 - const val htlcPenaltyWeight = 581 // based on spending an HTLC-Success output (would be 571 with HTLC-Timeout) + sealed class TxGenerationSkipped { + // @formatter:off + object OutputNotFound : TxGenerationSkipped() { override fun toString() = "output not found (probably trimmed)" } + object AmountBelowDustLimit : TxGenerationSkipped() { override fun toString() = "amount is below dust limit" } + // @formatter:on + } private fun weight2feeMsat(feerate: FeeratePerKw, weight: Int): MilliSatoshi = (feerate.toLong() * weight).msat fun weight2fee(feerate: FeeratePerKw, weight: Int): Satoshi = weight2feeMsat(feerate, weight).truncateToSatoshi() - /** - * @param fee tx fee - * @param weight tx weight - * @return the fee rate (in Satoshi/Kw) for this tx - */ fun fee2rate(fee: Satoshi, weight: Int): FeeratePerKw = FeeratePerKw((fee * 1000L) / weight.toLong()) /** As defined in https://github.com/lightning/bolts/blob/master/03-transactions.md#dust-limits */ @@ -218,42 +884,42 @@ object Transactions { } /** Offered HTLCs below this amount will be trimmed. */ - fun offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = dustLimit + weight2fee(spec.feerate, Commitments.HTLC_TIMEOUT_WEIGHT) + fun offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = dustLimit + weight2fee(spec.feerate, commitmentFormat.htlcTimeoutWeight) - fun trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): List { - val threshold = offeredHtlcTrimThreshold(dustLimit, spec) + fun trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): List { + val threshold = offeredHtlcTrimThreshold(dustLimit, spec, commitmentFormat) return spec.htlcs .filterIsInstance() .filter { it.add.amountMsat >= threshold } } /** Received HTLCs below this amount will be trimmed. */ - fun receivedHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = dustLimit + weight2fee(spec.feerate, Commitments.HTLC_SUCCESS_WEIGHT) + fun receivedHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = dustLimit + weight2fee(spec.feerate, commitmentFormat.htlcSuccessWeight) - fun trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): List { - val threshold = receivedHtlcTrimThreshold(dustLimit, spec) + fun trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): List { + val threshold = receivedHtlcTrimThreshold(dustLimit, spec, commitmentFormat) return spec.htlcs .filterIsInstance() .filter { it.add.amountMsat >= threshold } } /** Fee for an un-trimmed HTLC. */ - fun htlcOutputFee(feerate: FeeratePerKw): MilliSatoshi = weight2feeMsat(feerate, Commitments.HTLC_OUTPUT_WEIGHT) + fun htlcOutputFee(feerate: FeeratePerKw, commitmentFormat: CommitmentFormat): MilliSatoshi = weight2feeMsat(feerate, commitmentFormat.htlcOutputWeight) /** - * While fees are generally computed in Satoshis (since this is the smallest on-chain unit), it may be useful in some - * cases to calculate it in MilliSatoshi to avoid rounding issues. - * If you are adding multiple fees together for example, you should always add them in MilliSatoshi and then round - * down to Satoshi. + * While fees are generally computed in satoshis (since this is the smallest on-chain unit), it may be useful in some + * cases to calculate it in milliSatoshi to avoid rounding issues. + * If you are adding multiple fees together for example, you should always add them in milliSatoshi and then round + * down to satoshi. */ - fun commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec): MilliSatoshi { - val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec) - val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec) - val weight = Commitments.COMMIT_WEIGHT + Commitments.HTLC_OUTPUT_WEIGHT * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) - return weight2feeMsat(spec.feerate, weight) + (Commitments.ANCHOR_AMOUNT * 2).toMilliSatoshi() + fun commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): MilliSatoshi { + val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec, commitmentFormat) + val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec, commitmentFormat) + val weight = commitmentFormat.commitWeight + commitmentFormat.htlcOutputWeight * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) + return weight2feeMsat(spec.feerate, weight) + (commitmentFormat.anchorAmount * 2).toMilliSatoshi() } - fun commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = commitTxFeeMsat(dustLimit, spec).truncateToSatoshi() + fun commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Satoshi = commitTxFeeMsat(dustLimit, spec, commitmentFormat).truncateToSatoshi() /** * @param commitTxNumber commit tx number @@ -299,29 +965,18 @@ object Transactions { fun decodeTxNumber(sequence: Long, locktime: Long): Long = ((sequence and 0xffffffL) shl 24) + (locktime and 0xffffffL) - /** - * Represent a link between a commitment spec item (to-local, to-remote, htlc) and the actual output in the commit tx - * - * @param output transaction output - * @param redeemScript redeem script that matches this output (most of them are p2wsh) - * @param commitmentOutput commitment spec item this output is built from - */ - data class CommitmentOutputLink(val output: TxOut, val redeemScript: List, val commitmentOutput: T) : Comparable> { - /** - * We sort HTLC outputs according to BIP69 + CLTV as tie-breaker for offered HTLC, we do this only for the outgoing - * HTLC because we must agree with the remote on the order of HTLC-Timeout transactions even for identical HTLC outputs. - * See https://github.com/lightningnetwork/lightning-rfc/issues/448#issuecomment-432074187. - */ - override fun compareTo(other: CommitmentOutputLink): Int { - val htlcA = (this.commitmentOutput as? OutHtlc)?.outgoingHtlc?.add - val htlcB = (other.commitmentOutput as? OutHtlc)?.outgoingHtlc?.add - return when { - htlcA != null && htlcB != null && htlcA.paymentHash == htlcB.paymentHash && htlcA.amountMsat == htlcB.amountMsat -> htlcA.cltvExpiry.compareTo(htlcB.cltvExpiry) - else -> LexicographicalOrdering.compare(this.output, other.output) - } + fun makeFundingScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): RedeemInfo { + return when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.multiSig2of2(localFundingKey, remoteFundingKey)) } } + fun makeFundingInputInfo(fundingTxId: TxId, fundingOutputIndex: Long, fundingAmount: Satoshi, localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): InputInfo { + val redeemInfo = makeFundingScript(localFundingKey, remoteFundingKey, commitmentFormat) + val fundingTxOut = TxOut(fundingAmount, redeemInfo.pubkeyScript) + return InputInfo(OutPoint(fundingTxId, fundingOutputIndex), fundingTxOut) + } + fun makeCommitTxOutputs( localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, @@ -329,62 +984,57 @@ object Transactions { payCommitTxFees: Boolean, dustLimit: Satoshi, toSelfDelay: CltvExpiryDelta, - spec: CommitmentSpec - ): TransactionsCommitmentOutputs { - val commitFee = commitTxFee(dustLimit, spec) - + commitmentFormat: CommitmentFormat, + spec: CommitmentSpec, + ): List { + val outputs = ArrayList() + val commitFee = commitTxFee(dustLimit, spec, commitmentFormat) val (toLocalAmount, toRemoteAmount) = if (payCommitTxFees) { Pair(spec.toLocal.truncateToSatoshi() - commitFee, spec.toRemote.truncateToSatoshi()) } else { Pair(spec.toLocal.truncateToSatoshi(), spec.toRemote.truncateToSatoshi() - commitFee) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway - val outputs = ArrayList>() - - if (toLocalAmount >= dustLimit) outputs.add( - CommitmentOutputLink( - TxOut(toLocalAmount, Script.pay2wsh(Scripts.toLocalDelayed(commitKeys.revocationPublicKey, toSelfDelay, commitKeys.localDelayedPaymentPublicKey))), - Scripts.toLocalDelayed(commitKeys.revocationPublicKey, toSelfDelay, commitKeys.localDelayedPaymentPublicKey), - CommitmentOutput.ToLocal - ) - ) - + if (toLocalAmount >= dustLimit) { + val redeemInfo = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys, toSelfDelay)) + } + outputs.add(CommitmentOutput.ToLocal(TxOut(toLocalAmount, redeemInfo.pubkeyScript))) + } if (toRemoteAmount >= dustLimit) { - outputs.add( - CommitmentOutputLink( - TxOut(toRemoteAmount, Script.pay2wsh(Scripts.toRemoteDelayed(commitKeys.remotePaymentPublicKey))), - Scripts.toRemoteDelayed(commitKeys.remotePaymentPublicKey), - CommitmentOutput.ToRemote - ) - ) + val redeemInfo = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toRemoteDelayed(commitKeys)) + } + outputs.add(CommitmentOutput.ToRemote(TxOut(toRemoteAmount, redeemInfo.pubkeyScript))) } - val untrimmedHtlcs = trimOfferedHtlcs(dustLimit, spec).isNotEmpty() || trimReceivedHtlcs(dustLimit, spec).isNotEmpty() - if (untrimmedHtlcs || toLocalAmount >= dustLimit) - outputs.add( - CommitmentOutputLink( - TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2wsh(Scripts.toAnchor(localFundingPubkey))), - Scripts.toAnchor(localFundingPubkey), - CommitmentOutput.ToLocalAnchor(localFundingPubkey) - ) - ) - if (untrimmedHtlcs || toRemoteAmount >= dustLimit) - outputs.add( - CommitmentOutputLink( - TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2wsh(Scripts.toAnchor(remoteFundingPubkey))), - Scripts.toAnchor(remoteFundingPubkey), - CommitmentOutput.ToLocalAnchor(remoteFundingPubkey) - ) - ) - - trimOfferedHtlcs(dustLimit, spec).forEach { htlc -> - val redeemScript = Scripts.htlcOffered(commitKeys.localHtlcPublicKey, commitKeys.remoteHtlcPublicKey, commitKeys.revocationPublicKey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray())) - outputs.add(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) + val untrimmedHtlcs = trimOfferedHtlcs(dustLimit, spec, commitmentFormat).isNotEmpty() || trimReceivedHtlcs(dustLimit, spec, commitmentFormat).isNotEmpty() + if (untrimmedHtlcs || toLocalAmount >= dustLimit) { + val redeemInfo = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toAnchor(localFundingPubkey)) + } + outputs.add(CommitmentOutput.ToLocalAnchor(TxOut(commitmentFormat.anchorAmount, redeemInfo.pubkeyScript))) + } + if (untrimmedHtlcs || toRemoteAmount >= dustLimit) { + val redeemInfo = when (commitmentFormat) { + CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toAnchor(remoteFundingPubkey)) + } + outputs.add(CommitmentOutput.ToRemoteAnchor(TxOut(commitmentFormat.anchorAmount, redeemInfo.pubkeyScript))) } - trimReceivedHtlcs(dustLimit, spec).forEach { htlc -> - val redeemScript = Scripts.htlcReceived(commitKeys.localHtlcPublicKey, commitKeys.remoteHtlcPublicKey, commitKeys.revocationPublicKey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray()), htlc.add.cltvExpiry) - outputs.add(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) + trimOfferedHtlcs(dustLimit, spec, commitmentFormat).forEach { htlc -> + val fee = weight2fee(spec.feerate, commitmentFormat.htlcTimeoutWeight) + val amountAfterFees = htlc.add.amountMsat.truncateToSatoshi() - fee + val redeemInfo = HtlcTimeoutTx.redeemInfo(commitKeys, htlc.add.paymentHash, commitmentFormat) + val htlcDelayedRedeemInfo = HtlcDelayedTx.redeemInfo(commitKeys, toSelfDelay, commitmentFormat) + outputs.add(OutHtlc(htlc, TxOut(htlc.add.amountMsat.truncateToSatoshi(), redeemInfo.pubkeyScript), TxOut(amountAfterFees, htlcDelayedRedeemInfo.pubkeyScript))) + } + trimReceivedHtlcs(dustLimit, spec, commitmentFormat).forEach { htlc -> + val fee = weight2fee(spec.feerate, commitmentFormat.htlcSuccessWeight) + val amountAfterFees = htlc.add.amountMsat.truncateToSatoshi() - fee + val redeemInfo = HtlcSuccessTx.redeemInfo(commitKeys, htlc.add.paymentHash, htlc.add.cltvExpiry, commitmentFormat) + val htlcDelayedRedeemInfo = HtlcDelayedTx.redeemInfo(commitKeys, toSelfDelay, commitmentFormat) + outputs.add(InHtlc(htlc, TxOut(htlc.add.amountMsat.truncateToSatoshi(), redeemInfo.pubkeyScript), TxOut(amountAfterFees, htlcDelayedRedeemInfo.pubkeyScript))) } return outputs.apply { sort() } @@ -396,354 +1046,44 @@ object Transactions { localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey, localIsChannelOpener: Boolean, - outputs: TransactionsCommitmentOutputs - ): TransactionWithInputInfo.CommitTx { + outputs: List + ): CommitTx { val txNumber = obscuredCommitTxNumber(commitTxNumber, localIsChannelOpener, localPaymentBasePoint, remotePaymentBasePoint) val (sequence, locktime) = encodeTxNumber(txNumber) - val tx = Transaction( version = 2, txIn = listOf(TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = sequence)), - txOut = outputs.map { it.output }, + txOut = outputs.map { it.txOut }, lockTime = locktime ) - - return TransactionWithInputInfo.CommitTx(commitTxInput, tx) - } - - sealed class TxResult { - data class Skipped(val why: TxGenerationSkipped) : TxResult() - data class Success(val result: T) : TxResult() - } - - private fun makeHtlcTimeoutTx( - commitTx: Transaction, - output: CommitmentOutputLink, - outputIndex: Int, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, - feerate: FeeratePerKw - ): TxResult { - val fee = weight2fee(feerate, Commitments.HTLC_TIMEOUT_WEIGHT) - val redeemScript = output.redeemScript - val htlc = output.commitmentOutput.outgoingHtlc.add - val amount = htlc.amountMsat.truncateToSatoshi() - fee - return if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), - txOut = listOf(TxOut(amount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))), - lockTime = htlc.cltvExpiry.toLong() - ) - TxResult.Success(TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx(input, tx, htlc.id)) - } - } - - private fun makeHtlcSuccessTx( - commitTx: Transaction, - output: CommitmentOutputLink, - outputIndex: Int, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toSelfDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, - feerate: FeeratePerKw - ): TxResult { - val fee = weight2fee(feerate, Commitments.HTLC_SUCCESS_WEIGHT) - val redeemScript = output.redeemScript - val htlc = output.commitmentOutput.incomingHtlc.add - val amount = htlc.amountMsat.truncateToSatoshi() - fee - return if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), - txOut = listOf(TxOut(amount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toSelfDelay, localDelayedPaymentPubkey)))), - lockTime = 0 - ) - TxResult.Success(TransactionWithInputInfo.HtlcTx.HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id)) - } + return CommitTx(commitTxInput, tx) } fun makeHtlcTxs( commitTx: Transaction, - commitKeys: CommitmentPublicKeys, - dustLimit: Satoshi, - toSelfDelay: CltvExpiryDelta, - feerate: FeeratePerKw, - outputs: TransactionsCommitmentOutputs - ): List { - val htlcTimeoutTxs = outputs - .mapIndexedNotNull map@{ outputIndex, link -> - val outHtlc = link.commitmentOutput as? OutHtlc ?: return@map null - val co = CommitmentOutputLink(link.output, link.redeemScript, outHtlc) - makeHtlcTimeoutTx(commitTx, co, outputIndex, dustLimit, commitKeys.revocationPublicKey, toSelfDelay, commitKeys.localDelayedPaymentPublicKey, feerate) - } - .mapNotNull { (it as? TxResult.Success)?.result } - - val htlcSuccessTxs = outputs - .mapIndexedNotNull map@{ outputIndex, link -> - val inHtlc = link.commitmentOutput as? InHtlc ?: return@map null - val co = CommitmentOutputLink(link.output, link.redeemScript, inHtlc) - makeHtlcSuccessTx(commitTx, co, outputIndex, dustLimit, commitKeys.revocationPublicKey, toSelfDelay, commitKeys.localDelayedPaymentPublicKey, feerate) - } - .mapNotNull { (it as? TxResult.Success)?.result } - - return (htlcTimeoutTxs + htlcSuccessTxs).sortedBy { it.input.outPoint.index } - } - - fun makeClaimHtlcSuccessTx( - commitTx: Transaction, - outputs: TransactionsCommitmentOutputs, - localDustLimit: Satoshi, - localHtlcPubkey: PublicKey, - remoteHtlcPubkey: PublicKey, - remoteRevocationPubkey: PublicKey, - localFinalScriptPubKey: ByteArray, - htlc: UpdateAddHtlc, - feerate: FeeratePerKw - ): TxResult { - val redeemScript = Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(htlc.paymentHash)) - return outputs.withIndex() - .firstOrNull { (it.value.commitmentOutput as? OutHtlc)?.outgoingHtlc?.add?.id == htlc.id } - ?.let { (outputIndex, _) -> - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - lockTime = htlc.cltvExpiry.toLong() - ) - val weight = addSigs(TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx(input, tx, htlc.id), PlaceHolderSig, ByteVector32.Zeroes).tx.weight() - val fee = weight2fee(feerate, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) - TxResult.Success(TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx(input, tx1, htlc.id)) - } - } - ?: TxResult.Skipped(TxGenerationSkipped.OutputNotFound) - } - - fun makeClaimHtlcTimeoutTx( - commitTx: Transaction, - outputs: TransactionsCommitmentOutputs, - localDustLimit: Satoshi, - localHtlcPubkey: PublicKey, - remoteHtlcPubkey: PublicKey, - remoteRevocationPubkey: PublicKey, - localFinalScriptPubKey: ByteArray, - htlc: UpdateAddHtlc, - feerate: FeeratePerKw - ): TxResult { - val redeemScript = Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(htlc.paymentHash), htlc.cltvExpiry) - return outputs.withIndex() - .firstOrNull { (it.value.commitmentOutput as? InHtlc)?.incomingHtlc?.add?.id == htlc.id } - ?.let { (outputIndex, _) -> - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - // unsigned tx - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - lockTime = htlc.cltvExpiry.toLong() - ) - val weight = addSigs(TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx(input, tx, htlc.id), PlaceHolderSig).tx.weight() - val fee = weight2fee(feerate, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) - TxResult.Success(TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx(input, tx1, htlc.id)) - } - } - ?: TxResult.Skipped(TxGenerationSkipped.OutputNotFound) - } - - fun makeClaimRemoteDelayedOutputTx( - commitTx: Transaction, localDustLimit: Satoshi, - localPaymentPubkey: PublicKey, - localFinalScriptPubKey: ByteVector, - feerate: FeeratePerKw - ): TxResult { - val redeemScript = Scripts.toRemoteDelayed(localPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) - - return when (val pubkeyScriptIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)) { - is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) - is TxResult.Success -> { - val outputIndex = pubkeyScriptIndex.result - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1)), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - lockTime = 0 - ) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feerate, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) - TxResult.Success(TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx(input, tx1)) - } + outputs: List, + commitmentFormat: CommitmentFormat + ): List { + return outputs.mapIndexedNotNull { outputIndex, output -> + when (output) { + is OutHtlc -> HtlcTimeoutTx.createUnsignedTx(commitTx, output, outputIndex, commitmentFormat) + is InHtlc -> HtlcSuccessTx.createUnsignedTx(commitTx, output, outputIndex, commitmentFormat) + else -> null } } } - fun makeClaimLocalDelayedOutputTx( - delayedOutputTx: Transaction, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, - localFinalScriptPubKey: ByteArray, - feerate: FeeratePerKw - ): TxResult { - val redeemScript = Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) - return when (val pubkeyScriptIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript)) { - is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) - is TxResult.Success -> { - val outputIndex = pubkeyScriptIndex.result - val input = InputInfo(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toLong())), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - lockTime = 0 - ) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(TransactionWithInputInfo.ClaimLocalDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feerate, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) - TxResult.Success(TransactionWithInputInfo.ClaimLocalDelayedOutputTx(input, tx1)) - } - } - } - } - - fun makeClaimDelayedOutputPenaltyTxs( - delayedOutputTx: Transaction, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, - localFinalScriptPubKey: ByteArray, - feerate: FeeratePerKw - ): List> { - val redeemScript = Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) - return when (val pubkeyScriptIndexes = findPubKeyScriptIndexes(delayedOutputTx, pubkeyScript)) { - is TxResult.Skipped -> listOf(TxResult.Skipped(pubkeyScriptIndexes.why)) - is TxResult.Success -> pubkeyScriptIndexes.result.map { outputIndex -> - val input = InputInfo(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - lockTime = 0 - ) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feerate, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) - TxResult.Success(TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx(input, tx1)) - } - } - } + sealed class ClosingTxFee { + data class PaidByUs(val fee: Satoshi) : ClosingTxFee() + data class PaidByThem(val fee: Satoshi) : ClosingTxFee() } - fun makeMainPenaltyTx( - commitTx: Transaction, - localDustLimit: Satoshi, - remoteRevocationPubkey: PublicKey, - localFinalScriptPubKey: ByteArray, - toRemoteDelay: CltvExpiryDelta, - remoteDelayedPaymentPubkey: PublicKey, - feerate: FeeratePerKw - ): TxResult { - val redeemScript = Scripts.toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) - return when (val pubkeyScriptIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)) { - is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) - is TxResult.Success -> { - val outputIndex = pubkeyScriptIndex.result - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - lockTime = 0 - ) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(TransactionWithInputInfo.MainPenaltyTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feerate, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) - TxResult.Success(TransactionWithInputInfo.MainPenaltyTx(input, tx1)) - } - } - } - } + /** Each closing attempt can result in multiple potential closing transactions, depending on which outputs are included. */ + data class ClosingTxs(val localAndRemote: ClosingTx?, val localOnly: ClosingTx?, val remoteOnly: ClosingTx?) { + val preferred: ClosingTx? = localAndRemote ?: localOnly ?: remoteOnly + val all: List = listOfNotNull(localAndRemote, localOnly, remoteOnly) - /** - * We already have the redeemScript, no need to build it - */ - fun makeHtlcPenaltyTx( - commitTx: Transaction, - htlcOutputIndex: Int, - redeemScript: ByteArray, - localDustLimit: Satoshi, - localFinalScriptPubKey: ByteArray, - feerate: FeeratePerKw - ): TxResult { - val input = InputInfo(OutPoint(commitTx, htlcOutputIndex.toLong()), commitTx.txOut[htlcOutputIndex], ByteVector(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - lockTime = 0 - ) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(TransactionWithInputInfo.HtlcPenaltyTx(input, tx), PlaceHolderSig, PlaceHolderPubKey).tx.weight() - val fee = weight2fee(feerate, weight) - val amount = input.txOut.amount - fee - return if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) - TxResult.Success(TransactionWithInputInfo.HtlcPenaltyTx(input, tx1)) - } + override fun toString(): String = "localAndRemote=${localAndRemote?.tx?.toString()}, localOnly=${localOnly?.tx?.toString()}, remoteOnly=${remoteOnly?.tx?.toString()}" } fun makeClosingTxs(input: InputInfo, spec: CommitmentSpec, fee: ClosingTxFee, lockTime: Long, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector): ClosingTxs { @@ -769,54 +1109,48 @@ object Transactions { return when { toLocalOutput != null && toRemoteOutput != null -> { val txLocalAndRemote = LexicographicalOrdering.sort(txNoOutput.copy(txOut = listOf(toLocalOutput, toRemoteOutput))) - val toLocalIndex = when (val i = findPubKeyScriptIndex(txLocalAndRemote, localScriptPubKey.toByteArray())) { - is TxResult.Skipped -> null - is TxResult.Success -> i.result + val toLocalIndex = when (val i = findPubKeyScriptIndex(txLocalAndRemote, localScriptPubKey)) { + is Either.Left -> null + is Either.Right -> i.value } ClosingTxs( - localAndRemote = TransactionWithInputInfo.ClosingTx(input, txLocalAndRemote, toLocalIndex), + localAndRemote = ClosingTx(input, txLocalAndRemote, toLocalIndex), // We also provide a version of the transaction without the remote output, which they may want to omit if not economical to spend. - localOnly = TransactionWithInputInfo.ClosingTx(input, txNoOutput.copy(txOut = listOf(toLocalOutput)), 0), + localOnly = ClosingTx(input, txNoOutput.copy(txOut = listOf(toLocalOutput)), 0), remoteOnly = null ) } toLocalOutput != null -> ClosingTxs( localAndRemote = null, - localOnly = TransactionWithInputInfo.ClosingTx(input, txNoOutput.copy(txOut = listOf(toLocalOutput)), 0), + localOnly = ClosingTx(input, txNoOutput.copy(txOut = listOf(toLocalOutput)), 0), remoteOnly = null, ) toRemoteOutput != null -> ClosingTxs( localAndRemote = null, localOnly = null, - remoteOnly = TransactionWithInputInfo.ClosingTx(input, txNoOutput.copy(txOut = listOf(toRemoteOutput)), null) + remoteOnly = ClosingTx(input, txNoOutput.copy(txOut = listOf(toRemoteOutput)), null) ) else -> ClosingTxs(localAndRemote = null, localOnly = null, remoteOnly = null) } } - private fun findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteArray): TxResult { - val outputIndex = tx.txOut.indexOfFirst { txOut -> txOut.publicKeyScript.contentEquals(pubkeyScript) } - return if (outputIndex >= 0) { - TxResult.Success(outputIndex) - } else { - TxResult.Skipped(TxGenerationSkipped.OutputNotFound) + /** We skip creating transactions spending commitment outputs when the remaining amount is below dust. */ + private fun skipTxIfBelowDust(txInfo: T, dustLimit: Satoshi): Either { + return when { + txInfo.tx.txOut.first().amount < dustLimit -> Either.Left(TxGenerationSkipped.AmountBelowDustLimit) + else -> Either.Right(txInfo) } } - private fun findPubKeyScriptIndexes(tx: Transaction, pubkeyScript: ByteArray): TxResult> { - val outputIndexes = tx.txOut.withIndex().filter { it.value.publicKeyScript.contentEquals(pubkeyScript) }.map { it.index } - return if (outputIndexes.isNotEmpty()) { - TxResult.Success(outputIndexes) + private fun findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteVector): Either { + val outputIndex = tx.txOut.indexOfFirst { txOut -> txOut.publicKeyScript == pubkeyScript } + return if (outputIndex >= 0) { + Either.Right(outputIndex) } else { - TxResult.Skipped(TxGenerationSkipped.OutputNotFound) + Either.Left(TxGenerationSkipped.OutputNotFound) } } - /** - * Default public key used for fee estimation - */ - val PlaceHolderPubKey = PrivateKey(ByteVector32.One).publicKey() - /** * This default sig takes 72B when encoded in DER (incl. 1B for the trailing sig hash), it is used for fee estimation * It is 72 bytes because our signatures are normalized (low-s) and will take up 72 bytes at most in DER format @@ -824,84 +1158,4 @@ object Transactions { val PlaceHolderSig = ByteVector64(ByteArray(64) { 0xaa.toByte() }) .also { check(Scripts.der(it, SigHash.SIGHASH_ALL).size() == 72) { "Should be 72 bytes but is ${Scripts.der(it, SigHash.SIGHASH_ALL).size()} bytes" } } - fun sign(tx: Transaction, inputIndex: Int, redeemScript: ByteArray, amount: Satoshi, key: PrivateKey, sigHash: Int = SigHash.SIGHASH_ALL): ByteVector64 { - val sigDER = tx.signInput(inputIndex, redeemScript, sigHash, amount, SigVersion.SIGVERSION_WITNESS_V0, key) - return Crypto.der2compact(sigDER) - } - - fun sign(txInfo: TransactionWithInputInfo, key: PrivateKey, sigHash: Int = SigHash.SIGHASH_ALL): ByteVector64 { - val inputIndex = txInfo.tx.txIn.indexOfFirst { it.outPoint == txInfo.input.outPoint } - require(inputIndex >= 0) { "transaction doesn't spend the input to sign" } - return sign(txInfo.tx, inputIndex, txInfo.input.redeemScript.toByteArray(), txInfo.amountIn, key, sigHash) - } - - fun addSigs( - commitTx: TransactionWithInputInfo.CommitTx, - localFundingPubkey: PublicKey, - remoteFundingPubkey: PublicKey, - localSig: ByteVector64, - remoteSig: ByteVector64 - ): TransactionWithInputInfo.CommitTx { - val witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, remoteFundingPubkey) - return commitTx.copy(tx = commitTx.tx.updateWitness(0, witness)) - } - - fun addSigs(mainPenaltyTx: TransactionWithInputInfo.MainPenaltyTx, revocationSig: ByteVector64): TransactionWithInputInfo.MainPenaltyTx { - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScript) - return mainPenaltyTx.copy(tx = mainPenaltyTx.tx.updateWitness(0, witness)) - } - - fun addSigs(htlcPenaltyTx: TransactionWithInputInfo.HtlcPenaltyTx, revocationSig: ByteVector64, revocationPubkey: PublicKey): TransactionWithInputInfo.HtlcPenaltyTx { - val witness = Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, htlcPenaltyTx.input.redeemScript) - return htlcPenaltyTx.copy(tx = htlcPenaltyTx.tx.updateWitness(0, witness)) - } - - fun addSigs(htlcSuccessTx: TransactionWithInputInfo.HtlcTx.HtlcSuccessTx, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32): TransactionWithInputInfo.HtlcTx.HtlcSuccessTx { - val witness = Scripts.witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScript) - return htlcSuccessTx.copy(tx = htlcSuccessTx.tx.updateWitness(0, witness)) - } - - fun addSigs(htlcTimeoutTx: TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx, localSig: ByteVector64, remoteSig: ByteVector64): TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx { - val witness = Scripts.witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScript) - return htlcTimeoutTx.copy(tx = htlcTimeoutTx.tx.updateWitness(0, witness)) - } - - fun addSigs(claimHtlcSuccessTx: TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx, localSig: ByteVector64, paymentPreimage: ByteVector32): TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx { - val witness = Scripts.witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, claimHtlcSuccessTx.input.redeemScript) - return claimHtlcSuccessTx.copy(tx = claimHtlcSuccessTx.tx.updateWitness(0, witness)) - } - - fun addSigs(claimHtlcTimeoutTx: TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx, localSig: ByteVector64): TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx { - val witness = Scripts.witnessClaimHtlcTimeoutFromCommitTx(localSig, claimHtlcTimeoutTx.input.redeemScript) - return claimHtlcTimeoutTx.copy(tx = claimHtlcTimeoutTx.tx.updateWitness(0, witness)) - } - - fun addSigs(claimRemoteDelayed: TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx, localSig: ByteVector64): TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx { - val witness = Scripts.witnessToRemoteDelayedAfterDelay(localSig, claimRemoteDelayed.input.redeemScript) - return claimRemoteDelayed.copy(tx = claimRemoteDelayed.tx.updateWitness(0, witness)) - } - - fun addSigs(claimLocalDelayed: TransactionWithInputInfo.ClaimLocalDelayedOutputTx, localSig: ByteVector64): TransactionWithInputInfo.ClaimLocalDelayedOutputTx { - val witness = Scripts.witnessToLocalDelayedAfterDelay(localSig, claimLocalDelayed.input.redeemScript) - return claimLocalDelayed.copy(tx = claimLocalDelayed.tx.updateWitness(0, witness)) - } - - fun addSigs(claimHtlcDelayedPenalty: TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx, revocationSig: ByteVector64): TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx { - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScript) - return claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness)) - } - - fun addSigs(closingTx: TransactionWithInputInfo.ClosingTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ByteVector64, remoteSig: ByteVector64): TransactionWithInputInfo.ClosingTx { - val witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, remoteFundingPubkey) - return closingTx.copy(tx = closingTx.tx.updateWitness(0, witness)) - } - - fun checkSpendable(txinfo: TransactionWithInputInfo): Try = runTrying { - txinfo.tx.correctlySpends(mapOf(txinfo.tx.txIn.first().outPoint to txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - - fun checkSig(txinfo: TransactionWithInputInfo, sig: ByteVector64, pubKey: PublicKey, sigHash: Int = SigHash.SIGHASH_ALL): Boolean { - val data = txinfo.tx.hashForSigning(0, txinfo.input.redeemScript.toByteArray(), sigHash, txinfo.amountIn, SigVersion.SIGVERSION_WITNESS_V0) - return Crypto.verifySignature(data, sig, pubKey) - } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 03478f344..2d4315a64 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -9,6 +9,7 @@ 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.ChannelSpendSignature import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.router.Announcements import fr.acinq.lightning.utils.* @@ -1217,7 +1218,7 @@ sealed class CommitSigs : HtlcMessage, HasChannelId, RequirePeerStorageStore { data class CommitSig( override val channelId: ByteVector32, - val signature: ByteVector64, + val signature: ChannelSpendSignature.IndividualSignature, val htlcSignatures: List, val tlvStream: TlvStream = TlvStream.empty() ) : CommitSigs() { @@ -1228,7 +1229,7 @@ data class CommitSig( override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) - LightningCodecs.writeBytes(signature, out) + LightningCodecs.writeBytes(signature.sig, out) LightningCodecs.writeU16(htlcSignatures.size, out) htlcSignatures.forEach { LightningCodecs.writeBytes(it, out) } TlvStreamSerializer(false, readers).write(tlvStream, out) @@ -1245,7 +1246,7 @@ data class CommitSig( override fun read(input: Input): CommitSig { val channelId = ByteVector32(LightningCodecs.bytes(input, 32)) - val sig = ByteVector64(LightningCodecs.bytes(input, 64)) + val sig = ChannelSpendSignature.IndividualSignature(ByteVector64(LightningCodecs.bytes(input, 64))) val numHtlcs = LightningCodecs.u16(input) val htlcSigs = ArrayList(numHtlcs) for (i in 1..numHtlcs) { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt deleted file mode 100644 index a0fd78d7d..000000000 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt +++ /dev/null @@ -1,366 +0,0 @@ -package fr.acinq.lightning.channel - -import fr.acinq.bitcoin.* -import fr.acinq.lightning.Lightning.randomBytes32 -import fr.acinq.lightning.Lightning.randomKey -import fr.acinq.lightning.blockchain.WatchConfirmed -import fr.acinq.lightning.blockchain.WatchSpent -import fr.acinq.lightning.blockchain.WatchSpentTriggered -import fr.acinq.lightning.channel.TestsHelper.claimHtlcSuccessTxs -import fr.acinq.lightning.channel.TestsHelper.claimHtlcTimeoutTxs -import fr.acinq.lightning.channel.TestsHelper.crossSign -import fr.acinq.lightning.channel.TestsHelper.htlcSuccessTxs -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.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.transactions.Transactions.InputInfo -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx -import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.utils.sat -import kotlin.test.* - -class ChannelDataTestsCommon : LightningTestSuite(), LoggingContext { - - override val logger = MDCLogger(loggerFactory.newLogger(this::class)) - - @Test - fun `local commit published`() { - val (lcp, _, _) = createClosingTransactions() - assertFalse(lcp.isConfirmed()) - assertFalse(lcp.isDone()) - - run { - val actions = lcp.run { doPublish(TestConstants.Alice.nodeParams, randomBytes32()) } - // We use watch-confirmed on the outputs only us can claim. - val watchConfirmed = actions.findWatches().map { it.txId }.toSet() - assertEquals(watchConfirmed, setOf(lcp.commitTx.txid, lcp.claimMainDelayedOutputTx!!.tx.txid) + lcp.claimHtlcDelayedTxs.map { it.tx.txid }.toSet()) - // We use watch-spent on the outputs both parties can claim (htlc outputs). - val watchSpent = actions.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet() - assertEquals(watchSpent, listOf(2L, 3L, 4L, 5L).map { OutPoint(lcp.commitTx.txid, it) }.toSet()) - val txs = actions.findPublishTxs().toSet() - assertEquals(txs, setOf(lcp.commitTx, lcp.claimMainDelayedOutputTx.tx) + lcp.htlcTxs.values.filterNotNull().map { it.tx } + lcp.claimHtlcDelayedTxs.map { it.tx }.toSet()) - } - - // Commit tx has been confirmed. - val lcp1 = lcp.update(lcp.commitTx) - assertTrue(lcp1.irrevocablySpent.isNotEmpty()) - assertTrue(lcp1.isConfirmed()) - assertFalse(lcp1.isDone()) - - // Main output has been confirmed. - val lcp2 = lcp1.update(lcp.claimMainDelayedOutputTx!!.tx) - assertTrue(lcp2.isConfirmed()) - assertFalse(lcp2.isDone()) - - // Our htlc-success txs and their 3rd-stage claim txs have been confirmed. - val lcp3 = lcp2.update(lcp.htlcSuccessTxs().first().tx).update(lcp.claimHtlcDelayedTxs[0].tx).update(lcp.htlcSuccessTxs().last().tx).update(lcp.claimHtlcDelayedTxs[1].tx) - assertTrue(lcp3.isConfirmed()) - assertFalse(lcp3.isDone()) - - run { - val actions = lcp3.run { doPublish(TestConstants.Bob.nodeParams, randomBytes32()) } - // The only remaining transactions to watch are the 3rd-stage txs for the htlc-timeout. - val watchConfirmed = actions.findWatches().map { it.txId }.toSet() - assertEquals(watchConfirmed, lcp.claimHtlcDelayedTxs.drop(2).map { it.tx.txid }.toSet()) - // We still watch the remaining unclaimed htlc outputs. - val watchSpent = actions.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet() - assertEquals(watchSpent, listOf(4L, 5L).map { OutPoint(lcp.commitTx.txid, it) }.toSet()) - val txs = actions.findPublishTxs() - assertEquals(txs, lcp.htlcTimeoutTxs().map { it.tx } + lcp.claimHtlcDelayedTxs.drop(2).map { it.tx }) - } - - // Scenario 1: our htlc-timeout txs and their 3rd-stage claim txs have been confirmed. - run { - val lcp4a = lcp3.update(lcp.htlcTimeoutTxs().first().tx).update(lcp.claimHtlcDelayedTxs[2].tx).update(lcp.htlcTimeoutTxs().last().tx) - assertTrue(lcp4a.isConfirmed()) - assertFalse(lcp4a.isDone()) - - val lcp4b = lcp4a.update(lcp.claimHtlcDelayedTxs[3].tx) - assertTrue(lcp4b.isConfirmed()) - assertTrue(lcp4b.isDone()) - } - - // Scenario 2: they claim the htlcs we sent before our htlc-timeout. - run { - val claimHtlcSuccess1 = lcp.htlcTimeoutTxs().first().tx.copy(txOut = listOf(TxOut(3_000.sat, ByteVector.empty), TxOut(2_500.sat, ByteVector.empty))) - val lcp4a = lcp3.update(claimHtlcSuccess1) - assertTrue(lcp4a.isConfirmed()) - assertFalse(lcp4a.isDone()) - - val claimHtlcSuccess2 = lcp.htlcTimeoutTxs().last().tx.copy(txOut = listOf(TxOut(3_500.sat, ByteVector.empty), TxOut(3_100.sat, ByteVector.empty))) - val lcp4b = lcp4a.update(claimHtlcSuccess2) - assertTrue(lcp4b.isConfirmed()) - assertTrue(lcp4b.isDone()) - } - } - - @Test - fun `remote commit published`() { - val (_, rcp, _) = createClosingTransactions() - assertFalse(rcp.isConfirmed()) - assertFalse(rcp.isDone()) - - run { - val actions = rcp.run { doPublish(TestConstants.Alice.nodeParams, randomBytes32()) } - // We use watch-confirmed on the outputs only us can claim. - val watchConfirmed = actions.findWatches().map { it.txId }.toSet() - assertEquals(watchConfirmed, setOf(rcp.commitTx.txid, rcp.claimMainOutputTx!!.tx.txid)) - // We use watch-spent on the outputs both parties can claim (htlc outputs). - val watchSpent = actions.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet() - assertEquals(watchSpent, listOf(2L, 3L, 4L, 5L).map { OutPoint(rcp.commitTx.txid, it) }.toSet()) - val txs = actions.findPublishTxs().toSet() - assertEquals(txs, setOf(rcp.claimMainOutputTx.tx) + rcp.claimHtlcTxs.values.filterNotNull().map { it.tx }.toSet()) - } - - // Commit tx has been confirmed. - val rcp1 = rcp.update(rcp.commitTx) - assertTrue(rcp1.irrevocablySpent.isNotEmpty()) - assertTrue(rcp1.isConfirmed()) - assertFalse(rcp1.isDone()) - - // Main output has been confirmed. - val rcp2 = rcp1.update(rcp.claimMainOutputTx!!.tx) - assertTrue(rcp2.isConfirmed()) - assertFalse(rcp2.isDone()) - - // One of our claim-htlc-success and claim-htlc-timeout has been confirmed. - val rcp3 = rcp2.update(rcp.claimHtlcSuccessTxs().first().tx).update(rcp.claimHtlcTimeoutTxs().first().tx) - assertTrue(rcp3.isConfirmed()) - assertFalse(rcp3.isDone()) - - run { - val actions = rcp3.run { doPublish(TestConstants.Bob.nodeParams, randomBytes32()) } - // Our main output has been confirmed already. - assertTrue(actions.findWatches().isEmpty()) - // We still watch the remaining unclaimed htlc outputs. - val watchSpent = actions.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet() - assertEquals(watchSpent, listOf(3L, 5L).map { OutPoint(rcp.commitTx.txid, it) }.toSet()) - val txs = actions.findPublishTxs().toSet() - assertEquals(txs, setOf(rcp.claimHtlcSuccessTxs().last().tx, rcp.claimHtlcTimeoutTxs().last().tx)) - } - - // Scenario 1: our remaining claim-htlc txs have been confirmed. - run { - val rcp4a = rcp3.update(rcp.claimHtlcSuccessTxs().last().tx) - assertTrue(rcp4a.isConfirmed()) - assertFalse(rcp4a.isDone()) - - val rcp4b = rcp4a.update(rcp.claimHtlcTimeoutTxs().last().tx) - assertTrue(rcp4b.isConfirmed()) - assertTrue(rcp4b.isDone()) - } - - // Scenario 2: they claim the remaining htlc outputs. - run { - val htlcSuccess = rcp.claimHtlcSuccessTxs().last().tx.copy(txOut = listOf(TxOut(3_000.sat, ByteVector.empty), TxOut(2_500.sat, ByteVector.empty))) - val rcp4a = rcp3.update(htlcSuccess) - assertTrue(rcp4a.isConfirmed()) - assertFalse(rcp4a.isDone()) - - val htlcTimeout = rcp.claimHtlcTimeoutTxs().last().tx.copy(txOut = listOf(TxOut(3_500.sat, ByteVector.empty), TxOut(3_100.sat, ByteVector.empty))) - val rcp4b = rcp4a.update(htlcTimeout) - assertTrue(rcp4b.isConfirmed()) - assertTrue(rcp4b.isDone()) - } - } - - @Test - fun `revoked commit published`() { - val (_, _, rvk) = createClosingTransactions() - assertFalse(rvk.isDone()) - - run { - val actions = rvk.run { doPublish(TestConstants.Alice.nodeParams, randomBytes32()) } - // We use watch-confirmed on the outputs only us can claim. - val watchConfirmed = actions.findWatches().map { it.txId }.toSet() - assertEquals(watchConfirmed, setOf(rvk.commitTx.txid, rvk.claimMainOutputTx!!.tx.txid)) - // We use watch-spent on the outputs both parties can claim (htlc outputs and the remote main output). - val watchSpent = actions.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet() - assertEquals(watchSpent, listOf(1L, 2L, 3L, 4L, 5L).map { OutPoint(rvk.commitTx.txid, it) }.toSet()) - val txs = actions.findPublishTxs().toSet() - assertEquals(txs, setOf(rvk.claimMainOutputTx.tx, rvk.mainPenaltyTx!!.tx) + rvk.htlcPenaltyTxs.map { it.tx }.toSet()) - } - - // Commit tx has been confirmed. - val rvk1 = rvk.update(rvk.commitTx) - assertTrue(rvk1.irrevocablySpent.isNotEmpty()) - assertFalse(rvk1.isDone()) - - // Main output has been confirmed. - val rvk2 = rvk1.update(rvk.claimMainOutputTx!!.tx) - assertFalse(rvk2.isDone()) - - // Two of our htlc penalty txs have been confirmed. - val rvk3 = rvk2.update(rvk.htlcPenaltyTxs[0].tx).update(rvk.htlcPenaltyTxs[1].tx) - assertFalse(rvk3.isDone()) - - run { - val actions = rvk3.run { doPublish(TestConstants.Bob.nodeParams, randomBytes32()) } - // Our main output has been confirmed already. - assertTrue(actions.findWatches().isEmpty()) - // We still watch the remaining unclaimed outputs (htlc and remote main output). - val watchSpent = actions.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet() - assertEquals(watchSpent, listOf(1L, 4L, 5L).map { OutPoint(rvk.commitTx.txid, it) }.toSet()) - val txs = actions.findPublishTxs().toSet() - assertEquals(txs, setOf(rvk.mainPenaltyTx!!.tx, rvk.htlcPenaltyTxs[2].tx, rvk.htlcPenaltyTxs[3].tx)) - } - - // Scenario 1: the remaining penalty txs have been confirmed. - run { - val rvk4a = rvk3.update(rvk.htlcPenaltyTxs[2].tx).update(rvk.htlcPenaltyTxs[3].tx) - assertFalse(rvk4a.isDone()) - - val rvk4b = rvk4a.update(rvk.mainPenaltyTx!!.tx) - assertTrue(rvk4b.isDone()) - } - - // Scenario 2: they claim the remaining outputs. - run { - val remoteMainOutput = rvk.mainPenaltyTx!!.tx.copy(txOut = listOf(TxOut(35_000.sat, ByteVector.empty))) - val rvk4a = rvk3.update(remoteMainOutput) - assertFalse(rvk4a.isDone()) - - val htlcSuccess = rvk.htlcPenaltyTxs[2].tx.copy(txOut = listOf(TxOut(3_000.sat, ByteVector.empty), TxOut(2_500.sat, ByteVector.empty))) - val htlcTimeout = rvk.htlcPenaltyTxs[3].tx.copy(txOut = listOf(TxOut(3_500.sat, ByteVector.empty), TxOut(3_100.sat, ByteVector.empty))) - // When Bob claims these outputs, the channel should call Helpers.claimRevokedHtlcTxOutputs to punish them by claiming the output of their htlc tx. - val rvk4b = rvk4a.update(htlcSuccess).update(htlcTimeout).copy( - claimHtlcDelayedPenaltyTxs = listOf( - Transaction(2, listOf(TxIn(OutPoint(htlcSuccess, 0), 0)), listOf(TxOut(5_000.sat, ByteVector.empty)), 0), - Transaction(2, listOf(TxIn(OutPoint(htlcTimeout, 0), 0)), listOf(TxOut(6_000.sat, ByteVector.empty)), 0) - ).map { ClaimHtlcDelayedOutputPenaltyTx(txInput(it), it) } - ) - assertFalse(rvk4b.isDone()) - - val actions = rvk4b.run { doPublish(TestConstants.Bob.nodeParams, randomBytes32()) } - assertTrue(actions.findWatches().isEmpty()) - // NB: the channel, after calling Helpers.claimRevokedHtlcTxOutputs, will put a watch-spent on the htlc-txs. - assertTrue(actions.findWatches().isEmpty()) - assertEquals(actions.findPublishTxs().toSet(), rvk4b.claimHtlcDelayedPenaltyTxs.map { it.tx }.toSet()) - - // We claim one of the remaining outputs, they claim the other. - val rvk5a = rvk4b.update(rvk4b.claimHtlcDelayedPenaltyTxs[0].tx) - assertFalse(rvk5a.isDone()) - val theyClaimHtlcTimeout = rvk4b.claimHtlcDelayedPenaltyTxs[1].tx.copy(txOut = listOf(TxOut(1_500.sat, ByteVector.empty), TxOut(2_500.sat, ByteVector.empty))) - val rvk5b = rvk5a.update(theyClaimHtlcTimeout) - assertTrue(rvk5b.isDone()) - } - } - - @Test - fun `identify htlc txs`() { - val (lcp, rcp) = run { - val (alice0, bob0) = reachNormal() - val (nodes1, _, _) = TestsHelper.addHtlc(250_000_000.msat, payer = alice0, payee = bob0) - val (alice1, bob1) = nodes1 - val (nodes2, preimageAlice, htlcAlice) = TestsHelper.addHtlc(100_000_000.msat, payer = alice1, payee = bob1) - val (alice2, bob2) = nodes2 - val (alice3, bob3) = crossSign(alice2, bob2) - val (nodes4, _, _) = TestsHelper.addHtlc(50_000_000.msat, payer = bob3, payee = alice3) - val (bob4, alice4) = nodes4 - val (nodes5, preimageBob, htlcBob) = TestsHelper.addHtlc(55_000_000.msat, payer = bob4, payee = alice4) - val (bob5, alice5) = nodes5 - val (bob6, alice6) = crossSign(bob5, alice5) - // Alice and Bob both know the preimage for only one of the two HTLCs they received. - val (alice7, _) = alice6.process(ChannelCommand.Htlc.Settlement.Fulfill(htlcBob.id, preimageBob)) - val (bob7, _) = bob6.process(ChannelCommand.Htlc.Settlement.Fulfill(htlcAlice.id, preimageAlice)) - // Alice publishes her commitment. - val (aliceClosing, _) = alice7.process(ChannelCommand.Close.ForceClose) - assertIs>(aliceClosing) - val lcp = aliceClosing.state.localCommitPublished - assertNotNull(lcp) - val (bobClosing, _) = bob7.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.state.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), lcp.commitTx))) - assertIs>(bobClosing) - val rcp = bobClosing.state.remoteCommitPublished - assertNotNull(rcp) - Pair(lcp, rcp) - } - - assertEquals(4, lcp.htlcTxs.size) - val htlcTimeoutTxs = lcp.htlcTimeoutTxs() - assertEquals(2, htlcTimeoutTxs.size) - val htlcSuccessTxs = lcp.htlcSuccessTxs() - assertEquals(1, htlcSuccessTxs.size) - - assertEquals(4, rcp.claimHtlcTxs.size) - val claimHtlcTimeoutTxs = rcp.claimHtlcTimeoutTxs() - assertEquals(2, claimHtlcTimeoutTxs.size) - val claimHtlcSuccessTxs = rcp.claimHtlcSuccessTxs() - assertEquals(1, claimHtlcSuccessTxs.size) - } - - companion object { - private fun txInput(tx: Transaction): InputInfo { - return InputInfo(tx.txIn.first().outPoint, TxOut(0.sat, ByteVector.empty), ByteVector.empty) - } - - private fun createClosingTransactions(): Triple { - val commitTx = Transaction( - 2, - listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), - listOf( - TxOut(50_000.sat, ByteVector.empty), // main output Alice - TxOut(40_000.sat, ByteVector.empty), // main output Bob - TxOut(4_000.sat, ByteVector.empty), // htlc received #1 - TxOut(5_000.sat, ByteVector.empty), // htlc received #2 - TxOut(6_000.sat, ByteVector.empty), // htlc sent #1 - TxOut(7_000.sat, ByteVector.empty), // htlc sent #2 - ), - 0 - ) - val claimMainAlice = Transaction(2, listOf(TxIn(OutPoint(commitTx, 0), 144)), listOf(TxOut(49_500.sat, ByteVector.empty)), 0) - val htlcSuccess1 = Transaction(2, listOf(TxIn(OutPoint(commitTx, 2), 1)), listOf(TxOut(3_500.sat, ByteVector.empty)), 0) - val htlcSuccess2 = Transaction(2, listOf(TxIn(OutPoint(commitTx, 3), 1)), listOf(TxOut(4_500.sat, ByteVector.empty)), 0) - val htlcTimeout1 = Transaction(2, listOf(TxIn(OutPoint(commitTx, 4), 1)), listOf(TxOut(5_500.sat, ByteVector.empty)), 0) - val htlcTimeout2 = Transaction(2, listOf(TxIn(OutPoint(commitTx, 5), 1)), listOf(TxOut(6_500.sat, ByteVector.empty)), 0) - - val localCommit = run { - val htlcTxs = mapOf( - htlcSuccess1.txIn.first().outPoint to HtlcSuccessTx(txInput(htlcSuccess1), htlcSuccess1, randomBytes32(), 0), - htlcSuccess2.txIn.first().outPoint to HtlcSuccessTx(txInput(htlcSuccess2), htlcSuccess2, randomBytes32(), 1), - htlcTimeout1.txIn.first().outPoint to HtlcTimeoutTx(txInput(htlcTimeout1), htlcTimeout1, 0), - htlcTimeout2.txIn.first().outPoint to HtlcTimeoutTx(txInput(htlcTimeout2), htlcTimeout2, 1), - ) - val claimHtlcDelayedTxs = listOf( - Transaction(2, listOf(TxIn(OutPoint(htlcSuccess1, 0), 1)), listOf(TxOut(3_400.sat, ByteVector.empty)), 0), - Transaction(2, listOf(TxIn(OutPoint(htlcSuccess2, 0), 1)), listOf(TxOut(4_400.sat, ByteVector.empty)), 0), - Transaction(2, listOf(TxIn(OutPoint(htlcTimeout1, 0), 1)), listOf(TxOut(5_400.sat, ByteVector.empty)), 0), - Transaction(2, listOf(TxIn(OutPoint(htlcTimeout2, 0), 1)), listOf(TxOut(6_400.sat, ByteVector.empty)), 0), - ).map { ClaimLocalDelayedOutputTx(txInput(it), it) } - val claimMain = ClaimLocalDelayedOutputTx(txInput(claimMainAlice), claimMainAlice) - LocalCommitPublished(commitTx, claimMain, htlcTxs, claimHtlcDelayedTxs) - } - - val remoteCommit = run { - val claimMain = ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx(txInput(claimMainAlice), claimMainAlice) - val claimHtlcTxs = mapOf( - htlcSuccess1.txIn.first().outPoint to ClaimHtlcSuccessTx(txInput(htlcSuccess1), htlcSuccess1, 0), - htlcSuccess2.txIn.first().outPoint to ClaimHtlcSuccessTx(txInput(htlcSuccess2), htlcSuccess2, 1), - htlcTimeout1.txIn.first().outPoint to ClaimHtlcTimeoutTx(txInput(htlcTimeout1), htlcTimeout1, 0), - htlcTimeout2.txIn.first().outPoint to ClaimHtlcTimeoutTx(txInput(htlcTimeout2), htlcTimeout2, 1), - ) - RemoteCommitPublished(commitTx, claimMain, claimHtlcTxs) - } - - val revokedCommit = run { - val mainPenalty = run { - val tx = Transaction(2, listOf(TxIn(OutPoint(commitTx, 1), 0)), listOf(TxOut(39_500.sat, ByteVector.empty)), 0) - MainPenaltyTx(txInput(tx), tx) - } - val claimMain = ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx(txInput(claimMainAlice), claimMainAlice) - val htlcPenaltyTxs = listOf(htlcSuccess1, htlcSuccess2, htlcTimeout1, htlcTimeout2).map { HtlcPenaltyTx(txInput(it), it) } - RevokedCommitPublished(commitTx, randomKey(), claimMain, mainPenalty, htlcPenaltyTxs) - } - - return Triple(localCommit, remoteCommit, revokedCommit) - } - } - -} diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt index 07c7e8bac..e9c05228e 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt @@ -4,17 +4,10 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 +import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.Lightning.randomKey -import fr.acinq.lightning.blockchain.WatchSpent -import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.Helpers.Closing.trimmedOrTimedOutHtlcs -import fr.acinq.lightning.channel.TestsHelper.claimHtlcSuccessTxs -import fr.acinq.lightning.channel.TestsHelper.claimHtlcTimeoutTxs -import fr.acinq.lightning.channel.TestsHelper.htlcSuccessTxs -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.LoggingContext import fr.acinq.lightning.logging.MDCLogger @@ -30,7 +23,10 @@ import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.IncorrectOrUnknownPaymentDetails import fr.acinq.lightning.wire.TxSignatures import fr.acinq.lightning.wire.UpdateAddHtlc -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { @@ -45,9 +41,6 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { fun `correct values for availableForSend - availableForReceive -- success case`() { val (alice, bob) = reachNormal(aliceFundingAmount = 800_000.sat, bobFundingAmount = 200_000.sat) - val aliceKeys = alice.ctx.keyManager.channelKeys(alice.commitments.params.localParams.fundingKeyPath) - val bobKeys = bob.ctx.keyManager.channelKeys(bob.commitments.params.localParams.fundingKeyPath) - val a = 774_660_000.msat // initial balance alice val b = 190_000_000.msat // initial balance bob val p = 42_000_000.msat // a->b payment @@ -72,11 +65,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc1.availableBalanceForSend(), b) assertEquals(bc1.availableBalanceForReceive(), a - p - htlcOutputFee) - val (ac2, commit1) = ac1.sendCommit(aliceKeys, logger).right!! + val (ac2, commit1) = ac1.sendCommit(alice.channelKeys, logger).right!! assertEquals(ac2.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac2.availableBalanceForReceive(), b) - val (bc2, revocation1) = bc1.receiveCommit(commit1, bobKeys, logger).right!! + val (bc2, revocation1) = bc1.receiveCommit(commit1, bob.channelKeys, logger).right!! assertEquals(bc2.availableBalanceForSend(), b) assertEquals(bc2.availableBalanceForReceive(), a - p - htlcOutputFee) @@ -84,11 +77,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac3.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac3.availableBalanceForReceive(), b) - val (bc3, commit2) = bc2.sendCommit(bobKeys, logger).right!! + val (bc3, commit2) = bc2.sendCommit(bob.channelKeys, logger).right!! assertEquals(bc3.availableBalanceForSend(), b) assertEquals(bc3.availableBalanceForReceive(), a - p - htlcOutputFee) - val (ac4, revocation2) = ac3.receiveCommit(commit2, aliceKeys, logger).right!! + val (ac4, revocation2) = ac3.receiveCommit(commit2, alice.channelKeys, logger).right!! assertEquals(ac4.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac4.availableBalanceForReceive(), b) @@ -105,11 +98,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac5.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac5.availableBalanceForReceive(), b + p) - val (bc6, commit3) = bc5.sendCommit(bobKeys, logger).right!! + val (bc6, commit3) = bc5.sendCommit(bob.channelKeys, logger).right!! assertEquals(bc6.availableBalanceForSend(), b + p) assertEquals(bc6.availableBalanceForReceive(), a - p - htlcOutputFee) - val (ac6, revocation3) = ac5.receiveCommit(commit3, aliceKeys, logger).right!! + val (ac6, revocation3) = ac5.receiveCommit(commit3, alice.channelKeys, logger).right!! assertEquals(ac6.availableBalanceForSend(), a - p) assertEquals(ac6.availableBalanceForReceive(), b + p) @@ -117,11 +110,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc7.availableBalanceForSend(), b + p) assertEquals(bc7.availableBalanceForReceive(), a - p) - val (ac7, commit4) = ac6.sendCommit(aliceKeys, logger).right!! + val (ac7, commit4) = ac6.sendCommit(alice.channelKeys, logger).right!! assertEquals(ac7.availableBalanceForSend(), a - p) assertEquals(ac7.availableBalanceForReceive(), b + p) - val (bc8, revocation4) = bc7.receiveCommit(commit4, bobKeys, logger).right!! + val (bc8, revocation4) = bc7.receiveCommit(commit4, bob.channelKeys, logger).right!! assertEquals(bc8.availableBalanceForSend(), b + p) assertEquals(bc8.availableBalanceForReceive(), a - p) @@ -134,9 +127,6 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { fun `correct values for availableForSend - availableForReceive -- failure case`() { val (alice, bob) = reachNormal(aliceFundingAmount = 800_000.sat, bobFundingAmount = 200_000.sat) - val aliceKeys = alice.ctx.keyManager.channelKeys(alice.commitments.params.localParams.fundingKeyPath) - val bobKeys = bob.ctx.keyManager.channelKeys(bob.commitments.params.localParams.fundingKeyPath) - val a = 774_660_000.msat // initial balance alice val b = 190_000_000.msat // initial balance bob val p = 42_000_000.msat // a->b payment @@ -161,11 +151,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc1.availableBalanceForSend(), b) assertEquals(bc1.availableBalanceForReceive(), a - p - htlcOutputFee) - val (ac2, commit1) = ac1.sendCommit(aliceKeys, logger).right!! + val (ac2, commit1) = ac1.sendCommit(alice.channelKeys, logger).right!! assertEquals(ac2.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac2.availableBalanceForReceive(), b) - val (bc2, revocation1) = bc1.receiveCommit(commit1, bobKeys, logger).right!! + val (bc2, revocation1) = bc1.receiveCommit(commit1, bob.channelKeys, logger).right!! assertEquals(bc2.availableBalanceForSend(), b) assertEquals(bc2.availableBalanceForReceive(), a - p - htlcOutputFee) @@ -173,11 +163,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac3.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac3.availableBalanceForReceive(), b) - val (bc3, commit2) = bc2.sendCommit(bobKeys, logger).right!! + val (bc3, commit2) = bc2.sendCommit(bob.channelKeys, logger).right!! assertEquals(bc3.availableBalanceForSend(), b) assertEquals(bc3.availableBalanceForReceive(), a - p - htlcOutputFee) - val (ac4, revocation2) = ac3.receiveCommit(commit2, aliceKeys, logger).right!! + val (ac4, revocation2) = ac3.receiveCommit(commit2, alice.channelKeys, logger).right!! assertEquals(ac4.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac4.availableBalanceForReceive(), b) @@ -194,11 +184,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac5.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac5.availableBalanceForReceive(), b) - val (bc6, commit3) = bc5.sendCommit(bobKeys, logger).right!! + val (bc6, commit3) = bc5.sendCommit(bob.channelKeys, logger).right!! assertEquals(bc6.availableBalanceForSend(), b) assertEquals(bc6.availableBalanceForReceive(), a - p - htlcOutputFee) - val (ac6, revocation3) = ac5.receiveCommit(commit3, aliceKeys, logger).right!! + val (ac6, revocation3) = ac5.receiveCommit(commit3, alice.channelKeys, logger).right!! assertEquals(ac6.availableBalanceForSend(), a) assertEquals(ac6.availableBalanceForReceive(), b) @@ -206,11 +196,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc7.availableBalanceForSend(), b) assertEquals(bc7.availableBalanceForReceive(), a) - val (ac7, commit4) = ac6.sendCommit(aliceKeys, logger).right!! + val (ac7, commit4) = ac6.sendCommit(alice.channelKeys, logger).right!! assertEquals(ac7.availableBalanceForSend(), a) assertEquals(ac7.availableBalanceForReceive(), b) - val (bc8, revocation4) = bc7.receiveCommit(commit4, bobKeys, logger).right!! + val (bc8, revocation4) = bc7.receiveCommit(commit4, bob.channelKeys, logger).right!! assertEquals(bc8.availableBalanceForSend(), b) assertEquals(bc8.availableBalanceForReceive(), a) @@ -223,9 +213,6 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { fun `correct values for availableForSend - availableForReceive -- multiple htlcs`() { val (alice, bob) = reachNormal(aliceFundingAmount = 800_000.sat, bobFundingAmount = 200_000.sat) - val aliceKeys = alice.ctx.keyManager.channelKeys(alice.commitments.params.localParams.fundingKeyPath) - val bobKeys = bob.ctx.keyManager.channelKeys(bob.commitments.params.localParams.fundingKeyPath) - val a = 774_660_000.msat // initial balance alice val b = 190_000_000.msat // initial balance bob val p1 = 18_000_000.msat // a->b payment @@ -271,11 +258,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac3.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee) assertEquals(ac3.availableBalanceForReceive(), b - p3) - val (ac4, commit1) = ac3.sendCommit(aliceKeys, logger).right!! + val (ac4, commit1) = ac3.sendCommit(alice.channelKeys, logger).right!! assertEquals(ac4.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee) assertEquals(ac4.availableBalanceForReceive(), b - p3) - val (bc4, revocation1) = bc3.receiveCommit(commit1, bobKeys, logger).right!! + val (bc4, revocation1) = bc3.receiveCommit(commit1, bob.channelKeys, logger).right!! assertEquals(bc4.availableBalanceForSend(), b - p3) assertEquals(bc4.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee) @@ -283,11 +270,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac5.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee) assertEquals(ac5.availableBalanceForReceive(), b - p3) - val (bc5, commit2) = bc4.sendCommit(bobKeys, logger).right!! + val (bc5, commit2) = bc4.sendCommit(bob.channelKeys, logger).right!! assertEquals(bc5.availableBalanceForSend(), b - p3) assertEquals(bc5.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee) - val (ac6, revocation2) = ac5.receiveCommit(commit2, aliceKeys, logger).right!! + val (ac6, revocation2) = ac5.receiveCommit(commit2, alice.channelKeys, logger).right!! assertEquals(ac6.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) // alice has acknowledged b's hltc so it needs to pay the fee for it assertEquals(ac6.availableBalanceForReceive(), b - p3) @@ -295,11 +282,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc6.availableBalanceForSend(), b - p3) assertEquals(bc6.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) - val (ac7, commit3) = ac6.sendCommit(aliceKeys, logger).right!! + val (ac7, commit3) = ac6.sendCommit(alice.channelKeys, logger).right!! assertEquals(ac7.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) assertEquals(ac7.availableBalanceForReceive(), b - p3) - val (bc7, revocation3) = bc6.receiveCommit(commit3, bobKeys, logger).right!! + val (bc7, revocation3) = bc6.receiveCommit(commit3, bob.channelKeys, logger).right!! assertEquals(bc7.availableBalanceForSend(), b - p3) assertEquals(bc7.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) @@ -334,11 +321,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc10.availableBalanceForSend(), b + p1 - p3) assertEquals(bc10.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) // the fee for p3 disappears - val (ac12, commit4) = ac11.sendCommit(aliceKeys, logger).right!! + val (ac12, commit4) = ac11.sendCommit(alice.channelKeys, logger).right!! assertEquals(ac12.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) assertEquals(ac12.availableBalanceForReceive(), b + p1 - p3) - val (bc11, revocation4) = bc10.receiveCommit(commit4, bobKeys, logger).right!! + val (bc11, revocation4) = bc10.receiveCommit(commit4, bob.channelKeys, logger).right!! assertEquals(bc11.availableBalanceForSend(), b + p1 - p3) assertEquals(bc11.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) @@ -346,11 +333,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac13.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) assertEquals(ac13.availableBalanceForReceive(), b + p1 - p3) - val (bc12, commit5) = bc11.sendCommit(bobKeys, logger).right!! + val (bc12, commit5) = bc11.sendCommit(bob.channelKeys, logger).right!! assertEquals(bc12.availableBalanceForSend(), b + p1 - p3) assertEquals(bc12.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) - val (ac14, revocation5) = ac13.receiveCommit(commit5, aliceKeys, logger).right!! + val (ac14, revocation5) = ac13.receiveCommit(commit5, alice.channelKeys, logger).right!! assertEquals(ac14.availableBalanceForSend(), a - p1 + p3) assertEquals(ac14.availableBalanceForReceive(), b + p1 - p3) @@ -358,11 +345,11 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc13.availableBalanceForSend(), b + p1 - p3) assertEquals(bc13.availableBalanceForReceive(), a - p1 + p3) - val (ac15, commit6) = ac14.sendCommit(aliceKeys, logger).right!! + val (ac15, commit6) = ac14.sendCommit(alice.channelKeys, logger).right!! assertEquals(ac15.availableBalanceForSend(), a - p1 + p3) assertEquals(ac15.availableBalanceForReceive(), b + p1 - p3) - val (bc14, revocation6) = bc13.receiveCommit(commit6, bobKeys, logger).right!! + val (bc14, revocation6) = bc13.receiveCommit(commit6, bob.channelKeys, logger).right!! assertEquals(bc14.availableBalanceForSend(), b + p1 - p3) assertEquals(bc14.availableBalanceForReceive(), a - p1 + p3) @@ -414,95 +401,28 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { } } - @Test - fun `find timed out htlcs`() { - val (alice, bob, timedOutHtlcs) = run { - val (alice0, bob0) = reachNormal() - // We have two identical HTLCs (MPP): - val (nodes1, _, htlcAlice1a) = TestsHelper.addHtlc(50_000_000.msat, payer = alice0, payee = bob0) - val (alice1, bob1) = nodes1 - val cmdAddAlice = ChannelCommand.Htlc.Add(htlcAlice1a.amountMsat, htlcAlice1a.paymentHash, htlcAlice1a.cltvExpiry, htlcAlice1a.onionRoutingPacket, UUID.randomUUID()) - val (alice2, bob2, htlcAlice1b) = TestsHelper.addHtlc(cmdAddAlice, alice1, bob1) - val (nodes3, preimageAlice2, htlcAlice2) = TestsHelper.addHtlc(60_000_000.msat, payer = alice2, payee = bob2) - val (alice3, bob3) = nodes3 - val (alice4, bob4) = TestsHelper.crossSign(alice3, bob3) - // We have two identical HTLCs (MPP): - val (nodes5, _, htlcBob1a) = TestsHelper.addHtlc(15_000_000.msat, payer = bob4, payee = alice4) - val (bob5, alice5) = nodes5 - val cmdAddBob = ChannelCommand.Htlc.Add(htlcBob1a.amountMsat, htlcBob1a.paymentHash, htlcBob1a.cltvExpiry, htlcBob1a.onionRoutingPacket, UUID.randomUUID()) - val (bob6, alice6, htlcBob1b) = TestsHelper.addHtlc(cmdAddBob, bob5, alice5) - val (nodes7, preimageBob2, htlcBob2) = TestsHelper.addHtlc(20_000_000.msat, payer = bob6, payee = alice6) - val (bob7, alice7) = nodes7 - val (bob8, alice8) = TestsHelper.crossSign(bob7, alice7) - // Alice and Bob both know the preimage for only one of the two HTLCs they received. - val (alice9, _) = alice8.process(ChannelCommand.Htlc.Settlement.Fulfill(htlcBob2.id, preimageBob2)) - val (bob9, _) = bob8.process(ChannelCommand.Htlc.Settlement.Fulfill(htlcAlice2.id, preimageAlice2)) - // Alice publishes her commitment. - val (aliceClosing, _) = alice9.process(ChannelCommand.Close.ForceClose) - assertIs>(aliceClosing) - val lcp = aliceClosing.state.localCommitPublished - assertNotNull(lcp) - val (bobClosing, _) = bob9.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.state.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), lcp.commitTx))) - assertIs>(bobClosing) - val rcp = bobClosing.state.remoteCommitPublished - assertNotNull(rcp) - Triple(aliceClosing, bobClosing, listOf(htlcAlice1a, htlcAlice1b, htlcAlice2, htlcBob1a, htlcBob1b, htlcBob2)) - } - - val lcp = alice.state.localCommitPublished!! - val localCommit = alice.state.commitments.latest.localCommit - val rcp = bob.state.remoteCommitPublished!! - val remoteCommit = bob.state.commitments.latest.remoteCommit - val dustLimit = TestConstants.Alice.nodeParams.dustLimit - val htlcTimeoutTxs = lcp.htlcTimeoutTxs() - val htlcSuccessTxs = lcp.htlcSuccessTxs() - val claimHtlcTimeoutTxs = rcp.claimHtlcTimeoutTxs() - val claimHtlcSuccessTxs = rcp.claimHtlcSuccessTxs() - - val aliceTimedOutHtlcs = htlcTimeoutTxs.map { htlcTimeout -> - val htlcs = trimmedOrTimedOutHtlcs(localCommit, lcp, dustLimit, htlcTimeout.tx) - assertEquals(1, htlcs.size) - htlcs.first() - } - assertEquals(timedOutHtlcs.take(3).toSet(), aliceTimedOutHtlcs.toSet()) - - val bobTimedOutHtlcs = claimHtlcTimeoutTxs.map { claimHtlcTimeout -> - val htlcs = trimmedOrTimedOutHtlcs(remoteCommit, rcp, dustLimit, claimHtlcTimeout.tx) - assertEquals(1, htlcs.size) - htlcs.first() - } - assertEquals(timedOutHtlcs.drop(3).toSet(), bobTimedOutHtlcs.toSet()) - - htlcSuccessTxs.forEach { htlcSuccess -> assertTrue(trimmedOrTimedOutHtlcs(localCommit, lcp, dustLimit, htlcSuccess.tx).isEmpty()) } - htlcSuccessTxs.forEach { htlcSuccess -> assertTrue(trimmedOrTimedOutHtlcs(remoteCommit, rcp, dustLimit, htlcSuccess.tx).isEmpty()) } - claimHtlcSuccessTxs.forEach { claimHtlcSuccess -> assertTrue(trimmedOrTimedOutHtlcs(localCommit, lcp, dustLimit, claimHtlcSuccess.tx).isEmpty()) } - claimHtlcSuccessTxs.forEach { claimHtlcSuccess -> assertTrue(trimmedOrTimedOutHtlcs(remoteCommit, rcp, dustLimit, claimHtlcSuccess.tx).isEmpty()) } - htlcTimeoutTxs.forEach { htlcTimeout -> assertTrue(trimmedOrTimedOutHtlcs(remoteCommit, rcp, dustLimit, htlcTimeout.tx).isEmpty()) } - claimHtlcTimeoutTxs.forEach { claimHtlcTimeout -> assertTrue(trimmedOrTimedOutHtlcs(localCommit, lcp, dustLimit, claimHtlcTimeout.tx).isEmpty()) } - } - companion object { 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, isInitiator, ByteVector.empty, Features.empty + val localChannelParams = LocalChannelParams( + randomKey().publicKey(), KeyPath("42"), isInitiator, isInitiator, ByteVector.empty, Features.empty ) - val remoteParams = RemoteParams( - randomKey().publicKey(), dustLimit, Long.MAX_VALUE, 1.msat, CltvExpiryDelta(144), 50, + val remoteChannelParams = RemoteChannelParams( + randomKey().publicKey(), randomKey().publicKey(), randomKey().publicKey(), randomKey().publicKey(), randomKey().publicKey(), Features.empty ) + val commitParams = CommitParams(dustLimit, Long.MAX_VALUE, 1.msat, CltvExpiryDelta(144), 50) val fundingAmount = (toLocal + toRemote).truncateToSatoshi() val dummyFundingScript = Scripts.multiSig2of2(randomKey().publicKey(), randomKey().publicKey()) val dummyFundingTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 1), 0)), listOf(TxOut(fundingAmount, Script.pay2wsh(dummyFundingScript))), 0) - val commitmentInput = Transactions.InputInfo(OutPoint(dummyFundingTx, 0), dummyFundingTx.txOut[0], dummyFundingScript) - val localCommitTx = Transactions.TransactionWithInputInfo.CommitTx(commitmentInput, Transaction(2, listOf(), listOf(), 0)) + val commitmentInput = Transactions.InputInfo(OutPoint(dummyFundingTx, 0), dummyFundingTx.txOut[0]) return Commitments( ChannelParams( channelId = randomBytes32(), channelConfig = ChannelConfig.standard, channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features), - localParams = localParams, - remoteParams = remoteParams, + localParams = localChannelParams, + remoteParams = remoteChannelParams, channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), ), CommitmentChanges( @@ -514,10 +434,15 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { active = listOf( Commitment( fundingTxIndex = 0, + fundingInput = commitmentInput.outPoint, + fundingAmount = fundingAmount, remoteFundingPubkey = randomKey().publicKey(), - LocalFundingStatus.ConfirmedFundingTx(dummyFundingTx, 500.sat, TxSignatures(randomBytes32(), TxId(randomBytes32()), listOf()), ShortChannelId(1729)), + LocalFundingStatus.ConfirmedFundingTx(listOf(), dummyFundingTx.txOut[0], 500.sat, TxSignatures(randomBytes32(), dummyFundingTx.txid, listOf()), ShortChannelId(1729)), RemoteFundingStatus.Locked, - LocalCommit(0, CommitmentSpec(setOf(), feeRatePerKw, toLocal, toRemote), PublishableTxs(localCommitTx, listOf())), + Transactions.CommitmentFormat.AnchorOutputs, + commitParams, + LocalCommit(0, CommitmentSpec(setOf(), feeRatePerKw, toLocal, toRemote), TxId(randomBytes32()), ChannelSpendSignature.IndividualSignature(randomBytes64()), listOf()), + commitParams, RemoteCommit(0, CommitmentSpec(setOf(), feeRatePerKw, toRemote, toLocal), TxId(randomBytes32()), randomKey().publicKey()), nextRemoteCommit = null, ) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index bdcb59eb8..2e5aa9997 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -75,7 +75,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val sharedTxA = receiveFinalMessage(alice7, txCompleteB2).second assertNotNull(sharedTxA.txComplete) - val (bob8, sharedTxB) = receiveFinalMessage(bob7, sharedTxA.txComplete!!) + val (bob8, sharedTxB) = receiveFinalMessage(bob7, sharedTxA.txComplete) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared output. @@ -180,7 +180,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_complete --> Bob val (alice5, sharedTxA) = receiveFinalMessage(alice4, txCompleteB) assertNotNull(sharedTxA.txComplete) - val (bob5, sharedTxB) = receiveFinalMessage(bob4, sharedTxA.txComplete!!) + val (bob5, sharedTxB) = receiveFinalMessage(bob4, sharedTxA.txComplete) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared output. @@ -250,7 +250,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_complete --> Bob val (alice5, sharedTxA) = receiveFinalMessage(alice4, txCompleteB) assertNotNull(sharedTxA.txComplete) - val (bob5, sharedTxB) = receiveFinalMessage(bob4, sharedTxA.txComplete!!) + val (bob5, sharedTxB) = receiveFinalMessage(bob4, sharedTxA.txComplete) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared output. @@ -311,7 +311,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_complete --> Bob val sharedTxA = receiveFinalMessage(alice5, txCompleteB4).second assertNotNull(sharedTxA.txComplete) - val (bob6, sharedTxB) = receiveFinalMessage(bob5, sharedTxA.txComplete!!) + val (bob6, sharedTxB) = receiveFinalMessage(bob5, sharedTxA.txComplete) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared output. @@ -374,7 +374,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice <-- tx_complete --- Bob val (bob3, sharedTxB) = receiveFinalMessage(bob2, txCompleteA2) assertNotNull(sharedTxB.txComplete) - val (alice4, sharedTxA) = receiveFinalMessage(alice3, sharedTxB.txComplete!!) + val (alice4, sharedTxA) = receiveFinalMessage(alice3, sharedTxB.txComplete) assertNull(sharedTxA.txComplete) // Alice cannot pay on-chain fees because she doesn't have inputs to contribute. @@ -427,11 +427,11 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_complete --> Bob val (alice5, sharedTxA) = receiveFinalMessage(alice4, txCompleteB) assertNotNull(sharedTxA.txComplete) - val (bob5, sharedTxB) = receiveFinalMessage(bob4, sharedTxA.txComplete!!) + val (bob5, sharedTxB) = receiveFinalMessage(bob4, sharedTxA.txComplete) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared input and the shared output. - assertEquals(listOf(inputA1, inputA2).count { it.sharedInput == f.fundingParamsA.sharedInput?.info?.outPoint }, 1) + assertEquals(listOf(inputA1, inputA2).count { it.sharedInput == f.fundingParamsA.sharedInput.info.outPoint }, 1) assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 200_000.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 129_999_400.msat) @@ -439,11 +439,11 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxA.sharedTx.totalAmountIn, 315_000.sat) assertNotNull(sharedTxA.sharedTx.sharedInput) - assertEquals(sharedTxA.sharedTx.localFees, 957_000.msat) + assertEquals(sharedTxA.sharedTx.localFees, 953_000.msat) assertEquals(sharedTxA.sharedTx.remoteFees, 357_000.msat) assertNotNull(sharedTxB.sharedTx.sharedInput) assertEquals(sharedTxB.sharedTx.localFees, 357_000.msat) - assertEquals(sharedTxB.sharedTx.remoteFees, 957_000.msat) + assertEquals(sharedTxB.sharedTx.remoteFees, 953_000.msat) // Bob sends signatures first as he contributed less than Alice. val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) @@ -500,12 +500,12 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_complete --> Bob val sharedTxA = receiveFinalMessage(alice3, txCompleteB).second assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete!!).second + val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete).second assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared input and the shared output. assertNull(inputA.previousTx) - assertEquals(inputA.sharedInput, f.fundingParamsA.sharedInput?.info?.outPoint) + assertEquals(inputA.sharedInput, f.fundingParamsA.sharedInput.info.outPoint) assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 108_500.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 48_999_700.msat) @@ -581,12 +581,12 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_complete --> Bob val sharedTxA = receiveFinalMessage(alice5, txCompleteB).second assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob5, sharedTxA.txComplete!!).second + val sharedTxB = receiveFinalMessage(bob5, sharedTxA.txComplete).second assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared input and the shared output. assertNull(inputA.previousTx) - assertEquals(inputA.sharedInput, f.fundingParamsA.sharedInput?.info?.outPoint) + assertEquals(inputA.sharedInput, f.fundingParamsA.sharedInput.info.outPoint) assertEquals(listOf(outputA1, outputA2, outputA3, outputA4).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 158_500.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 99_000_825.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 59_499_175.msat) @@ -659,22 +659,22 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_complete --> Bob val sharedTxA = receiveFinalMessage(alice5, txCompleteB).second assertNotNull(sharedTxA.txComplete) - val (bob6, sharedTxB) = receiveFinalMessage(bob5, sharedTxA.txComplete!!) + val (bob6, sharedTxB) = receiveFinalMessage(bob5, sharedTxA.txComplete) assertNull(sharedTxB.txComplete) // Alice is responsible for adding the shared input and the shared output. - assertEquals(listOf(inputA1, inputA2).count { it.sharedInput == f.fundingParamsA.sharedInput?.info?.outPoint }, 1) + assertEquals(listOf(inputA1, inputA2).count { it.sharedInput == f.fundingParamsA.sharedInput.info.outPoint }, 1) assertEquals(listOf(outputA1, outputA2, outputA3).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 290_000.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 174_000_333.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 115_999_667.msat) assertEquals(sharedTxA.sharedTx.totalAmountIn, 375_000.sat) assertNotNull(sharedTxA.sharedTx.sharedInput) - assertEquals(sharedTxA.sharedTx.localFees, 1_081_000.msat) + assertEquals(sharedTxA.sharedTx.localFees, 1_077_000.msat) assertEquals(sharedTxA.sharedTx.remoteFees, 481_000.msat) assertNotNull(sharedTxB.sharedTx.sharedInput) assertEquals(sharedTxB.sharedTx.localFees, 481_000.msat) - assertEquals(sharedTxB.sharedTx.remoteFees, 1_081_000.msat) + assertEquals(sharedTxB.sharedTx.remoteFees, 1_077_000.msat) // Bob sends signatures first as he did not contribute. val signedTxB = sharedTxB.sharedTx.sign(bob6, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) @@ -726,7 +726,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice <-- tx_complete --- Bob val (bob3, sharedTxB) = receiveFinalMessage(bob2, txCompleteA) assertNotNull(sharedTxB.txComplete) - val (alice4, sharedTxA) = receiveFinalMessage(alice3, sharedTxB.txComplete!!) + val (alice4, sharedTxA) = receiveFinalMessage(alice3, sharedTxB.txComplete) // Alice signs first since she didn't contribute. val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB) @@ -784,7 +784,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val swapInKeys = TestConstants.Alice.keyManager.swapInOnChainWallet val privKey = randomKey() val pubKey = privKey.publicKey() - val fundingParams = InteractiveTxParams(randomBytes32(), true, 150_000.sat, 50_000.sat, pubKey, 0, 660.sat, FeeratePerKw(2500.sat)) + val fundingParams = InteractiveTxParams(randomBytes32(), true, 150_000.sat, 50_000.sat, pubKey, 0, 660.sat, Transactions.CommitmentFormat.AnchorOutputs, FeeratePerKw(2500.sat)) run { val previousTx = Transaction(2, listOf(), listOf(TxOut(293.sat, Script.pay2wpkh(pubKey))), 0) val result = FundingContributions.create(channelKeys, swapInKeys, fundingParams, listOf(WalletState.Utxo(previousTx.txid, 0, 0, previousTx, WalletState.AddressMeta.Single)), null).left @@ -816,7 +816,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val channelKeys = TestConstants.Alice.keyManager.run { channelKeys(newFundingKeyPath(isChannelOpener = true)) } val swapInKeys = TestConstants.Alice.keyManager.swapInOnChainWallet val walletKey = randomKey().publicKey() - val fundingParams = InteractiveTxParams(randomBytes32(), true, 0.sat, 250_000.sat, walletKey, 0, 660.sat, FeeratePerKw(2500.sat)) + val fundingParams = InteractiveTxParams(randomBytes32(), true, 0.sat, 250_000.sat, walletKey, 0, 660.sat, Transactions.CommitmentFormat.AnchorOutputs, FeeratePerKw(2500.sat)) val fees = LiquidityAds.Fees(3000.sat, 2000.sat) val paymentDetails = LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(listOf(randomBytes32())) run { @@ -1041,7 +1041,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_complete --> Bob val sharedTxA = receiveFinalMessage(alice3, txCompleteB).second assertNotNull(sharedTxA.txComplete) - val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete!!).second + val sharedTxB = receiveFinalMessage(bob3, sharedTxA.txComplete).second assertNull(sharedTxB.txComplete) // Alice didn't send her user key, so Bob thinks there aren't any swap inputs @@ -1271,12 +1271,12 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val channelId: ByteVector32, val keyManagerA: KeyManager, val channelKeysA: ChannelKeys, - val localParamsA: LocalParams, + val localParamsA: LocalChannelParams, val fundingParamsA: InteractiveTxParams, val fundingContributionsA: FundingContributions, val keyManagerB: KeyManager, val channelKeysB: ChannelKeys, - val localParamsB: LocalParams, + val localParamsB: LocalChannelParams, val fundingParamsB: InteractiveTxParams, val fundingContributionsB: FundingContributions ) { @@ -1295,6 +1295,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { dustLimit: Satoshi, lockTime: Long, nonInitiatorPaysCommitFees: Boolean = false, + commitmentFormat: Transactions.CommitmentFormat = Transactions.CommitmentFormat.AnchorOutputs, ): Fixture { val channelId = randomBytes32() val fundingTxIndex = 0L @@ -1306,8 +1307,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val swapInKeysB = TestConstants.Bob.keyManager.swapInOnChainWallet val fundingPubkeyA = channelKeysA.fundingKey(fundingTxIndex).publicKey() val fundingPubkeyB = channelKeysB.fundingKey(fundingTxIndex).publicKey() - val fundingParamsA = InteractiveTxParams(channelId, true, fundingAmountA, fundingAmountB, fundingPubkeyB, lockTime, dustLimit, targetFeerate) - val fundingParamsB = InteractiveTxParams(channelId, false, fundingAmountB, fundingAmountA, fundingPubkeyA, lockTime, dustLimit, targetFeerate) + val fundingParamsA = InteractiveTxParams(channelId, true, fundingAmountA, fundingAmountB, fundingPubkeyB, lockTime, dustLimit, commitmentFormat, targetFeerate) + val fundingParamsB = InteractiveTxParams(channelId, false, fundingAmountB, fundingAmountA, fundingPubkeyA, lockTime, dustLimit, commitmentFormat, targetFeerate) val walletA = createWallet(swapInKeysA, utxosA, legacyUtxosA) val contributionsA = FundingContributions.create(channelKeysA, swapInKeysA, fundingParamsA, null, walletA, listOf(), null, randomKey().publicKey()) assertNotNull(contributionsA.right) @@ -1328,6 +1329,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { dustLimit: Satoshi, lockTime: Long, nonInitiatorPaysCommitFees: Boolean = false, + commitmentFormat: Transactions.CommitmentFormat = Transactions.CommitmentFormat.AnchorOutputs, ): Either { val channelId = randomBytes32() val localParamsA = TestConstants.Alice.channelParams(payCommitTxFees = !nonInitiatorPaysCommitFees) @@ -1341,10 +1343,10 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingScript = Script.write(Script.pay2wsh(redeemScript)).byteVector() val previousFundingAmount = (balanceA + balanceB).truncateToSatoshi() val previousFundingTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(previousFundingAmount, fundingScript)), 0) - val inputInfo = Transactions.InputInfo(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0], redeemScript) - val sharedInputA = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, fundingPubkeyB) + val inputInfo = Transactions.InputInfo(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0]) + val sharedInputA = SharedFundingInput(inputInfo, fundingTxIndex, fundingPubkeyB, commitmentFormat) val nextFundingPubkeyB = channelKeysB.fundingKey(fundingTxIndex + 1).publicKey() - val fundingParamsA = InteractiveTxParams(channelId, true, fundingContributionA, fundingContributionB, sharedInputA, nextFundingPubkeyB, outputsA, lockTime, dustLimit, targetFeerate) + val fundingParamsA = InteractiveTxParams(channelId, true, fundingContributionA, fundingContributionB, sharedInputA, nextFundingPubkeyB, outputsA, commitmentFormat, lockTime, dustLimit, targetFeerate) return FundingContributions.create(channelKeysA, swapInKeysA, fundingParamsA, Pair(sharedInputA, SharedFundingInputBalances(balanceA, balanceB, 0.msat)), listOf(), outputsA, null, randomKey().publicKey()) } @@ -1361,6 +1363,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { dustLimit: Satoshi, lockTime: Long, nonInitiatorPaysCommitFees: Boolean = false, + commitmentFormat: Transactions.CommitmentFormat = Transactions.CommitmentFormat.AnchorOutputs, ): Fixture { val channelId = randomBytes32() val fundingTxIndex = 0L @@ -1376,13 +1379,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingScript = Script.write(Script.pay2wsh(redeemScript)).byteVector() val previousFundingAmount = (balanceA + balanceB).truncateToSatoshi() val previousFundingTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(previousFundingAmount, fundingScript)), 0) - val inputInfo = Transactions.InputInfo(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0], redeemScript) - val sharedInputA = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, fundingPubkeyB) - val sharedInputB = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, fundingPubkeyA) + val inputInfo = Transactions.InputInfo(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0]) + val sharedInputA = SharedFundingInput(inputInfo, fundingTxIndex, fundingPubkeyB, commitmentFormat) + val sharedInputB = SharedFundingInput(inputInfo, fundingTxIndex, fundingPubkeyA, commitmentFormat) val nextFundingPubkeyA = channelKeysA.fundingKey(fundingTxIndex + 1).publicKey() val nextFundingPubkeyB = channelKeysB.fundingKey(fundingTxIndex + 1).publicKey() - val fundingParamsA = InteractiveTxParams(channelId, true, fundingContributionA, fundingContributionB, sharedInputA, nextFundingPubkeyB, outputsA, lockTime, dustLimit, targetFeerate) - val fundingParamsB = InteractiveTxParams(channelId, false, fundingContributionB, fundingContributionA, sharedInputB, nextFundingPubkeyA, outputsB, lockTime, dustLimit, targetFeerate) + val fundingParamsA = InteractiveTxParams(channelId, true, fundingContributionA, fundingContributionB, sharedInputA, nextFundingPubkeyB, outputsA, commitmentFormat, lockTime, dustLimit, targetFeerate) + val fundingParamsB = InteractiveTxParams(channelId, false, fundingContributionB, fundingContributionA, sharedInputB, nextFundingPubkeyA, outputsB, commitmentFormat, lockTime, dustLimit, targetFeerate) val walletA = createWallet(swapInKeysA, utxosA) val contributionsA = FundingContributions.create(channelKeysA, swapInKeysA, fundingParamsA, Pair(sharedInputA, SharedFundingInputBalances(balanceA, balanceB, 0.msat)), walletA, outputsA, null, randomKey().publicKey()) assertNotNull(contributionsA.right) @@ -1396,14 +1399,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val (sender1, action1) = sender.send() assertIs(action1) assertIs(action1.msg) - return Pair(sender1, action1.msg as M) + return Pair(sender1, action1.msg) } private inline fun receiveMessage(receiver: InteractiveTxSession, msg: InteractiveTxConstructionMessage): Pair { val (receiver1, action1) = receiver.receive(msg) assertIs(action1) assertIs(action1.msg) - return Pair(receiver1, action1.msg as M) + return Pair(receiver1, action1.msg) } private fun receiveFinalMessage(receiver: InteractiveTxSession, msg: TxComplete): Pair { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt index 5888936e9..2e05db717 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt @@ -1,19 +1,22 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* +import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.crypto.CommitmentPublicKeys import fr.acinq.lightning.crypto.LocalKeyManager +import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.utils.toByteVector32 import kotlin.test.Test import kotlin.test.assertNotEquals import kotlin.test.assertTrue class RecoveryTestsCommon { + @Test fun `use funding pubkeys from published commitment to spend our output`() { // Alice creates and uses a LN channel to Bob @@ -39,25 +42,28 @@ class RecoveryTestsCommon { val keyManager = LocalKeyManager(seed, Chain.Regtest, TestConstants.aliceSwapInServerXpub) // recompute our channel keys from the extracted funding pubkey and see if we can find and spend our output + // we only need our payment key and basepoint for our main output fun findAndSpend(fundingKey: PublicKey): Transaction? { val channelKeys = keyManager.recoverChannelKeys(fundingKey) - val localPaymentPoint = channelKeys.paymentBasepoint - val mainTx = Transactions.makeClaimRemoteDelayedOutputTx( + val commitKeys = RemoteCommitmentKeys( + ourPaymentKey = channelKeys.paymentKey, + theirDelayedPaymentPublicKey = randomKey().publicKey(), + ourPaymentBasePoint = channelKeys.paymentBasepoint, + ourHtlcKey = randomKey(), + theirHtlcPublicKey = randomKey().publicKey(), + revocationPublicKey = randomKey().publicKey() + ) + val finalScript = Script.write(Script.pay2wpkh(fundingKey)).byteVector() + val mainTx = Transactions.ClaimRemoteDelayedOutputTx.createUnsignedTx( + commitKeys, commitTx, TestConstants.Bob.nodeParams.dustLimit, - localPaymentPoint, - Script.write(Script.pay2wpkh(fundingKey)).toByteVector(), - FeeratePerKw(Satoshi(750)) - ) - return when (mainTx) { - is Transactions.TxResult.Success -> { - val sig = Transactions.sign(mainTx.result, channelKeys.paymentKey) - val signedTx = Transactions.addSigs(mainTx.result, sig).tx - Transaction.correctlySpends(signedTx, commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - signedTx - } - else -> null - } + finalScript, + FeeratePerKw(750.sat()), + Transactions.CommitmentFormat.AnchorOutputs + ).map { it.sign().tx }.right + mainTx?.let { Transaction.correctlySpends(it, commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + return mainTx } // this is the script of the output that we're spending @@ -69,9 +75,17 @@ class RecoveryTestsCommon { // this is what our main output script should be fun ourDelayedOutputScript(pub: PublicKey): List { val channelKeys = keyManager.recoverChannelKeys(pub) - return Script.pay2wsh(Scripts.toRemoteDelayed(channelKeys.paymentBasepoint)) + val commitKeys = CommitmentPublicKeys( + localDelayedPaymentPublicKey = randomKey().publicKey(), + remotePaymentPublicKey = channelKeys.paymentBasepoint, + localHtlcPublicKey = randomKey().publicKey(), + remoteHtlcPublicKey = randomKey().publicKey(), + revocationPublicKey = randomKey().publicKey() + ) + return Script.pay2wsh(Scripts.toRemoteDelayed(commitKeys)) } assertTrue(outputScript == ourDelayedOutputScript(pub1) || outputScript == ourDelayedOutputScript(pub2)) } + } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index d5c812a26..d26ccc4cb 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -11,6 +11,7 @@ import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.channel.states.* +import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.db.ChannelCloseOutgoingPayment.ChannelClosingType import fr.acinq.lightning.json.JsonSerializers @@ -38,7 +39,9 @@ internal inline fun List.hasOutgoi internal inline fun List.findWatches(): List = filterIsInstance().map { it.watch }.filterIsInstance() internal inline fun List.findWatch(): T = findWatches().firstOrNull() ?: fail("cannot find watch ${T::class}") internal inline fun List.hasWatch() = assertNotNull(findWatches().firstOrNull(), "cannot find watch ${T::class}") -internal fun List.hasWatchFundingSpent(txId: TxId): WatchSpent = hasWatch().also { assertEquals(txId, it.txId); assertIs(it.event) } +internal fun List.hasWatchFundingSpent(txId: TxId): WatchSpent = findWatches().firstOrNull { it.event is WatchSpent.ChannelSpent }?.also { assertEquals(txId, it.txId) } ?: fail("cannot find watch-funding-spent") +internal fun List.hasWatchOutputSpent(outpoint: OutPoint): WatchSpent = findWatches().firstOrNull { it.event is WatchSpent.ClosingOutputSpent && it.txId == outpoint.txid && it.outputIndex.toLong() == outpoint.index } ?: fail("cannot find watch-output-spent") +internal fun List.hasWatchOutputsSpent(outpoints: Set): Set = outpoints.map { hasWatchOutputSpent(it) }.toSet() internal fun List.hasWatchConfirmed(txId: TxId): WatchConfirmed = assertNotNull(findWatches().firstOrNull { it.txId == txId }) // Commands @@ -50,6 +53,7 @@ internal inline fun List.hasCommand( // Transactions internal fun List.findPublishTxs(): List = filterIsInstance().map { it.tx } internal fun List.hasPublishTx(tx: Transaction) = assertContains(findPublishTxs(), tx) +internal fun List.findPublishTxs(txType: ChannelAction.Blockchain.PublishTx.Type): List = filterIsInstance().filter { it.txType == txType }.map { it.tx } internal fun List.hasPublishTx(txType: ChannelAction.Blockchain.PublishTx.Type): Transaction = assertNotNull(filterIsInstance().firstOrNull { it.txType == txType }).tx internal inline fun List.findCommandErrorOpt(): T? { @@ -91,6 +95,41 @@ data class LNChannel( else -> error("no commitments in state ${state::class}") } } + val channelKeys: ChannelKeys by lazy { + when { + state is WaitForFundingSigned -> state.channelParams.localParams.channelKeys(ctx.keyManager) + state is Offline && state.state is WaitForFundingSigned -> state.state.channelParams.localParams.channelKeys(ctx.keyManager) + state is Syncing && state.state is WaitForFundingSigned -> state.state.channelParams.localParams.channelKeys(ctx.keyManager) + state is ChannelStateWithCommitments -> state.commitments.channelParams.localParams.channelKeys(ctx.keyManager) + state is Offline && state.state is ChannelStateWithCommitments -> state.state.commitments.channelParams.localParams.channelKeys(ctx.keyManager) + state is Syncing && state.state is ChannelStateWithCommitments -> state.state.commitments.channelParams.localParams.channelKeys(ctx.keyManager) + else -> error("no channel keys in state ${state::class}") + } + } + + fun signCommitTx(): Transaction = commitments.latest.fullySignedCommitTx(channelKeys) + + fun unsignedHtlcTxs(): List = commitments.latest.unsignedHtlcTxs(channelKeys).map { it.first } + + fun signHtlcTimeoutTxs(): List { + val commitKeys = commitments.latest.localKeys(channelKeys) + return commitments.latest.unsignedHtlcTxs(channelKeys).mapNotNull { (htlcTx, remoteSig) -> + when (htlcTx) { + is Transactions.HtlcSuccessTx -> null + is Transactions.HtlcTimeoutTx -> htlcTx.sign(commitKeys, remoteSig) + } + } + } + + fun signHtlcSuccessTxs(preimages: Set): List { + val commitKeys = commitments.latest.localKeys(channelKeys) + return commitments.latest.unsignedHtlcTxs(channelKeys).mapNotNull { (htlcTx, remoteSig) -> + when (htlcTx) { + is Transactions.HtlcSuccessTx -> preimages.find { it.sha256() == htlcTx.paymentHash }?.let { p -> htlcTx.sign(commitKeys, remoteSig, p) } + is Transactions.HtlcTimeoutTx -> null + } + } + } fun process(cmd: ChannelCommand): Pair, List> = runBlocking { state @@ -201,6 +240,11 @@ object TestsHelper { FeeratePerKw.CommitmentFeerate, TestConstants.feeratePerKw, aliceChannelParams, + TestConstants.Alice.nodeParams.dustLimit, + TestConstants.Alice.nodeParams.htlcMinimum, + TestConstants.Alice.nodeParams.maxHtlcValueInFlightMsat, + TestConstants.Alice.nodeParams.maxAcceptedHtlcs, + TestConstants.Alice.nodeParams.toRemoteDelayBlocks, bobInit, channelFlags, ChannelConfig.standard, @@ -224,6 +268,11 @@ object TestsHelper { bobFundingAmount, bobWallet, bobChannelParams, + TestConstants.Bob.nodeParams.dustLimit, + TestConstants.Bob.nodeParams.htlcMinimum, + TestConstants.Bob.nodeParams.maxHtlcValueInFlightMsat, + TestConstants.Bob.nodeParams.maxAcceptedHtlcs, + TestConstants.Bob.nodeParams.toRemoteDelayBlocks, ChannelConfig.standard, aliceInit, TestConstants.fundingRates @@ -244,7 +293,7 @@ object TestsHelper { bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, requestRemoteFunding: Satoshi? = null, zeroConf: Boolean = false, - ): Triple, LNChannel, Transaction> { + ): Triple, LNChannel, TxId> { val (alice, channelReadyAlice, bob, channelReadyBob) = WaitForChannelReadyTestsCommon.init( channelType, aliceFeatures, @@ -262,11 +311,11 @@ object TestsHelper { val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(channelReadyAlice)) assertIs>(bob1) actionsBob1.has() - val fundingTx = when (val fundingStatus = alice.commitments.latest.localFundingStatus) { - is LocalFundingStatus.UnconfirmedFundingTx -> fundingStatus.sharedTx.tx.buildUnsignedTx() - is LocalFundingStatus.ConfirmedFundingTx -> fundingStatus.signedTx + val fundingTxId = when (val fundingStatus = alice.commitments.latest.localFundingStatus) { + is LocalFundingStatus.UnconfirmedFundingTx -> fundingStatus.sharedTx.txId + is LocalFundingStatus.ConfirmedFundingTx -> fundingStatus.txId } - return Triple(alice1, bob1, fundingTx) + return Triple(alice1, bob1, fundingTxId) } suspend fun mutualCloseAlice(alice: LNChannel, bob: LNChannel, closingFeerate: FeeratePerKw, scriptPubKey: ByteVector? = null): Triple, LNChannel, Transaction> { @@ -293,8 +342,7 @@ object TestsHelper { assertIs>(alice3) val closingTx = actionsAlice3.findPublishTxs().first() actionsAlice3.hasWatchConfirmed(closingTx.txid) - val commitInput = alice1.commitments.latest.commitInput - Transaction.correctlySpends(closingTx, mapOf(commitInput.outPoint to commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(closingTx, mapOf(alice1.commitments.latest.fundingInput to alice1.commitments.latest.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) assertEquals(ChannelCloseResponse.Success(closingTx.txid, closingComplete.fees), cmd.replyTo.await()) return Triple(alice3, bob2, closingTx) } @@ -323,68 +371,83 @@ object TestsHelper { assertIs>(bob3) val closingTx = actionsBob3.findPublishTxs().first() actionsBob3.hasWatchConfirmed(closingTx.txid) - val commitInput = alice1.commitments.latest.commitInput - Transaction.correctlySpends(closingTx, mapOf(commitInput.outPoint to commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(closingTx, mapOf(alice1.commitments.latest.fundingInput to alice1.commitments.latest.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) assertEquals(ChannelCloseResponse.Success(closingTx.txid, closingComplete.fees), cmd.replyTo.await()) return Triple(alice2, bob3, closingTx) } - fun localClose(s: LNChannel): Pair, LocalCommitPublished> { + data class LocalCloseTxs(val mainTx: Transaction, val htlcSuccessTxs: List, val htlcTimeoutTxs: List) + + fun localClose(s: LNChannel, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): Triple, LocalCommitPublished, LocalCloseTxs> { assertIs>(s) - assertContains(s.state.commitments.params.channelFeatures.features, Feature.AnchorOutputs) - // an error occurs and s publishes their commit tx - val commitTx = s.state.commitments.latest.localCommit.publishableTxs.commitTx.tx + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, s.state.commitments.latest.commitmentFormat) + // An error occurs and we publish our commit tx. + val commitTxId = s.state.commitments.latest.localCommit.txId val (s1, actions1) = s.process(ChannelCommand.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) assertIs>(s1) actions1.has() actions1.find().also { - assertEquals(commitTx.txid, it.txId) + assertEquals(commitTxId, it.txId) assertEquals(ChannelClosingType.Local, it.closingType) } val localCommitPublished = s1.state.localCommitPublished assertNotNull(localCommitPublished) - assertEquals(commitTx, localCommitPublished.commitTx) - actions1.hasPublishTx(commitTx) - assertNotNull(localCommitPublished.claimMainDelayedOutputTx) - actions1.hasPublishTx(localCommitPublished.claimMainDelayedOutputTx.tx) - Transaction.correctlySpends(localCommitPublished.claimMainDelayedOutputTx.tx, localCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - // all htlcs success/timeout should be published - localCommitPublished.htlcTxs.values.filterNotNull().forEach { htlcTx -> - Transaction.correctlySpends(htlcTx.tx, localCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - actions1.hasPublishTx(htlcTx.tx) - } - // and their outputs should be claimed - localCommitPublished.claimHtlcDelayedTxs.forEach { claimHtlcDelayed -> actions1.hasPublishTx(claimHtlcDelayed.tx) } - - // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - val expectedWatchConfirmed = buildSet { - add(localCommitPublished.commitTx.txid) - add(localCommitPublished.claimMainDelayedOutputTx.tx.txid) - addAll(localCommitPublished.claimHtlcDelayedTxs.map { it.tx.txid }) - } + assertEquals(commitTxId, localCommitPublished.commitTx.txid) + // It may be strictly greater if we don't have the preimage for some of our received HTLCs, or if we haven't fulfilled them yet. + assertTrue(localCommitPublished.incomingHtlcs.size >= htlcSuccessCount) + assertEquals(htlcTimeoutCount, localCommitPublished.outgoingHtlcs.size) + // We're not claiming the outputs of htlc txs yet. + assertTrue(localCommitPublished.htlcDelayedOutputs.isEmpty()) + actions1.hasPublishTx(localCommitPublished.commitTx) + Transaction.correctlySpends(localCommitPublished.commitTx, mapOf(s.commitments.latest.fundingInput to s.commitments.latest.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assertNotNull(localCommitPublished.localOutput) + val mainTx = actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) + Transaction.correctlySpends(mainTx, localCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcSuccessTxs = actions1.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcSuccessTx) + assertEquals(htlcSuccessCount, htlcSuccessTxs.size) + val htlcTimeoutTxs = actions1.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx) + assertEquals(htlcTimeoutCount, htlcTimeoutTxs.size) + (htlcSuccessTxs + htlcTimeoutTxs).forEach { htlcTx -> Transaction.correctlySpends(htlcTx, localCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + + // We watch the confirmation of the commitment transaction. val watchConfirmed = actions1.findWatches() watchConfirmed.forEach { assertEquals(WatchConfirmed.ClosingTxConfirmed, it.event) } - assertEquals(expectedWatchConfirmed, watchConfirmed.map { it.txId }.toSet()) + assertEquals(setOf(commitTxId), watchConfirmed.map { it.txId }.toSet()) - // we watch outputs of the commitment tx that both parties may spend + // We watch outputs of the commitment tx that we want to claim. val watchSpent = actions1.findWatches() watchSpent.forEach { watch -> assertIs(watch.event) - assertEquals(watch.txId, commitTx.txid) + assertEquals(watch.txId, commitTxId) + } + val watchedOutputs = watchSpent.map { w -> OutPoint(w.txId, w.outputIndex.toLong()) }.toSet() + assertTrue(watchedOutputs.contains(localCommitPublished.localOutput)) + assertTrue(watchedOutputs.containsAll(localCommitPublished.htlcOutputs)) + + // Once our closing transactions are published, we watch for their confirmation. + var closingState: LNChannel = s1 + (listOf(mainTx) + htlcSuccessTxs + htlcTimeoutTxs).forEach { tx -> + val event = WatchSpent.ClosingOutputSpent(tx.txOut.first().amount) + val (s2, actions2) = closingState.process(ChannelCommand.WatchReceived(WatchSpentTriggered(closingState.channelId, event, tx))) + assertIs>(s2) + actions2.hasWatchConfirmed(tx.txid) + closingState = s2 } - assertEquals(localCommitPublished.htlcTxs.keys, watchSpent.map { OutPoint(commitTx, it.outputIndex.toLong()) }.toSet()) - return s1 to localCommitPublished + return Triple(closingState, localCommitPublished, LocalCloseTxs(mainTx, htlcSuccessTxs, htlcTimeoutTxs)) } - fun remoteClose(rCommitTx: Transaction, s: LNChannel): Pair, RemoteCommitPublished> { + data class RemoteCloseTxs(val mainTx: Transaction, val htlcSuccessTxs: List, val htlcTimeoutTxs: List) + + fun remoteClose(rCommitTx: Transaction, s: LNChannel, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): Triple, RemoteCommitPublished, RemoteCloseTxs> { assertIs>(s) - assertContains(s.state.commitments.params.channelFeatures.features, Feature.AnchorOutputs) - // we make s believe r unilaterally closed the channel + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, s.state.commitments.latest.commitmentFormat) + // Our peer has unilaterally closed the channel. val (s1, actions1) = s.process(ChannelCommand.WatchReceived(WatchSpentTriggered(s.state.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), rCommitTx))) assertIs>(s1) + // If we're transitioning to the closing state, we store that as an outgoing (on-chain) payment. if (s.state !is Closing) { val channelBalance = s.state.commitments.latest.localCommit.spec.toLocal if (channelBalance > 0.msat) { @@ -398,59 +461,68 @@ object TestsHelper { val remoteCommitPublished = s1.state.remoteCommitPublished ?: s1.state.nextRemoteCommitPublished ?: s1.state.futureRemoteCommitPublished assertNotNull(remoteCommitPublished) assertNull(s1.state.localCommitPublished) + // It may be strictly greater if we don't have the preimage for some of our received HTLCs, or if we haven't fulfilled them yet. + assertTrue(remoteCommitPublished.incomingHtlcs.size >= htlcSuccessCount) + assertEquals(htlcTimeoutCount, remoteCommitPublished.outgoingHtlcs.size) + assertNotNull(remoteCommitPublished.localOutput) + val mainTx = actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + Transaction.correctlySpends(mainTx, rCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcSuccessTxs = actions1.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcSuccessTx) + assertEquals(htlcSuccessCount, htlcSuccessTxs.size) + val htlcTimeoutTxs = actions1.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcTimeoutTx) + assertEquals(htlcTimeoutCount, htlcTimeoutTxs.size) + (htlcSuccessTxs + htlcTimeoutTxs).forEach { htlcTx -> Transaction.correctlySpends(htlcTx, rCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + + // We watch the confirmation of the remote commitment transaction. + val watchConfirmed = actions1.findWatches() + watchConfirmed.forEach { assertEquals(WatchConfirmed.ClosingTxConfirmed, it.event) } + assertEquals(setOf(rCommitTx.txid), watchConfirmed.map { it.txId }.toSet()) - // if s has a main output in the commit tx (when it has a non-dust balance), it should be claimed - remoteCommitPublished.claimMainOutputTx?.let { claimMain -> - Transaction.correctlySpends(claimMain.tx, rCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - actions1.hasPublishTx(claimMain.tx) - } - // all htlcs success/timeout should be claimed - remoteCommitPublished.claimHtlcTxs.values.filterNotNull().forEach { claimHtlc -> - Transaction.correctlySpends(claimHtlc.tx, rCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - actions1.hasPublishTx(claimHtlc.tx) - } - - // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - val watchConfirmedList = actions1.findWatches() - watchConfirmedList.forEach { assertEquals(WatchConfirmed.ClosingTxConfirmed, it.event) } - assertEquals(rCommitTx.txid, watchConfirmedList.first().txId) - remoteCommitPublished.claimMainOutputTx?.let { claimMain -> - assertEquals(claimMain.tx.txid, watchConfirmedList.drop(1).first().txId) - } - - // we watch outputs of the commitment tx that both parties may spend + // We watch outputs of the commitment tx that we want to claim. val watchSpent = actions1.findWatches() watchSpent.forEach { watch -> assertIs(watch.event) assertEquals(watch.txId, rCommitTx.txid) } - assertEquals(remoteCommitPublished.claimHtlcTxs.keys, watchSpent.map { OutPoint(rCommitTx, it.outputIndex.toLong()) }.toSet()) + val watchedOutputs = watchSpent.map { w -> OutPoint(w.txId, w.outputIndex.toLong()) }.toSet() + assertTrue(watchedOutputs.contains(remoteCommitPublished.localOutput)) + assertTrue(watchedOutputs.containsAll(remoteCommitPublished.htlcOutputs)) + + // Once our closing transactions are published, we watch for their confirmation. + var closingState: LNChannel = s1 + (listOf(mainTx) + htlcSuccessTxs + htlcTimeoutTxs).forEach { tx -> + val event = WatchSpent.ClosingOutputSpent(tx.txOut.first().amount) + val (s2, actions2) = closingState.process(ChannelCommand.WatchReceived(WatchSpentTriggered(closingState.channelId, event, tx))) + assertIs>(s2) + actions2.hasWatchConfirmed(tx.txid) + closingState = s2 + } - // s is now in CLOSING state with txs pending for confirmation before going in CLOSED state - return s1 to remoteCommitPublished + return Triple(closingState, remoteCommitPublished, RemoteCloseTxs(mainTx, htlcSuccessTxs, htlcTimeoutTxs)) } fun useAlternativeCommitSig(s: LNChannel, commitment: Commitment, alternative: CommitSigTlv.AlternativeFeerateSig): Transaction { - val channelKeys = s.commitments.params.localParams.channelKeys(s.ctx.keyManager) - val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - val commitKeys = channelKeys.localCommitmentKeys(s.commitments.params, commitment.localCommit.index) + val channelKeys = s.commitments.channelKeys(s.ctx.keyManager) + val fundingKey = commitment.localFundingKey(channelKeys) + val commitKeys = channelKeys.localCommitmentKeys(s.commitments.channelParams, commitment.localCommit.index) val alternativeSpec = commitment.localCommit.spec.copy(feerate = alternative.feerate) - val fundingTxIndex = commitment.fundingTxIndex - val commitInput = commitment.commitInput + val alternativeSig = ChannelSpendSignature.IndividualSignature(alternative.sig) val remoteFundingPubKey = commitment.remoteFundingPubkey val (localCommitTx, _) = Commitments.makeLocalTxs( - channelParams = s.commitments.params, + channelParams = s.commitments.channelParams, + commitParams = commitment.localCommitParams, commitKeys = commitKeys, commitTxNumber = commitment.localCommit.index, localFundingKey = fundingKey, remoteFundingPubKey = remoteFundingPubKey, - commitmentInput = commitInput, - spec = alternativeSpec + commitmentInput = commitment.commitInput(fundingKey), + commitmentFormat = commitment.commitmentFormat, + spec = alternativeSpec, ) - val localSig = Transactions.sign(localCommitTx, channelKeys.fundingKey(fundingTxIndex)) - val signedCommitTx = Transactions.addSigs(localCommitTx, fundingKey.publicKey(), remoteFundingPubKey, localSig, alternative.sig) - assertTrue(Transactions.checkSpendable(signedCommitTx).isSuccess) - return signedCommitTx.tx + val localSig = localCommitTx.sign(fundingKey, remoteFundingPubKey) + val signedCommitTx = localCommitTx.aggregateSigs(fundingKey.publicKey(), remoteFundingPubKey, localSig, alternativeSig) + Transaction.correctlySpends(signedCommitTx, mapOf(commitment.fundingInput to commitment.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + return signedCommitTx } fun signAndRevack(alice: LNChannel, bob: LNChannel): Pair, LNChannel> { @@ -589,20 +661,4 @@ object TestsHelper { } } - fun LocalCommitPublished.htlcSuccessTxs(): List { - return htlcTxs.values.filterIsInstance() - } - - fun LocalCommitPublished.htlcTimeoutTxs(): List { - return htlcTxs.values.filterIsInstance() - } - - fun RemoteCommitPublished.claimHtlcSuccessTxs(): List { - return claimHtlcTxs.values.filterIsInstance() - } - - fun RemoteCommitPublished.claimHtlcTimeoutTxs(): List { - return claimHtlcTxs.values.filterIsInstance() - } - } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt index d683cc358..1c408ed8b 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt @@ -2,7 +2,6 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* import fr.acinq.lightning.CltvExpiryDelta -import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchConfirmedTriggered @@ -10,13 +9,9 @@ import fr.acinq.lightning.blockchain.WatchSpent import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.TestsHelper.addHtlc -import fr.acinq.lightning.channel.TestsHelper.claimHtlcSuccessTxs -import fr.acinq.lightning.channel.TestsHelper.claimHtlcTimeoutTxs import fr.acinq.lightning.channel.TestsHelper.crossSign import fr.acinq.lightning.channel.TestsHelper.failHtlc import fr.acinq.lightning.channel.TestsHelper.fulfillHtlc -import fr.acinq.lightning.channel.TestsHelper.htlcSuccessTxs -import fr.acinq.lightning.channel.TestsHelper.htlcTimeoutTxs import fr.acinq.lightning.channel.TestsHelper.localClose import fr.acinq.lightning.channel.TestsHelper.makeCmdAdd import fr.acinq.lightning.channel.TestsHelper.reachNormal @@ -25,6 +20,7 @@ import fr.acinq.lightning.channel.TestsHelper.useAlternativeCommitSig import fr.acinq.lightning.db.ChannelCloseOutgoingPayment.ChannelClosingType import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat @@ -132,68 +128,73 @@ class ClosingTestsCommon : LightningTestSuite() { @Test fun `recv ClosingTxConfirmed -- local commit`() { val (alice0, bob0) = reachNormal() - val (aliceClosing, localCommitPublished, htlcs) = run { - // alice sends an htlc to bob - val (nodes1, _, htlc1) = addHtlc(50_000_000.msat, alice0, bob0) - val (alice1, bob1) = nodes1 - // alice sends an htlc below dust to bob - val amountBelowDust = alice0.state.commitments.params.localParams.dustLimit.toMilliSatoshi() - 100.msat - val (nodes2, _, htlc2) = addHtlc(amountBelowDust, alice1, bob1) - val (alice2, bob2) = nodes2 - val (alice3, _) = crossSign(alice2, bob2) - val (aliceClosing, localCommitPublished) = localClose(alice3) - Triple(aliceClosing, localCommitPublished, setOf(htlc1, htlc2)) - } + // alice sends an htlc to bob + val (nodes1, _, htlc1) = addHtlc(50_000_000.msat, alice0, bob0) + val (alice1, bob1) = nodes1 + // alice sends an htlc below dust to bob + val amountBelowDust = alice0.state.commitments.latest.localCommitParams.dustLimit.toMilliSatoshi() - 100.msat + val (nodes2, _, htlc2) = addHtlc(amountBelowDust, alice1, bob1) + val (alice2, bob2) = nodes2 + val (alice3, _) = crossSign(alice2, bob2) + val (aliceClosing, localCommitPublished, closingTxs) = localClose(alice3, htlcTimeoutCount = 1) // actual test starts here - assertNotNull(localCommitPublished.claimMainDelayedOutputTx) - assertTrue(localCommitPublished.htlcSuccessTxs().isEmpty()) - assertEquals(1, localCommitPublished.htlcTimeoutTxs().size) - assertEquals(1, localCommitPublished.claimHtlcDelayedTxs.size) - - val watchConfirmed = listOf( - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, localCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 200, 0, localCommitPublished.claimMainDelayedOutputTx.tx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 201, 0, localCommitPublished.htlcTimeoutTxs().first().tx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 202, 0, localCommitPublished.claimHtlcDelayedTxs.first().tx) - ) - - var alice = aliceClosing - val addSettledActions = watchConfirmed.dropLast(1).flatMap { - val (aliceNew, actions) = alice.process(ChannelCommand.WatchReceived(it)) - assertIs>(aliceNew) - assertTrue(actions.contains(ChannelAction.Storage.StoreState(aliceNew.state))) - alice = aliceNew - actions.filterIsInstance() - } - - // We notify the payment handler that the htlcs have been failed. - assertEquals(2, addSettledActions.size) - val addSettledFail = addSettledActions.filterIsInstance() - assertEquals(htlcs, addSettledFail.map { it.htlc }.toSet()) - assertTrue(addSettledFail.all { it.result is ChannelAction.HtlcResult.Fail.OnChainFail }) - - val irrevocablySpent = setOf(localCommitPublished.commitTx, localCommitPublished.claimMainDelayedOutputTx.tx, localCommitPublished.htlcTimeoutTxs().first().tx) - assertEquals(irrevocablySpent, alice.state.localCommitPublished!!.irrevocablySpent.values.toSet()) - - val (aliceClosed, actions) = alice.process(ChannelCommand.WatchReceived(watchConfirmed.last())) - assertIs(aliceClosed.state) - assertEquals( - listOf(ChannelAction.Storage.StoreState(aliceClosed.state)), - actions.filterIsInstance() - ) - assertContains(actions, ChannelAction.Storage.SetLocked(localCommitPublished.commitTx.txid)) + assertNotNull(localCommitPublished.localOutput) + assertEquals(1, localCommitPublished.htlcOutputs.size) + assertEquals(1, closingTxs.htlcTimeoutTxs.size) + assertTrue(localCommitPublished.htlcDelayedOutputs.isEmpty()) + + // The commit tx confirms. + val (aliceClosing1, actions1) = aliceClosing.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 6, localCommitPublished.commitTx))) + assertIs>(aliceClosing1) + assertEquals(2, actions1.size) + actions1.has() + assertEquals(setOf(htlc2), actions1.filterIsInstance().map { it.htlc }.toSet()) + actions1.filterIsInstance().forEach { assertIs(it.result) } + assertNotNull(aliceClosing1.state.localCommitPublished) + assertTrue(aliceClosing1.state.localCommitPublished.isConfirmed) + assertFalse(aliceClosing1.state.localCommitPublished.isDone) + + // Our main transaction confirms. + val (aliceClosing2, actions2) = aliceClosing1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 45, 0, closingTxs.mainTx))) + assertIs>(aliceClosing2) + assertEquals(1, actions2.size) + actions2.has() + + // Our HTLC-timeout transaction confirms. + val (aliceClosing3, actions3) = aliceClosing2.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 45, 1, closingTxs.htlcTimeoutTxs.first()))) + assertIs>(aliceClosing3) + assertEquals(4, actions3.size) + actions3.has() + assertEquals(setOf(htlc1), actions3.filterIsInstance().map { it.htlc }.toSet()) + actions3.filterIsInstance().forEach { assertIs(it.result) } + val htlcDelayedTx = actions3.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.HtlcDelayedTx) + assertNotNull(aliceClosing3.state.localCommitPublished) + assertEquals(setOf(htlcDelayedTx.txIn.first().outPoint), aliceClosing3.state.localCommitPublished.htlcDelayedOutputs) + assertEquals(setOf(localCommitPublished.commitTx, closingTxs.mainTx, closingTxs.htlcTimeoutTxs.first()), aliceClosing3.state.localCommitPublished.irrevocablySpent.values.toSet()) + assertFalse(aliceClosing3.state.localCommitPublished.isDone) + actions3.hasWatchOutputSpent(htlcDelayedTx.txIn.first().outPoint) + + // Our HTLC-delayed transaction confirms. + val (aliceClosing4, actions4) = aliceClosing3.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(50_000.sat), htlcDelayedTx))) + assertIs>(aliceClosing3) + assertEquals(1, actions4.size) + actions4.hasWatchConfirmed(htlcDelayedTx.txid) + val (aliceClosing5, actions5) = aliceClosing4.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 51, 113, htlcDelayedTx))) + assertIs(aliceClosing5.state) + assertEquals(2, actions5.size) + actions5.has() + assertContains(actions5, ChannelAction.Storage.SetLocked(localCommitPublished.commitTx.txid)) } @Test fun `recv ClosingTxConfirmed -- local commit -- non-initiator pays commit fees`() { val (alice0, bob0) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) - assertFalse(alice0.commitments.params.localParams.paysCommitTxFees) - assertTrue(bob0.commitments.params.localParams.paysCommitTxFees) - val (alice1, localCommitPublished) = localClose(alice0) + assertFalse(alice0.commitments.channelParams.localParams.paysCommitTxFees) + assertTrue(bob0.commitments.channelParams.localParams.paysCommitTxFees) + val (alice1, localCommitPublished, closingTxs) = localClose(alice0) val (alice2, _) = alice1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 7, localCommitPublished.commitTx))) - val claimMain = localCommitPublished.claimMainDelayedOutputTx!!.tx - val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 3, claimMain))) + val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 3, closingTxs.mainTx))) assertIs(alice3.state) assertEquals(2, actions3.size) actions3.has() @@ -204,7 +205,7 @@ class ClosingTestsCommon : LightningTestSuite() { fun `recv ClosingTxConfirmed -- local commit with multiple htlcs for the same payment`() { val (alice0, bob0) = reachNormal() // alice sends an htlc to bob - val (aliceClosing, localCommitPublished) = run { + val (aliceClosing, localCommitPublished, closingTxs) = run { val (nodes1, preimage, _) = addHtlc(30_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 // and more htlcs with the same payment_hash @@ -212,41 +213,36 @@ class ClosingTestsCommon : LightningTestSuite() { val (alice2, bob2, _) = addHtlc(cmd2, alice1, bob1) val (_, cmd3) = makeCmdAdd(30_000_000.msat, bob0.staticParams.nodeParams.nodeId, alice2.currentBlockHeight.toLong(), preimage) val (alice3, bob3, _) = addHtlc(cmd3, alice2, bob2) - val amountBelowDust = alice0.state.commitments.params.localParams.dustLimit.toMilliSatoshi() - 100.msat + val amountBelowDust = alice0.state.commitments.latest.localCommitParams.dustLimit.toMilliSatoshi() - 100.msat val (_, dustCmd) = makeCmdAdd(amountBelowDust, bob0.staticParams.nodeParams.nodeId, alice3.currentBlockHeight.toLong(), preimage) val (alice4, bob4, _) = addHtlc(dustCmd, alice3, bob3) val (_, cmd4) = makeCmdAdd(20_000_000.msat, bob0.staticParams.nodeParams.nodeId, alice4.currentBlockHeight.toLong() + 1, preimage) val (alice5, bob5, _) = addHtlc(cmd4, alice4, bob4) val (alice6, _) = crossSign(alice5, bob5) - localClose(alice6) + localClose(alice6, htlcTimeoutCount = 4) } // actual test starts here - assertNotNull(localCommitPublished.claimMainDelayedOutputTx) - assertTrue(localCommitPublished.htlcSuccessTxs().isEmpty()) - assertEquals(4, localCommitPublished.htlcTimeoutTxs().size) - assertEquals(4, localCommitPublished.claimHtlcDelayedTxs.size) + assertNotNull(localCommitPublished.localOutput) + assertTrue(closingTxs.htlcSuccessTxs.isEmpty()) + assertEquals(4, closingTxs.htlcTimeoutTxs.size) // if commit tx and htlc-timeout txs end up in the same block, we may receive the htlc-timeout confirmation before the commit tx confirmation val watchConfirmed = listOf( - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, localCommitPublished.htlcTimeoutTxs()[2].tx), + WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, closingTxs.htlcTimeoutTxs[2]), WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, localCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 200, 0, localCommitPublished.claimMainDelayedOutputTx.tx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 202, 0, localCommitPublished.htlcTimeoutTxs()[1].tx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 203, 2, localCommitPublished.claimHtlcDelayedTxs[2].tx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 202, 1, localCommitPublished.htlcTimeoutTxs()[0].tx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 203, 0, localCommitPublished.claimHtlcDelayedTxs[0].tx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 203, 1, localCommitPublished.claimHtlcDelayedTxs[1].tx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 203, 0, localCommitPublished.htlcTimeoutTxs()[3].tx), - WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 203, 3, localCommitPublished.claimHtlcDelayedTxs[3].tx) + WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 200, 0, closingTxs.mainTx), + WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 202, 0, closingTxs.htlcTimeoutTxs[1]), + WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 202, 1, closingTxs.htlcTimeoutTxs[0]), + WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 203, 0, closingTxs.htlcTimeoutTxs[3]), ) - confirmWatchedTxs(aliceClosing, watchConfirmed) + confirmClosingTxs(aliceClosing, watchConfirmed) } @Test fun `recv ClosingTxConfirmed -- local commit with htlcs only signed by local`() { val (alice0, bob0) = reachNormal() - val aliceCommitTx = alice0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val aliceCommitTx = alice0.signCommitTx() val (aliceClosing, localCommitPublished, add) = run { // alice sends an htlc to bob val (nodes1, _, add) = addHtlc(50_000_000.msat, alice0, bob0) @@ -259,8 +255,7 @@ class ClosingTestsCommon : LightningTestSuite() { } assertEquals(aliceCommitTx, localCommitPublished.commitTx) - assertTrue(localCommitPublished.htlcTimeoutTxs().isEmpty()) - assertTrue(localCommitPublished.htlcSuccessTxs().isEmpty()) + assertTrue(localCommitPublished.htlcOutputs.isEmpty()) val (alice1, actions1) = aliceClosing.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, aliceCommitTx))) assertIs(alice1.state) @@ -279,7 +274,7 @@ class ClosingTestsCommon : LightningTestSuite() { // Bob sends an htlc to Alice. val (nodes1, r, htlc) = addHtlc(110_000_000.msat, bob0, alice0) val (bob1, alice1) = crossSign(nodes1.first, nodes1.second) - val aliceCommitTx = alice1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val aliceCommitTx = alice1.signCommitTx() assertEquals(5, aliceCommitTx.txOut.size) // 2 main outputs + 2 anchors + 1 htlc // Alice fulfills the HTLC but Bob doesn't receive the signature. @@ -295,53 +290,40 @@ class ClosingTestsCommon : LightningTestSuite() { } // Then we make Alice unilaterally close the channel. - val (_, localCommitPublished) = localClose(alice2) + val (_, localCommitPublished) = localClose(alice2, htlcSuccessCount = 1) assertEquals(aliceCommitTx.txid, localCommitPublished.commitTx.txid) - assertTrue(localCommitPublished.htlcTimeoutTxs().isEmpty()) - assertEquals(1, localCommitPublished.htlcSuccessTxs().size) + assertEquals(1, localCommitPublished.htlcOutputs.size) } @Test fun `recv ClosingTxConfirmed -- local commit -- followed by preimage`() { val (alice0, bob0) = reachNormal() - val (aliceClosing, localCommitPublished, fulfill) = run { - // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. - val (nodes1, preimage, htlc) = addHtlc(110_000_000.msat, bob0, alice0) - val (bob1, alice1) = nodes1 - // An HTLC Alice -> Bob is cross-signed and will timeout later. - val (nodes2, _, _) = addHtlc(95_000_000.msat, alice1, bob1) - val (alice2, bob2) = nodes2 - val (alice3, _) = crossSign(alice2, bob2) - val (aliceClosing, localCommitPublished) = localClose(alice3) - Triple(aliceClosing, localCommitPublished, ChannelCommand.Htlc.Settlement.Fulfill(htlc.id, preimage, commit = true)) - } - - assertNotNull(localCommitPublished.claimMainDelayedOutputTx) + // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. + val (nodes1, preimage, htlc) = addHtlc(110_000_000.msat, bob0, alice0) + val (bob1, alice1) = nodes1 + // An HTLC Alice -> Bob is cross-signed and will timeout later. + val (nodes2, _, _) = addHtlc(95_000_000.msat, alice1, bob1) + val (alice2, bob2) = nodes2 + val (alice3, _) = crossSign(alice2, bob2) + val (aliceClosing, localCommitPublished, closingTxs) = localClose(alice3, htlcTimeoutCount = 1) + assertNotNull(localCommitPublished.localOutput) // we don't have the preimage to claim the htlc-success yet - assertTrue(localCommitPublished.htlcSuccessTxs().isEmpty()) - assertEquals(1, localCommitPublished.htlcTimeoutTxs().size) - assertEquals(1, localCommitPublished.claimHtlcDelayedTxs.size) + assertTrue(closingTxs.htlcSuccessTxs.isEmpty()) + assertEquals(2, localCommitPublished.htlcOutputs.size) // Alice receives the preimage for the first HTLC from the payment handler; she can now claim the corresponding HTLC output. - val (aliceFulfill, actionsFulfill) = aliceClosing.process(fulfill) + val (aliceFulfill, actionsFulfill) = aliceClosing.process(ChannelCommand.Htlc.Settlement.Fulfill(htlc.id, preimage, commit = true)) assertIs>(aliceFulfill) - assertEquals(1, aliceFulfill.state.localCommitPublished!!.htlcSuccessTxs().size) - assertEquals(1, aliceFulfill.state.localCommitPublished.htlcTimeoutTxs().size) - assertEquals(2, aliceFulfill.state.localCommitPublished.claimHtlcDelayedTxs.size) - val htlcSuccess = aliceFulfill.state.localCommitPublished.htlcSuccessTxs().first() - actionsFulfill.hasPublishTx(htlcSuccess.tx) - assertTrue(actionsFulfill.findWatches().map { Pair(it.txId, it.outputIndex.toLong()) }.contains(Pair(localCommitPublished.commitTx.txid, htlcSuccess.input.outPoint.index))) - Transaction.correctlySpends(htlcSuccess.tx, localCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcSuccessTx = actionsFulfill.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.HtlcSuccessTx) + Transaction.correctlySpends(htlcSuccessTx, localCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val watchConfirmed = listOf( WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, localCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 0, localCommitPublished.htlcTimeoutTxs()[0].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 1, htlcSuccess.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 215, 1, aliceFulfill.state.localCommitPublished.claimHtlcDelayedTxs[0].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 215, 0, aliceFulfill.state.localCommitPublished.claimHtlcDelayedTxs[1].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 250, 0, localCommitPublished.claimMainDelayedOutputTx.tx) + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 0, closingTxs.htlcTimeoutTxs[0]), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 1, htlcSuccessTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 250, 0, closingTxs.mainTx) ) - confirmWatchedTxs(aliceFulfill, watchConfirmed) + confirmClosingTxs(aliceFulfill, watchConfirmed) } @Test @@ -368,10 +350,8 @@ class ClosingTestsCommon : LightningTestSuite() { Triple(aliceClosing, localCommitPublished, htlc) } - assertNotNull(localCommitPublished.claimMainDelayedOutputTx) - assertTrue(localCommitPublished.htlcSuccessTxs().isEmpty()) - assertTrue(localCommitPublished.htlcTimeoutTxs().isEmpty()) - assertTrue(localCommitPublished.claimHtlcDelayedTxs.isEmpty()) + assertNotNull(localCommitPublished.localOutput) + assertTrue(localCommitPublished.htlcOutputs.isEmpty()) val (alice1, actions1) = aliceClosing.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, localCommitPublished.commitTx))) assertIs(alice1.state) @@ -396,22 +376,19 @@ class ClosingTestsCommon : LightningTestSuite() { // Bob has the preimage for those HTLCs, but Alice force-closes before receiving it. val (bob4, actionsBob4) = bob3.process(ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, preimage)) actionsBob4.hasOutgoingMessage() // ignored - val (alice4, localCommitPublished) = localClose(alice3) - assertEquals(2, localCommitPublished.htlcTimeoutTxs().size) - assertTrue(localCommitPublished.htlcSuccessTxs().isEmpty()) - assertEquals(2, localCommitPublished.claimHtlcDelayedTxs.size) + val (alice4, localCommitPublished, closingTxs) = localClose(alice3, htlcTimeoutCount = 2) + assertEquals(2, localCommitPublished.htlcOutputs.size) val (_, actionsBob5) = bob4.process(ChannelCommand.WatchReceived(WatchSpentTriggered(bob0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), localCommitPublished.commitTx))) actionsBob5.has() val claimHtlcSuccessTxs = actionsBob5.filterIsInstance().filter { it.txType == ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcSuccessTx }.map { it.tx } assertEquals(2, claimHtlcSuccessTxs.size) - assertEquals(2, claimHtlcSuccessTxs.flatMap { it.txIn.map { it.outPoint } }.toSet().size) + assertEquals(2, claimHtlcSuccessTxs.flatMap { tx -> tx.txIn.map { it.outPoint } }.toSet().size) // Alice extracts the preimage and forwards it to the payment handler. val (alice5, actionsAlice5) = alice4.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(20_000.sat), claimHtlcSuccessTxs.first()))) - assertEquals(4, actionsAlice5.size) - actionsAlice5.has() - assertEquals(WatchConfirmed(alice0.channelId, claimHtlcSuccessTxs.first(), alice0.staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ClosingTxConfirmed), actionsAlice5.findWatch()) + assertEquals(3, actionsAlice5.size) + actionsAlice5.hasWatchConfirmed(claimHtlcSuccessTxs.first().txid) val addSettled = actionsAlice5.filterIsInstance() assertEquals(setOf(htlc1, htlc2), addSettled.map { it.htlc }.toSet()) assertEquals(setOf(ChannelAction.HtlcResult.Fulfill.OnChainFulfill(preimage)), addSettled.map { it.result }.toSet()) @@ -426,134 +403,128 @@ class ClosingTestsCommon : LightningTestSuite() { val watchConfirmed = listOf( WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 41, 0, localCommitPublished.commitTx), WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 0, claimHtlcSuccessTxs.last()), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 200, 0, localCommitPublished.claimMainDelayedOutputTx!!.tx) + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 200, 0, closingTxs.mainTx) ) - confirmWatchedTxs(alice6, watchConfirmed) + confirmClosingTxs(alice6, watchConfirmed) } @Test fun `recv ChannelEvent Restore -- local commit`() { val (alice0, bob0) = reachNormal() - val (aliceClosing, localCommitPublished) = run { + val (aliceClosing, localCommitPublished, closingTxs) = run { // alice sends an htlc to bob val (nodes1, _, _) = addHtlc(50_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 val (alice2, _) = crossSign(alice1, bob1) - val (aliceClosing, localCommitPublished) = localClose(alice2) - Pair(aliceClosing, localCommitPublished) + localClose(alice2, htlcTimeoutCount = 1) } - assertNotNull(localCommitPublished.claimMainDelayedOutputTx) - assertTrue(localCommitPublished.htlcSuccessTxs().isEmpty()) - assertEquals(1, localCommitPublished.htlcTimeoutTxs().size) - assertEquals(1, localCommitPublished.claimHtlcDelayedTxs.size) + assertNotNull(localCommitPublished.localOutput) + assertEquals(1, localCommitPublished.htlcOutputs.size) - // Simulate a wallet restart + // Simulate a wallet restart: we republish closing transactions. val initState = LNChannel(aliceClosing.ctx, WaitForInit) val (alice1, actions1) = initState.process(ChannelCommand.Init.Restore(aliceClosing.state)) assertIs(alice1.state) assertEquals(aliceClosing, alice1) + assertEquals(7, actions1.size) actions1.doesNotHave() + actions1.hasWatchFundingSpent(aliceClosing.commitments.latest.fundingInput.txid) + actions1.hasPublishTx(localCommitPublished.commitTx) + actions1.hasWatchConfirmed(localCommitPublished.commitTx.txid) + actions1.hasPublishTx(closingTxs.mainTx) + actions1.hasWatchOutputSpent(closingTxs.mainTx.txIn.first().outPoint) + actions1.hasPublishTx(closingTxs.htlcTimeoutTxs.first()) + actions1.hasWatchOutputSpent(closingTxs.htlcTimeoutTxs.first().txIn.first().outPoint) + + // Our HTLC-timeout transaction confirms: we publish an HTLC-delayed transaction. + val (alice2, actions2) = alice1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 0, 0, closingTxs.htlcTimeoutTxs.first()))) + assertIs(alice2.state) + val htlcDelayedTx = actions2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.HtlcDelayedTx) + actions2.hasWatchOutputSpent(htlcDelayedTx.txIn.first().outPoint) - // We should republish closing transactions - val txs = listOf( - localCommitPublished.commitTx, - localCommitPublished.claimMainDelayedOutputTx.tx, - localCommitPublished.htlcTimeoutTxs().first().tx, - localCommitPublished.claimHtlcDelayedTxs.first().tx, - ) - assertEquals(actions1.findPublishTxs(), txs) - val watchConfirmed = listOf( - localCommitPublished.commitTx.txid, - localCommitPublished.claimMainDelayedOutputTx.tx.txid, - localCommitPublished.claimHtlcDelayedTxs.first().tx.txid, - ) - assertEquals(actions1.findWatches().map { it.txId }, watchConfirmed) - val watchSpent = listOf( - localCommitPublished.commitTx.txIn.first().outPoint, - localCommitPublished.htlcTimeoutTxs().first().input.outPoint, - ) - assertEquals(actions1.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }, watchSpent) + // Sinmulate a wallet restart: we republish 3rd-stage transactions. + val (alice3, actions3) = initState.process(ChannelCommand.Init.Restore(alice2.state)) + assertIs(alice3.state) + assertEquals(alice2, alice3) + assertEquals(6, actions3.size) + actions3.hasPublishTx(localCommitPublished.commitTx) + actions3.hasWatchConfirmed(localCommitPublished.commitTx.txid) + actions3.hasPublishTx(closingTxs.mainTx) + actions3.hasWatchOutputSpent(closingTxs.mainTx.txIn.first().outPoint) + actions3.hasPublishTx(htlcDelayedTx) + actions3.hasWatchOutputSpent(htlcDelayedTx.txIn.first().outPoint) } @Test fun `recv ChannelSpent -- remote commit`() { val (alice0, bob0) = reachNormal() - val bobCommitTx = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob0.signCommitTx() assertEquals(4, bobCommitTx.txOut.size) // main outputs and anchors val (_, remoteCommitPublished) = remoteClose(bobCommitTx, alice0) - assertNotNull(remoteCommitPublished.claimMainOutputTx) - assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) - assertTrue(remoteCommitPublished.claimHtlcTimeoutTxs().isEmpty()) + assertNotNull(remoteCommitPublished.htlcOutputs) + assertTrue(remoteCommitPublished.htlcOutputs.isEmpty()) } @Test fun `recv ClosingTxConfirmed -- remote commit`() { val (alice0, bob0) = reachNormal() - val (aliceClosing, remoteCommitPublished, htlcs) = run { - // alice sends an htlc to bob - val (nodes1, _, htlc1) = addHtlc(50_000_000.msat, alice0, bob0) - val (alice1, bob1) = nodes1 - // alice sends an htlc below dust to bob - val amountBelowDust = alice0.commitments.params.localParams.dustLimit.toMilliSatoshi() - 100.msat - val (nodes2, _, htlc2) = addHtlc(amountBelowDust, alice1, bob1) - val (alice2, bob2) = nodes2 - val (alice3, bob3) = crossSign(alice2, bob2) - val (aliceClosing, remoteCommitPublished) = remoteClose(bob3.commitments.latest.localCommit.publishableTxs.commitTx.tx, alice3) - Triple(aliceClosing, remoteCommitPublished, listOf(htlc1, htlc2)) - } + // alice sends an htlc to bob + val (nodes1, _, htlc1) = addHtlc(50_000_000.msat, alice0, bob0) + val (alice1, bob1) = nodes1 + // alice sends an htlc below dust to bob + val amountBelowDust = alice0.commitments.latest.localCommitParams.dustLimit.toMilliSatoshi() - 100.msat + val (nodes2, _, htlc2) = addHtlc(amountBelowDust, alice1, bob1) + val (alice2, bob2) = nodes2 + val (alice3, bob3) = crossSign(alice2, bob2) + val (aliceClosing, remoteCommitPublished, closingTxs) = remoteClose(bob3.signCommitTx(), alice3, htlcTimeoutCount = 1) // actual test starts here - assertNotNull(remoteCommitPublished.claimMainOutputTx) - assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) - assertEquals(1, remoteCommitPublished.claimHtlcTimeoutTxs().size) + assertNotNull(remoteCommitPublished.localOutput) + assertEquals(1, remoteCommitPublished.htlcOutputs.size) - val watchConfirmed = listOf( - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, remoteCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, remoteCommitPublished.claimMainOutputTx.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 201, 0, remoteCommitPublished.claimHtlcTimeoutTxs().first().tx), - ) - - var alice = aliceClosing - val addSettledActions = watchConfirmed.dropLast(1).flatMap { - val (aliceNew, actions) = alice.process(ChannelCommand.WatchReceived(it)) - assertIs>(aliceNew) - assertTrue(actions.contains(ChannelAction.Storage.StoreState(aliceNew.state))) - alice = aliceNew - actions.filterIsInstance() - } - - // We notify the payment handler that the dust htlc has been failed. - assertEquals(1, addSettledActions.size) - val dustHtlcFail = addSettledActions.filterIsInstance().first() - assertEquals(htlcs[1], dustHtlcFail.htlc) - assertTrue(dustHtlcFail.result is ChannelAction.HtlcResult.Fail.OnChainFail) - - val irrevocablySpent = setOf(remoteCommitPublished.commitTx, remoteCommitPublished.claimMainOutputTx.tx) - assertEquals(irrevocablySpent, alice.state.remoteCommitPublished!!.irrevocablySpent.values.toSet()) - - val (aliceClosed, actions) = alice.process(ChannelCommand.WatchReceived(watchConfirmed.last())) - assertIs(aliceClosed.state) - assertTrue(actions.contains(ChannelAction.Storage.StoreState(aliceClosed.state))) - - assertContains(actions, ChannelAction.Storage.SetLocked(remoteCommitPublished.commitTx.txid)) - // We notify the payment handler that the non-dust htlc has been failed. - val htlcFail = actions.filterIsInstance().first() - assertEquals(htlcs[0], htlcFail.htlc) - assertTrue(htlcFail.result is ChannelAction.HtlcResult.Fail.OnChainFail) - assertEquals(3, actions.size) + // The commit tx confirms. + val (aliceClosing1, actions1) = aliceClosing.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 6, remoteCommitPublished.commitTx))) + assertIs>(aliceClosing1) + assertEquals(2, actions1.size) + actions1.has() + assertEquals(setOf(htlc2), actions1.filterIsInstance().map { it.htlc }.toSet()) + actions1.filterIsInstance().forEach { assertIs(it.result) } + assertNotNull(aliceClosing1.state.remoteCommitPublished) + assertTrue(aliceClosing1.state.remoteCommitPublished.isConfirmed) + assertFalse(aliceClosing1.state.remoteCommitPublished.isDone) + + // Our main transaction confirms. + val (aliceClosing2, actions2) = aliceClosing1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 45, 0, closingTxs.mainTx))) + assertIs>(aliceClosing2) + assertEquals(1, actions2.size) + actions2.has() + assertNotNull(aliceClosing2.state.remoteCommitPublished) + assertFalse(aliceClosing2.state.remoteCommitPublished.isDone) + assertEquals(setOf(remoteCommitPublished.commitTx, closingTxs.mainTx), aliceClosing2.state.remoteCommitPublished.irrevocablySpent.values.toSet()) + + // Our HTLC-timeout transaction confirms. + val (aliceClosing3, actions3) = aliceClosing2.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 45, 1, closingTxs.htlcTimeoutTxs.first()))) + assertIs>(aliceClosing3) + assertEquals(3, actions3.size) + actions3.has() + assertEquals(setOf(htlc1), actions3.filterIsInstance().map { it.htlc }.toSet()) + actions3.filterIsInstance().forEach { assertIs(it.result) } + assertNotNull(aliceClosing3.state.state.remoteCommitPublished) + assertTrue(aliceClosing3.state.state.remoteCommitPublished.isDone) + assertEquals(setOf(remoteCommitPublished.commitTx, closingTxs.mainTx, closingTxs.htlcTimeoutTxs.first()), aliceClosing3.state.state.remoteCommitPublished.irrevocablySpent.values.toSet()) + assertContains(actions3, ChannelAction.Storage.SetLocked(remoteCommitPublished.commitTx.txid)) } @Test fun `recv ClosingTxConfirmed -- remote commit -- non-initiator pays commit fees`() { val (alice0, bob0) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) - assertFalse(alice0.commitments.params.localParams.paysCommitTxFees) - assertTrue(bob0.commitments.params.localParams.paysCommitTxFees) - val remoteCommitTx = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx - val (alice1, remoteCommitPublished) = remoteClose(remoteCommitTx, alice0) + assertFalse(alice0.commitments.channelParams.localParams.paysCommitTxFees) + assertTrue(bob0.commitments.channelParams.localParams.paysCommitTxFees) + val remoteCommitTx = bob0.signCommitTx() + val (alice1, _, closingTxs) = remoteClose(remoteCommitTx, alice0) val (alice2, _) = alice1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 7, remoteCommitTx))) - val claimMain = remoteCommitPublished.claimMainOutputTx!!.tx - val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 3, claimMain))) + val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.state.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 3, closingTxs.mainTx))) assertIs(alice3.state) assertEquals(2, actions3.size) actions3.has() @@ -564,7 +535,7 @@ class ClosingTestsCommon : LightningTestSuite() { fun `recv ClosingTxConfirmed -- remote commit with multiple htlcs for the same payment`() { val (alice0, bob0) = reachNormal() // alice sends an htlc to bob - val (aliceClosing, remoteCommitPublished) = run { + val (aliceClosing, remoteCommitPublished, closingTxs) = run { val (nodes1, preimage, _) = addHtlc(30_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 // and more htlcs with the same payment_hash @@ -573,28 +544,28 @@ class ClosingTestsCommon : LightningTestSuite() { val (_, cmd3) = makeCmdAdd(20_000_000.msat, bob0.staticParams.nodeParams.nodeId, alice2.currentBlockHeight.toLong() - 1, preimage) val (alice3, bob3, _) = addHtlc(cmd3, alice2, bob2) val (alice4, bob4) = crossSign(alice3, bob3) - remoteClose(bob4.commitments.latest.localCommit.publishableTxs.commitTx.tx, alice4) + remoteClose(bob4.signCommitTx(), alice4, htlcTimeoutCount = 3) } // actual test starts here - assertNotNull(remoteCommitPublished.claimMainOutputTx) - assertEquals(3, remoteCommitPublished.claimHtlcTimeoutTxs().size) + assertNotNull(remoteCommitPublished.localOutput) + assertEquals(3, remoteCommitPublished.htlcOutputs.size) // if commit tx and claim-htlc-timeout txs end up in the same block, we may receive them in any order val watchConfirmed = listOf( - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, remoteCommitPublished.claimHtlcTimeoutTxs()[1].tx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, closingTxs.htlcTimeoutTxs[1]), WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, remoteCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 201, 0, remoteCommitPublished.claimHtlcTimeoutTxs()[2].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 200, 0, remoteCommitPublished.claimMainOutputTx.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 204, 0, remoteCommitPublished.claimHtlcTimeoutTxs()[0].tx) + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 201, 0, closingTxs.htlcTimeoutTxs[2]), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 200, 0, closingTxs.mainTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 204, 0, closingTxs.htlcTimeoutTxs[0]) ) - confirmWatchedTxs(aliceClosing, watchConfirmed) + confirmClosingTxs(aliceClosing, watchConfirmed) } @Test fun `recv ClosingTxConfirmed -- remote commit with htlcs only signed by local in next remote commit`() { val (alice0, bob0) = reachNormal() - val bobCommitTx = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob0.signCommitTx() val (aliceClosing, remoteCommitPublished, add) = run { // alice sends an htlc to bob val (nodes1, _, add) = addHtlc(50_000_000.msat, alice0, bob0) @@ -606,7 +577,7 @@ class ClosingTestsCommon : LightningTestSuite() { Triple(aliceClosing, remoteCommitPublished, add) } - assertTrue(remoteCommitPublished.claimHtlcTimeoutTxs().isEmpty()) + assertTrue(remoteCommitPublished.htlcOutputs.isEmpty()) val (alice1, actions1) = aliceClosing.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, bobCommitTx))) assertIs(alice1.state) @@ -622,49 +593,43 @@ class ClosingTestsCommon : LightningTestSuite() { @Test fun `recv ClosingTxConfirmed -- remote commit -- followed by preimage`() { val (alice0, bob0) = reachNormal() - val (aliceClosing, remoteCommitPublished, fulfill) = run { - // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. - val (nodes1, preimage, htlc) = addHtlc(110_000_000.msat, bob0, alice0) - val (bob1, alice1) = nodes1 - // An HTLC Alice -> Bob is cross-signed and will timeout later. - val (nodes2, _, _) = addHtlc(95_000_000.msat, alice1, bob1) - val (alice2, bob2) = nodes2 - val (alice3, bob3) = crossSign(alice2, bob2) - // Now Bob publishes his commit tx (force-close). - val bobCommitTx = bob3.state.commitments.latest.localCommit.publishableTxs.commitTx.tx - assertEquals(6, bobCommitTx.txOut.size) // two main outputs + 2 anchors + 2 HTLCs - val (aliceClosing, remoteCommitPublished) = remoteClose(bobCommitTx, alice3) - Triple(aliceClosing, remoteCommitPublished, ChannelCommand.Htlc.Settlement.Fulfill(htlc.id, preimage, commit = true)) - } + // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. + val (nodes1, preimage, htlc) = addHtlc(110_000_000.msat, bob0, alice0) + val (bob1, alice1) = nodes1 + // An HTLC Alice -> Bob is cross-signed and will timeout later. + val (nodes2, _, _) = addHtlc(95_000_000.msat, alice1, bob1) + val (alice2, bob2) = nodes2 + val (alice3, bob3) = crossSign(alice2, bob2) + // Now Bob publishes his commit tx (force-close). + val bobCommitTx = bob3.signCommitTx() + assertEquals(6, bobCommitTx.txOut.size) // two main outputs + 2 anchors + 2 HTLCs + val (aliceClosing, remoteCommitPublished, closingTxs) = remoteClose(bobCommitTx, alice3, htlcTimeoutCount = 1) - assertNotNull(remoteCommitPublished.claimMainOutputTx) + assertNotNull(remoteCommitPublished.localOutput) // we don't have the preimage to claim the htlc-success yet - assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) - assertEquals(1, remoteCommitPublished.claimHtlcTimeoutTxs().size) + assertTrue(closingTxs.htlcSuccessTxs.isEmpty()) + assertEquals(1, closingTxs.htlcTimeoutTxs.size) + assertEquals(2, remoteCommitPublished.htlcOutputs.size) // Alice receives the preimage for the first HTLC from the payment handler; she can now claim the corresponding HTLC output. - val (aliceFulfill, actionsFulfill) = aliceClosing.process(fulfill) + val (aliceFulfill, actionsFulfill) = aliceClosing.process(ChannelCommand.Htlc.Settlement.Fulfill(htlc.id, preimage, commit = true)) assertIs>(aliceFulfill) - assertEquals(1, aliceFulfill.state.remoteCommitPublished!!.claimHtlcSuccessTxs().size) - assertEquals(1, aliceFulfill.state.remoteCommitPublished.claimHtlcTimeoutTxs().size) - val claimHtlcSuccess = aliceFulfill.state.remoteCommitPublished.claimHtlcSuccessTxs().first() - actionsFulfill.hasPublishTx(claimHtlcSuccess.tx) - assertTrue(actionsFulfill.findWatches().map { Pair(it.txId, it.outputIndex.toLong()) }.contains(Pair(remoteCommitPublished.commitTx.txid, claimHtlcSuccess.input.outPoint.index))) - Transaction.correctlySpends(claimHtlcSuccess.tx, remoteCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val claimHtlcSuccess = actionsFulfill.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcSuccessTx) + Transaction.correctlySpends(claimHtlcSuccess, remoteCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val watchConfirmed = listOf( WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, remoteCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 0, remoteCommitPublished.claimHtlcTimeoutTxs()[0].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 0, claimHtlcSuccess.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 250, 0, remoteCommitPublished.claimMainOutputTx.tx) + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 0, closingTxs.htlcTimeoutTxs.first()), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 0, claimHtlcSuccess), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 250, 0, closingTxs.mainTx) ) - confirmWatchedTxs(aliceFulfill, watchConfirmed) + confirmClosingTxs(aliceFulfill, watchConfirmed) } @Test fun `recv ClosingTxConfirmed -- remote commit -- alternative feerate`() { val (alice0, bob0) = reachNormal() - val (bobClosing, remoteCommitPublished) = run { + val (bobClosing, remoteCommitPublished, closingTxs) = run { val (nodes1, r, htlc) = addHtlc(75_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 val (alice2, bob2) = crossSign(alice1, bob1) @@ -683,12 +648,11 @@ class ClosingTestsCommon : LightningTestSuite() { remoteClose(alternativeCommitTx, bob6) } - assertNotNull(remoteCommitPublished.claimMainOutputTx) - val claimMain = remoteCommitPublished.claimMainOutputTx.tx - Transaction.correctlySpends(claimMain, remoteCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assertNotNull(remoteCommitPublished.localOutput) + Transaction.correctlySpends(closingTxs.mainTx, remoteCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val (bobClosing1, _) = bobClosing.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, remoteCommitPublished.commitTx))) - val (bobClosed, actions) = bobClosing1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, claimMain))) + val (bobClosed, actions) = bobClosing1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, closingTxs.mainTx))) assertIs(bobClosed.state) assertTrue(actions.contains(ChannelAction.Storage.StoreState(bobClosed.state))) } @@ -706,26 +670,25 @@ class ClosingTestsCommon : LightningTestSuite() { // Bob has the preimage for those HTLCs, but he force-closes before Alice receives it. val (bob4, actionsBob4) = bob3.process(ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, preimage)) actionsBob4.hasOutgoingMessage() // ignored - val (_, remoteCommitPublished) = localClose(bob4) // + val (_, remoteCommitPublished, bobClosingTxs) = localClose(bob4, htlcSuccessCount = 2) // Bob claims the htlc outputs from his own commit tx using its preimage. - assertEquals(setOf(htlc1.id, htlc2.id), remoteCommitPublished.htlcSuccessTxs().map { it.htlcId }.toSet()) - assertEquals(setOf(preimage.sha256()), remoteCommitPublished.htlcSuccessTxs().map { it.paymentHash }.toSet()) - val htlcSuccessTxs = remoteCommitPublished.htlcSuccessTxs().map { it.tx } + assertEquals(setOf(htlc1.id, htlc2.id), remoteCommitPublished.incomingHtlcs.values.toSet()) // Alice extracts the preimage and forwards it to the payment handler. val (alice4, actionsAlice4) = alice3.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), remoteCommitPublished.commitTx))) actionsAlice4.has() actionsAlice4.hasWatchConfirmed(remoteCommitPublished.commitTx.txid) - val (alice5, actionsAlice5) = alice4.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(50_000.sat), htlcSuccessTxs.first()))) - assertEquals(4, actionsAlice5.size) - actionsAlice5.has() - actionsAlice5.hasWatchConfirmed(htlcSuccessTxs.first().txid) + val mainTx = actionsAlice4.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + actionsAlice4.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(50_000.sat), bobClosingTxs.htlcSuccessTxs.first()))) + assertEquals(3, actionsAlice5.size) + actionsAlice5.hasWatchConfirmed(bobClosingTxs.htlcSuccessTxs.first().txid) val addSettled = actionsAlice5.filterIsInstance() assertEquals(setOf(htlc1, htlc2), addSettled.map { it.htlc }.toSet()) assertEquals(setOf(ChannelAction.HtlcResult.Fulfill.OnChainFulfill(preimage)), addSettled.map { it.result }.toSet()) // The HTLC-success transaction confirms: nothing to do, preimage has already been relayed. - val (alice6, actionsAlice6) = alice5.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, htlcSuccessTxs.first()))) + val (alice6, actionsAlice6) = alice5.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, bobClosingTxs.htlcSuccessTxs.first()))) assertIs>(alice6) assertEquals(1, actionsAlice6.size) actionsAlice6.has() @@ -733,53 +696,41 @@ class ClosingTestsCommon : LightningTestSuite() { // The remaining transactions confirm. val watchConfirmed = listOf( WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, remoteCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 0, htlcSuccessTxs.last()), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 0, alice6.state.remoteCommitPublished?.claimMainOutputTx?.tx!!), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 0, bobClosingTxs.htlcSuccessTxs.last()), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 0, mainTx), ) - confirmWatchedTxs(alice6, watchConfirmed) + confirmClosingTxs(alice6, watchConfirmed) } @Test fun `recv ChannelEvent Restore -- remote commit`() { val (alice0, bob0) = reachNormal() - val (aliceClosing, remoteCommitPublished) = run { + val (aliceClosing, remoteCommitPublished, closingTxs) = run { // alice sends an htlc to bob val (nodes1, _, _) = addHtlc(50_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 val (alice2, bob2) = crossSign(alice1, bob1) assertIs(bob2.state) - val bobCommitTx = bob2.commitments.latest.localCommit.publishableTxs.commitTx.tx - val (aliceClosing, remoteCommitPublished) = remoteClose(bobCommitTx, alice2) - Pair(aliceClosing, remoteCommitPublished) + val bobCommitTx = bob2.signCommitTx() + remoteClose(bobCommitTx, alice2, htlcTimeoutCount = 1) } - assertNotNull(remoteCommitPublished.claimMainOutputTx) - assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) - assertEquals(1, remoteCommitPublished.claimHtlcTimeoutTxs().size) + assertNotNull(remoteCommitPublished.localOutput) + assertEquals(1, remoteCommitPublished.htlcOutputs.size) // Simulate a wallet restart val initState = LNChannel(aliceClosing.ctx, WaitForInit) val (alice1, actions1) = initState.process(ChannelCommand.Init.Restore(aliceClosing.state)) assertIs(alice1.state) assertEquals(aliceClosing, alice1) + assertEquals(6, actions1.size) actions1.doesNotHave() - - // We should republish closing transactions - val txs = listOf( - remoteCommitPublished.claimMainOutputTx.tx, - remoteCommitPublished.claimHtlcTimeoutTxs().first().tx, - ) - assertEquals(actions1.findPublishTxs(), txs) - val watchConfirmed = listOf( - remoteCommitPublished.commitTx.txid, - remoteCommitPublished.claimMainOutputTx.tx.txid, - ) - assertEquals(actions1.findWatches().map { it.txId }, watchConfirmed) - val watchSpent = listOf( - remoteCommitPublished.commitTx.txIn.first().outPoint, - remoteCommitPublished.claimHtlcTimeoutTxs().first().input.outPoint, - ) - assertEquals(actions1.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }, watchSpent) + actions1.hasWatchFundingSpent(aliceClosing.commitments.latest.fundingInput.txid) + actions1.hasWatchConfirmed(remoteCommitPublished.commitTx.txid) + actions1.hasPublishTx(closingTxs.mainTx) + actions1.hasWatchOutputSpent(closingTxs.mainTx.txIn.first().outPoint) + actions1.hasPublishTx(closingTxs.htlcTimeoutTxs.first()) + actions1.hasWatchOutputSpent(closingTxs.htlcTimeoutTxs.first().txIn.first().outPoint) } @Test @@ -788,20 +739,20 @@ class ClosingTestsCommon : LightningTestSuite() { // alice sends an htlc to bob val (nodes1, _, _) = addHtlc(50_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 - val bobCommitTx1 = bob1.state.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx1 = bob1.signCommitTx() // alice signs it, but bob doesn't revoke val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Commitment.Sign) val commitSig = actionsAlice2.hasOutgoingMessage() val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSig)) actionsBob2.hasOutgoingMessage() // not forwarded to Alice (malicious Bob) // Bob publishes the next commit tx. - val bobCommitTx2 = (bob2.state as Normal).commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx2 = bob2.signCommitTx() assertNotEquals(bobCommitTx1.txid, bobCommitTx2.txid) - val (aliceClosing, remoteCommitPublished) = remoteClose(bobCommitTx2, alice2) + val (aliceClosing, remoteCommitPublished) = remoteClose(bobCommitTx2, alice2, htlcTimeoutCount = 1) - assertNotNull(remoteCommitPublished.claimMainOutputTx) - assertEquals(1, remoteCommitPublished.claimHtlcTimeoutTxs().size) - assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) + assertNotNull(remoteCommitPublished.localOutput) + assertEquals(1, remoteCommitPublished.outgoingHtlcs.size) + assertTrue(remoteCommitPublished.incomingHtlcs.isEmpty()) assertNull(aliceClosing.state.remoteCommitPublished) assertNotNull(aliceClosing.state.nextRemoteCommitPublished) @@ -812,14 +763,14 @@ class ClosingTestsCommon : LightningTestSuite() { fun `recv ClosingTxConfirmed -- next remote commit`() { val (alice0, bob0) = reachNormal() // alice sends an htlc to bob - val (aliceClosing, remoteCommitPublished) = run { + val (aliceClosing, remoteCommitPublished, closingTxs) = run { val (nodes1, preimage, _) = addHtlc(30_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 // and more htlcs with the same payment_hash val (_, cmd2) = makeCmdAdd(25_000_000.msat, bob0.staticParams.nodeParams.nodeId, alice1.currentBlockHeight.toLong() - 1, preimage) val (alice2, bob2, _) = addHtlc(cmd2, alice1, bob1) val (alice3, bob3) = crossSign(alice2, bob2) - val bobCommitTx1 = bob2.state.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx1 = bob2.signCommitTx() // add more htlcs that bob doesn't revoke val (_, cmd3) = makeCmdAdd(20_000_000.msat, bob0.staticParams.nodeParams.nodeId, alice3.currentBlockHeight.toLong(), preimage) val (alice4, bob4, _) = addHtlc(cmd3, alice3, bob3) @@ -828,83 +779,77 @@ class ClosingTestsCommon : LightningTestSuite() { val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(commitSig)) actionsBob5.hasOutgoingMessage() // not forwarded to Alice (malicious Bob) // Bob publishes the next commit tx. - val bobCommitTx2 = (bob5.state as Normal).commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx2 = bob5.signCommitTx() assertNotEquals(bobCommitTx1.txid, bobCommitTx2.txid) - remoteClose(bobCommitTx2, alice5) + remoteClose(bobCommitTx2, alice5, htlcTimeoutCount = 3) } // actual test starts here assertNotNull(aliceClosing.state.nextRemoteCommitPublished) - assertNotNull(remoteCommitPublished.claimMainOutputTx) - assertEquals(3, remoteCommitPublished.claimHtlcTimeoutTxs().size) - assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) + assertNotNull(remoteCommitPublished.localOutput) + assertEquals(3, remoteCommitPublished.outgoingHtlcs.size) + assertTrue(remoteCommitPublished.incomingHtlcs.isEmpty()) val watchConfirmed = listOf( WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, remoteCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 201, 0, remoteCommitPublished.claimHtlcTimeoutTxs()[1].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 201, 0, remoteCommitPublished.claimHtlcTimeoutTxs()[2].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 202, 0, remoteCommitPublished.claimMainOutputTx.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 204, 0, remoteCommitPublished.claimHtlcTimeoutTxs()[0].tx) + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 201, 0, closingTxs.htlcTimeoutTxs[1]), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 201, 0, closingTxs.htlcTimeoutTxs[2]), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 202, 0, closingTxs.mainTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 204, 0, closingTxs.htlcTimeoutTxs[0]) ) - confirmWatchedTxs(aliceClosing, watchConfirmed) + confirmClosingTxs(aliceClosing, watchConfirmed) } @Test fun `recv ClosingTxConfirmed -- next remote commit -- followed by preimage`() { val (alice0, bob0) = reachNormal() - val (aliceClosing, remoteCommitPublished, fulfill) = run { - // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. - val (nodes1, preimage, htlc) = addHtlc(110_000_000.msat, bob0, alice0) - val (bob1, alice1) = nodes1 - // An HTLC Alice -> Bob is cross-signed and will timeout later. - val (nodes2, _, _) = addHtlc(95_000_000.msat, alice1, bob1) - val (alice2, bob2) = nodes2 - val (alice3, bob3) = crossSign(alice2, bob2) - // add another htlc that bob doesn't revoke - val (nodes4, _, _) = addHtlc(20_000_000.msat, alice3, bob3) - val (alice4, bob4) = nodes4 - val (alice5, actionsAlice5) = alice4.process(ChannelCommand.Commitment.Sign) - val commitSig = actionsAlice5.hasOutgoingMessage() - val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(commitSig)) - actionsBob5.hasOutgoingMessage() // not forwarded to Alice (malicious Bob) - // Now Bob publishes his commit tx (force-close). - val bobCommitTx = (bob5.state as Normal).commitments.latest.localCommit.publishableTxs.commitTx.tx - assertEquals(7, bobCommitTx.txOut.size) // two main outputs + 2 anchors + 3 HTLCs - val (aliceClosing, remoteCommitPublished) = remoteClose(bobCommitTx, alice5) - Triple(aliceClosing, remoteCommitPublished, ChannelCommand.Htlc.Settlement.Fulfill(htlc.id, preimage, commit = true)) - } + // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. + val (nodes1, preimage, htlc) = addHtlc(110_000_000.msat, bob0, alice0) + val (bob1, alice1) = nodes1 + // An HTLC Alice -> Bob is cross-signed and will timeout later. + val (nodes2, _, _) = addHtlc(95_000_000.msat, alice1, bob1) + val (alice2, bob2) = nodes2 + val (alice3, bob3) = crossSign(alice2, bob2) + // add another htlc that bob doesn't revoke + val (nodes4, _, _) = addHtlc(20_000_000.msat, alice3, bob3) + val (alice4, bob4) = nodes4 + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.Commitment.Sign) + val commitSig = actionsAlice5.hasOutgoingMessage() + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(commitSig)) + actionsBob5.hasOutgoingMessage() // not forwarded to Alice (malicious Bob) + // Now Bob publishes his commit tx (force-close). + val bobCommitTx = bob5.signCommitTx() + assertEquals(7, bobCommitTx.txOut.size) // two main outputs + 2 anchors + 3 HTLCs + val (aliceClosing, remoteCommitPublished, closingTxs) = remoteClose(bobCommitTx, alice5, htlcTimeoutCount = 2) assertNotNull(aliceClosing.state.nextRemoteCommitPublished) - assertNotNull(remoteCommitPublished.claimMainOutputTx) + assertNotNull(remoteCommitPublished.localOutput) // we don't have the preimage to claim the htlc-success yet - assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) - assertEquals(2, remoteCommitPublished.claimHtlcTimeoutTxs().size) + assertEquals(1, remoteCommitPublished.incomingHtlcs.size) + assertTrue(closingTxs.htlcSuccessTxs.isEmpty()) + assertEquals(2, remoteCommitPublished.outgoingHtlcs.size) // Alice receives the preimage for the first HTLC from the payment handler; she can now claim the corresponding HTLC output. - val (aliceFulfill, actionsFulfill) = aliceClosing.process(fulfill) + val (aliceFulfill, actionsFulfill) = aliceClosing.process(ChannelCommand.Htlc.Settlement.Fulfill(htlc.id, preimage, commit = true)) assertIs>(aliceFulfill) - assertEquals(1, aliceFulfill.state.nextRemoteCommitPublished!!.claimHtlcSuccessTxs().size) - assertEquals(2, aliceFulfill.state.nextRemoteCommitPublished.claimHtlcTimeoutTxs().size) - val claimHtlcSuccess = aliceFulfill.state.nextRemoteCommitPublished.claimHtlcSuccessTxs().first() - actionsFulfill.hasPublishTx(claimHtlcSuccess.tx) - assertTrue(actionsFulfill.findWatches().map { Pair(it.txId, it.outputIndex.toLong()) }.contains(Pair(remoteCommitPublished.commitTx.txid, claimHtlcSuccess.input.outPoint.index))) - Transaction.correctlySpends(claimHtlcSuccess.tx, remoteCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val claimHtlcSuccess = actionsFulfill.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcSuccessTx) + Transaction.correctlySpends(claimHtlcSuccess, remoteCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) actionsFulfill.doesNotHave() val watchConfirmed = listOf( WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 1, remoteCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 0, remoteCommitPublished.claimHtlcTimeoutTxs()[0].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 1, claimHtlcSuccess.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 3, remoteCommitPublished.claimHtlcTimeoutTxs()[1].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 250, 0, remoteCommitPublished.claimMainOutputTx.tx) + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 0, closingTxs.htlcTimeoutTxs[0]), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 1, claimHtlcSuccess), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 210, 3, closingTxs.htlcTimeoutTxs[1]), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 250, 0, closingTxs.mainTx) ) - confirmWatchedTxs(aliceFulfill, watchConfirmed) + confirmClosingTxs(aliceFulfill, watchConfirmed) } @Test fun `recv ClosingTxConfirmed -- next remote commit -- alternative feerate`() { val (alice0, bob0) = reachNormal() - val (bobClosing, remoteCommitPublished) = run { + val (bobClosing, remoteCommitPublished, closingTxs) = run { val (nodes1, r, htlc) = addHtlc(75_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 val (alice2, bob2) = crossSign(alice1, bob1) @@ -916,12 +861,11 @@ class ClosingTestsCommon : LightningTestSuite() { remoteClose(alternativeCommitTx, bob4) } - assertNotNull(remoteCommitPublished.claimMainOutputTx) - val claimMain = remoteCommitPublished.claimMainOutputTx.tx - Transaction.correctlySpends(claimMain, remoteCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assertNotNull(remoteCommitPublished.localOutput) + Transaction.correctlySpends(closingTxs.mainTx, remoteCommitPublished.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val (bobClosing1, _) = bobClosing.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, remoteCommitPublished.commitTx))) - val (bobClosed, actions) = bobClosing1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, claimMain))) + val (bobClosed, actions) = bobClosing1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, closingTxs.mainTx))) assertIs(bobClosed.state) assertTrue(actions.contains(ChannelAction.Storage.StoreState(bobClosed.state))) } @@ -960,8 +904,8 @@ class ClosingTestsCommon : LightningTestSuite() { // Bob closes the channel using his latest commitment, which doesn't contain any htlc. val bobCommit = bob8.commitments.latest.localCommit - assertTrue(bobCommit.publishableTxs.htlcTxsAndSigs.isEmpty()) - val commitTx = bobCommit.publishableTxs.commitTx.tx + assertTrue(bobCommit.htlcRemoteSigs.isEmpty()) + val commitTx = bob8.signCommitTx() val (alice8, actionsAlice8) = alice7.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), commitTx))) assertTrue(actionsAlice8.filterIsInstance().isEmpty()) actionsAlice8.hasWatchConfirmed(commitTx.txid) @@ -998,13 +942,13 @@ class ClosingTestsCommon : LightningTestSuite() { // At that point, the HTLCs are not in Alice's commitment anymore. // But Bob has not revoked his commitment yet that contains them. // Bob claims the htlc outputs from his previous commit tx using its preimage. - val remoteCommitPublished = run { + val (remoteCommitPublished, bobClosingTxs) = run { val (bob5, _) = bob4.process(ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, preimage)) val (bob6, _) = bob5.process(ChannelCommand.Htlc.Settlement.Fulfill(htlc2.id, preimage)) - val (_, remoteCommitPublished) = localClose(bob6) - assertEquals(3, remoteCommitPublished.htlcTxs.size) - assertEquals(2, remoteCommitPublished.htlcSuccessTxs().size) // Bob doesn't have the preimage for the last HTLC. - remoteCommitPublished + val (_, remoteCommitPublished, closingTxs) = localClose(bob6, htlcSuccessCount = 2) + assertEquals(3, remoteCommitPublished.incomingHtlcs.size) + assertEquals(2, closingTxs.htlcSuccessTxs.size) // Bob doesn't have the preimage for the last HTLC. + Pair(remoteCommitPublished, closingTxs) } // Alice prepares Claim-HTLC-timeout transactions for each HTLC. @@ -1013,14 +957,12 @@ class ClosingTestsCommon : LightningTestSuite() { actionsAlice6.has() assertNotNull(alice6.state.remoteCommitPublished) actionsAlice6.hasWatchConfirmed(remoteCommitPublished.commitTx.txid) - alice6.state.remoteCommitPublished.claimMainOutputTx?.let { - actionsAlice6.hasPublishTx(it.tx) - actionsAlice6.hasWatchConfirmed(it.tx.txid) - } - val claimHtlcTimeoutTxs = alice6.state.remoteCommitPublished.claimHtlcTimeoutTxs() - assertEquals(setOf(htlc1.id, htlc2.id, htlc3.id), claimHtlcTimeoutTxs.map { it.htlcId }.toSet()) - claimHtlcTimeoutTxs.forEach { actionsAlice6.hasPublishTx(it.tx) } - assertEquals(claimHtlcTimeoutTxs.map { it.input.outPoint }.toSet(), actionsAlice6.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet()) + val mainTx = actionsAlice6.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + actionsAlice6.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + val claimHtlcTimeoutTxs = actionsAlice6.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcTimeoutTx) + assertEquals(setOf(htlc1.id, htlc2.id, htlc3.id), alice6.state.remoteCommitPublished.outgoingHtlcs.values.toSet()) + assertEquals(alice6.state.remoteCommitPublished.outgoingHtlcs.keys, claimHtlcTimeoutTxs.map { it.txIn.first().outPoint }.toSet()) + actionsAlice6.hasWatchOutputsSpent(alice6.state.remoteCommitPublished.htlcOutputs) // Bob's commitment confirms. val (alice7, actionsAlice7) = alice6.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 7, remoteCommitPublished.commitTx))) @@ -1028,15 +970,15 @@ class ClosingTestsCommon : LightningTestSuite() { actionsAlice7.has() // Alice extracts the preimage from Bob's HTLC-success and forwards it upstream. - val (alice8, actionsAlice8) = alice7.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(50_000.sat), remoteCommitPublished.htlcSuccessTxs().first().tx))) + val (alice8, actionsAlice8) = alice7.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(50_000.sat), bobClosingTxs.htlcSuccessTxs.first()))) val addSettled = actionsAlice8.filterIsInstance() assertEquals(setOf(htlc1, htlc2), addSettled.map { it.htlc }.toSet()) assertEquals(setOf(ChannelAction.HtlcResult.Fulfill.OnChainFulfill(preimage)), addSettled.map { it.result }.toSet()) // Alice's Claim-HTLC-timeout transaction confirms: we relay the failure upstream. - val claimHtlcTimeout = claimHtlcTimeoutTxs.find { it.htlcId == htlc3.id } + val claimHtlcTimeout = claimHtlcTimeoutTxs.find { tx -> alice6.state.remoteCommitPublished.outgoingHtlcs.filter { it.value == htlc3.id }.contains(tx.txIn.first().outPoint) } assertNotNull(claimHtlcTimeout) - val (alice9, actionsAlice9) = alice8.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 1, claimHtlcTimeout.tx))) + val (alice9, actionsAlice9) = alice8.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 1, claimHtlcTimeout))) assertIs>(alice9) assertEquals(2, actionsAlice9.size) actionsAlice9.has() @@ -1045,11 +987,11 @@ class ClosingTestsCommon : LightningTestSuite() { // The remaining transactions confirm. val watchConfirmed = listOf( - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 0, remoteCommitPublished.htlcSuccessTxs().first().tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 1, remoteCommitPublished.htlcSuccessTxs().last().tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 0, alice9.state.remoteCommitPublished?.claimMainOutputTx?.tx!!), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 0, bobClosingTxs.htlcSuccessTxs.first()), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 1, bobClosingTxs.htlcSuccessTxs.last()), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 0, mainTx), ) - confirmWatchedTxs(alice9, watchConfirmed) + confirmClosingTxs(alice9, watchConfirmed) } @Test @@ -1069,27 +1011,28 @@ class ClosingTestsCommon : LightningTestSuite() { val (_, bob4) = crossSign(alice3, bob3) // At that point, the HTLCs are not in Alice's commitment yet. - val (bob5, remoteCommitPublished) = localClose(bob4) - assertEquals(3, remoteCommitPublished.htlcTxs.size) + val (bob5, remoteCommitPublished, bobClosingTxs5) = localClose(bob4) + assertEquals(3, remoteCommitPublished.incomingHtlcs.size) // Bob doesn't have the preimage yet for any of those HTLCs. - remoteCommitPublished.htlcTxs.forEach { assertNull(it.value) } + assertTrue(bobClosingTxs5.htlcSuccessTxs.isEmpty()) // Bob receives the preimage for the first two HTLCs. val (bob6, actionsBob6) = bob5.process(ChannelCommand.Htlc.Settlement.Fulfill(htlc1.id, preimage)) assertIs>(bob6) - assertEquals(setOf(htlc1.id, htlc2.id), bob6.state.localCommitPublished?.htlcSuccessTxs().orEmpty().map { it.htlcId }.toSet()) - val htlcSuccessTxs = actionsBob6.filterIsInstance().filter { it.txType == ChannelAction.Blockchain.PublishTx.Type.HtlcSuccessTx } + val htlcSuccessTxs = actionsBob6.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcSuccessTx) assertEquals(2, htlcSuccessTxs.size) - val batchHtlcSuccessTx = Transaction(2, htlcSuccessTxs.flatMap { it.tx.txIn }, htlcSuccessTxs.flatMap { it.tx.txOut }, 0) + assertEquals(setOf(htlc1.id, htlc2.id), htlcSuccessTxs.map { tx -> remoteCommitPublished.incomingHtlcs[tx.txIn.first().outPoint] }.toSet()) + val batchHtlcSuccessTx = Transaction(2, htlcSuccessTxs.flatMap { it.txIn }, htlcSuccessTxs.flatMap { it.txOut }, 0) // Alice prepares Claim-HTLC-timeout transactions for each HTLC. val (alice5, actionsAlice5) = alice4.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), remoteCommitPublished.commitTx))) assertIs>(alice5) val rcp = alice5.state.nextRemoteCommitPublished assertNotNull(rcp) - assertEquals(setOf(htlc1.id, htlc2.id, htlc3.id), rcp.claimHtlcTxs.values.filterNotNull().map { it.htlcId }.toSet()) - val claimHtlcTimeoutTxs = actionsAlice5.filterIsInstance().filter { it.txType == ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcTimeoutTx } + val mainTx = actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + assertEquals(setOf(htlc1.id, htlc2.id, htlc3.id), rcp.outgoingHtlcs.values.toSet()) + val claimHtlcTimeoutTxs = actionsAlice5.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcTimeoutTx) assertEquals(3, claimHtlcTimeoutTxs.size) - assertEquals(rcp.claimHtlcTxs.keys, actionsAlice5.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet()) + actionsAlice5.hasWatchOutputsSpent(rcp.outgoingHtlcs.keys) // Alice extracts the preimage from Bob's batched HTLC-success and forwards it upstream. val (alice6, actionsAlice6) = alice5.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(50_000.sat), batchHtlcSuccessTx))) @@ -1099,9 +1042,9 @@ class ClosingTestsCommon : LightningTestSuite() { actionsAlice6.hasWatchConfirmed(batchHtlcSuccessTx.txid) // Alice's Claim-HTLC-timeout transaction confirms: we relay the failure upstream. - val claimHtlcTimeout = rcp.claimHtlcTimeoutTxs().find { it.htlcId == htlc3.id } + val claimHtlcTimeout = claimHtlcTimeoutTxs.find { tx -> rcp.outgoingHtlcs.filter { it.value == htlc3.id }.contains(tx.txIn.first().outPoint) } assertNotNull(claimHtlcTimeout) - val (alice7, actionsAlice7) = alice6.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, claimHtlcTimeout.tx))) + val (alice7, actionsAlice7) = alice6.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, claimHtlcTimeout))) assertIs>(alice7) val addFailed = actionsAlice7.filterIsInstance() assertEquals(setOf(htlc3), addFailed.map { it.htlc }.toSet()) @@ -1110,20 +1053,20 @@ class ClosingTestsCommon : LightningTestSuite() { val watchConfirmed = listOf( WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 40, 0, remoteCommitPublished.commitTx), WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 41, 1, batchHtlcSuccessTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 7, alice7.state.nextRemoteCommitPublished?.claimMainOutputTx?.tx!!), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 47, 7, mainTx), ) - confirmWatchedTxs(alice7, watchConfirmed) + confirmClosingTxs(alice7, watchConfirmed) } @Test fun `recv ChannelEvent Restore -- next remote commit`() { val (alice0, bob0) = reachNormal() - val (aliceClosing, remoteCommitPublished) = run { + val (aliceClosing, remoteCommitPublished, closingTxs) = run { // alice sends an htlc to bob val (nodes1, preimage, _) = addHtlc(30_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 assertIs(bob1.state) - val bobCommitTx1 = bob1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx1 = bob1.signCommitTx() // add more htlcs that bob doesn't revoke val (_, cmd1) = makeCmdAdd(20_000_000.msat, bob0.staticParams.nodeParams.nodeId, alice1.currentBlockHeight.toLong(), preimage) val (alice2, bob2, _) = addHtlc(cmd1, alice1, bob1) @@ -1133,40 +1076,30 @@ class ClosingTestsCommon : LightningTestSuite() { assertIs(bob3.state) actionsBob3.hasOutgoingMessage() // not forwarded to Alice (malicious Bob) // Bob publishes the next commit tx. - val bobCommitTx2 = bob3.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx2 = bob3.signCommitTx() assertNotEquals(bobCommitTx1.txid, bobCommitTx2.txid) - remoteClose(bobCommitTx2, alice3) + remoteClose(bobCommitTx2, alice3, htlcTimeoutCount = 2) } - assertNotNull(remoteCommitPublished.claimMainOutputTx) - assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) - assertEquals(2, remoteCommitPublished.claimHtlcTimeoutTxs().size) + assertNotNull(remoteCommitPublished.localOutput) + assertTrue(remoteCommitPublished.incomingHtlcs.isEmpty()) + assertEquals(2, remoteCommitPublished.outgoingHtlcs.size) // Simulate a wallet restart val initState = LNChannel(aliceClosing.ctx, WaitForInit) val (alice1, actions1) = initState.process(ChannelCommand.Init.Restore(aliceClosing.state)) assertTrue(alice1.state is Closing) assertEquals(aliceClosing, alice1) + assertEquals(8, actions1.size) actions1.doesNotHave() - - // We should republish closing transactions - val txs = listOf( - remoteCommitPublished.claimMainOutputTx.tx, - remoteCommitPublished.claimHtlcTimeoutTxs().first().tx, - remoteCommitPublished.claimHtlcTimeoutTxs().last().tx, - ) - assertEquals(actions1.findPublishTxs(), txs) - val watchConfirmed = listOf( - remoteCommitPublished.commitTx.txid, - remoteCommitPublished.claimMainOutputTx.tx.txid, - ) - assertEquals(actions1.findWatches().map { it.txId }, watchConfirmed) - val watchSpent = listOf( - remoteCommitPublished.commitTx.txIn.first().outPoint, - remoteCommitPublished.claimHtlcTimeoutTxs().first().input.outPoint, - remoteCommitPublished.claimHtlcTimeoutTxs().last().input.outPoint, - ) - assertEquals(actions1.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }, watchSpent) + actions1.hasWatchFundingSpent(aliceClosing.commitments.latest.fundingInput.txid) + actions1.hasWatchConfirmed(remoteCommitPublished.commitTx.txid) + actions1.hasPublishTx(closingTxs.mainTx) + actions1.hasWatchOutputSpent(closingTxs.mainTx.txIn.first().outPoint) + closingTxs.htlcTimeoutTxs.forEach { tx -> + actions1.hasPublishTx(tx) + actions1.hasWatchOutputSpent(tx.txIn.first().outPoint) + } } @Test @@ -1192,8 +1125,8 @@ class ClosingTestsCommon : LightningTestSuite() { Pair(alice7, bob7) } - val localInit = Init(alice0.commitments.params.localParams.features) - val remoteInit = Init(bob0.commitments.params.localParams.features) + val localInit = Init(alice0.commitments.channelParams.localParams.features) + val remoteInit = Init(bob0.commitments.channelParams.localParams.features) // then we manually replace alice's state with an older one and reconnect them. val (alice1, aliceActions1) = LNChannel(alice0.ctx, Offline(alice0.state)).process(ChannelCommand.Connected(localInit, remoteInit)) @@ -1217,7 +1150,7 @@ class ClosingTestsCommon : LightningTestSuite() { // bob receives the error from alice and publishes his local commitment tx val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(errorA)) assertIs(bob3.state) - val bobCommitTx = bob2.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob2.signCommitTx() assertEquals(6, bobCommitTx.txOut.size) // 2 main outputs + 2 anchors + 2 HTLCs bobActions3.find().also { assertEquals(bobCommitTx.txid, it.txId) @@ -1234,11 +1167,10 @@ class ClosingTestsCommon : LightningTestSuite() { } val futureRemoteCommitPublished = alice3.state.futureRemoteCommitPublished assertNotNull(futureRemoteCommitPublished) - assertEquals(bobCommitTx.txid, aliceActions3.findWatches()[0].txId) + aliceActions3.hasWatchConfirmed(bobCommitTx.txid) // alice is able to claim its main output - val aliceTxs = aliceActions3.findPublishTxs() - assertEquals(listOf(futureRemoteCommitPublished.claimMainOutputTx!!.tx), aliceTxs) - Transaction.correctlySpends(futureRemoteCommitPublished.claimMainOutputTx.tx, listOf(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val mainTx = aliceActions3.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + Transaction.correctlySpends(mainTx, listOf(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // simulate a wallet restart run { @@ -1246,9 +1178,8 @@ class ClosingTestsCommon : LightningTestSuite() { val (alice4, actions4) = initState.process(ChannelCommand.Init.Restore(alice3.state)) assertIs(alice4.state) assertEquals(alice3, alice4) - assertEquals(actions4.findPublishTxs(), listOf(futureRemoteCommitPublished.claimMainOutputTx.tx)) - assertEquals(actions4.findWatches().map { it.txId }, listOf(bobCommitTx.txid, futureRemoteCommitPublished.claimMainOutputTx.tx.txid)) - assertEquals(actions4.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }, listOf(bobCommitTx.txIn.first().outPoint)) + actions4.hasWatchConfirmed(bobCommitTx.txid) + actions4.hasWatchOutputSpent(mainTx.txIn.first().outPoint) actions4.doesNotHave() } @@ -1256,7 +1187,7 @@ class ClosingTestsCommon : LightningTestSuite() { assertIs(alice4.state) assertEquals(listOf(ChannelAction.Storage.StoreState(alice4.state)), aliceActions4) - val (alice5, aliceActions5) = alice4.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 60, 3, aliceTxs[0]))) + val (alice5, aliceActions5) = alice4.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 60, 3, mainTx))) assertIs(alice5.state) assertEquals( listOf(ChannelAction.Storage.StoreState(alice5.state)), @@ -1270,7 +1201,7 @@ class ClosingTestsCommon : LightningTestSuite() { val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose() // bob publishes one of his revoked txs - val bobRevokedTx = bobCommitTxs[1].commitTx.tx + val bobRevokedTx = bobCommitTxs[1].commitTx assertEquals(6, bobRevokedTx.txOut.size) val (alice1, aliceActions1) = alice0.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobRevokedTx))) @@ -1284,21 +1215,25 @@ class ClosingTestsCommon : LightningTestSuite() { } // alice creates penalty txs - run { + val (mainTx, penaltyTx) = run { assertEquals(1, alice1.state.revokedCommitPublished.size) val revokedCommitPublished = alice1.state.revokedCommitPublished[0] assertEquals(bobRevokedTx, revokedCommitPublished.commitTx) - assertNotNull(revokedCommitPublished.claimMainOutputTx) - assertNotNull(revokedCommitPublished.mainPenaltyTx) - assertTrue(revokedCommitPublished.htlcPenaltyTxs.isEmpty()) - assertTrue(revokedCommitPublished.claimHtlcDelayedPenaltyTxs.isEmpty()) - Transaction.correctlySpends(revokedCommitPublished.mainPenaltyTx.tx, bobRevokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - // alice publishes txs for the main outputs - assertEquals(setOf(revokedCommitPublished.claimMainOutputTx.tx, revokedCommitPublished.mainPenaltyTx.tx), aliceActions1.findPublishTxs().toSet()) - // alice watches confirmation for the commit tx and her main output - assertEquals(setOf(bobRevokedTx.txid, revokedCommitPublished.claimMainOutputTx.tx.txid), aliceActions1.findWatches().map { it.txId }.toSet()) - // alice watches bob's main output - assertEquals(setOf(revokedCommitPublished.mainPenaltyTx.input.outPoint.index), aliceActions1.findWatches().map { it.outputIndex.toLong() }.toSet()) + assertNotNull(revokedCommitPublished.localOutput) + assertNotNull(revokedCommitPublished.remoteOutput) + assertTrue(revokedCommitPublished.htlcOutputs.isEmpty()) + assertTrue(revokedCommitPublished.htlcDelayedOutputs.isEmpty()) + val mainTx = aliceActions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + assertEquals(revokedCommitPublished.localOutput, mainTx.txIn.first().outPoint) + Transaction.correctlySpends(mainTx, bobRevokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val penaltyTx = aliceActions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) + assertEquals(revokedCommitPublished.remoteOutput, penaltyTx.txIn.first().outPoint) + Transaction.correctlySpends(penaltyTx, bobRevokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assertTrue(aliceActions1.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcPenaltyTx).isEmpty()) + aliceActions1.hasWatchConfirmed(bobRevokedTx.txid) + aliceActions1.hasWatchOutputsSpent(setOf(revokedCommitPublished.localOutput, revokedCommitPublished.remoteOutput)) + aliceActions1.hasWatchOutputsSpent(revokedCommitPublished.htlcOutputs) + Pair(mainTx, penaltyTx) } // alice fetches information about the revoked htlcs @@ -1314,27 +1249,15 @@ class ClosingTestsCommon : LightningTestSuite() { aliceActions2.has() aliceActions2.doesNotHave() - // alice creates htlc penalty txs and rebroadcasts main txs + // alice creates htlc penalty txs assertEquals(1, alice2.state.revokedCommitPublished.size) val revokedCommitPublished = alice2.state.revokedCommitPublished[0] assertEquals(bobRevokedTx, revokedCommitPublished.commitTx) - assertNotNull(revokedCommitPublished.claimMainOutputTx) - assertNotNull(revokedCommitPublished.mainPenaltyTx) - assertEquals(2, revokedCommitPublished.htlcPenaltyTxs.size) - assertTrue(revokedCommitPublished.claimHtlcDelayedPenaltyTxs.isEmpty()) - revokedCommitPublished.htlcPenaltyTxs.forEach { Transaction.correctlySpends(it.tx, bobRevokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - // alice publishes txs for all outputs - val aliceTxs = setOf(revokedCommitPublished.claimMainOutputTx.tx, revokedCommitPublished.mainPenaltyTx.tx) + revokedCommitPublished.htlcPenaltyTxs.map { it.tx }.toSet() - assertEquals(aliceTxs, aliceActions2.findPublishTxs().toSet()) - // alice watches confirmation for the commit tx and her main output - assertEquals(setOf(bobRevokedTx.txid, revokedCommitPublished.claimMainOutputTx.tx.txid), aliceActions2.findWatches().map { it.txId }.toSet()) - // alice watches bob's outputs - val outputsToWatch = buildSet { - add(revokedCommitPublished.mainPenaltyTx.input.outPoint) - addAll(revokedCommitPublished.htlcPenaltyTxs.map { it.input.outPoint }) - } - assertEquals(3, outputsToWatch.size) - assertEquals(outputsToWatch, aliceActions2.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet()) + assertEquals(2, revokedCommitPublished.htlcOutputs.size) + assertTrue(revokedCommitPublished.htlcDelayedOutputs.isEmpty()) + val htlcPenaltyTxs = aliceActions2.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcPenaltyTx) + assertEquals(2, htlcPenaltyTxs.size) + htlcPenaltyTxs.forEach { Transaction.correctlySpends(it, bobRevokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } // simulate a wallet restart run { @@ -1343,36 +1266,39 @@ class ClosingTestsCommon : LightningTestSuite() { assertIs(alice3.state) assertEquals(alice2, alice3) actions3.doesNotHave() - // alice republishes transactions - assertEquals(aliceTxs, actions3.findPublishTxs().toSet()) - assertEquals(setOf(bobRevokedTx.txid, revokedCommitPublished.claimMainOutputTx.tx.txid), actions3.findWatches().map { it.txId }.toSet()) - val watchSpent = outputsToWatch + alice3.commitments.latest.commitInput.outPoint - assertEquals(watchSpent, actions3.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet()) + actions3.hasPublishTx(mainTx) + actions3.hasPublishTx(penaltyTx) + assertEquals(ChannelAction.Storage.GetHtlcInfos(bobRevokedTx.txid, 2), actions3.find()) + val (alice4, actions4) = alice3.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobRevokedTx.txid, htlcInfos)) + assertIs(alice4.state) + assertEquals(alice2, alice4) + htlcPenaltyTxs.forEach { tx -> actions4.hasPublishTx(tx) } + actions4.hasWatchOutputsSpent((listOf(mainTx, penaltyTx) + htlcPenaltyTxs).map { it.txIn.first().outPoint }.toSet()) } val watchConfirmed = listOf( WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, revokedCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, revokedCommitPublished.claimMainOutputTx.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 5, revokedCommitPublished.mainPenaltyTx.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 50, 1, revokedCommitPublished.htlcPenaltyTxs[1].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 52, 2, revokedCommitPublished.htlcPenaltyTxs[0].tx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, mainTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 5, penaltyTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 50, 1, htlcPenaltyTxs.last()), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 52, 2, htlcPenaltyTxs.first()), ) - confirmWatchedTxs(alice2, watchConfirmed) + confirmClosingTxs(alice2, watchConfirmed) } @Test fun `recv ChannelSpent -- multiple revoked tx`() { val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose() - assertEquals(bobCommitTxs.size, bobCommitTxs.map { it.commitTx.tx.txid }.toSet().size) // all commit txs are distinct + assertEquals(bobCommitTxs.size, bobCommitTxs.map { it.commitTx.txid }.toSet().size) // all commit txs are distinct // bob publishes one of his revoked txs - val (alice1, aliceActions1) = alice0.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTxs[0].commitTx.tx))) + val (alice1, aliceActions1) = alice0.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTxs[0].commitTx))) assertIs(alice1.state) aliceActions1.hasOutgoingMessage() aliceActions1.has() aliceActions1.find().also { - assertEquals(bobCommitTxs[0].commitTx.tx.txid, it.txId) + assertEquals(bobCommitTxs[0].commitTx.txid, it.txId) assertEquals(ChannelClosingType.Revoked, it.closingType) assertTrue(it.isSentToDefaultAddress) } @@ -1382,16 +1308,17 @@ class ClosingTestsCommon : LightningTestSuite() { run { // alice publishes txs for the main outputs assertEquals(2, aliceActions1.findPublishTxs().size) - // alice watches confirmation for the commit tx and her main output - assertEquals(2, aliceActions1.findWatches().size) - // alice watches bob's main output - assertEquals(1, aliceActions1.findWatches().size) + val mainTx = aliceActions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + val penaltyTx = aliceActions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) + listOf(mainTx, penaltyTx).forEach { tx -> Transaction.correctlySpends(tx, bobCommitTxs[0].commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + aliceActions1.hasWatchConfirmed(bobCommitTxs[0].commitTx.txid) + aliceActions1.hasWatchOutputsSpent(listOf(mainTx, penaltyTx).map { it.txIn.first().outPoint }.toSet()) // alice fetches information about the revoked htlcs - assertEquals(ChannelAction.Storage.GetHtlcInfos(bobCommitTxs[0].commitTx.tx.txid, 0), aliceActions1.find()) + assertEquals(ChannelAction.Storage.GetHtlcInfos(bobCommitTxs[0].commitTx.txid, 0), aliceActions1.find()) } // bob publishes another one of his revoked txs - val (alice2, aliceActions2) = alice1.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTxs[1].commitTx.tx))) + val (alice2, aliceActions2) = alice1.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTxs[1].commitTx))) assertIs(alice2.state) aliceActions2.hasOutgoingMessage() aliceActions2.has() @@ -1399,83 +1326,70 @@ class ClosingTestsCommon : LightningTestSuite() { assertEquals(2, alice2.state.revokedCommitPublished.size) // alice creates penalty txs - run { + val (mainTx, penaltyTx) = run { // alice publishes txs for the main outputs assertEquals(2, aliceActions2.findPublishTxs().size) - // alice watches confirmation for the commit tx and her main output - assertEquals(2, aliceActions2.findWatches().size) - // alice watches bob's main output - assertEquals(1, aliceActions2.findWatches().size) + val mainTx = aliceActions2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + val penaltyTx = aliceActions2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) + listOf(mainTx, penaltyTx).forEach { tx -> Transaction.correctlySpends(tx, bobCommitTxs[1].commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + aliceActions2.hasWatchConfirmed(bobCommitTxs[1].commitTx.txid) + aliceActions2.hasWatchOutputsSpent(listOf(mainTx, penaltyTx).map { it.txIn.first().outPoint }.toSet()) // alice fetches information about the revoked htlcs - assertEquals(ChannelAction.Storage.GetHtlcInfos(bobCommitTxs[1].commitTx.tx.txid, 2), aliceActions2.find()) + assertEquals(ChannelAction.Storage.GetHtlcInfos(bobCommitTxs[1].commitTx.txid, 2), aliceActions2.find()) + Pair(mainTx, penaltyTx) } - val (alice3, aliceActions3) = alice2.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobCommitTxs[0].commitTx.tx.txid, listOf())) + val (alice3, aliceActions3) = alice2.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobCommitTxs[0].commitTx.txid, listOf())) assertIs(alice3.state) assertNull(aliceActions3.findOutgoingMessageOpt()) aliceActions3.has() aliceActions3.doesNotHave() - - // alice rebroadcasts main txs for bob's first revoked commitment (no htlc in this commitment) - run { - assertEquals(2, alice3.state.revokedCommitPublished.size) - val revokedCommitPublished = alice3.state.revokedCommitPublished[0] - assertEquals(bobCommitTxs[0].commitTx.tx, revokedCommitPublished.commitTx) - assertNotNull(revokedCommitPublished.claimMainOutputTx) - assertNotNull(revokedCommitPublished.mainPenaltyTx) - Transaction.correctlySpends(revokedCommitPublished.mainPenaltyTx.tx, bobCommitTxs[0].commitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assertTrue(revokedCommitPublished.htlcPenaltyTxs.isEmpty()) - assertTrue(revokedCommitPublished.claimHtlcDelayedPenaltyTxs.isEmpty()) - // alice publishes txs for all outputs - assertEquals(2, aliceActions3.findPublishTxs().size) - assertEquals(2, aliceActions3.findWatches().size) - assertEquals(1, aliceActions3.findWatches().size) + assertEquals(2, alice3.state.revokedCommitPublished.size) + alice3.state.revokedCommitPublished.forEach { rvk -> + assertNotNull(rvk.localOutput) + assertNotNull(rvk.remoteOutput) + assertTrue(rvk.htlcOutputs.isEmpty()) + assertTrue(rvk.htlcDelayedOutputs.isEmpty()) } val htlcInfos = listOf( ChannelAction.Storage.HtlcInfo(alice0.channelId, 2, htlcsAlice[0].paymentHash, htlcsAlice[0].cltvExpiry), ChannelAction.Storage.HtlcInfo(alice0.channelId, 2, htlcsBob[0].paymentHash, htlcsBob[0].cltvExpiry), ) - val (alice4, aliceActions4) = alice3.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobCommitTxs[1].commitTx.tx.txid, htlcInfos)) + val (alice4, aliceActions4) = alice3.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobCommitTxs[1].commitTx.txid, htlcInfos)) assertIs>(alice4) assertIs(alice4.state) assertNull(aliceActions4.findOutgoingMessageOpt()) aliceActions4.has() aliceActions4.doesNotHave() - - // alice creates htlc penalty txs and rebroadcasts main txs for bob's second commitment - run { - assertEquals(2, alice4.state.revokedCommitPublished.size) - val revokedCommitPublished = alice4.state.revokedCommitPublished[1] - assertEquals(bobCommitTxs[1].commitTx.tx, revokedCommitPublished.commitTx) - assertNotNull(revokedCommitPublished.claimMainOutputTx) - assertNotNull(revokedCommitPublished.mainPenaltyTx) - Transaction.correctlySpends(revokedCommitPublished.mainPenaltyTx.tx, bobCommitTxs[1].commitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assertEquals(2, revokedCommitPublished.htlcPenaltyTxs.size) - assertTrue(revokedCommitPublished.claimHtlcDelayedPenaltyTxs.isEmpty()) - revokedCommitPublished.htlcPenaltyTxs.forEach { Transaction.correctlySpends(it.tx, bobCommitTxs[1].commitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - // alice publishes txs for all outputs - assertEquals(4, aliceActions4.findPublishTxs().size) - assertEquals(2, aliceActions4.findWatches().size) - assertEquals(3, aliceActions4.findWatches().size) - - // this revoked transaction is the one to confirm - val watchConfirmed = listOf( - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, revokedCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, revokedCommitPublished.claimMainOutputTx.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 5, revokedCommitPublished.mainPenaltyTx.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 50, 1, revokedCommitPublished.htlcPenaltyTxs[1].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 52, 2, revokedCommitPublished.htlcPenaltyTxs[0].tx), - ) - confirmWatchedTxs(alice4, watchConfirmed) - } + // alice creates htlc penalty txs + assertEquals(2, alice4.state.revokedCommitPublished.size) + val revokedCommitPublished = alice4.state.revokedCommitPublished[1] + assertEquals(bobCommitTxs[1].commitTx, revokedCommitPublished.commitTx) + val htlcPenaltyTxs = aliceActions4.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcPenaltyTx) + assertEquals(2, revokedCommitPublished.htlcOutputs.size) + assertEquals(2, htlcPenaltyTxs.size) + assertEquals(htlcPenaltyTxs.map { it.txIn.first().outPoint }.toSet(), revokedCommitPublished.htlcOutputs) + aliceActions4.hasWatchOutputsSpent(revokedCommitPublished.htlcOutputs) + assertTrue(revokedCommitPublished.htlcDelayedOutputs.isEmpty()) + htlcPenaltyTxs.forEach { Transaction.correctlySpends(it, bobCommitTxs[1].commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + + // this revoked transaction is the one to confirm + val watchConfirmed = listOf( + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, revokedCommitPublished.commitTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, mainTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 5, penaltyTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 50, 1, htlcPenaltyTxs.last()), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 52, 2, htlcPenaltyTxs.first()), + ) + confirmClosingTxs(alice4, watchConfirmed) } @Test fun `recv ClosingTxConfirmed -- one revoked tx + pending htlcs`() { val (alice0, bob0) = reachNormal() // bob's first commit tx doesn't contain any htlc - assertEquals(4, bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx.txOut.size) // 2 main outputs + 2 anchors + assertEquals(4, bob0.signCommitTx().txOut.size) // 2 main outputs + 2 anchors // bob's second commit tx contains 2 incoming htlcs val (alice1, bob1, htlcs1) = run { @@ -1485,7 +1399,7 @@ class ClosingTestsCommon : LightningTestSuite() { val (alice2, bob2) = nodes2 val (alice3, bob3) = crossSign(alice2, bob2) assertIs(bob3.state) - assertEquals(6, bob3.commitments.latest.localCommit.publishableTxs.commitTx.tx.txOut.size) + assertEquals(6, bob3.signCommitTx().txOut.size) Triple(alice3, bob3, listOf(htlc1, htlc2)) } @@ -1500,12 +1414,12 @@ class ClosingTestsCommon : LightningTestSuite() { assertIs(bob4.state) val (alice5, bob5) = crossSign(alice4, bob4) assertIs(bob5.state) - assertEquals(7, bob5.state.commitments.latest.localCommit.publishableTxs.commitTx.tx.txOut.size) + assertEquals(7, bob5.signCommitTx().txOut.size) Triple(alice5, bob5, listOf(htlc3, htlc4)) } // bob publishes a revoked tx - val bobRevokedTx = bob1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobRevokedTx = bob1.signCommitTx() val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobRevokedTx))) assertIs(alice3.state) actions3.hasOutgoingMessage() @@ -1538,106 +1452,127 @@ class ClosingTestsCommon : LightningTestSuite() { fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx`() { val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose() - // bob publishes one of his revoked txs - val bobRevokedTx = bobCommitTxs[2] - assertEquals(8, bobRevokedTx.commitTx.tx.txOut.size) + // Bob publishes one of his revoked txs. + val bobRevokedTx = bobCommitTxs[2].commitTx + assertEquals(8, bobRevokedTx.txOut.size) - val (alice1, aliceActions1) = alice0.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobRevokedTx.commitTx.tx))) + val (alice1, aliceActions1) = alice0.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobRevokedTx))) assertIs(alice1.state) aliceActions1.hasOutgoingMessage() aliceActions1.has() aliceActions1.find().also { - assertEquals(bobRevokedTx.commitTx.tx.txid, it.txId) + assertEquals(bobRevokedTx.txid, it.txId) assertEquals(ChannelClosingType.Revoked, it.closingType) assertTrue(it.isSentToDefaultAddress) } - // alice creates penalty txs - run { + // Alice creates penalty txs for the main outputs. + val (mainTx, penaltyTx) = run { val revokedCommitPublished = alice1.state.revokedCommitPublished[0] - assertTrue(revokedCommitPublished.htlcPenaltyTxs.isEmpty()) - assertTrue(revokedCommitPublished.claimHtlcDelayedPenaltyTxs.isEmpty()) + assertNotNull(revokedCommitPublished.localOutput) + assertNotNull(revokedCommitPublished.remoteOutput) + assertTrue(revokedCommitPublished.htlcOutputs.isEmpty()) + assertTrue(revokedCommitPublished.htlcDelayedOutputs.isEmpty()) // alice publishes txs for the main outputs and sets watches assertEquals(2, aliceActions1.findPublishTxs().size) - assertEquals(2, aliceActions1.findWatches().size) - assertEquals(1, aliceActions1.findWatches().size) + val mainTx = aliceActions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + val penaltyTx = aliceActions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) + aliceActions1.hasWatchConfirmed(bobRevokedTx.txid) + aliceActions1.hasWatchOutputsSpent(listOf(mainTx, penaltyTx).map { it.txIn.first().outPoint }.toSet()) + Pair(mainTx, penaltyTx) } - // alice fetches information about the revoked htlcs - assertEquals(ChannelAction.Storage.GetHtlcInfos(bobRevokedTx.commitTx.tx.txid, 4), aliceActions1.find()) + // Alice fetches information about the revoked htlcs. + assertEquals(ChannelAction.Storage.GetHtlcInfos(bobRevokedTx.txid, 4), aliceActions1.find()) val htlcInfos = listOf( ChannelAction.Storage.HtlcInfo(alice0.channelId, 4, htlcsAlice[0].paymentHash, htlcsAlice[0].cltvExpiry), ChannelAction.Storage.HtlcInfo(alice0.channelId, 4, htlcsAlice[1].paymentHash, htlcsAlice[1].cltvExpiry), ChannelAction.Storage.HtlcInfo(alice0.channelId, 4, htlcsBob[0].paymentHash, htlcsBob[0].cltvExpiry), ChannelAction.Storage.HtlcInfo(alice0.channelId, 4, htlcsBob[1].paymentHash, htlcsBob[1].cltvExpiry), ) - val (alice2, aliceActions2) = alice1.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobRevokedTx.commitTx.tx.txid, htlcInfos)) + val (alice2, aliceActions2) = alice1.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobRevokedTx.txid, htlcInfos)) assertIs(alice2.state) assertNull(aliceActions2.findOutgoingMessageOpt()) aliceActions2.has() aliceActions2.doesNotHave() - // alice creates htlc penalty txs and rebroadcasts main txs - run { + // Alice creates htlc penalty txs. + val htlcPenaltyTxs = run { val revokedCommitPublished = alice2.state.revokedCommitPublished[0] - assertNotNull(revokedCommitPublished.claimMainOutputTx) - assertNotNull(revokedCommitPublished.mainPenaltyTx) - assertEquals(4, revokedCommitPublished.htlcPenaltyTxs.size) - assertTrue(revokedCommitPublished.claimHtlcDelayedPenaltyTxs.isEmpty()) - revokedCommitPublished.htlcPenaltyTxs.forEach { Transaction.correctlySpends(it.tx, bobRevokedTx.commitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - // alice publishes txs for all outputs - assertEquals(setOf(revokedCommitPublished.claimMainOutputTx.tx, revokedCommitPublished.mainPenaltyTx.tx) + revokedCommitPublished.htlcPenaltyTxs.map { it.tx }.toSet(), aliceActions2.findPublishTxs().toSet()) - // alice watches confirmation for the commit tx and her main output - assertEquals(setOf(bobRevokedTx.commitTx.tx.txid, revokedCommitPublished.claimMainOutputTx.tx.txid), aliceActions2.findWatches().map { it.txId }.toSet()) - // alice watches bob's outputs - val outputsToWatch = buildSet { - add(revokedCommitPublished.mainPenaltyTx.input.outPoint.index) - addAll(revokedCommitPublished.htlcPenaltyTxs.map { it.input.outPoint.index }) - } - assertEquals(5, outputsToWatch.size) - assertEquals(outputsToWatch, aliceActions2.findWatches().map { it.outputIndex.toLong() }.toSet()) + assertNotNull(revokedCommitPublished.localOutput) + assertNotNull(revokedCommitPublished.remoteOutput) + assertEquals(4, revokedCommitPublished.htlcOutputs.size) + assertTrue(revokedCommitPublished.htlcDelayedOutputs.isEmpty()) + val htlcPenaltyTxs = aliceActions2.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcPenaltyTx) + assertEquals(4, htlcPenaltyTxs.size) + htlcPenaltyTxs.forEach { Transaction.correctlySpends(it, bobRevokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + aliceActions2.hasWatchOutputsSpent(htlcPenaltyTxs.map { it.txIn.first().outPoint }.toSet()) + htlcPenaltyTxs + } - // bob manages to claim 2 htlc outputs before alice can penalize him: 1 htlc-success and 1 htlc-timeout. - val bobHtlcSuccessTx = bobRevokedTx.htlcTxsAndSigs.find { htlcsAlice[0].amountMsat == it.txinfo.amountIn.toMilliSatoshi() }!! - val bobHtlcTimeoutTx = bobRevokedTx.htlcTxsAndSigs.find { htlcsBob[1].amountMsat == it.txinfo.amountIn.toMilliSatoshi() }!! - val bobOutpoints = listOf(bobHtlcSuccessTx, bobHtlcTimeoutTx).map { it.txinfo.input.outPoint }.toSet() - assertEquals(2, bobOutpoints.size) + // Bob manages to claim 2 htlc outputs before alice can penalize him: 1 htlc-success and 1 htlc-timeout. + val bobHtlcSuccessTx = bobCommitTxs[2].htlcSuccessTxs.find { it.paymentHash == htlcsAlice[0].paymentHash }!! + val bobHtlcTimeoutTx = bobCommitTxs[2].htlcTimeoutTxs.find { it.paymentHash == htlcsBob[1].paymentHash }!! + val bobOutpoints = listOf(bobHtlcTimeoutTx, bobHtlcSuccessTx).map { it.input.outPoint }.toSet() + assertEquals(2, bobOutpoints.size) + + // Alice reacts by publishing penalty txs that spend bob's htlc transactions. + val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(bobHtlcSuccessTx.amountIn), bobHtlcSuccessTx.tx))) + assertEquals(1, actions3.size) + actions3.hasWatchConfirmed(bobHtlcSuccessTx.tx.txid) + val (alice4, actions4) = alice3.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 0, 0, bobHtlcSuccessTx.tx))) + assertIs(alice4.state) + assertEquals(3, actions4.size) + assertEquals(1, alice4.state.revokedCommitPublished[0].htlcDelayedOutputs.size) + assertTrue(actions4.contains(ChannelAction.Storage.StoreState(alice4.state))) + val htlcSuccessPenalty = actions4.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcDelayedOutputPenaltyTx) + Transaction.correctlySpends(htlcSuccessPenalty, bobHtlcSuccessTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actions4.hasWatchOutputSpent(htlcSuccessPenalty.txIn.first().outPoint) + + val (alice5, actions5) = alice4.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(bobHtlcTimeoutTx.amountIn), bobHtlcTimeoutTx.tx))) + assertEquals(1, actions5.size) + actions5.hasWatchConfirmed(bobHtlcTimeoutTx.tx.txid) + val (alice6, actions6) = alice5.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 0, 0, bobHtlcTimeoutTx.tx))) + assertIs(alice6.state) + assertEquals(3, actions6.size) + assertEquals(2, alice6.state.revokedCommitPublished[0].htlcDelayedOutputs.size) + assertTrue(actions6.contains(ChannelAction.Storage.StoreState(alice6.state))) + val htlcTimeoutPenalty = actions6.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcDelayedOutputPenaltyTx) + Transaction.correctlySpends(htlcTimeoutPenalty, bobHtlcTimeoutTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actions6.hasWatchOutputSpent(htlcTimeoutPenalty.txIn.first().outPoint) + + val remainingHtlcPenaltyTxs = htlcPenaltyTxs.filterNot { bobOutpoints.contains(it.txIn.first().outPoint) } + assertEquals(2, remainingHtlcPenaltyTxs.size) + + // We re-publish penalty transactions on restart. + run { + val initState = LNChannel(alice6.ctx, WaitForInit) + val (alice7, actions7) = initState.process(ChannelCommand.Init.Restore(alice6.state)) + assertIs(alice7.state) + assertEquals(alice6, alice7) + actions7.hasPublishTx(mainTx) + actions7.hasPublishTx(penaltyTx) + assertEquals(ChannelAction.Storage.GetHtlcInfos(bobRevokedTx.txid, 4), actions7.find()) + val (alice8, actions8) = alice7.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobRevokedTx.txid, htlcInfos)) + assertIs>(alice8) + assertIs(alice8.state) + assertEquals(alice7, alice8) + assertEquals(8, actions8.size) + actions8.has() + actions8.hasWatchConfirmed(bobRevokedTx.txid) + remainingHtlcPenaltyTxs.forEach { tx -> actions8.hasPublishTx(tx) } + actions8.hasWatchOutputsSpent((listOf(mainTx, penaltyTx) + remainingHtlcPenaltyTxs).map { it.txIn.first().outPoint }.toSet()) - // alice reacts by publishing penalty txs that spend bob's htlc transactions - val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(bobHtlcSuccessTx.txinfo.amountIn), bobHtlcSuccessTx.txinfo.tx))) - assertIs(alice3.state) - assertEquals(4, actions3.size) - assertEquals(1, alice3.state.revokedCommitPublished[0].claimHtlcDelayedPenaltyTxs.size) - assertTrue(actions3.contains(ChannelAction.Storage.StoreState(alice3.state))) - assertEquals(WatchConfirmed(alice0.channelId, bobHtlcSuccessTx.txinfo.tx, alice0.staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ClosingTxConfirmed), actions3.findWatch()) - actions3.hasPublishTx(alice3.state.revokedCommitPublished[0].claimHtlcDelayedPenaltyTxs[0].tx) - assertEquals(WatchSpent(alice0.channelId, bobHtlcSuccessTx.txinfo.tx, alice3.state.revokedCommitPublished[0].claimHtlcDelayedPenaltyTxs[0].input.outPoint.index.toInt(), WatchSpent.ClosingOutputSpent(bobHtlcSuccessTx.txinfo.tx.txOut[0].amount)), actions3.findWatch()) - - val (alice4, actions4) = alice3.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(bobHtlcTimeoutTx.txinfo.amountIn), bobHtlcTimeoutTx.txinfo.tx))) - assertIs>(alice4) - assertIs(alice4.state) - assertEquals(4, actions4.size) - assertEquals(2, alice4.state.revokedCommitPublished[0].claimHtlcDelayedPenaltyTxs.size) - assertTrue(actions4.contains(ChannelAction.Storage.StoreState(alice4.state))) - assertEquals(WatchConfirmed(alice0.channelId, bobHtlcTimeoutTx.txinfo.tx, alice0.staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ClosingTxConfirmed), actions4.findWatch()) - actions4.hasPublishTx(alice4.state.revokedCommitPublished[0].claimHtlcDelayedPenaltyTxs[1].tx) - assertEquals(WatchSpent(alice0.channelId, bobHtlcTimeoutTx.txinfo.tx, alice4.state.revokedCommitPublished[0].claimHtlcDelayedPenaltyTxs[1].input.outPoint.index.toInt(), WatchSpent.ClosingOutputSpent(bobHtlcTimeoutTx.txinfo.tx.txOut[0].amount)), actions4.findWatch()) - - val claimHtlcDelayedPenaltyTxs = alice4.state.revokedCommitPublished[0].claimHtlcDelayedPenaltyTxs - val remainingHtlcPenaltyTxs = revokedCommitPublished.htlcPenaltyTxs.filterNot { bobOutpoints.contains(it.input.outPoint) } - assertEquals(2, remainingHtlcPenaltyTxs.size) val watchConfirmed = listOf( - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, revokedCommitPublished.commitTx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, revokedCommitPublished.claimMainOutputTx.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 5, revokedCommitPublished.mainPenaltyTx.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 50, 1, remainingHtlcPenaltyTxs[1].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 50, 2, bobHtlcSuccessTx.txinfo.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 50, 3, remainingHtlcPenaltyTxs[0].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 51, 3, claimHtlcDelayedPenaltyTxs[0].tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 51, 0, bobHtlcTimeoutTx.txinfo.tx), - WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 51, 1, claimHtlcDelayedPenaltyTxs[1].tx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 0, bobRevokedTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 0, mainTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 43, 5, penaltyTx), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 50, 1, remainingHtlcPenaltyTxs[1]), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 50, 3, remainingHtlcPenaltyTxs[0]), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 51, 3, htlcSuccessPenalty), + WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 51, 1, htlcTimeoutPenalty), ) - confirmWatchedTxs(alice4, watchConfirmed) + confirmClosingTxs(alice8, watchConfirmed) } } @@ -1646,10 +1581,10 @@ class ClosingTestsCommon : LightningTestSuite() { val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose() // bob publishes one of his revoked txs - val bobRevokedTx = bobCommitTxs[2] - assertEquals(8, bobRevokedTx.commitTx.tx.txOut.size) + val bobRevokedTx = bobCommitTxs[2].commitTx + assertEquals(8, bobRevokedTx.txOut.size) - val (alice1, _) = alice0.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobRevokedTx.commitTx.tx))) + val (alice1, _) = alice0.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobRevokedTx))) assertIs(alice1.state) // alice fetches information about the revoked htlcs @@ -1659,59 +1594,52 @@ class ClosingTestsCommon : LightningTestSuite() { ChannelAction.Storage.HtlcInfo(alice0.channelId, 4, htlcsBob[0].paymentHash, htlcsBob[0].cltvExpiry), ChannelAction.Storage.HtlcInfo(alice0.channelId, 4, htlcsBob[1].paymentHash, htlcsBob[1].cltvExpiry), ) - val (alice2, _) = alice1.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobRevokedTx.commitTx.tx.txid, htlcInfos)) + val (alice2, _) = alice1.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobRevokedTx.txid, htlcInfos)) assertIs(alice2.state) // bob claims multiple htlc outputs in a single transaction (this is possible with anchor outputs because signatures // use sighash_single | sighash_anyonecanpay) - assertEquals(4, bobRevokedTx.htlcTxsAndSigs.size) + assertEquals(2, bobCommitTxs[2].htlcSuccessTxs.size) + assertEquals(2, bobCommitTxs[2].htlcTimeoutTxs.size) val bobHtlcTx = Transaction( 2, listOf( TxIn(OutPoint(TxId(Lightning.randomBytes32()), 4), listOf(), 1), // unrelated utxo (maybe used for fee bumping) - bobRevokedTx.htlcTxsAndSigs[0].txinfo.tx.txIn.first(), - bobRevokedTx.htlcTxsAndSigs[1].txinfo.tx.txIn.first(), - bobRevokedTx.htlcTxsAndSigs[2].txinfo.tx.txIn.first(), - bobRevokedTx.htlcTxsAndSigs[3].txinfo.tx.txIn.first(), + bobCommitTxs[2].htlcSuccessTxs[0].tx.txIn.first(), + bobCommitTxs[2].htlcTimeoutTxs[1].tx.txIn.first(), + bobCommitTxs[2].htlcTimeoutTxs[0].tx.txIn.first(), + bobCommitTxs[2].htlcSuccessTxs[1].tx.txIn.first(), ), listOf( TxOut(10_000.sat, listOf()), // unrelated output (maybe change output) - bobRevokedTx.htlcTxsAndSigs[0].txinfo.tx.txOut.first(), - bobRevokedTx.htlcTxsAndSigs[1].txinfo.tx.txOut.first(), - bobRevokedTx.htlcTxsAndSigs[2].txinfo.tx.txOut.first(), - bobRevokedTx.htlcTxsAndSigs[3].txinfo.tx.txOut.first(), + bobCommitTxs[2].htlcSuccessTxs[0].tx.txOut.first(), + bobCommitTxs[2].htlcTimeoutTxs[1].tx.txOut.first(), + bobCommitTxs[2].htlcTimeoutTxs[0].tx.txOut.first(), + bobCommitTxs[2].htlcSuccessTxs[1].tx.txOut.first(), ), 0 ) - val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(bobRevokedTx.htlcTxsAndSigs[0].txinfo.amountIn), bobHtlcTx))) - assertIs(alice3.state) - assertEquals(10, actions3.size) - assertEquals(4, alice3.state.revokedCommitPublished[0].claimHtlcDelayedPenaltyTxs.size) - val claimHtlcDelayedPenaltyTxs = alice3.state.revokedCommitPublished[0].claimHtlcDelayedPenaltyTxs - claimHtlcDelayedPenaltyTxs.forEach { Transaction.correctlySpends(it.tx, bobHtlcTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - assertEquals(setOf(OutPoint(bobHtlcTx, 1), OutPoint(bobHtlcTx, 2), OutPoint(bobHtlcTx, 3), OutPoint(bobHtlcTx, 4)), claimHtlcDelayedPenaltyTxs.map { it.input.outPoint }.toSet()) - assertTrue(actions3.contains(ChannelAction.Storage.StoreState(alice3.state))) - assertEquals(WatchConfirmed(alice0.channelId, bobHtlcTx, alice0.staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ClosingTxConfirmed), actions3.findWatch()) - actions3.hasPublishTx(claimHtlcDelayedPenaltyTxs[0].tx) - actions3.hasPublishTx(claimHtlcDelayedPenaltyTxs[1].tx) - actions3.hasPublishTx(claimHtlcDelayedPenaltyTxs[2].tx) - actions3.hasPublishTx(claimHtlcDelayedPenaltyTxs[3].tx) - val watchSpent = actions3.findWatches().toSet() - val expected = setOf( - WatchSpent(alice0.channelId, bobHtlcTx, 1, WatchSpent.ClosingOutputSpent(claimHtlcDelayedPenaltyTxs[0].amountIn)), - WatchSpent(alice0.channelId, bobHtlcTx, 2, WatchSpent.ClosingOutputSpent(claimHtlcDelayedPenaltyTxs[1].amountIn)), - WatchSpent(alice0.channelId, bobHtlcTx, 3, WatchSpent.ClosingOutputSpent(claimHtlcDelayedPenaltyTxs[2].amountIn)), - WatchSpent(alice0.channelId, bobHtlcTx, 4, WatchSpent.ClosingOutputSpent(claimHtlcDelayedPenaltyTxs[3].amountIn)), - ) - assertEquals(expected, watchSpent) + val (alice3, actions3) = alice2.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice0.channelId, WatchSpent.ClosingOutputSpent(0.sat), bobHtlcTx))) + assertEquals(1, actions3.size) + actions3.hasWatchConfirmed(bobHtlcTx.txid) + val (alice4, actions4) = alice3.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 0, 0, bobHtlcTx))) + assertIs(alice4.state) + assertEquals(9, actions4.size) + assertTrue(actions4.contains(ChannelAction.Storage.StoreState(alice4.state))) + assertEquals(4, alice4.state.revokedCommitPublished[0].htlcDelayedOutputs.size) + assertEquals(setOf(1, 2, 3, 4).map { OutPoint(bobHtlcTx.txid, it.toLong()) }.toSet(), alice4.state.revokedCommitPublished[0].htlcDelayedOutputs) + val htlcDelayedPenaltyTxs = actions4.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcDelayedOutputPenaltyTx) + assertEquals(alice4.state.revokedCommitPublished[0].htlcDelayedOutputs, htlcDelayedPenaltyTxs.map { it.txIn.first().outPoint }.toSet()) + htlcDelayedPenaltyTxs.forEach { Transaction.correctlySpends(it, bobHtlcTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + actions4.hasWatchOutputsSpent(alice4.state.revokedCommitPublished[0].htlcDelayedOutputs) } @Test fun `recv ChannelReestablish`() { val (alice, bob) = reachNormal() val (alice1, lcp) = localClose(alice) - val bobCurrentPerCommitmentPoint = bob.commitments.params.localParams.channelKeys(bob.ctx.keyManager).commitmentPoint(bob.commitments.localCommitIndex) + val bobCurrentPerCommitmentPoint = bob.channelKeys.commitmentPoint(bob.commitments.localCommitIndex) val channelReestablish = ChannelReestablish(bob.channelId, 42, 42, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint) val (alice2, actions2) = alice1.process(ChannelCommand.MessageReceived(channelReestablish)) assertIs(alice2.state) @@ -1729,7 +1657,7 @@ class ClosingTestsCommon : LightningTestSuite() { val (nodes1, _, htlc) = addHtlc(30_000_000.msat, alice, bob) val (alice1, bob1) = nodes1 val (alice2, _) = crossSign(alice1, bob1) - val (alice3, _) = localClose(alice2) + val (alice3, _) = localClose(alice2, htlcTimeoutCount = 1) Pair(alice3, htlc) } @@ -1750,22 +1678,26 @@ class ClosingTestsCommon : LightningTestSuite() { } companion object { - data class RevokedCloseFixture(val alice: LNChannel, val bob: LNChannel, val bobRevokedTxs: List, val htlcsAlice: List, val htlcsBob: List) + data class RevokedTxs(val commitTx: Transaction, val htlcTimeoutTxs: List, val htlcSuccessTxs: List) + + data class RevokedCloseFixture(val alice: LNChannel, val bob: LNChannel, val bobRevokedTxs: List, val htlcsAlice: List, val htlcsBob: List) fun prepareRevokedClose(): RevokedCloseFixture { val (aliceInit, bobInit) = reachNormal() var mutableAlice: LNChannel = aliceInit var mutableBob: LNChannel = bobInit + val preimages: MutableSet = mutableSetOf() // Bob's first commit tx doesn't contain any htlc - val commitTx1 = bobInit.commitments.latest.localCommit.publishableTxs - assertEquals(4, commitTx1.commitTx.tx.txOut.size) // 2 main outputs + 2 anchors + val revokedTxs1 = RevokedTxs(bobInit.signCommitTx(), listOf(), listOf()) + assertEquals(4, revokedTxs1.commitTx.txOut.size) // 2 main outputs + 2 anchors // Bob's second commit tx contains 1 incoming htlc and 1 outgoing htlc - val (commitTx2, htlcAlice1, htlcBob1) = run { - val (nodes1, _, htlcAlice) = addHtlc(35_000_000.msat, mutableAlice, mutableBob) + val (revokedTxs2, htlcAlice1, htlcBob1) = run { + val (nodes1, preimage, htlcAlice) = addHtlc(35_000_000.msat, mutableAlice, mutableBob) mutableAlice = nodes1.first mutableBob = nodes1.second + preimages.add(preimage) with(crossSign(mutableAlice, mutableBob)) { mutableAlice = first @@ -1781,17 +1713,21 @@ class ClosingTestsCommon : LightningTestSuite() { mutableAlice = second } - val commitTx = mutableBob.commitments.latest.localCommit.publishableTxs - Triple(commitTx, htlcAlice, htlcBob) + val commitTx = mutableBob.signCommitTx() + val htlcTimeoutTxs = mutableBob.signHtlcTimeoutTxs() + assertEquals(1, htlcTimeoutTxs.size) + val htlcSuccessTxs = mutableBob.signHtlcSuccessTxs(preimages) + assertEquals(1, htlcSuccessTxs.size) + Triple(RevokedTxs(commitTx, htlcTimeoutTxs, htlcSuccessTxs), htlcAlice, htlcBob) } - assertEquals(6, commitTx2.commitTx.tx.txOut.size) - assertEquals(6, mutableAlice.commitments.latest.localCommit.publishableTxs.commitTx.tx.txOut.size) + assertEquals(6, revokedTxs2.commitTx.txOut.size) // Bob's third commit tx contains 2 incoming htlcs and 2 outgoing htlcs - val (commitTx3, htlcAlice2, htlcBob2) = run { - val (nodes1, _, htlcAlice) = addHtlc(25_000_000.msat, mutableAlice, mutableBob) + val (revokedTxs3, htlcAlice2, htlcBob2) = run { + val (nodes1, preimage, htlcAlice) = addHtlc(25_000_000.msat, mutableAlice, mutableBob) mutableAlice = nodes1.first mutableBob = nodes1.second + preimages.add(preimage) with(crossSign(mutableAlice, mutableBob)) { mutableAlice = first @@ -1807,14 +1743,17 @@ class ClosingTestsCommon : LightningTestSuite() { mutableAlice = second } - val commitTx = mutableBob.commitments.latest.localCommit.publishableTxs - Triple(commitTx, htlcAlice, htlcBob) + val commitTx = mutableBob.signCommitTx() + val htlcTimeoutTxs = mutableBob.signHtlcTimeoutTxs() + assertEquals(2, htlcTimeoutTxs.size) + val htlcSuccessTxs = mutableBob.signHtlcSuccessTxs(preimages) + assertEquals(2, htlcSuccessTxs.size) + Triple(RevokedTxs(commitTx, htlcTimeoutTxs, htlcSuccessTxs), htlcAlice, htlcBob) } - assertEquals(8, commitTx3.commitTx.tx.txOut.size) - assertEquals(8, mutableAlice.commitments.latest.localCommit.publishableTxs.commitTx.tx.txOut.size) + assertEquals(8, revokedTxs3.commitTx.txOut.size) // Bob's fourth commit tx doesn't contain any htlc - val commitTx4 = run { + val revokedTxs4 = run { listOf(htlcAlice1, htlcAlice2).forEach { htlcAlice -> val nodes = failHtlc(htlcAlice.id, mutableAlice, mutableBob) mutableAlice = nodes.first @@ -1829,29 +1768,47 @@ class ClosingTestsCommon : LightningTestSuite() { mutableAlice = first mutableBob = second } - mutableBob.commitments.latest.localCommit.publishableTxs + RevokedTxs(mutableBob.signCommitTx(), listOf(), listOf()) } - assertEquals(4, commitTx4.commitTx.tx.txOut.size) - assertEquals(4, mutableAlice.commitments.latest.localCommit.publishableTxs.commitTx.tx.txOut.size) + assertEquals(4, revokedTxs4.commitTx.txOut.size) - return RevokedCloseFixture(mutableAlice, mutableBob, listOf(commitTx1, commitTx2, commitTx3, commitTx4), listOf(htlcAlice1, htlcAlice2), listOf(htlcBob1, htlcBob2)) + return RevokedCloseFixture(mutableAlice, mutableBob, listOf(revokedTxs1, revokedTxs2, revokedTxs3, revokedTxs4), listOf(htlcAlice1, htlcAlice2), listOf(htlcBob1, htlcBob2)) } - private fun confirmWatchedTxs(firstClosingState: LNChannel, watchConfirmed: List) { + private fun confirmClosingTxs(firstClosingState: LNChannel, watchConfirmed: List) { var alice = firstClosingState watchConfirmed.dropLast(1).forEach { - val (aliceNew, actions) = alice.process(ChannelCommand.WatchReceived(it)) - assertIs>(aliceNew) - assertTrue(actions.contains(ChannelAction.Storage.StoreState(aliceNew.state))) - // The only other possible actions are for settling htlcs - assertEquals(actions.size - 1, actions.count { action -> action is ChannelAction.ProcessCmdRes }) - alice = aliceNew + val alice1 = confirmClosingTx(alice, it) + assertIs>(alice1) + alice = alice1 } - - val (aliceClosed, actions) = alice.process(ChannelCommand.WatchReceived(watchConfirmed.last())) + val aliceClosed = confirmClosingTx(alice, watchConfirmed.last()) assertIs(aliceClosed.state) - assertContains(actions, ChannelAction.Storage.StoreState(aliceClosed.state)) - actions.has() + } + + private fun confirmClosingTx(closing: LNChannel, watch: WatchConfirmedTriggered): LNChannel { + val (closing1, actions1) = closing.process(ChannelCommand.WatchReceived(watch)) + actions1.has() + // If this was an HTLC transaction, we may publish an HTLC-delayed transaction. + val htlcDelayedTx = actions1.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcDelayedTx).firstOrNull() + htlcDelayedTx?.let { tx -> actions1.hasWatchOutputSpent(tx.txIn.first().outPoint) } + // The HTLC-delayed transaction confirms as well. + return when (htlcDelayedTx) { + null -> closing1 + else -> { + val htlcDelayedWatch = WatchConfirmedTriggered(closing.channelId, WatchConfirmed.ClosingTxConfirmed, 0, 0, htlcDelayedTx) + val (closing2, actions2) = closing1.process(ChannelCommand.WatchReceived(htlcDelayedWatch)) + actions2.has() + when (closing2.state) { + is Closed -> { + assertEquals(2, actions2.size) + actions2.has() + } + else -> assertEquals(1, actions2.size) + } + closing2 + } + } } } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt index 6fb278d55..2194fd0fa 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt @@ -1,7 +1,6 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* -import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.WatchConfirmed @@ -374,7 +373,7 @@ class NegotiatingTestsCommon : LightningTestSuite() { val (alice, bob, closingComplete, _) = init() val (alice1, bob1) = signClosingTxAlice(alice, bob, closingComplete) - val bobCommitTx = bob1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob1.signCommitTx() val (alice2, actions2) = alice1.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTx))) assertIs>(alice2) assertEquals(5, actions2.size) @@ -383,7 +382,7 @@ class NegotiatingTestsCommon : LightningTestSuite() { actions2.has() val claimMain = actions2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) actions2.hasWatchConfirmed(bobCommitTx.txid) - actions2.hasWatchConfirmed(claimMain.txid) + actions2.hasWatchOutputSpent(claimMain.txIn.first().outPoint) actions2.has() } @@ -391,7 +390,7 @@ class NegotiatingTestsCommon : LightningTestSuite() { fun `recv ChannelSpent -- revoked tx`() { val (alice, bob, revokedCommit) = run { val (alice0, bob0) = reachNormal() - val revokedCommit = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val revokedCommit = bob0.signCommitTx() val (nodes1, r, htlc) = addHtlc(50_000_000.msat, alice0, bob0) val (alice2, bob2) = crossSign(nodes1.first, nodes1.second) val (alice3, bob3) = fulfillHtlc(htlc.id, r, alice2, bob2) @@ -417,7 +416,7 @@ class NegotiatingTestsCommon : LightningTestSuite() { val claimMain = actionsAlice4.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) actionsAlice4.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) actionsAlice4.hasWatchConfirmed(revokedCommit.txid) - actionsAlice4.hasWatchConfirmed(claimMain.txid) + actionsAlice4.hasWatchOutputSpent(claimMain.txIn.first().outPoint) } @Test @@ -445,8 +444,9 @@ class NegotiatingTestsCommon : LightningTestSuite() { val (alice, _) = init() val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) assertIs>(alice1) - actions1.hasPublishTx(alice.commitments.latest.localCommit.publishableTxs.commitTx.tx) - assertTrue(actions1.findWatches().map { it.txId }.contains(alice.commitments.latest.localCommit.publishableTxs.commitTx.tx.txid)) + val commitTx = alice.signCommitTx() + actions1.hasPublishTx(commitTx) + actions1.hasWatchConfirmed(commitTx.txid) } companion object { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt index aca196b9f..1ebfd656a 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt @@ -6,25 +6,25 @@ import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomBytes32 -import fr.acinq.lightning.blockchain.* +import fr.acinq.lightning.Lightning.randomBytes64 +import fr.acinq.lightning.blockchain.WatchConfirmed +import fr.acinq.lightning.blockchain.WatchConfirmedTriggered +import fr.acinq.lightning.blockchain.WatchSpent +import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.ChannelAction.Blockchain.PublishTx.Type import fr.acinq.lightning.channel.TestsHelper.addHtlc -import fr.acinq.lightning.channel.TestsHelper.claimHtlcSuccessTxs -import fr.acinq.lightning.channel.TestsHelper.claimHtlcTimeoutTxs import fr.acinq.lightning.channel.TestsHelper.crossSign import fr.acinq.lightning.channel.TestsHelper.fulfillHtlc -import fr.acinq.lightning.channel.TestsHelper.htlcSuccessTxs -import fr.acinq.lightning.channel.TestsHelper.htlcTimeoutTxs import fr.acinq.lightning.channel.TestsHelper.makeCmdAdd import fr.acinq.lightning.channel.TestsHelper.reachNormal import fr.acinq.lightning.channel.TestsHelper.signAndRevack import fr.acinq.lightning.crypto.sphinx.Sphinx import fr.acinq.lightning.router.Announcements -import fr.acinq.lightning.serialization.channel.Encryption.from import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.transactions.Transactions.commitTxFeeMsat import fr.acinq.lightning.transactions.Transactions.weight2fee import fr.acinq.lightning.transactions.incomings @@ -157,7 +157,7 @@ class NormalTestsCommon : LightningTestSuite() { val add = defaultAdd.copy(amount = 0.msat) val (bob1, actions) = bob0.process(add) val actualError = actions.findCommandError() - val expectedError = HtlcValueTooSmall(bob0.channelId, alice0.commitments.params.localParams.htlcMinimum, 0.msat) + val expectedError = HtlcValueTooSmall(bob0.channelId, alice0.commitments.latest.localCommitParams.htlcMinimum, 0.msat) assertEquals(expectedError, actualError) assertEquals(bob0, bob1) } @@ -165,8 +165,8 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Htlc_Add -- increasing balance but still below reserve`() { val (alice0, bob0) = reachNormal(bobFundingAmount = 0.sat) - assertFalse(alice0.commitments.params.channelFeatures.hasFeature(Feature.ZeroReserveChannels)) - assertFalse(bob0.commitments.params.channelFeatures.hasFeature(Feature.ZeroReserveChannels)) + assertFalse(alice0.commitments.channelParams.channelFeatures.hasFeature(Feature.ZeroReserveChannels)) + assertFalse(bob0.commitments.channelParams.channelFeatures.hasFeature(Feature.ZeroReserveChannels)) assertEquals(0.msat, bob0.commitments.availableBalanceForSend()) val cmdAdd = defaultAdd.copy(amount = 1_500.msat) @@ -225,9 +225,10 @@ class NormalTestsCommon : LightningTestSuite() { run { // We can sign those HTLCs and make Alice drop below her reserve. val (_, alice4) = crossSign(bob3, alice3) - val aliceCommit = alice4.commitments.active.first().localCommit - assertTrue(aliceCommit.publishableTxs.commitTx.tx.txOut.all { txOut -> txOut.amount > 0.sat }) - val aliceBalance = aliceCommit.spec.toLocal - commitTxFeeMsat(alice4.commitments.params.localParams.dustLimit, aliceCommit.spec) + val aliceCommit = alice4.commitments.latest.localCommit + val commitTx = alice4.signCommitTx() + assertTrue(commitTx.txOut.all { txOut -> txOut.amount > 0.sat }) + val aliceBalance = aliceCommit.spec.toLocal - commitTxFeeMsat(alice4.commitments.latest.localCommitParams.dustLimit, aliceCommit.spec, alice4.commitments.latest.commitmentFormat) assertTrue(aliceBalance >= 0.msat) assertTrue(aliceBalance < alice4.commitments.latest.localChannelReserve) } @@ -271,7 +272,8 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Htlc_Add -- over their max in-flight htlc value`() { val bob0 = run { val (_, bob) = reachNormal() - bob.copy(state = bob.state.copy(commitments = bob.commitments.copy(params = bob.commitments.params.copy(remoteParams = bob.commitments.params.remoteParams.copy(maxHtlcValueInFlightMsat = 100_000_000))))) + val remoteCommitParams = bob.commitments.latest.remoteCommitParams.copy(maxHtlcValueInFlightMsat = 100_000_000) + bob.copy(state = bob.state.copy(commitments = bob.state.commitments.copy(active = bob.state.commitments.active.map { it.copy(remoteCommitParams = remoteCommitParams) }))) } val (_, actions) = bob0.process(defaultAdd.copy(amount = 101_000_000.msat)) val actualError = actions.findCommandError() @@ -283,7 +285,8 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Htlc_Add -- over our max in-flight htlc value`() { val bob0 = run { val (_, bob) = reachNormal() - bob.copy(state = bob.state.copy(commitments = bob.commitments.copy(params = bob.commitments.params.copy(localParams = bob.commitments.params.localParams.copy(maxHtlcValueInFlightMsat = 100_000_000))))) + val remoteCommitParams = bob.commitments.latest.remoteCommitParams.copy(maxHtlcValueInFlightMsat = 100_000_000) + bob.copy(state = bob.state.copy(commitments = bob.state.commitments.copy(active = bob.state.commitments.active.map { it.copy(remoteCommitParams = remoteCommitParams) }))) } val (_, actions) = bob0.process(defaultAdd.copy(amount = 101_000_000.msat)) val actualError = actions.findCommandError() @@ -295,7 +298,8 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Htlc_Add -- over max in-flight htlc value with duplicate amounts`() { val bob0 = run { val (_, bob) = reachNormal() - bob.copy(state = bob.state.copy(commitments = bob.commitments.copy(params = bob.commitments.params.copy(remoteParams = bob.commitments.params.remoteParams.copy(maxHtlcValueInFlightMsat = 100_000_000))))) + val remoteCommitParams = bob.commitments.latest.remoteCommitParams.copy(maxHtlcValueInFlightMsat = 100_000_000) + bob.copy(state = bob.state.copy(commitments = bob.state.commitments.copy(active = bob.state.commitments.active.map { it.copy(remoteCommitParams = remoteCommitParams) }))) } val (bob1, actionsBob1) = bob0.process(defaultAdd.copy(amount = 50_500_000.msat)) actionsBob1.hasOutgoingMessage() @@ -323,7 +327,7 @@ class NormalTestsCommon : LightningTestSuite() { val (_, actions) = alice1.process(defaultAdd.copy(amount = 1_000_000.msat)) val actualError = actions.findCommandError() - val expectedError = TooManyAcceptedHtlcs(alice0.channelId, maximum = 100) + val expectedError = TooManyAcceptedHtlcs(alice0.channelId, maximum = 80) assertEquals(expectedError, actualError) } @@ -331,7 +335,8 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Htlc_Add -- over max offered htlcs`() { val (alice0, _) = reachNormal() // Bob accepts a maximum of 100 htlcs, but for Alice that value is only 5 - val alice1 = alice0.copy(state = alice0.state.copy(commitments = alice0.commitments.copy(params = alice0.commitments.params.copy(localParams = alice0.commitments.params.localParams.copy(maxAcceptedHtlcs = 5))))) + val localCommitParams = alice0.commitments.latest.localCommitParams.copy(maxAcceptedHtlcs = 5) + val alice1 = alice0.copy(state = alice0.state.copy(commitments = alice0.commitments.copy(active = alice0.commitments.active.map { it.copy(localCommitParams = localCommitParams) }))) val alice2 = run { var alice = alice1 @@ -487,7 +492,8 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv UpdateAddHtlc -- over max in-flight htlc value`() { val alice0 = run { val (alice, _) = reachNormal() - alice.copy(state = alice.state.copy(commitments = alice.commitments.copy(params = alice.commitments.params.copy(localParams = alice.commitments.params.localParams.copy(maxHtlcValueInFlightMsat = 100_000_000))))) + val localCommitParams = alice.commitments.latest.localCommitParams.copy(maxHtlcValueInFlightMsat = 100_000_000) + alice.copy(state = alice.state.copy(commitments = alice.commitments.copy(active = alice.commitments.active.map { it.copy(localCommitParams = localCommitParams) }))) } val add = UpdateAddHtlc(alice0.channelId, 0, 101_000_000.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(alice0.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket) val (alice1, actions1) = alice0.process(ChannelCommand.MessageReceived(add)) @@ -567,10 +573,10 @@ class NormalTestsCommon : LightningTestSuite() { assertTrue(alice0.staticParams.nodeParams.dustLimit > bob0.staticParams.nodeParams.dustLimit) // we're gonna exchange two htlcs in each direction, the goal is to have bob's commitment have 4 htlcs, and alice's // commitment only have 3. We will then check that alice indeed persisted 4 htlcs, and bob only 3. - val aliceMinReceive = TestConstants.Alice.nodeParams.dustLimit + weight2fee(FeeratePerKw.CommitmentFeerate, Commitments.HTLC_SUCCESS_WEIGHT) - val aliceMinOffer = TestConstants.Alice.nodeParams.dustLimit + weight2fee(FeeratePerKw.CommitmentFeerate, Commitments.HTLC_TIMEOUT_WEIGHT) - val bobMinReceive = TestConstants.Bob.nodeParams.dustLimit + weight2fee(FeeratePerKw.CommitmentFeerate, Commitments.HTLC_SUCCESS_WEIGHT) - val bobMinOffer = TestConstants.Bob.nodeParams.dustLimit + weight2fee(FeeratePerKw.CommitmentFeerate, Commitments.HTLC_TIMEOUT_WEIGHT) + val aliceMinReceive = TestConstants.Alice.nodeParams.dustLimit + weight2fee(FeeratePerKw.CommitmentFeerate, Transactions.CommitmentFormat.AnchorOutputs.htlcSuccessWeight) + val aliceMinOffer = TestConstants.Alice.nodeParams.dustLimit + weight2fee(FeeratePerKw.CommitmentFeerate, Transactions.CommitmentFormat.AnchorOutputs.htlcTimeoutWeight) + val bobMinReceive = TestConstants.Bob.nodeParams.dustLimit + weight2fee(FeeratePerKw.CommitmentFeerate, Transactions.CommitmentFormat.AnchorOutputs.htlcSuccessWeight) + val bobMinOffer = TestConstants.Bob.nodeParams.dustLimit + weight2fee(FeeratePerKw.CommitmentFeerate, Transactions.CommitmentFormat.AnchorOutputs.htlcTimeoutWeight) val addAlice1 = bobMinReceive + 10.sat // will be in alice and bob tx val addAlice2 = bobMinReceive + 20.sat // will be in alice and bob tx val addBob1 = aliceMinReceive + 10.sat // will be in alice and bob tx @@ -648,9 +654,9 @@ class NormalTestsCommon : LightningTestSuite() { assertEquals(htlcCount, commitSig.htlcSignatures.size) val (bob2, _) = bob1.process(ChannelCommand.MessageReceived(commitSig)) assertIs>(bob2) - val htlcTxs = bob2.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs + val htlcTxs = bob2.unsignedHtlcTxs() assertEquals(htlcCount, htlcTxs.size) - val amounts = htlcTxs.map { it.txinfo.tx.txOut.first().amount.sat } + val amounts = htlcTxs.map { it.tx.txOut.first().amount.sat } assertEquals(amounts, amounts.sorted()) } @@ -736,7 +742,7 @@ class NormalTestsCommon : LightningTestSuite() { actions3.hasOutgoingMessage() assertIs>(bob3) assertTrue(bob3.commitments.latest.localCommit.spec.htlcs.incomings().any { it.id == htlc.id }) - assertEquals(1, bob3.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) + assertEquals(1, bob3.commitments.latest.localCommit.htlcRemoteSigs.size) assertEquals(bob1.commitments.latest.localCommit.spec.toLocal, bob3.commitments.latest.localCommit.spec.toLocal) assertEquals(0, bob3.commitments.changes.remoteChanges.acked.size) assertEquals(1, bob3.commitments.changes.remoteChanges.signed.size) @@ -756,8 +762,8 @@ class NormalTestsCommon : LightningTestSuite() { val (alice3, _) = alice2.process(ChannelCommand.MessageReceived(commitSig)) assertIs>(alice3) assertTrue(alice3.commitments.latest.localCommit.spec.htlcs.outgoings().any { it.id == htlc.id }) - assertEquals(1, alice3.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) - assertEquals(1, alice3.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) + assertEquals(1, alice3.commitments.latest.localCommit.htlcRemoteSigs.size) + assertEquals(1, alice3.commitments.latest.localCommit.htlcRemoteSigs.size) assertEquals(bob1.commitments.latest.localCommit.spec.toLocal, bob3.commitments.latest.localCommit.spec.toLocal) } @@ -785,14 +791,14 @@ class NormalTestsCommon : LightningTestSuite() { val (alice9, _) = alice8.process(ChannelCommand.MessageReceived(commitSig)) assertIs>(alice9) assertEquals(1, alice9.commitments.latest.localCommit.index) - assertEquals(3, alice9.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) + assertEquals(3, alice9.commitments.latest.localCommit.htlcRemoteSigs.size) } @Test fun `recv CommitSig -- multiple htlcs in both directions -- non-initiator pays commit fees`() { val (alice0, bob0) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) - assertFalse(alice0.commitments.params.localParams.paysCommitTxFees) - assertTrue(bob0.commitments.params.localParams.paysCommitTxFees) + assertFalse(alice0.commitments.channelParams.localParams.paysCommitTxFees) + assertTrue(bob0.commitments.channelParams.localParams.paysCommitTxFees) val (nodes1, _, _) = addHtlc(75_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 val (nodes2, _, _) = addHtlc(500_000.msat, alice1, bob1) @@ -802,8 +808,8 @@ class NormalTestsCommon : LightningTestSuite() { val (nodes4, _, _) = addHtlc(100_000.msat, bob3, alice3) val (bob4, alice4) = nodes4 val (alice5, bob5) = crossSign(alice4, bob4) - assertEquals(2, alice5.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) - assertEquals(2, bob5.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) + assertEquals(2, alice5.commitments.latest.localCommit.htlcRemoteSigs.size) + assertEquals(2, bob5.commitments.latest.localCommit.htlcRemoteSigs.size) // Alice opened the channel, but Bob is paying the commitment fees. assertEquals(alice5.commitments.latest.localCommit.spec.toLocal - alice5.commitments.latest.localChannelReserve.toMilliSatoshi(), alice5.commitments.availableBalanceForSend()) assertTrue(bob5.commitments.availableBalanceForSend() < bob5.commitments.latest.localCommit.spec.toLocal - bob5.commitments.latest.localChannelReserve.toMilliSatoshi()) @@ -845,61 +851,51 @@ class NormalTestsCommon : LightningTestSuite() { val (_, bob3) = crossSign(alice2, bob2) assertIs>(bob3) assertTrue(bob3.commitments.latest.localCommit.spec.htlcs.incomings().any { it.id == htlc1.id }) - assertEquals(2, bob3.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) + assertEquals(2, bob3.commitments.latest.localCommit.htlcRemoteSigs.size) assertEquals(bob2.commitments.latest.localCommit.spec.toLocal, bob3.commitments.latest.localCommit.spec.toLocal) - assertEquals(2, bob3.commitments.latest.localCommit.publishableTxs.commitTx.tx.txOut.count { it.amount == 50_000.sat }) + assertEquals(2, bob3.signCommitTx().txOut.count { it.amount == 50_000.sat }) } @Test fun `recv CommitSig -- no changes`() { val (_, bob0) = reachNormal() - val tx = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val tx = bob0.signCommitTx() // signature is invalid but it doesn't matter - val sig = CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, emptyList()) + val sig = CommitSig(ByteVector32.Zeroes, ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes), emptyList()) val (bob1, actionsBob1) = bob0.process(ChannelCommand.MessageReceived(sig)) assertIs>(bob1) actionsBob1.hasOutgoingMessage() - assertEquals(2, actionsBob1.filterIsInstance().count()) - assertEquals(tx, actionsBob1.filterIsInstance().first().tx) - assertEquals(tx.txid, actionsBob1.filterIsInstance().last().tx.txIn.first().outPoint.txid) + assertEquals(2, actionsBob1.findPublishTxs().size) + actionsBob1.hasPublishTx(tx) + val mainTx = actionsBob1.hasPublishTx(Type.ClaimLocalDelayedOutputTx) + Transaction.correctlySpends(mainTx, listOf(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assertEquals(2, actionsBob1.findWatches().count()) - actionsBob1.findWatches().first().run { - assertEquals(WatchConfirmed.ClosingTxConfirmed, event) - assertEquals(tx.txid, txId) - } - actionsBob1.findWatches().last().run { - assertEquals(WatchConfirmed.ClosingTxConfirmed, event) - assertEquals(actionsBob1.filterIsInstance().last().tx.txid, txId) - } + assertEquals(1, actionsBob1.findWatches().count()) + actionsBob1.hasWatchConfirmed(tx.txid) + actionsBob1.hasWatchOutputSpent(mainTx.txIn.first().outPoint) } @Test fun `recv CommitSig -- invalid signature`() { val (_, bob0) = reachNormal() - val tx = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val tx = bob0.signCommitTx() // signature is invalid but it doesn't matter - val sig = CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, emptyList()) + val sig = CommitSig(ByteVector32.Zeroes, ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes), emptyList()) val (bob1, actionsBob1) = bob0.process(ChannelCommand.MessageReceived(sig)) assertIs>(bob1) actionsBob1.hasOutgoingMessage() - assertEquals(2, actionsBob1.filterIsInstance().count()) - assertEquals(tx, actionsBob1.filterIsInstance().first().tx) - assertEquals(tx.txid, actionsBob1.filterIsInstance().last().tx.txIn.first().outPoint.txid) + assertEquals(2, actionsBob1.findPublishTxs().size) + actionsBob1.hasPublishTx(tx) + val mainTx = actionsBob1.hasPublishTx(Type.ClaimLocalDelayedOutputTx) + Transaction.correctlySpends(mainTx, listOf(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assertEquals(2, actionsBob1.findWatches().count()) - actionsBob1.findWatches().first().run { - assertEquals(WatchConfirmed.ClosingTxConfirmed, event) - assertEquals(tx.txid, txId) - } - actionsBob1.findWatches().last().run { - assertEquals(WatchConfirmed.ClosingTxConfirmed, event) - assertEquals(actionsBob1.filterIsInstance().last().tx.txid, txId) - } + assertEquals(1, actionsBob1.findWatches().count()) + actionsBob1.hasWatchConfirmed(tx.txid) + actionsBob1.hasWatchOutputSpent(mainTx.txIn.first().outPoint) } @Test @@ -907,7 +903,7 @@ class NormalTestsCommon : LightningTestSuite() { val (alice0, bob0) = reachNormal() val (alice1, bob1) = addHtlc(50_000_000.msat, alice0, bob0).first assertIs>(bob1) - val tx = bob1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val tx = bob1.signCommitTx() val (_, actionsAlice2) = alice1.process(ChannelCommand.Commitment.Sign) val commitSig = actionsAlice2.findOutgoingMessage() @@ -916,19 +912,14 @@ class NormalTestsCommon : LightningTestSuite() { val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(badCommitSig)) assertIs>(bob2) actionsBob2.hasOutgoingMessage() - assertEquals(2, actionsBob2.filterIsInstance().count()) - assertEquals(tx, actionsBob2.filterIsInstance().first().tx) - assertEquals(tx.txid, actionsBob2.filterIsInstance().last().tx.txIn.first().outPoint.txid) - - assertEquals(2, actionsBob2.findWatches().count()) - actionsBob2.findWatches().first().run { - assertEquals(WatchConfirmed.ClosingTxConfirmed, event) - assertEquals(tx.txid, txId) - } - actionsBob2.findWatches().last().run { - assertEquals(WatchConfirmed.ClosingTxConfirmed, event) - assertEquals(actionsBob2.filterIsInstance().last().tx.txid, txId) - } + assertEquals(2, actionsBob2.findPublishTxs().count()) + actionsBob2.hasPublishTx(tx) + val mainTx = actionsBob2.hasPublishTx(Type.ClaimLocalDelayedOutputTx) + Transaction.correctlySpends(mainTx, listOf(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + assertEquals(1, actionsBob2.findWatches().count()) + actionsBob2.hasWatchConfirmed(tx.txid) + actionsBob2.hasWatchOutputSpent(mainTx.txIn.first().outPoint) } @Test @@ -936,27 +927,22 @@ class NormalTestsCommon : LightningTestSuite() { val (alice0, bob0) = reachNormal() val (alice1, bob1) = addHtlc(50_000_000.msat, alice0, bob0).first assertIs>(bob1) - val tx = bob1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val tx = bob1.signCommitTx() val (_, actionsAlice2) = alice1.process(ChannelCommand.Commitment.Sign) val commitSig = actionsAlice2.findOutgoingMessage() - val badCommitSig = commitSig.copy(htlcSignatures = listOf(commitSig.signature)) + val badCommitSig = commitSig.copy(htlcSignatures = listOf(randomBytes64())) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(badCommitSig)) assertIs>(bob2) actionsBob2.hasOutgoingMessage() - assertEquals(2, actionsBob2.filterIsInstance().count()) - assertEquals(tx, actionsBob2.filterIsInstance().first().tx) - assertEquals(tx.txid, actionsBob2.filterIsInstance().last().tx.txIn.first().outPoint.txid) - - assertEquals(2, actionsBob2.findWatches().count()) - actionsBob2.findWatches().first().run { - assertEquals(WatchConfirmed.ClosingTxConfirmed, event) - assertEquals(tx.txid, txId) - } - actionsBob2.findWatches().last().run { - assertEquals(WatchConfirmed.ClosingTxConfirmed, event) - assertEquals(actionsBob2.filterIsInstance().last().tx.txid, txId) - } + assertEquals(2, actionsBob2.findPublishTxs().count()) + actionsBob2.hasPublishTx(tx) + val mainTx = actionsBob2.hasPublishTx(Type.ClaimLocalDelayedOutputTx) + Transaction.correctlySpends(mainTx, listOf(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + assertEquals(1, actionsBob2.findWatches().count()) + actionsBob2.hasWatchConfirmed(tx.txid) + actionsBob2.hasWatchOutputSpent(mainTx.txIn.first().outPoint) } @Test @@ -1069,7 +1055,7 @@ class NormalTestsCommon : LightningTestSuite() { val (alice0, bob0) = reachNormal() val (alice1, bob1) = addHtlc(50_000_000.msat, alice0, bob0).first assertIs>(alice1) - val tx = alice1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val tx = alice1.signCommitTx() val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Commitment.Sign) val commitSig0 = actionsAlice2.findOutgoingMessage() @@ -1080,7 +1066,7 @@ class NormalTestsCommon : LightningTestSuite() { assertIs>(alice3) actionsAlice3.hasOutgoingMessage() assertEquals(2, actionsAlice3.filterIsInstance().count()) - assertEquals(2, actionsAlice3.findWatches().count()) + assertEquals(1, actionsAlice3.findWatches().count()) actionsAlice3.hasPublishTx(tx) } @@ -1090,13 +1076,13 @@ class NormalTestsCommon : LightningTestSuite() { val (alice1, _) = addHtlc(50_000_000.msat, alice0, bob0).first assertIs>(alice1) assertTrue(alice1.commitments.remoteNextCommitInfo.isRight) - val tx = alice1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val tx = alice1.signCommitTx() val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey()))) assertIs>(alice2) actionsAlice2.hasOutgoingMessage() assertEquals(2, actionsAlice2.filterIsInstance().count()) - assertEquals(2, actionsAlice2.findWatches().count()) + assertEquals(1, actionsAlice2.findWatches().count()) actionsAlice2.hasPublishTx(tx) } @@ -1161,13 +1147,12 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv UpdateFulfillHtlc -- unknown htlc id`() { val (alice0, _) = reachNormal() - val commitTx = alice0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice0.signCommitTx() val (alice1, actions1) = alice0.process(ChannelCommand.MessageReceived(UpdateFulfillHtlc(alice0.channelId, 42, randomBytes32()))) assertIs>(alice1) assertTrue(actions1.contains(ChannelAction.Storage.StoreState(alice1.state))) assertEquals(commitTx, actions1.hasPublishTx(Type.CommitTx)) - assertTrue(actions1.findWatches().isNotEmpty()) - assertTrue(actions1.filterIsInstance().all { it.watch is WatchConfirmed }) + actions1.hasWatchConfirmed(commitTx.txid) val error = actions1.findOutgoingMessage() assertEquals(error.toAscii(), UnknownHtlcId(alice0.channelId, 42).message) } @@ -1179,13 +1164,12 @@ class NormalTestsCommon : LightningTestSuite() { val (alice1, actions1) = nodes.first.process(ChannelCommand.Commitment.Sign) assertIs>(alice1) actions1.findOutgoingMessage() - val commitTx = alice1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice1.signCommitTx() val (alice2, actions2) = alice1.process(ChannelCommand.MessageReceived(UpdateFulfillHtlc(alice1.channelId, htlc.id, r))) assertIs>(alice2) assertTrue(actions2.contains(ChannelAction.Storage.StoreState(alice2.state))) assertEquals(commitTx, actions2.hasPublishTx(Type.CommitTx)) - assertTrue(actions2.findWatches().isNotEmpty()) - assertTrue(actions2.filterIsInstance().all { it.watch is WatchConfirmed }) + actions2.hasWatchConfirmed(commitTx.txid) val error = actions2.findOutgoingMessage() assertEquals(error.toAscii(), UnknownHtlcId(alice0.channelId, 0).message) } @@ -1196,13 +1180,16 @@ class NormalTestsCommon : LightningTestSuite() { val (nodes, _, htlc) = addHtlc(50_000_000.msat, alice0, bob0) val (alice1, _) = crossSign(nodes.first, nodes.second) assertIs>(alice1) - val commitTx = alice1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice1.signCommitTx() val (alice2, actions2) = alice1.process(ChannelCommand.MessageReceived(UpdateFulfillHtlc(alice0.channelId, htlc.id, ByteVector32.Zeroes))) assertIs>(alice2) assertTrue(actions2.contains(ChannelAction.Storage.StoreState(alice2.state))) assertEquals(commitTx, actions2.hasPublishTx(Type.CommitTx)) - assertEquals(4, actions2.filterIsInstance().size) // commit tx + main delayed + htlc-timeout + htlc delayed - assertEquals(4, actions2.filterIsInstance().size) + actions2.hasWatchConfirmed(commitTx.txid) + val mainTx = actions2.hasPublishTx(Type.ClaimLocalDelayedOutputTx) + actions2.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + val htlcTimeoutTx = actions2.hasPublishTx(Type.HtlcTimeoutTx) + actions2.hasWatchOutputSpent(htlcTimeoutTx.txIn.first().outPoint) val error = actions2.findOutgoingMessage() assertEquals(error.toAscii(), InvalidHtlcPreimage(alice0.channelId, htlc.id).message) } @@ -1290,13 +1277,12 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv UpdateFailHtlc -- unknown htlc id`() { val (alice0, _) = reachNormal() - val commitTx = alice0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice0.signCommitTx() val (alice1, actions1) = alice0.process(ChannelCommand.MessageReceived(UpdateFailHtlc(alice0.channelId, 42, ByteVector.empty))) assertIs>(alice1) assertTrue(actions1.contains(ChannelAction.Storage.StoreState(alice1.state))) assertEquals(commitTx, actions1.hasPublishTx(Type.CommitTx)) - assertTrue(actions1.findWatches().isNotEmpty()) - assertTrue(actions1.filterIsInstance().all { it.watch is WatchConfirmed }) + actions1.hasWatchConfirmed(commitTx.txid) val error = actions1.findOutgoingMessage() assertEquals(error.toAscii(), UnknownHtlcId(alice0.channelId, 42).message) } @@ -1308,13 +1294,12 @@ class NormalTestsCommon : LightningTestSuite() { val (alice1, actions1) = nodes.first.process(ChannelCommand.Commitment.Sign) assertIs>(alice1) actions1.findOutgoingMessage() - val commitTx = alice1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice1.signCommitTx() val (alice2, actions2) = alice1.process(ChannelCommand.MessageReceived(UpdateFailHtlc(alice1.channelId, htlc.id, ByteVector.empty))) assertIs>(alice2) assertTrue(actions2.contains(ChannelAction.Storage.StoreState(alice2.state))) assertEquals(commitTx, actions2.hasPublishTx(Type.CommitTx)) - assertTrue(actions2.findWatches().isNotEmpty()) - assertTrue(actions2.filterIsInstance().all { it.watch is WatchConfirmed }) + actions2.hasWatchConfirmed(commitTx.txid) val error = actions2.findOutgoingMessage() assertEquals(error.toAscii(), UnknownHtlcId(alice0.channelId, 0).message) } @@ -1340,13 +1325,12 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv UpdateFailMalformedHtlc -- unknown htlc id`() { val (alice0, _) = reachNormal() - val commitTx = alice0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice0.signCommitTx() val (alice1, actions1) = alice0.process(ChannelCommand.MessageReceived(UpdateFailMalformedHtlc(alice0.channelId, 42, ByteVector32.Zeroes, FailureMessage.BADONION))) assertIs>(alice1) assertTrue(actions1.contains(ChannelAction.Storage.StoreState(alice1.state))) assertEquals(commitTx, actions1.hasPublishTx(Type.CommitTx)) - assertTrue(actions1.findWatches().isNotEmpty()) - assertTrue(actions1.filterIsInstance().all { it.watch is WatchConfirmed }) + actions1.hasWatchConfirmed(commitTx.txid) val error = actions1.findOutgoingMessage() assertEquals(error.toAscii(), UnknownHtlcId(alice0.channelId, 42).message) } @@ -1357,14 +1341,17 @@ class NormalTestsCommon : LightningTestSuite() { val (nodes, _, htlc) = addHtlc(50_000_000.msat, alice0, bob0) val (alice1, _) = crossSign(nodes.first, nodes.second) assertIs>(alice1) - val commitTx = alice1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice1.signCommitTx() val (alice2, actions2) = alice1.process(ChannelCommand.MessageReceived(UpdateFailMalformedHtlc(alice0.channelId, htlc.id, Sphinx.hash(htlc.onionRoutingPacket), 42))) assertIs>(alice2) assertTrue(actions2.contains(ChannelAction.Storage.StoreState(alice2.state))) assertEquals(commitTx, actions2.hasPublishTx(Type.CommitTx)) - assertEquals(4, actions2.filterIsInstance().size) // commit tx + main delayed + htlc-timeout + htlc delayed - assertEquals(4, actions2.filterIsInstance().size) + actions2.hasWatchConfirmed(commitTx.txid) + val mainTx = actions2.hasPublishTx(Type.ClaimLocalDelayedOutputTx) + actions2.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + val htlcTimeoutTx = actions2.hasPublishTx(Type.HtlcTimeoutTx) + actions2.hasWatchOutputSpent(htlcTimeoutTx.txIn.first().outPoint) val error = actions2.findOutgoingMessage() assertEquals(error.toAscii(), InvalidFailureCode(alice0.channelId).message) } @@ -1412,13 +1399,13 @@ class NormalTestsCommon : LightningTestSuite() { val (nodes, _, _) = addHtlc(alice.commitments.availableBalanceForSend(), alice, bob) val (_, bob1) = crossSign(nodes.first, nodes.second) assertIs>(bob1) - val commitTx = bob1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = bob1.signCommitTx() val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw.CommitmentFeerate * 4) val (bob2, actions) = bob1.process(ChannelCommand.MessageReceived(fee)) assertIs>(bob2) actions.hasPublishTx(commitTx) - actions.hasWatch() + actions.hasWatchConfirmed(commitTx.txid) val error = actions.findOutgoingMessage() assertEquals(error.toAscii(), CannotAffordFees(bob.channelId, missing = 11_240.sat, reserve = 10_000.sat, fees = 26_580.sat).message) } @@ -1429,8 +1416,9 @@ class NormalTestsCommon : LightningTestSuite() { assertEquals(FeeratePerKw.CommitmentFeerate, bob.commitments.latest.localCommit.spec.feerate) val (bob1, actions) = bob.process(ChannelCommand.MessageReceived(UpdateFee(bob.channelId, FeeratePerKw(252.sat)))) assertIs>(bob1) - actions.hasPublishTx(bob.commitments.latest.localCommit.publishableTxs.commitTx.tx) - actions.hasWatch() + val commitTx = bob.signCommitTx() + actions.hasPublishTx(commitTx) + actions.hasWatchConfirmed(commitTx.txid) val error = actions.findOutgoingMessage() assertTrue(error.toAscii().contains("emote fee rate is too small: remoteFeeratePerKw=252")) } @@ -1554,7 +1542,7 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv Shutdown -- no pending htlcs`() { val (alice, bob) = reachNormal() - val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(Shutdown(alice.channelId, bob.commitments.params.localParams.defaultFinalScriptPubKey))) + val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(Shutdown(alice.channelId, bob.commitments.channelParams.localParams.defaultFinalScriptPubKey))) assertIs>(alice1) actions1.hasOutgoingMessage() } @@ -1586,7 +1574,7 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv Shutdown -- with unacked received htlcs`() { val (alice, bob) = reachNormal() val (nodes, _, _) = addHtlc(50000000.msat, payer = alice, payee = bob) - val (bob1, actions1) = nodes.second.process(ChannelCommand.MessageReceived(Shutdown(alice.channelId, alice.commitments.params.localParams.defaultFinalScriptPubKey))) + val (bob1, actions1) = nodes.second.process(ChannelCommand.MessageReceived(Shutdown(alice.channelId, alice.commitments.channelParams.localParams.defaultFinalScriptPubKey))) assertIs>(bob1) actions1.hasOutgoingMessage() assertEquals(2, actions1.filterIsInstance().count()) @@ -1670,7 +1658,7 @@ class NormalTestsCommon : LightningTestSuite() { val (_, bob1) = crossSign(nodes.first, nodes.second) // actual test begins - val (bob2, actions1) = bob1.process(ChannelCommand.MessageReceived(Shutdown(bob.channelId, alice.commitments.params.localParams.defaultFinalScriptPubKey))) + val (bob2, actions1) = bob1.process(ChannelCommand.MessageReceived(Shutdown(bob.channelId, alice.commitments.channelParams.localParams.defaultFinalScriptPubKey))) assertIs>(bob2) actions1.hasOutgoingMessage() } @@ -1762,7 +1750,7 @@ class NormalTestsCommon : LightningTestSuite() { // bob -> alice : 50 000 000 (alice has the preimage) => spend immediately using the preimage // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout - val bobCommitTx = bob8.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob8.signCommitTx() assertEquals(8, bobCommitTx.txOut.size) // 2 main outputs, 4 pending htlcs and 2 anchors val (aliceClosing, actions) = alice8.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice8.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTx))) @@ -1770,28 +1758,30 @@ class NormalTestsCommon : LightningTestSuite() { assertTrue(actions.isNotEmpty()) // in response to that, alice publishes its claim txs - val claimTxs = actions.filterIsInstance().map { it.tx } - assertEquals(4, claimTxs.size) // in addition to its main output, alice can only claim 3 out of 4 htlcs, // she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val amountClaimed = claimTxs.map { claimHtlcTx -> - assertEquals(1, claimHtlcTx.txIn.size) - assertEquals(1, claimHtlcTx.txOut.size) - Transaction.correctlySpends(claimHtlcTx, listOf(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - claimHtlcTx.txOut[0].amount + val mainTx = actions.hasPublishTx(Type.ClaimRemoteDelayedOutputTx) + val htlcSuccessTxs = actions.findPublishTxs(Type.ClaimHtlcSuccessTx) + assertEquals(1, htlcSuccessTxs.size) + val htlcTimeoutTxs = actions.findPublishTxs(Type.ClaimHtlcTimeoutTx) + assertEquals(2, htlcTimeoutTxs.size) + val amountClaimed = (listOf(mainTx) + htlcSuccessTxs + htlcTimeoutTxs).map { claimTx -> + assertEquals(1, claimTx.txIn.size) + assertEquals(1, claimTx.txOut.size) + Transaction.correctlySpends(claimTx, listOf(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + claimTx.txOut[0].amount }.sum() - // at best we have a little less than 450 000 + 250 000 + 100 000 + 50 000 = 850 000 (because fees) - assertEquals(879_710.sat, amountClaimed) + // at best we have a little less than 500 000 + 250 000 + 100 000 + 50 000 = 900 000 (because fees) + assertEquals(879_720.sat, amountClaimed) - val rcp = aliceClosing.state.remoteCommitPublished!! - val watchConfirmed = actions.findWatches() - assertEquals(2, watchConfirmed.size) - assertEquals(bobCommitTx.txid, watchConfirmed[0].txId) - assertEquals(rcp.claimMainOutputTx!!.tx.txid, watchConfirmed[1].txId) - assertEquals(4, actions.findWatches().count { it.event is WatchSpent.ClosingOutputSpent }) - assertEquals(4, rcp.claimHtlcTxs.size) - assertEquals(1, rcp.claimHtlcSuccessTxs().size) - assertEquals(2, rcp.claimHtlcTimeoutTxs().size) + val rcp = aliceClosing.state.remoteCommitPublished + assertNotNull(rcp) + assertEquals(4, rcp.htlcOutputs.size) + actions.hasWatchConfirmed(bobCommitTx.txid) + assertEquals(1, actions.findWatches().size) + actions.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + actions.hasWatchOutputsSpent(rcp.htlcOutputs) + actions.findWatches().forEach { assertIs(it.event) } } @Test @@ -1832,7 +1822,7 @@ class NormalTestsCommon : LightningTestSuite() { // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout // bob publishes his current commit tx - val bobCommitTx = bob9.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob9.signCommitTx() assertEquals(7, bobCommitTx.txOut.size) // 2 main outputs, 3 pending htlcs and 2 anchors val (aliceClosing, actions9) = alice9.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice9.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTx))) @@ -1840,28 +1830,29 @@ class NormalTestsCommon : LightningTestSuite() { assertTrue(actions9.isNotEmpty()) // in response to that, alice publishes its claim txs - val claimTxs = actions9.filterIsInstance().map { it.tx } - assertEquals(3, claimTxs.size) // alice can only claim 2 out of 3 htlcs, // she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val amountClaimed = claimTxs.map { claimHtlcTx -> - assertEquals(1, claimHtlcTx.txIn.size) - assertEquals(1, claimHtlcTx.txOut.size) - Transaction.correctlySpends(claimHtlcTx, listOf(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - claimHtlcTx.txOut[0].amount + val mainTx = actions9.hasPublishTx(Type.ClaimRemoteDelayedOutputTx) + assertTrue(actions9.findPublishTxs(Type.ClaimHtlcSuccessTx).isEmpty()) + val htlcTimeoutTxs = actions9.findPublishTxs(Type.ClaimHtlcTimeoutTx) + assertEquals(2, htlcTimeoutTxs.size) + val amountClaimed = (listOf(mainTx) + htlcTimeoutTxs).map { claimTx -> + assertEquals(1, claimTx.txIn.size) + assertEquals(1, claimTx.txOut.size) + Transaction.correctlySpends(claimTx, listOf(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + claimTx.txOut[0].amount }.sum() // at best we have a little less than 500 000 + 250 000 + 100 000 = 850 000 (because fees) - assertEquals(883_440.sat, amountClaimed) + assertEquals(883_450.sat, amountClaimed) - val rcp = aliceClosing.state.nextRemoteCommitPublished!! - val watchConfirmed = actions9.findWatches() - assertEquals(2, watchConfirmed.size) - assertEquals(bobCommitTx.txid, watchConfirmed[0].txId) - assertEquals(rcp.claimMainOutputTx!!.tx.txid, watchConfirmed[1].txId) - assertEquals(3, actions9.findWatches().count { it.event is WatchSpent.ClosingOutputSpent }) - assertEquals(3, rcp.claimHtlcTxs.size) - assertEquals(0, rcp.claimHtlcSuccessTxs().size) - assertEquals(2, rcp.claimHtlcTimeoutTxs().size) + val rcp = aliceClosing.state.nextRemoteCommitPublished + assertNotNull(rcp) + assertEquals(3, rcp.htlcOutputs.size) + actions9.hasWatchConfirmed(bobCommitTx.txid) + assertEquals(1, actions9.findWatches().size) + actions9.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + actions9.hasWatchOutputsSpent(rcp.htlcOutputs) + actions9.findWatches().forEach { assertIs(it.event) } } @Test @@ -1888,7 +1879,7 @@ class NormalTestsCommon : LightningTestSuite() { alice = first bob = second } - return Pair(bob.commitments.latest.localCommit.publishableTxs.commitTx.tx, add) + return Pair(bob.signCommitTx(), add) } val (txs, adds) = run { @@ -1915,6 +1906,11 @@ class NormalTestsCommon : LightningTestSuite() { assertIs>(aliceClosing1) assertEquals(1, aliceClosing1.state.revokedCommitPublished.size) actions1.hasOutgoingMessage() + val mainTx = actions1.hasPublishTx(Type.ClaimRemoteDelayedOutputTx) + actions1.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + val penaltyTx = actions1.hasPublishTx(Type.MainPenaltyTx) + actions1.hasWatchOutputSpent(penaltyTx.txIn.first().outPoint) + listOf(mainTx, penaltyTx).forEach { Transaction.correctlySpends(it, revokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } assertEquals(ChannelAction.Storage.GetHtlcInfos(revokedTx.txid, 4), actions1.find()) val (aliceClosing2, actions2) = aliceClosing1.process(ChannelCommand.Closing.GetHtlcInfosResponse(revokedTx.txid, adds.take(4).map { ChannelAction.Storage.HtlcInfo(alice.channelId, 4, it.paymentHash, it.cltvExpiry) })) @@ -1922,29 +1918,21 @@ class NormalTestsCommon : LightningTestSuite() { assertEquals(1, aliceClosing2.state.revokedCommitPublished.size) assertNull(actions2.findOutgoingMessageOpt()) assertNull(actions2.findOpt()) - - val claimTxs = actions2.findPublishTxs() - assertEquals(6, claimTxs.size) - val mainOutputTx = claimTxs[0] - val mainPenaltyTx = claimTxs[1] - Transaction.correctlySpends(mainPenaltyTx, revokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assertEquals(setOf(revokedTx.txid, mainOutputTx.txid), actions2.findWatches().map { it.txId }.toSet()) - - val htlcPenaltyTxs = claimTxs.drop(2) + val htlcPenaltyTxs = actions2.findPublishTxs() + assertEquals(4, htlcPenaltyTxs.size) assertTrue(htlcPenaltyTxs.all { it.txIn.size == 1 }) htlcPenaltyTxs.forEach { Transaction.correctlySpends(it, revokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } val htlcInputs = htlcPenaltyTxs.map { it.txIn.first().outPoint }.toSet() assertEquals(4, htlcInputs.size) // each htlc-penalty tx spends a different output - assertEquals(5, actions2.findWatches().count { it.event is WatchSpent.ClosingOutputSpent }) - assertEquals(htlcInputs + mainPenaltyTx.txIn.first().outPoint, actions2.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet()) + actions2.hasWatchOutputsSpent(htlcInputs) // two main outputs are 760 000 and 200 000 (minus fees) - assertEquals(798_070.sat, mainOutputTx.txOut[0].amount) - assertEquals(147_580.sat, mainPenaltyTx.txOut[0].amount) - assertEquals(7_095.sat, htlcPenaltyTxs[0].txOut[0].amount) - assertEquals(7_095.sat, htlcPenaltyTxs[1].txOut[0].amount) - assertEquals(7_095.sat, htlcPenaltyTxs[2].txOut[0].amount) - assertEquals(7_095.sat, htlcPenaltyTxs[3].txOut[0].amount) + assertEquals(798_070.sat, mainTx.txOut[0].amount) + assertEquals(147_585.sat, penaltyTx.txOut[0].amount) + assertEquals(7_100.sat, htlcPenaltyTxs[0].txOut[0].amount) + assertEquals(7_100.sat, htlcPenaltyTxs[1].txOut[0].amount) + assertEquals(7_100.sat, htlcPenaltyTxs[2].txOut[0].amount) + assertEquals(7_100.sat, htlcPenaltyTxs[3].txOut[0].amount) } @Test @@ -1975,11 +1963,11 @@ class NormalTestsCommon : LightningTestSuite() { actions.has() val lcp = alice3.state.localCommitPublished actions.hasPublishTx(lcp.commitTx) - assertEquals(1, lcp.htlcTimeoutTxs().size) - assertEquals(1, lcp.claimHtlcDelayedTxs.size) - assertEquals(4, actions.findPublishTxs().size) // commit tx + main output + htlc-timeout + claim-htlc-delayed - assertEquals(3, actions.findWatches().size) // commit tx + main output + claim-htlc-delayed - assertEquals(1, actions.findWatches().size) // htlc-timeout + val mainTx = actions.hasPublishTx(Type.ClaimLocalDelayedOutputTx) + val htlcTimeoutTx = actions.hasPublishTx(Type.HtlcTimeoutTx) + actions.hasWatchConfirmed(lcp.commitTx.txid) + actions.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + actions.hasWatchOutputSpent(htlcTimeoutTx.txIn.first().outPoint) } private fun checkFulfillTimeout(bob: LNChannel, actions: List) { @@ -1989,18 +1977,28 @@ class NormalTestsCommon : LightningTestSuite() { actions.has() val lcp = bob.state.localCommitPublished - assertNotNull(lcp.claimMainDelayedOutputTx) - assertEquals(1, lcp.htlcSuccessTxs().size) - Transaction.correctlySpends(lcp.htlcSuccessTxs().first().tx, lcp.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assertEquals(1, lcp.claimHtlcDelayedTxs.size) - Transaction.correctlySpends(lcp.claimHtlcDelayedTxs.first().tx, lcp.htlcSuccessTxs().first().tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val txs = setOf(lcp.commitTx, lcp.claimMainDelayedOutputTx.tx, lcp.htlcSuccessTxs().first().tx, lcp.claimHtlcDelayedTxs.first().tx) - assertEquals(txs, actions.findPublishTxs().toSet()) - val watchConfirmed = listOf(lcp.commitTx, lcp.claimMainDelayedOutputTx.tx, lcp.claimHtlcDelayedTxs.first().tx).map { it.txid }.toSet() - assertEquals(watchConfirmed, actions.findWatches().map { it.txId }.toSet()) - val watchSpent = setOf(lcp.htlcSuccessTxs().first().input.outPoint) - assertEquals(watchSpent, actions.findWatches().map { OutPoint(lcp.commitTx, it.outputIndex.toLong()) }.toSet()) + assertNotNull(lcp.localOutput) + assertEquals(1, lcp.htlcOutputs.size) + assertTrue(lcp.htlcDelayedOutputs.isEmpty()) + + val mainTx = actions.hasPublishTx(Type.ClaimLocalDelayedOutputTx) + actions.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + val htlcSuccessTxs = actions.findPublishTxs(Type.HtlcSuccessTx) + assertEquals(1, htlcSuccessTxs.size) + val htlcSuccessTx = htlcSuccessTxs.first() + actions.hasWatchOutputSpent(htlcSuccessTx.txIn.first().outPoint) + Transaction.correctlySpends(htlcSuccessTx, lcp.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val (bob1, actions1) = bob.process(ChannelCommand.WatchReceived(WatchSpentTriggered(bob.channelId, WatchSpent.ClosingOutputSpent(htlcSuccessTx.txOut[0].amount), htlcSuccessTx))) + actions1.hasWatchConfirmed(htlcSuccessTx.txid) + + val (bob2, actions2) = bob1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 7, htlcSuccessTx))) + assertIs>(bob2) + assertNotNull(bob2.state.localCommitPublished) + assertEquals(setOf(OutPoint(htlcSuccessTx, 0)), bob2.state.localCommitPublished.htlcDelayedOutputs) + val htlcDelayedTx = actions2.hasPublishTx(Type.HtlcDelayedTx) + actions2.hasWatchOutputSpent(htlcDelayedTx.txIn.first().outPoint) + Transaction.correctlySpends(htlcDelayedTx, htlcSuccessTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @Test @@ -2115,57 +2113,48 @@ class NormalTestsCommon : LightningTestSuite() { // an error occurs and alice publishes her commit tx assertIs>(alice3) - val aliceCommitTx = alice3.commitments.latest.localCommit.publishableTxs.commitTx + val aliceCommitTx = alice3.signCommitTx() + assertEquals(aliceCommitTx.txOut.size, 8) // 2 anchor outputs + 2 main output + 4 pending htlcs val (alice4, actions) = alice3.process(ChannelCommand.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) assertIs>(alice4) assertNotNull(alice4.state.localCommitPublished) - assertEquals(alice4.state.localCommitPublished.commitTx, aliceCommitTx.tx) - assertEquals(4, alice4.state.localCommitPublished.htlcTxs.size) - assertEquals(1, alice4.state.localCommitPublished.htlcSuccessTxs().size) - assertEquals(2, alice4.state.localCommitPublished.htlcTimeoutTxs().size) - assertEquals(3, alice4.state.localCommitPublished.claimHtlcDelayedTxs.size) + assertEquals(alice4.state.localCommitPublished.commitTx, aliceCommitTx) + assertNotNull(alice4.state.localCommitPublished.localOutput) + assertEquals(4, alice4.state.localCommitPublished.htlcOutputs.size) - val txs = actions.filterIsInstance().map { it.tx } - assertEquals(8, txs.size) // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage // so we expect 8 transactions: // - alice's current commit tx // - 1 tx to claim the main delayed output // - 3 txs for each htlc - // - 3 txs for each delayed output of the claimed htlc - - assertEquals(aliceCommitTx.tx, txs[0]) - assertEquals(aliceCommitTx.tx.txOut.size, 8) // 2 anchor outputs + 2 main output + 4 pending htlcs - // the main delayed output spends the commitment transaction - Transaction.correctlySpends(txs[1], aliceCommitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // 2nd stage transactions spend the commitment transaction - Transaction.correctlySpends(txs[2], aliceCommitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(txs[3], aliceCommitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(txs[4], aliceCommitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // 3rd stage transactions spend their respective HTLC-Success/HTLC-Timeout transactions - Transaction.correctlySpends(txs[5], txs[2], ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(txs[6], txs[3], ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(txs[7], txs[4], ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - assertEquals( - actions.findWatches().map { it.txId }, - listOf( - txs[0].txid, // commit tx - txs[1].txid, // main delayed - txs[5].txid, // htlc-delayed - txs[6].txid, // htlc-delayed - txs[7].txid // htlc-delayed - ) - ) - assertEquals(4, actions.findWatches().size) + assertEquals(5, actions.findPublishTxs().size) + actions.hasPublishTx(aliceCommitTx) + val mainTx = actions.hasPublishTx(Type.ClaimLocalDelayedOutputTx) + Transaction.correctlySpends(mainTx, aliceCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcTimeoutTxs = actions.findPublishTxs(Type.HtlcTimeoutTx) + assertEquals(2, htlcTimeoutTxs.size) + htlcTimeoutTxs.forEach { Transaction.correctlySpends(it, aliceCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + val htlcSuccessTxs = actions.findPublishTxs(Type.HtlcSuccessTx) + assertEquals(1, htlcSuccessTxs.size) + htlcSuccessTxs.forEach { Transaction.correctlySpends(it, aliceCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + actions.hasWatchConfirmed(aliceCommitTx.txid) + actions.hasWatchOutputSpent(alice4.state.localCommitPublished.localOutput) + actions.hasWatchOutputsSpent(alice4.state.localCommitPublished.htlcOutputs) + + (htlcTimeoutTxs + htlcSuccessTxs).forEach { htlcTx -> + val (alice5, actions5) = alice4.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice4.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 3, htlcTx))) + assertIs>(alice5) + val htlcDelayedTx = actions5.hasPublishTx(Type.HtlcDelayedTx) + Transaction.correctlySpends(htlcDelayedTx, listOf(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actions5.hasWatchOutputSpent(htlcDelayedTx.txIn.first().outPoint) + assertTrue(alice5.state.localCommitPublished!!.htlcDelayedOutputs.contains(htlcDelayedTx.txIn.first().outPoint)) + } } @Test fun `receive Error -- nothing at stake`() { val (_, bob0) = reachNormal(bobFundingAmount = 0.sat) - val bobCommitTx = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob0.signCommitTx() val (bob1, actions) = bob0.process(ChannelCommand.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) val txs = actions.filterIsInstance().map { it.tx } assertEquals(txs, listOf(bobCommitTx)) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt index 2e1dd992e..a88baccde 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt @@ -1,13 +1,16 @@ package fr.acinq.lightning.channel.states -import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.PrivateKey +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.Transaction import fr.acinq.lightning.CltvExpiryDelta -import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomBytes32 -import fr.acinq.lightning.blockchain.* +import fr.acinq.lightning.blockchain.WatchConfirmed +import fr.acinq.lightning.blockchain.WatchConfirmedTriggered +import fr.acinq.lightning.blockchain.WatchSpent +import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.* -import fr.acinq.lightning.channel.TestsHelper.htlcSuccessTxs -import fr.acinq.lightning.channel.TestsHelper.htlcTimeoutTxs import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.UUID @@ -53,8 +56,8 @@ class OfflineTestsCommon : LightningTestSuite() { val (alice, bob) = TestsHelper.reachNormal(bobUsePeerStorage = false) val (alice1, bob1) = disconnect(alice, bob) - val localInit = Init(alice.commitments.params.localParams.features.initFeatures()) - val remoteInit = Init(bob.commitments.params.localParams.features.initFeatures()) + val localInit = Init(alice.commitments.channelParams.localParams.features.initFeatures()) + val remoteInit = Init(bob.commitments.channelParams.localParams.features.initFeatures()) val (alice2, actions) = alice1.process(ChannelCommand.Connected(localInit, remoteInit)) assertIs(alice2.state) @@ -65,8 +68,8 @@ class OfflineTestsCommon : LightningTestSuite() { val bobCommitments = bob.commitments val aliceCommitments = alice.commitments - val bobCurrentPerCommitmentPoint = bobCommitments.params.localParams.channelKeys(bob.ctx.keyManager).commitmentPoint(bobCommitments.localCommitIndex) - val aliceCurrentPerCommitmentPoint = aliceCommitments.params.localParams.channelKeys(alice.ctx.keyManager).commitmentPoint(aliceCommitments.localCommitIndex) + val bobCurrentPerCommitmentPoint = bob.channelKeys.commitmentPoint(bobCommitments.localCommitIndex) + val aliceCurrentPerCommitmentPoint = alice.channelKeys.commitmentPoint(aliceCommitments.localCommitIndex) // alice didn't receive any update or sig assertEquals( @@ -106,8 +109,8 @@ class OfflineTestsCommon : LightningTestSuite() { } val (alice1, bob1) = disconnect(alice0, bob0) - val localInit = Init(alice0.commitments.params.localParams.features) - val remoteInit = Init(bob0.commitments.params.localParams.features) + val localInit = Init(alice0.commitments.channelParams.localParams.features) + val remoteInit = Init(bob0.commitments.channelParams.localParams.features) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Connected(localInit, remoteInit)) assertIs(alice2.state) @@ -118,8 +121,8 @@ class OfflineTestsCommon : LightningTestSuite() { val bobCommitments = bob0.commitments val aliceCommitments = alice0.commitments - val bobCurrentPerCommitmentPoint = bobCommitments.params.localParams.channelKeys(bob0.ctx.keyManager).commitmentPoint(bobCommitments.localCommitIndex) - val aliceCurrentPerCommitmentPoint = aliceCommitments.params.localParams.channelKeys(alice0.ctx.keyManager).commitmentPoint(aliceCommitments.localCommitIndex) + val bobCurrentPerCommitmentPoint = bob0.channelKeys.commitmentPoint(bobCommitments.localCommitIndex) + val aliceCurrentPerCommitmentPoint = alice0.channelKeys.commitmentPoint(aliceCommitments.localCommitIndex) // alice didn't receive any update or sig assertEquals(channelReestablishA, ChannelReestablish(alice0.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), aliceCurrentPerCommitmentPoint)) @@ -179,8 +182,8 @@ class OfflineTestsCommon : LightningTestSuite() { } val (alice1, bob1) = disconnect(alice0, bob0) - val localInit = Init(alice0.commitments.params.localParams.features) - val remoteInit = Init(bob0.commitments.params.localParams.features) + val localInit = Init(alice0.commitments.channelParams.localParams.features) + val remoteInit = Init(bob0.commitments.channelParams.localParams.features) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Connected(localInit, remoteInit)) assertIs(alice2.state) @@ -191,8 +194,8 @@ class OfflineTestsCommon : LightningTestSuite() { val bobCommitments = bob0.commitments val aliceCommitments = alice0.commitments - val bobCurrentPerCommitmentPoint = bobCommitments.params.localParams.channelKeys(bob0.ctx.keyManager).commitmentPoint(bobCommitments.localCommitIndex) - val aliceCurrentPerCommitmentPoint = aliceCommitments.params.localParams.channelKeys(alice0.ctx.keyManager).commitmentPoint(aliceCommitments.localCommitIndex) + val bobCurrentPerCommitmentPoint = bob0.channelKeys.commitmentPoint(bobCommitments.localCommitIndex) + val aliceCurrentPerCommitmentPoint = alice0.channelKeys.commitmentPoint(aliceCommitments.localCommitIndex) // alice didn't receive any update or sig assertEquals(channelReestablishA, ChannelReestablish(alice0.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), aliceCurrentPerCommitmentPoint)) @@ -242,8 +245,8 @@ class OfflineTestsCommon : LightningTestSuite() { } val (alice1, bob1) = disconnect(alice0, bob0) - val initA = Init(alice0.commitments.params.localParams.features) - val initB = Init(bob0.commitments.params.localParams.features) + val initA = Init(alice0.commitments.channelParams.localParams.features) + val initB = Init(bob0.commitments.channelParams.localParams.features) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Connected(initA, initB)) assertIs(alice2.state) val channelReestablishA = actionsAlice2.findOutgoingMessage() @@ -307,8 +310,8 @@ class OfflineTestsCommon : LightningTestSuite() { // we manually replace alice's state with an older one val alice1 = aliceTmp1.copy(state = Offline(aliceOld.state)) - val localInit = Init(alice.commitments.params.localParams.features) - val remoteInit = Init(bob.commitments.params.localParams.features) + val localInit = Init(alice.commitments.channelParams.localParams.features) + val remoteInit = Init(bob.commitments.channelParams.localParams.features) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Connected(localInit, remoteInit)) assertIs(alice2.state) @@ -333,18 +336,18 @@ class OfflineTestsCommon : LightningTestSuite() { val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(errorA)) assertIs(bob4.state) assertNotNull(bob4.state.localCommitPublished) - val bobCommitTx = bob4.state.localCommitPublished!!.commitTx + val bobCommitTx = bob4.state.localCommitPublished.commitTx actionsBob4.hasPublishTx(bobCommitTx) // alice is able to claim her main output val (alice4, actionsAlice4) = alice3.process(ChannelCommand.WatchReceived(WatchSpentTriggered(aliceOld.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTx))) assertIs(alice4.state) assertNotNull(alice4.state.futureRemoteCommitPublished) - assertEquals(bobCommitTx, alice4.state.futureRemoteCommitPublished!!.commitTx) - assertNotNull(alice4.state.futureRemoteCommitPublished!!.claimMainOutputTx) - assertTrue(alice4.state.futureRemoteCommitPublished!!.claimHtlcTxs.isEmpty()) - actionsAlice4.hasPublishTx(alice4.state.futureRemoteCommitPublished!!.claimMainOutputTx!!.tx) - Transaction.correctlySpends(alice4.state.futureRemoteCommitPublished!!.claimMainOutputTx!!.tx, bobCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assertEquals(bobCommitTx, alice4.state.futureRemoteCommitPublished.commitTx) + assertNotNull(alice4.state.futureRemoteCommitPublished.localOutput) + assertTrue(alice4.state.futureRemoteCommitPublished.htlcOutputs.isEmpty()) + val mainTx = actionsAlice4.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + Transaction.correctlySpends(mainTx, bobCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @Test @@ -355,12 +358,12 @@ class OfflineTestsCommon : LightningTestSuite() { val (aliceTmp, bobTmp) = TestsHelper.addHtlc(250_000_000.msat, alice0, bob0).first TestsHelper.crossSign(aliceTmp, bobTmp) } - val bobCommitTx = bob1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob1.signCommitTx() // We simulate a disconnection followed by a reconnection. val (alice2, bob2) = disconnect(alice1, bob1) - val localInit = Init(alice0.commitments.params.localParams.features) - val remoteInit = Init(bob0.commitments.params.localParams.features) + val localInit = Init(alice0.commitments.channelParams.localParams.features) + val remoteInit = Init(bob0.commitments.channelParams.localParams.features) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.Connected(localInit, remoteInit)) assertIs>(alice3) actionsAlice3.findOutgoingMessage() @@ -384,17 +387,11 @@ class OfflineTestsCommon : LightningTestSuite() { assertEquals(actionsAlice5.size, 7) val remoteCommitPublished = alice5.state.remoteCommitPublished assertNotNull(remoteCommitPublished) - assertEquals(remoteCommitPublished.claimHtlcTxs.size, 1) - val claimMainTx = remoteCommitPublished.claimMainOutputTx!!.tx - val claimHtlcTx = remoteCommitPublished.claimHtlcTxs.values.first()!!.tx - listOf(claimMainTx, claimHtlcTx).forEach { Transaction.correctlySpends(it, listOf(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - actionsAlice5.hasPublishTx(claimMainTx) - actionsAlice5.hasPublishTx(claimHtlcTx) - assertEquals(actionsAlice5.findWatches().map { it.txId }.toSet(), setOf(bobCommitTx.txid, claimMainTx.txid)) - val watchHtlcOutputSpent = actionsAlice5.findWatch() - assertIs(watchHtlcOutputSpent.event) - assertEquals(watchHtlcOutputSpent.txId, remoteCommitPublished.claimHtlcTxs.keys.first().txid) - assertEquals(watchHtlcOutputSpent.outputIndex, remoteCommitPublished.claimHtlcTxs.keys.first().index.toInt()) + assertEquals(remoteCommitPublished.htlcOutputs.size, 1) + val mainTx = actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + val htlcTx = actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcTimeoutTx) + listOf(mainTx, htlcTx).forEach { Transaction.correctlySpends(it, listOf(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + actionsAlice5.hasWatchOutputsSpent(listOf(mainTx, htlcTx).flatMap { tx -> tx.txIn.map { it.outPoint } }.toSet()) actionsAlice5.has() actionsAlice5.has() } @@ -403,7 +400,7 @@ class OfflineTestsCommon : LightningTestSuite() { fun `counterparty lies about having a more recent commitment and publishes revoked commitment`() { val (alice0, bob0) = TestsHelper.reachNormal(bobUsePeerStorage = false) // We sign a new commitment to make sure the first one is revoked. - val bobRevokedCommitTx = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobRevokedCommitTx = bob0.signCommitTx() val (alice1, bob1) = run { val (aliceTmp, bobTmp) = TestsHelper.addHtlc(250_000_000.msat, alice0, bob0).first TestsHelper.crossSign(aliceTmp, bobTmp) @@ -411,8 +408,8 @@ class OfflineTestsCommon : LightningTestSuite() { // We simulate a disconnection followed by a reconnection. val (alice2, bob2) = disconnect(alice1, bob1) - val localInit = Init(alice0.commitments.params.localParams.features) - val remoteInit = Init(bob0.commitments.params.localParams.features) + val localInit = Init(alice0.commitments.channelParams.localParams.features) + val remoteInit = Init(bob0.commitments.channelParams.localParams.features) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.Connected(localInit, remoteInit)) assertIs>(alice3) actionsAlice3.findOutgoingMessage() @@ -436,17 +433,11 @@ class OfflineTestsCommon : LightningTestSuite() { assertEquals(actionsAlice5.size, 9) val revokedCommitPublished = alice5.state.revokedCommitPublished.firstOrNull() assertNotNull(revokedCommitPublished) - val claimMainTx = revokedCommitPublished.claimMainOutputTx!!.tx - val claimMainPenaltyTx = revokedCommitPublished.mainPenaltyTx!!.tx - listOf(claimMainTx, claimMainPenaltyTx).forEach { Transaction.correctlySpends(it, listOf(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - actionsAlice5.hasPublishTx(claimMainTx) - actionsAlice5.hasPublishTx(claimMainPenaltyTx) + val mainTx = actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + val penaltyTx = actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) + listOf(mainTx, penaltyTx).forEach { Transaction.correctlySpends(it, listOf(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + actionsAlice5.hasWatchOutputsSpent(listOf(mainTx, penaltyTx).flatMap { tx -> tx.txIn.map { it.outPoint } }.toSet()) assertEquals(actionsAlice5.find().revokedCommitTxId, bobRevokedCommitTx.txid) - assertEquals(actionsAlice5.findWatches().map { it.txId }.toSet(), setOf(bobRevokedCommitTx.txid, claimMainTx.txid)) - val watchSpent = actionsAlice5.findWatch() - assertIs(watchSpent.event) - assertEquals(watchSpent.txId, bobRevokedCommitTx.txid) - assertEquals(watchSpent.outputIndex, claimMainPenaltyTx.txIn.first().outPoint.index.toInt()) actionsAlice5.has() actionsAlice5.has() actionsAlice5.hasOutgoingMessage() @@ -485,8 +476,8 @@ class OfflineTestsCommon : LightningTestSuite() { actions1.hasWatch() assertIs(alice1.state) - val localInit = Init(alice.commitments.params.localParams.features) - val remoteInit = Init(bob.commitments.params.localParams.features) + val localInit = Init(alice.commitments.channelParams.localParams.features) + val remoteInit = Init(bob.commitments.channelParams.localParams.features) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Connected(localInit, remoteInit)) assertIs(alice2.state) @@ -535,8 +526,8 @@ class OfflineTestsCommon : LightningTestSuite() { // Alice and Bob are disconnected. val (alice1, bob1) = disconnect(alice, bob) - val aliceInit = Init(alice.commitments.params.localParams.features) - val bobInit = Init(bob.commitments.params.localParams.features) + val aliceInit = Init(alice.commitments.channelParams.localParams.features) + val bobInit = Init(bob.commitments.channelParams.localParams.features) val (alice2, actionsAlice) = alice1.process(ChannelCommand.Connected(aliceInit, bobInit)) val (bob2, _) = bob1.process(ChannelCommand.Connected(bobInit, aliceInit)) @@ -559,8 +550,8 @@ class OfflineTestsCommon : LightningTestSuite() { fun `wait for their channel reestablish when using channel backup`() { val (alice, bob) = TestsHelper.reachNormal() val (alice1, bob1) = disconnect(alice, bob) - val localInit = Init(alice.commitments.params.localParams.features) - val remoteInit = Init(bob.commitments.params.localParams.features) + val localInit = Init(alice.commitments.channelParams.localParams.features) + val remoteInit = Init(bob.commitments.channelParams.localParams.features) val (alice2, actions) = alice1.process(ChannelCommand.Connected(localInit, remoteInit)) assertIs(alice2.state) @@ -699,13 +690,13 @@ class OfflineTestsCommon : LightningTestSuite() { assertNotNull(alice4.state.localCommitPublished) actions.hasOutgoingMessage() actions.has() - val lcp = alice4.state.localCommitPublished!! + val lcp = alice4.state.localCommitPublished actions.hasPublishTx(lcp.commitTx) - assertEquals(1, lcp.htlcTimeoutTxs().size) - assertEquals(1, lcp.claimHtlcDelayedTxs.size) - assertEquals(4, actions.findPublishTxs().size) // commit tx + main output + htlc-timeout + claim-htlc-delayed - assertEquals(3, actions.findWatches().size) // commit tx + main output + claim-htlc-delayed - assertEquals(1, actions.findWatches().size) // htlc-timeout + assertEquals(1, lcp.htlcOutputs.size) + assertEquals(0, lcp.htlcDelayedOutputs.size) + assertEquals(3, actions.findPublishTxs().size) // commit tx + main output + htlc-timeout + actions.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) + actions.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx) } @Test @@ -727,19 +718,13 @@ class OfflineTestsCommon : LightningTestSuite() { assertNotNull(bob4.state.localCommitPublished) actions4.has() - val lcp = bob4.state.localCommitPublished!! - assertNotNull(lcp.claimMainDelayedOutputTx) - assertEquals(1, lcp.htlcSuccessTxs().size) - Transaction.correctlySpends(lcp.htlcSuccessTxs().first().tx, lcp.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assertEquals(1, lcp.claimHtlcDelayedTxs.size) - Transaction.correctlySpends(lcp.claimHtlcDelayedTxs.first().tx, lcp.htlcSuccessTxs().first().tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val txs = setOf(lcp.commitTx, lcp.claimMainDelayedOutputTx!!.tx, lcp.htlcSuccessTxs().first().tx, lcp.claimHtlcDelayedTxs.first().tx) - assertEquals(txs, actions4.findPublishTxs().toSet()) - val watchConfirmed = listOf(lcp.commitTx, lcp.claimMainDelayedOutputTx!!.tx, lcp.claimHtlcDelayedTxs.first().tx).map { it.txid }.toSet() - assertEquals(watchConfirmed, actions4.findWatches().map { it.txId }.toSet()) - val watchSpent = setOf(lcp.htlcSuccessTxs().first().input.outPoint) - assertEquals(watchSpent, actions4.findWatches().map { OutPoint(lcp.commitTx, it.outputIndex.toLong()) }.toSet()) + val lcp = bob4.state.localCommitPublished + assertNotNull(lcp.localOutput) + assertEquals(1, lcp.htlcOutputs.size) + val htlcSuccessTx = actions4.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.HtlcSuccessTx) + Transaction.correctlySpends(htlcSuccessTx, lcp.commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actions4.hasWatchConfirmed(lcp.commitTx.txid) + actions4.hasWatchOutputsSpent(lcp.htlcOutputs) } @Test @@ -747,7 +732,7 @@ class OfflineTestsCommon : LightningTestSuite() { val (alice, _) = TestsHelper.reachNormal() val (alice1, _) = alice.process(ChannelCommand.Disconnected) assertIs(alice1.state) - val commitTx = alice1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice1.signCommitTx() val (alice2, actions2) = alice1.process(ChannelCommand.Close.ForceClose) assertIs(alice2.state) assertTrue(actions2.contains(ChannelAction.Storage.StoreState(alice2.state))) @@ -782,7 +767,7 @@ class OfflineTestsCommon : LightningTestSuite() { val (alice2, _) = LNChannel(alice1.ctx, WaitForInit).process(ChannelCommand.Init.Restore(alice1.state.state)) assertIs(alice2.state) val aliceInit = Init((alice.state as WaitForFundingSigned).channelParams.localParams.features.initFeatures()) - val bobInit = Init((bob.state as WaitForFundingCreated).localParams.features.initFeatures()) + val bobInit = Init((bob.state as WaitForFundingCreated).localChannelParams.features.initFeatures()) val (alice3, actions3) = alice2.process(ChannelCommand.Connected(aliceInit, bobInit)) assertIs(alice3.state) assertEquals(alice.state, alice3.state.state) @@ -816,23 +801,23 @@ class OfflineTestsCommon : LightningTestSuite() { assertIs(bob4.state.rbfStatus) assertIs(alice5.state) assertIs(alice5.state.rbfStatus) - Triple(alice5, bob4, (alice5.state.rbfStatus as RbfStatus.WaitingForSigs).session.fundingTx.txId) + Triple(alice5, bob4, alice5.state.rbfStatus.session.fundingTx.txId) } - val aliceInit = Init(alice.commitments.params.localParams.features) - val bobInit = Init(bob.commitments.params.localParams.features) + val aliceInit = Init(alice.commitments.channelParams.localParams.features) + val bobInit = Init(bob.commitments.channelParams.localParams.features) // Bob has not received Alice's tx_complete, so he's not storing the RBF attempt. val (bob1, _) = bob.process(ChannelCommand.Disconnected) assertIs(bob1.state) assertIs(bob1.state.state) - assertEquals((bob1.state.state as WaitForFundingConfirmed).rbfStatus, RbfStatus.None) + assertEquals(bob1.state.state.rbfStatus, RbfStatus.None) // Alice has sent commit_sig, so she's storing the RBF attempt. val (alice1, _) = alice.process(ChannelCommand.Disconnected) assertIs(alice1.state) assertIs(alice1.state.state) - assertIs((alice1.state.state as WaitForFundingConfirmed).rbfStatus) + assertIs(alice1.state.state.rbfStatus) // On reconnection, Alice tries to resume the RBF signing session: Bob reacts by aborting it. val (alice2, _) = LNChannel(alice1.ctx, WaitForInit).process(ChannelCommand.Init.Restore(alice1.state.state)) @@ -866,7 +851,8 @@ class OfflineTestsCommon : LightningTestSuite() { val bob = run { val (alice, bob) = TestsHelper.reachNormal() // alice publishes her commitment tx - val (bob1, _) = bob.process(ChannelCommand.WatchReceived(WatchSpentTriggered(bob.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), alice.commitments.latest.localCommit.publishableTxs.commitTx.tx))) + val commitTx = alice.signCommitTx() + val (bob1, _) = bob.process(ChannelCommand.WatchReceived(WatchSpentTriggered(bob.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), commitTx))) assertIs>(bob1) assertNull(bob1.state.closingTypeAlreadyKnown()) bob1 @@ -877,16 +863,14 @@ class OfflineTestsCommon : LightningTestSuite() { assertIs(state1.state) assertEquals(4, actions.size) val watchSpent = actions.hasWatch() - assertEquals(bob.commitments.latest.commitInput.outPoint.txid, watchSpent.txId) + assertEquals(bob.commitments.latest.fundingInput.txid, watchSpent.txId) + assertEquals(bob.commitments.latest.fundingInput.index, watchSpent.outputIndex.toLong()) val remoteCommitPublished = bob.state.remoteCommitPublished assertNotNull(remoteCommitPublished) - val claimMainOutputTx = remoteCommitPublished.claimMainOutputTx - assertNotNull(claimMainOutputTx) - actions.hasPublishTx(claimMainOutputTx.tx) - val watches = actions.findWatches() - assertEquals(2, watches.size) - assertNotNull(watches.first { it.txId == remoteCommitPublished.commitTx.txid }) - assertNotNull(watches.first { it.txId == claimMainOutputTx.tx.txid }) + assertNotNull(remoteCommitPublished.localOutput) + val mainTx = actions.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + actions.hasWatchConfirmed(remoteCommitPublished.commitTx.txid) + actions.hasWatchOutputSpent(mainTx.txIn.first().outPoint) } companion object { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt index 973514350..5c9dcead7 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt @@ -7,7 +7,6 @@ import fr.acinq.lightning.Lightning import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* -import fr.acinq.lightning.channel.TestsHelper.htlcTimeoutTxs import fr.acinq.lightning.channel.TestsHelper.reachNormal import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.tests.TestConstants @@ -339,7 +338,7 @@ class QuiescenceTestsCommon : LightningTestSuite() { UpdateFailHtlc(bob3.channelId, htlc.id, Lightning.randomBytes32()), UpdateFee(bob3.channelId, FeeratePerKw(500.sat)), UpdateAddHtlc(Lightning.randomBytes32(), htlc.id + 1, 50000000.msat, Lightning.randomBytes32(), CltvExpiry(alice.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket), - Shutdown(alice.channelId, alice.commitments.params.localParams.defaultFinalScriptPubKey), + Shutdown(alice.channelId, alice.commitments.channelParams.localParams.defaultFinalScriptPubKey), ).forEach { // both parties will respond to a forbidden msg while quiescent with a warning (and disconnect) val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(it)) @@ -453,11 +452,11 @@ class QuiescenceTestsCommon : LightningTestSuite() { assertIs(alice4.state) val lcp = alice4.state.localCommitPublished assertNotNull(lcp) - assertEquals(1, lcp.htlcTxs.size) - val htlcTimeoutTxs = lcp.htlcTimeoutTxs() + assertEquals(1, lcp.htlcOutputs.size) + val htlcTimeoutTxs = alice4.signHtlcTimeoutTxs() assertEquals(1, htlcTimeoutTxs.size) actionsAlice4.hasPublishTx(lcp.commitTx) - actionsAlice4.hasPublishTx(lcp.htlcTimeoutTxs().first().tx) + actionsAlice4.hasPublishTx(htlcTimeoutTxs.first().tx) } @Test @@ -574,8 +573,8 @@ class QuiescenceTestsCommon : LightningTestSuite() { data class PostReconnectionState(val alice: LNChannel, val bob: LNChannel, val actionsAlice: List, val actionsBob: List) fun reconnect(alice: LNChannel, bob: LNChannel): PostReconnectionState { - val aliceInit = Init(alice.commitments.params.localParams.features) - val bobInit = Init(bob.commitments.params.localParams.features) + val aliceInit = Init(alice.commitments.channelParams.localParams.features) + val bobInit = Init(bob.commitments.channelParams.localParams.features) val (alice1, actionsAlice1) = alice.process(ChannelCommand.Connected(aliceInit, bobInit)) assertIs>(alice1) val channelReestablishA = actionsAlice1.findOutgoingMessage() diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt index 3c2bc0360..74c65c300 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt @@ -8,18 +8,16 @@ import fr.acinq.lightning.Features import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.WatchConfirmed +import fr.acinq.lightning.blockchain.WatchConfirmedTriggered import fr.acinq.lightning.blockchain.WatchSpent import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.TestsHelper.addHtlc -import fr.acinq.lightning.channel.TestsHelper.claimHtlcSuccessTxs -import fr.acinq.lightning.channel.TestsHelper.claimHtlcTimeoutTxs import fr.acinq.lightning.channel.TestsHelper.crossSign import fr.acinq.lightning.channel.TestsHelper.fulfillHtlc import fr.acinq.lightning.channel.TestsHelper.makeCmdAdd import fr.acinq.lightning.channel.TestsHelper.reachNormal import fr.acinq.lightning.channel.TestsHelper.signAndRevack -import fr.acinq.lightning.serialization.channel.Encryption.from import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.UUID @@ -37,7 +35,7 @@ class ShutdownTestsCommon : LightningTestSuite() { val (bob1, actions1) = bob.process(add) assertIs>(bob1) assertTrue(actions1.any { it is ChannelAction.ProcessCmdRes.AddFailed && it.error == ChannelUnavailable(bob.channelId) }) - assertEquals(bob1.commitments.params.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) + assertEquals(bob1.commitments.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) } @Test @@ -47,7 +45,7 @@ class ShutdownTestsCommon : LightningTestSuite() { val (bob1, actions1) = bob.process(add) assertIs>(bob1) assertTrue(actions1.any { it is ChannelAction.ProcessCmdRes.AddFailed && it.error == ChannelUnavailable(bob.channelId) }) - assertEquals(bob1.commitments.params.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels, Feature.DualFunding))) + assertEquals(bob1.commitments.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.ZeroReserveChannels, Feature.DualFunding))) } @Test @@ -90,8 +88,8 @@ class ShutdownTestsCommon : LightningTestSuite() { val (alice, _) = init() val (alice1, actions) = alice.process(ChannelCommand.MessageReceived(UpdateFulfillHtlc(alice.channelId, 42, r1))) actions.hasOutgoingMessage() - // Alice should publish: commit tx + main delayed tx + 2 * htlc timeout txs + 2 * htlc delayed txs - assertEquals(6, actions.findPublishTxs().size) + // Alice should publish: commit tx + main delayed tx + 2 * htlc timeout txs + assertEquals(4, actions.findPublishTxs().size) assertIs>(alice1) } @@ -100,8 +98,8 @@ class ShutdownTestsCommon : LightningTestSuite() { val (alice, _) = init() val (alice1, actions) = alice.process(ChannelCommand.MessageReceived(UpdateFulfillHtlc(alice.channelId, 0, randomBytes32()))) actions.hasOutgoingMessage() - // Alice should publish: commit tx + main delayed tx + 2 * htlc timeout txs + 2 * htlc delayed txs - assertEquals(6, actions.filterIsInstance().size) + // Alice should publish: commit tx + main delayed tx + 2 * htlc timeout txs + assertEquals(4, actions.filterIsInstance().size) assertIs>(alice1) } @@ -161,12 +159,12 @@ class ShutdownTestsCommon : LightningTestSuite() { @Test fun `recv UpdateFailHtlc -- unknown htlc id`() { val (alice, _) = init() - val commitTx = alice.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice.signCommitTx() val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(UpdateFailHtlc(alice.channelId, 42, ByteVector.empty))) assertIs>(alice1) assertTrue(actions1.contains(ChannelAction.Storage.StoreState(alice1.state))) assertEquals(commitTx, actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx)) - assertTrue(actions1.findWatches().isNotEmpty()) + actions1.hasWatchConfirmed(commitTx.txid) assertTrue(actions1.findWatches().isNotEmpty()) val error = actions1.findOutgoingMessage() assertEquals(error.toAscii(), UnknownHtlcId(alice.channelId, 42).message) @@ -189,9 +187,10 @@ class ShutdownTestsCommon : LightningTestSuite() { val (alice1, actions) = alice.process(ChannelCommand.MessageReceived(fail)) assertIs>(alice1) assertTrue(actions.contains(ChannelAction.Storage.StoreState(alice1.state))) - assertEquals(alice.commitments.latest.localCommit.publishableTxs.commitTx.tx, actions.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx)) - assertEquals(6, actions.filterIsInstance().size) // commit tx + main delayed + htlc-timeout + htlc delayed - assertEquals(6, actions.filterIsInstance().size) + val commitTx = alice.signCommitTx() + assertEquals(commitTx, actions.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx)) + actions.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) + actions.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx) val error = actions.findOutgoingMessage() assertEquals(error.toAscii(), InvalidFailureCode(alice.channelId).message) } @@ -266,7 +265,7 @@ class ShutdownTestsCommon : LightningTestSuite() { assertIs>(alice3) actionsAlice3.hasOutgoingMessage() assertNotNull(alice3.state.localCommitPublished) - actionsAlice3.hasPublishTx(alice2.commitments.latest.localCommit.publishableTxs.commitTx.tx) + actionsAlice3.hasPublishTx(alice3.state.localCommitPublished.commitTx) } @Test @@ -278,11 +277,11 @@ class ShutdownTestsCommon : LightningTestSuite() { val sig = actionsBob2.hasOutgoingMessage() val (alice1, _) = alice0.process(ChannelCommand.MessageReceived(fulfill)) assertIs>(alice1) - val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(sig.copy(signature = ByteVector64.Zeroes))) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(sig.copy(signature = ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes)))) assertIs>(alice2) actionsAlice2.hasOutgoingMessage() assertNotNull(alice2.state.localCommitPublished) - actionsAlice2.hasPublishTx(alice1.commitments.latest.localCommit.publishableTxs.commitTx.tx) + actionsAlice2.hasPublishTx(alice2.state.localCommitPublished.commitTx) } @Test @@ -341,7 +340,7 @@ class ShutdownTestsCommon : LightningTestSuite() { val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(revack.copy(perCommitmentSecret = randomKey()))) assertIs>(bob3) assertNotNull(bob3.state.localCommitPublished) - actionsBob3.hasPublishTx(bob2.commitments.latest.localCommit.publishableTxs.commitTx.tx) + actionsBob3.hasPublishTx(bob3.state.localCommitPublished.commitTx) actionsBob3.hasOutgoingMessage() } @@ -351,7 +350,7 @@ class ShutdownTestsCommon : LightningTestSuite() { val (alice1, actions1) = alice0.process(ChannelCommand.MessageReceived(RevokeAndAck(alice0.channelId, randomKey(), randomKey().publicKey()))) assertIs>(alice1) assertNotNull(alice1.state.localCommitPublished) - actions1.hasPublishTx(alice0.commitments.latest.localCommit.publishableTxs.commitTx.tx) + actions1.hasPublishTx(alice1.state.localCommitPublished.commitTx) actions1.hasOutgoingMessage() } @@ -369,7 +368,7 @@ class ShutdownTestsCommon : LightningTestSuite() { @Test fun `recv CheckHtlcTimeout -- an htlc timed out`() { val (alice, _) = init() - val commitTx = alice.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = alice.signCommitTx() val htlcExpiry = alice.commitments.latest.localCommit.spec.htlcs.map { it.add.cltvExpiry }.first() val (alice1, actions1) = run { val tmp = alice.copy(ctx = alice.ctx.copy(currentBlockHeight = htlcExpiry.toLong().toInt())) @@ -385,13 +384,11 @@ class ShutdownTestsCommon : LightningTestSuite() { fun `recv BITCOIN_FUNDING_SPENT -- their commit`() { val (alice, bob) = init() // bob publishes his current commit tx, which contains two pending htlcs alice->bob - val bobCommitTx = bob.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob.signCommitTx() assertEquals(6, bobCommitTx.txOut.size) // 2 main outputs + 2 anchors + 2 pending htlcs - val (_, remoteCommitPublished) = TestsHelper.remoteClose(bobCommitTx, alice) - assertNotNull(remoteCommitPublished.claimMainOutputTx) - assertEquals(2, remoteCommitPublished.claimHtlcTxs.size) - assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) - assertEquals(2, remoteCommitPublished.claimHtlcTimeoutTxs().size) + val (_, remoteCommitPublished) = TestsHelper.remoteClose(bobCommitTx, alice, htlcTimeoutCount = 2) + assertNotNull(remoteCommitPublished.localOutput) + assertEquals(2, remoteCommitPublished.htlcOutputs.size) } @Test @@ -411,17 +408,17 @@ class ShutdownTestsCommon : LightningTestSuite() { shutdown(alice4, bob4) } - val bobCommitTx = bob.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob.signCommitTx() assertEquals(6, bobCommitTx.txOut.size) // 2 main outputs + 2 anchors + 2 pending htlc val (alice1, aliceActions1) = alice.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTx))) assertIs>(alice1) assertNotNull(alice1.state.nextRemoteCommitPublished) aliceActions1.has() val rcp = alice1.state.nextRemoteCommitPublished - assertNotNull(rcp.claimMainOutputTx) - assertEquals(2, rcp.claimHtlcTxs.size) - assertTrue(rcp.claimHtlcSuccessTxs().isEmpty()) - assertEquals(2, rcp.claimHtlcTimeoutTxs().size) + assertNotNull(rcp.localOutput) + assertEquals(2, rcp.htlcOutputs.size) + assertTrue(aliceActions1.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcSuccessTx).isEmpty()) + assertEquals(2, aliceActions1.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcTimeoutTx).size) } @Test @@ -435,7 +432,7 @@ class ShutdownTestsCommon : LightningTestSuite() { val (alice3, bob3) = nodes3 val (alice4, bob4) = crossSign(alice3, bob3) val (alice5, bob5) = shutdown(alice4, bob4) - Triple(alice5, bob5, bob2.state.commitments.latest.localCommit.publishableTxs.commitTx.tx) + Triple(alice5, bob5, bob2.signCommitTx()) } assertEquals(5, revokedTx.txOut.size) // 2 main outputs + 2 anchors + 1 pending htlc @@ -446,8 +443,8 @@ class ShutdownTestsCommon : LightningTestSuite() { aliceActions1.has() aliceActions1.has() val rvk = alice1.state.revokedCommitPublished.first() - assertNotNull(rvk.claimMainOutputTx) - assertNotNull(rvk.mainPenaltyTx) + assertNotNull(rvk.localOutput) + assertNotNull(rvk.remoteOutput) } @Test @@ -493,39 +490,35 @@ class ShutdownTestsCommon : LightningTestSuite() { private fun testLocalForceClose(alice: LNChannel, actions: List) { assertIs>(alice) - val aliceCommitTx = alice.commitments.latest.localCommit.publishableTxs.commitTx + val aliceCommitTx = alice.signCommitTx() + assertEquals(aliceCommitTx.txOut.size, 6) // 2 anchor outputs + 2 main output + 2 pending htlcs val lcp = alice.state.localCommitPublished assertNotNull(lcp) - assertEquals(lcp.commitTx, aliceCommitTx.tx) + assertEquals(lcp.commitTx, aliceCommitTx) - val txs = actions.filterIsInstance().map { it.tx } - assertEquals(6, txs.size) // alice has sent 2 htlcs so we expect 6 transactions: // - alice's current commit tx // - 1 tx to claim the main delayed output // - 2 txs for each htlc - // - 2 txs for each delayed output of the claimed htlc - assertEquals(aliceCommitTx.tx, txs[0]) - assertEquals(aliceCommitTx.tx.txOut.size, 6) // 2 anchor outputs + 2 main output + 2 pending htlcs - // the main delayed output spends the commitment transaction - Transaction.correctlySpends(txs[1], aliceCommitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // 2nd stage transactions spend the commitment transaction - Transaction.correctlySpends(txs[2], aliceCommitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(txs[3], aliceCommitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // 3rd stage transactions spend their respective HTLC-Success/HTLC-Timeout transactions - Transaction.correctlySpends(txs[4], txs[2], ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(txs[5], txs[3], ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - val expectedWatchConfirmed = listOf( - txs[0].txid, // commit tx - txs[1].txid, // main delayed - txs[4].txid, // htlc-delayed - txs[5].txid, // htlc-delayed - ) - assertEquals(actions.findWatches().map { it.txId }, expectedWatchConfirmed) - assertEquals(lcp.htlcTxs.keys, actions.findWatches().map { OutPoint(aliceCommitTx.tx, it.outputIndex.toLong()) }.toSet()) + assertEquals(4, actions.findPublishTxs().size) + actions.hasPublishTx(aliceCommitTx) + val mainTx = actions.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) + Transaction.correctlySpends(mainTx, aliceCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcTimeoutTxs = actions.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx) + assertEquals(2, htlcTimeoutTxs.size) + htlcTimeoutTxs.forEach { Transaction.correctlySpends(it, aliceCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + actions.hasWatchConfirmed(aliceCommitTx.txid) + actions.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + actions.hasWatchOutputsSpent(lcp.htlcOutputs) + + htlcTimeoutTxs.forEach { htlcTx -> + val (alice1, actions1) = alice.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice.channelId, WatchConfirmed.ClosingTxConfirmed, 42, 3, htlcTx))) + assertIs>(alice1) + val htlcDelayedTx = actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.HtlcDelayedTx) + Transaction.correctlySpends(htlcDelayedTx, htlcTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actions1.hasWatchOutputSpent(htlcDelayedTx.txIn.first().outPoint) + assertTrue(alice1.state.localCommitPublished!!.htlcDelayedOutputs.contains(htlcDelayedTx.txIn.first().outPoint)) + } } @Test diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index ddc4e0f9e..869af93a7 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -56,12 +56,12 @@ class SpliceTestsCommon : LightningTestSuite() { val (bob4, alice4) = crossSign(nodes3.first, nodes3.second, commitmentsCount = 2) alice4.commitments.active.forEach { c -> - val commitTx = c.localCommit.publishableTxs.commitTx.tx - Transaction.correctlySpends(commitTx, mapOf(c.commitInput.outPoint to c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val commitTx = c.fullySignedCommitTx(alice4.commitments.channelParams, alice4.channelKeys) + Transaction.correctlySpends(commitTx, mapOf(c.fundingInput to c.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } bob4.state.commitments.active.forEach { c -> - val commitTx = c.localCommit.publishableTxs.commitTx.tx - Transaction.correctlySpends(commitTx, mapOf(c.commitInput.outPoint to c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val commitTx = c.fullySignedCommitTx(bob4.commitments.channelParams, bob4.channelKeys) + Transaction.correctlySpends(commitTx, mapOf(c.fundingInput to c.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } // Alice fulfills that HTLC in both commitments. @@ -69,12 +69,12 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice6, bob6) = crossSign(alice5, bob5, commitmentsCount = 2) alice6.state.commitments.active.forEach { c -> - val commitTx = c.localCommit.publishableTxs.commitTx.tx - Transaction.correctlySpends(commitTx, mapOf(c.commitInput.outPoint to c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val commitTx = c.fullySignedCommitTx(alice6.commitments.channelParams, alice6.channelKeys) + Transaction.correctlySpends(commitTx, mapOf(c.fundingInput to c.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } bob6.state.commitments.active.forEach { c -> - val commitTx = c.localCommit.publishableTxs.commitTx.tx - Transaction.correctlySpends(commitTx, mapOf(c.commitInput.outPoint to c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val commitTx = c.fullySignedCommitTx(bob6.commitments.channelParams, bob6.channelKeys) + Transaction.correctlySpends(commitTx, mapOf(c.fundingInput to c.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } resolveHtlcs(alice6, bob6, htlcs, commitmentsCount = 2) @@ -234,7 +234,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) val defaultSpliceAck = actionsBob2.findOutgoingMessage() assertNull(defaultSpliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey) + val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey, Transactions.CommitmentFormat.AnchorOutputs).pubkeyScript run { val willFund = fundingRates.validateRequest(bob.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)?.willFund assertNotNull(willFund) @@ -314,7 +314,7 @@ class SpliceTestsCommon : LightningTestSuite() { val spliceAck = actionsAlice2.hasOutgoingMessage() // We don't implement the liquidity provider side, so we must fake it. assertNull(spliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey) + val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, Transactions.CommitmentFormat.AnchorOutputs).pubkeyScript val willFund = fundingRates.validateRequest(alice.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)!!.willFund val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceAck.copy(fundingContribution = liquidityRequest.requestedAmount, tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))))) assertEquals(1, actionsBob3.size) @@ -335,7 +335,7 @@ class SpliceTestsCommon : LightningTestSuite() { val spliceAck = actionsAlice2.hasOutgoingMessage() // We don't implement the liquidity provider side, so we must fake it. assertNull(spliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey) + val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, Transactions.CommitmentFormat.AnchorOutputs).pubkeyScript val willFund = fundingRates.validateRequest(alice.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)!!.willFund val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceAck.copy(fundingContribution = liquidityRequest.requestedAmount, tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))))) assertEquals(1, actionsBob3.size) @@ -401,7 +401,7 @@ class SpliceTestsCommon : LightningTestSuite() { val spliceAck = actionsAlice2.hasOutgoingMessage() // We don't implement the liquidity provider side, so we must fake it. assertNull(spliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey) + val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, Transactions.CommitmentFormat.AnchorOutputs).pubkeyScript val willFund = fundingRates.validateRequest(alice.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)!!.willFund val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceAck.copy(fundingContribution = fundingRequest.requestedAmount, tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))))) actionsBob3.hasOutgoingMessage() @@ -1272,14 +1272,14 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice1, bob1) = spliceOut(alice0, bob0, 75_000.sat) // Bob force-closes using the latest active commitment. - val bobCommitTx = bob1.commitments.active.first().localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob1.signCommitTx() val (bob2, actionsBob2) = bob1.process(ChannelCommand.Close.ForceClose) assertIs(bob2.state) - assertEquals(actionsBob2.size, 17) - assertEquals(actionsBob2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx).txid, bobCommitTx.txid) + assertEquals(actionsBob2.size, 13) + actionsBob2.hasPublishTx(bobCommitTx) val claimMain = actionsBob2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) actionsBob2.hasWatchConfirmed(bobCommitTx.txid) - actionsBob2.hasWatchConfirmed(claimMain.txid) + actionsBob2.hasWatchOutputSpent(claimMain.txIn.first().outPoint) actionsBob2.has() actionsBob2.hasOutgoingMessage() actionsBob2.has() @@ -1314,8 +1314,8 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice1, bob1) = spliceOut(alice0, bob0, 75_000.sat) // Bob force-closes using an older active commitment. - assertEquals(bob1.commitments.active.map { it.localCommit.publishableTxs.commitTx.tx }.toSet().size, 2) - val bobCommitTx = bob1.commitments.active.last().localCommit.publishableTxs.commitTx.tx + assertEquals(bob1.commitments.active.map { it.localCommit.txId }.toSet().size, 2) + val bobCommitTx = bob1.commitments.active.last().fullySignedCommitTx(bob.commitments.channelParams, bob.channelKeys) handlePreviousRemoteClose(alice1, bobCommitTx) } @@ -1328,7 +1328,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice4, bob4) = exchangeSpliceSigs(alice3, commitSigAlice3, bob3, commitSigBob3) // Bob force-closes using an older active commitment with an alternative feerate. - assertEquals(bob4.commitments.active.map { it.localCommit.publishableTxs.commitTx.tx }.toSet().size, 3) + assertEquals(bob4.commitments.active.map { it.localCommit.txId }.toSet().size, 3) val bobCommitTx = useAlternativeCommitSig(bob4, bob4.commitments.active[1], commitSigAlice1.alternativeFeerateSigs.first()) handlePreviousRemoteClose(alice4, bobCommitTx) } @@ -1348,7 +1348,7 @@ class SpliceTestsCommon : LightningTestSuite() { // Bob force-closes using an inactive commitment. assertNotEquals(bob2.commitments.active.first().fundingTxId, bob2.commitments.inactive.first().fundingTxId) - val bobCommitTx = bob2.commitments.inactive.first().localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob2.commitments.inactive.first().fullySignedCommitTx(bob.commitments.channelParams, bob.channelKeys) handlePreviousRemoteClose(alice1, bobCommitTx) } @@ -1357,7 +1357,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice, bob) = reachNormalWithConfirmedFundingTx() val (alice0, bob0, _) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) - val bobCommitTx = bob1.commitments.active.first().localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob1.commitments.active.first().fullySignedCommitTx(bob.commitments.channelParams, bob.channelKeys) // Alice sends an HTLC to Bob, which revokes the previous commitment. val (nodes2, _, _) = addHtlc(25_000_000.msat, alice1, bob1) @@ -1393,7 +1393,7 @@ class SpliceTestsCommon : LightningTestSuite() { // We make a first splice transaction, but don't exchange splice_locked. val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val spliceTx1 = bob1.commitments.latest.localFundingStatus.signedTx!! - val bobRevokedCommitTx = bob1.commitments.active.last().localCommit.publishableTxs.commitTx.tx + val bobRevokedCommitTx = bob1.commitments.active.last().fullySignedCommitTx(bob.commitments.channelParams, bob.channelKeys) // We make a second splice transaction, but don't exchange splice_locked. val (alice2, bob2) = spliceOut(alice1, bob1, 60_000.sat) // From Alice's point of view, we now have two unconfirmed splices, both active. @@ -1427,14 +1427,14 @@ class SpliceTestsCommon : LightningTestSuite() { val rvk = alice10.state.revokedCommitPublished.firstOrNull() assertNotNull(rvk) // Alice reacts by punishing Bob. - assertNotNull(rvk.claimMainOutputTx) - actionsAlice10.hasPublishTx(rvk.claimMainOutputTx.tx) - actionsAlice10.hasWatchConfirmed(rvk.claimMainOutputTx.tx.txid) - assertNotNull(rvk.mainPenaltyTx) - actionsAlice10.hasPublishTx(rvk.mainPenaltyTx.tx) - actionsAlice10.hasWatch().also { assertEquals(rvk.mainPenaltyTx.input.outPoint, OutPoint(it.txId, it.outputIndex.toLong())) } - Transaction.correctlySpends(rvk.mainPenaltyTx.tx, listOf(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - actionsAlice10.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) } + assertNotNull(rvk.localOutput) + val mainTx = actionsAlice10.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + Transaction.correctlySpends(mainTx, bobRevokedCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actionsAlice10.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + assertNotNull(rvk.remoteOutput) + val penaltyTx = actionsAlice10.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) + Transaction.correctlySpends(penaltyTx, bobRevokedCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actionsAlice10.hasWatchOutputSpent(penaltyTx.txIn.first().outPoint) // Alice marks every outgoing HTLC as failed, including the ones that don't appear in the revoked commitment. val outgoingHtlcs = htlcs.aliceToBob.map { it.second }.toSet() + setOf(htlcOut1, htlcOut2) val addSettled = actionsAlice10.filterIsInstance() @@ -1446,23 +1446,23 @@ class SpliceTestsCommon : LightningTestSuite() { val htlcInfos = (htlcs.aliceToBob + htlcs.bobToAlice).map { ChannelAction.Storage.HtlcInfo(bob0.channelId, getHtlcInfos.commitmentNumber, it.second.paymentHash, it.second.cltvExpiry) } val (alice11, actionsAlice11) = alice10.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobRevokedCommitTx.txid, htlcInfos)) assertIs>(alice11) - val htlcPenaltyTxs = alice11.state.revokedCommitPublished.first().htlcPenaltyTxs - assertEquals(htlcs.aliceToBob.size + htlcs.bobToAlice.size, htlcPenaltyTxs.map { it.input.outPoint }.toSet().size) - htlcPenaltyTxs.forEach { - actionsAlice11.hasPublishTx(it.tx) - Transaction.correctlySpends(it.tx, listOf(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - assertTrue(actionsAlice11.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet().containsAll(htlcPenaltyTxs.map { it.input.outPoint })) + val rvk1 = alice11.state.revokedCommitPublished.firstOrNull() + assertNotNull(rvk1) + val htlcPenaltyTxs = actionsAlice11.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcPenaltyTx) + assertEquals(htlcs.aliceToBob.size + htlcs.bobToAlice.size, rvk1.htlcOutputs.size) + assertEquals(rvk1.htlcOutputs, htlcPenaltyTxs.flatMap { tx -> tx.txIn.map { it.outPoint } }.toSet()) + htlcPenaltyTxs.forEach { Transaction.correctlySpends(it, bobRevokedCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + actionsAlice11.hasWatchOutputsSpent(rvk1.htlcOutputs) // The remaining transactions confirm. val (alice12, _) = alice11.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 0, bobRevokedCommitTx))) - val (alice13, _) = alice12.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 1, rvk.claimMainOutputTx.tx))) - val (alice14, _) = alice13.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 3, rvk.mainPenaltyTx.tx))) - val (alice15, _) = alice14.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 2, htlcPenaltyTxs[0].tx))) - val (alice16, _) = alice15.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 5, htlcPenaltyTxs[1].tx))) - val (alice17, _) = alice16.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 7, htlcPenaltyTxs[2].tx))) + val (alice13, _) = alice12.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 1, mainTx))) + val (alice14, _) = alice13.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 3, penaltyTx))) + val (alice15, _) = alice14.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 2, htlcPenaltyTxs[0]))) + val (alice16, _) = alice15.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 5, htlcPenaltyTxs[1]))) + val (alice17, _) = alice16.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 7, htlcPenaltyTxs[2]))) assertIs(alice17.state) - val (alice18, _) = alice17.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 6, htlcPenaltyTxs[3].tx))) + val (alice18, _) = alice17.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 6, htlcPenaltyTxs[3]))) assertIs(alice18.state) } @@ -1480,7 +1480,7 @@ class SpliceTestsCommon : LightningTestSuite() { assertIs>(bob2) assertEquals(bob2.commitments.active.size, 1) assertEquals(bob2.commitments.inactive.size, 1) - val bobRevokedCommitTx = bob2.commitments.inactive.first().localCommit.publishableTxs.commitTx.tx + val bobRevokedCommitTx = bob2.commitments.inactive.first().fullySignedCommitTx(bob.commitments.channelParams, bob.channelKeys) // Alice sends an HTLC to Bob, which revokes the inactive commitment. val (nodes3, _, htlcOut1) = addHtlc(25_000_000.msat, alice2, bob2) @@ -1516,12 +1516,14 @@ class SpliceTestsCommon : LightningTestSuite() { assertIs>(alice12) assertEquals(actionsAlice12.hasWatchConfirmed(bobRevokedCommitTx.txid).event, WatchConfirmed.AlternativeCommitTxConfirmed) // Alice attempts to force-close with her latest active commitment. - val localCommit = actionsAlice12.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx) - assertEquals(localCommit.txid, alice12.commitments.active.first().localCommit.publishableTxs.commitTx.tx.txid) + val localCommitTx = actionsAlice12.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx) + assertEquals(localCommitTx.txid, alice12.commitments.active.first().localCommit.txId) + assertNotNull(alice12.state.localCommitPublished) val localOutgoingHtlcs = htlcs.aliceToBob.map { it.second }.toSet() + setOf(htlcOut1, htlcOut2) // the last HTLC was not signed by Bob yet - assertEquals(localOutgoingHtlcs.size, actionsAlice12.filterIsInstance().filter { it.txType == ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx }.size) + assertEquals(localOutgoingHtlcs.size, actionsAlice12.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx).size) val incomingHtlcs = htlcs.bobToAlice.map { it.second }.toSet() + setOf(htlcIn) - assertEquals(incomingHtlcs.size + localOutgoingHtlcs.size, actionsAlice12.findWatches().size) + assertEquals(incomingHtlcs.size + localOutgoingHtlcs.size, alice12.state.localCommitPublished.htlcOutputs.size) + actionsAlice12.hasWatchOutputsSpent(alice12.state.localCommitPublished.htlcOutputs) actionsAlice12.has() // Bob's revoked commit tx confirms. @@ -1531,14 +1533,14 @@ class SpliceTestsCommon : LightningTestSuite() { val rvk = alice13.state.revokedCommitPublished.firstOrNull() assertNotNull(rvk) // Alice reacts by punishing Bob. - assertNotNull(rvk.claimMainOutputTx) - actionsAlice13.hasPublishTx(rvk.claimMainOutputTx.tx) - actionsAlice13.hasWatchConfirmed(rvk.claimMainOutputTx.tx.txid) - assertNotNull(rvk.mainPenaltyTx) - actionsAlice13.hasPublishTx(rvk.mainPenaltyTx.tx) - actionsAlice13.hasWatch().also { assertEquals(rvk.mainPenaltyTx.input.outPoint, OutPoint(it.txId, it.outputIndex.toLong())) } - Transaction.correctlySpends(rvk.mainPenaltyTx.tx, listOf(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - actionsAlice13.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) } + assertNotNull(rvk.localOutput) + val mainTx = actionsAlice13.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + Transaction.correctlySpends(mainTx, bobRevokedCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actionsAlice13.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + assertNotNull(rvk.remoteOutput) + val penaltyTx = actionsAlice13.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) + Transaction.correctlySpends(penaltyTx, bobRevokedCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actionsAlice13.hasWatchOutputSpent(penaltyTx.txIn.first().outPoint) // Alice marks every outgoing HTLC as failed, including the ones that don't appear in the revoked commitment. val addSettled = actionsAlice13.filterIsInstance() val outgoingHtlcs = htlcs.aliceToBob.map { it.second }.toSet() + setOf(htlcOut1, htlcOut2, htlcOut3) @@ -1550,23 +1552,22 @@ class SpliceTestsCommon : LightningTestSuite() { val htlcInfos = (htlcs.aliceToBob + htlcs.bobToAlice).map { ChannelAction.Storage.HtlcInfo(bob0.channelId, getHtlcInfos.commitmentNumber, it.second.paymentHash, it.second.cltvExpiry) } val (alice14, actionsAlice14) = alice13.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobRevokedCommitTx.txid, htlcInfos)) assertIs>(alice14) - val htlcPenaltyTxs = alice14.state.revokedCommitPublished.first().htlcPenaltyTxs - assertEquals(htlcs.aliceToBob.size + htlcs.bobToAlice.size, htlcPenaltyTxs.map { it.input.outPoint }.toSet().size) - htlcPenaltyTxs.forEach { - actionsAlice14.hasPublishTx(it.tx) - Transaction.correctlySpends(it.tx, listOf(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - assertTrue(actionsAlice14.findWatches().map { OutPoint(it.txId, it.outputIndex.toLong()) }.toSet().containsAll(htlcPenaltyTxs.map { it.input.outPoint })) + val rvk1 = alice14.state.revokedCommitPublished.firstOrNull() + assertNotNull(rvk1) + val htlcPenaltyTxs = actionsAlice14.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcPenaltyTx) + assertEquals(htlcs.aliceToBob.size + htlcs.bobToAlice.size, rvk1.htlcOutputs.size) + assertEquals(rvk1.htlcOutputs, htlcPenaltyTxs.flatMap { tx -> tx.txIn.map { it.outPoint } }.toSet()) + htlcPenaltyTxs.forEach { Transaction.correctlySpends(it, bobRevokedCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } // The remaining transactions confirm. val (alice15, _) = alice14.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 0, bobRevokedCommitTx))) - val (alice16, _) = alice15.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 1, rvk.claimMainOutputTx.tx))) - val (alice17, _) = alice16.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 3, rvk.mainPenaltyTx.tx))) - val (alice18, _) = alice17.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 2, htlcPenaltyTxs[0].tx))) - val (alice19, _) = alice18.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 5, htlcPenaltyTxs[1].tx))) - val (alice20, _) = alice19.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 7, htlcPenaltyTxs[2].tx))) + val (alice16, _) = alice15.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 1, mainTx))) + val (alice17, _) = alice16.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 3, penaltyTx))) + val (alice18, _) = alice17.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 2, htlcPenaltyTxs[0]))) + val (alice19, _) = alice18.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 5, htlcPenaltyTxs[1]))) + val (alice20, _) = alice19.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 7, htlcPenaltyTxs[2]))) assertIs(alice20.state) - val (alice21, _) = alice20.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 6, htlcPenaltyTxs[3].tx))) + val (alice21, _) = alice20.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 6, htlcPenaltyTxs[3]))) assertIs(alice21.state) } @@ -1595,12 +1596,17 @@ class SpliceTestsCommon : LightningTestSuite() { private fun reachNormalWithConfirmedFundingTx(zeroConf: Boolean = false): Pair, LNChannel> { val (alice, bob) = reachNormal(zeroConf = zeroConf) - val fundingTx = alice.commitments.latest.localFundingStatus.signedTx!! - val (alice1, _) = alice.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice.channelId, WatchConfirmed.ChannelFundingDepthOk, 42, 3, fundingTx))) - val (bob1, _) = bob.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob.channelId, WatchConfirmed.ChannelFundingDepthOk, 42, 3, fundingTx))) - assertIs>(alice1) - assertIs>(bob1) - return Pair(alice1, bob1) + return when (val fundingStatus = alice.commitments.latest.localFundingStatus) { + is LocalFundingStatus.UnconfirmedFundingTx -> { + val fundingTx = fundingStatus.signedTx!! + val (alice1, _) = alice.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice.channelId, WatchConfirmed.ChannelFundingDepthOk, 42, 3, fundingTx))) + val (bob1, _) = bob.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob.channelId, WatchConfirmed.ChannelFundingDepthOk, 42, 3, fundingTx))) + assertIs>(alice1) + assertIs>(bob1) + Pair(alice1, bob1) + } + is LocalFundingStatus.ConfirmedFundingTx -> Pair(alice, bob) + } } private fun createSpliceOutRequest(amount: Satoshi): ChannelCommand.Commitment.Splice.Request = ChannelCommand.Commitment.Splice.Request( @@ -1856,8 +1862,8 @@ class SpliceTestsCommon : LightningTestSuite() { assertIs(bob1.state) assertTrue(actionsBob1.isEmpty()) - val aliceInit = Init(alice1.commitments.params.localParams.features) - val bobInit = Init(bob1.commitments.params.localParams.features) + val aliceInit = Init(alice1.commitments.channelParams.localParams.features) + val bobInit = Init(bob1.commitments.channelParams.localParams.features) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Connected(aliceInit, bobInit)) assertIs>(alice2) val channelReestablish = actionsAlice2.findOutgoingMessage() @@ -1876,9 +1882,9 @@ class SpliceTestsCommon : LightningTestSuite() { val claimRemoteDelayedOutputTx = actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) Transaction.correctlySpends(claimRemoteDelayedOutputTx, remoteCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) actions1.hasWatchConfirmed(remoteCommitTx.txid) - actions1.hasWatchConfirmed(claimRemoteDelayedOutputTx.txid) - assertEquals(commitment.localCommit.spec.htlcs.outgoings().size, actions1.filterIsInstance().filter { it.txType == ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcTimeoutTx }.size) - assertEquals(commitment.localCommit.spec.htlcs.size, actions1.findWatches().size) + actions1.hasWatchOutputSpent(claimRemoteDelayedOutputTx.txIn.first().outPoint) + assertEquals(commitment.localCommit.spec.htlcs.outgoings().size, actions1.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcTimeoutTx).size) + actions1.hasWatchOutputsSpent(channel1.state.remoteCommitPublished!!.htlcOutputs) // Remote commit confirms. val (channel2, actions2) = channel1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(channel1.channelId, WatchConfirmed.ClosingTxConfirmed, channel1.currentBlockHeight, 42, remoteCommitTx))) @@ -1907,16 +1913,16 @@ class SpliceTestsCommon : LightningTestSuite() { assertIs(alice2.state) // Alice attempts to force-close and in parallel puts a watch on the remote commit. val localCommit = actionsAlice2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx) - assertEquals(localCommit.txid, alice1.commitments.active.first().localCommit.publishableTxs.commitTx.tx.txid) + assertEquals(localCommit.txid, alice1.commitments.active.first().localCommit.txId) val claimMain = actionsAlice2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) assertEquals( alice1.commitments.active.first().localCommit.spec.htlcs.outgoings().size, - actionsAlice2.filterIsInstance().filter { it.txType == ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx }.size + actionsAlice2.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx).size ) actionsAlice2.hasWatchConfirmed(localCommit.txid) - actionsAlice2.hasWatchConfirmed(claimMain.txid) + actionsAlice2.hasWatchOutputSpent(claimMain.txIn.first().outPoint) assertEquals(actionsAlice2.hasWatchConfirmed(bobCommitTx.txid).event, WatchConfirmed.AlternativeCommitTxConfirmed) - assertEquals(alice1.commitments.active.first().localCommit.spec.htlcs.size, actionsAlice2.findWatches().size) + actionsAlice2.hasWatchOutputsSpent(alice2.state.localCommitPublished!!.htlcOutputs) actionsAlice2.has() actionsAlice2.has() @@ -1939,34 +1945,31 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(actionsAlice2.size, 9) val rvk = alice2.state.revokedCommitPublished.firstOrNull() assertNotNull(rvk) - assertNotNull(rvk.claimMainOutputTx) - actionsAlice2.hasPublishTx(rvk.claimMainOutputTx.tx) - Transaction.correctlySpends(rvk.claimMainOutputTx.tx, bobCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assertNotNull(rvk.mainPenaltyTx) - actionsAlice2.hasPublishTx(rvk.mainPenaltyTx.tx) - Transaction.correctlySpends(rvk.mainPenaltyTx.tx, bobCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - val watchCommitConfirmed = actionsAlice2.hasWatchConfirmed(bobCommitTx.txid) - assertEquals(WatchConfirmed.ClosingTxConfirmed, watchCommitConfirmed.event) - actionsAlice2.hasWatchConfirmed(rvk.claimMainOutputTx.tx.txid) - val watchSpent = actionsAlice2.hasWatch() - assertEquals(watchSpent.txId, rvk.mainPenaltyTx.input.outPoint.txid) - assertEquals(watchSpent.outputIndex, rvk.mainPenaltyTx.input.outPoint.index.toInt()) + assertNotNull(rvk.localOutput) + val mainTx = actionsAlice2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + Transaction.correctlySpends(mainTx, bobCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actionsAlice2.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + assertNotNull(rvk.remoteOutput) + val penaltyTx = actionsAlice2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) + Transaction.correctlySpends(penaltyTx, bobCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actionsAlice2.hasWatchOutputSpent(penaltyTx.txIn.first().outPoint) + actionsAlice2.hasWatchConfirmed(bobCommitTx.txid).also { assertEquals(WatchConfirmed.ClosingTxConfirmed, it.event) } assertEquals(actionsAlice2.find().revokedCommitTxId, bobCommitTx.txid) actionsAlice2.hasOutgoingMessage() actionsAlice2.has() actionsAlice2.has() // Bob's commitment confirms. - val (alice3, actionsAlice3) = alice2.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice2.channelId, watchCommitConfirmed.event, alice2.currentBlockHeight, 43, bobCommitTx))) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice2.channelId, WatchConfirmed.ClosingTxConfirmed, alice2.currentBlockHeight, 43, bobCommitTx))) assertIs(alice3.state) actionsAlice3.has() // Alice's transactions confirm. - val (alice4, actionsAlice4) = alice3.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice3.channelId, WatchConfirmed.ClosingTxConfirmed, alice3.currentBlockHeight, 44, rvk.claimMainOutputTx.tx))) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice3.channelId, WatchConfirmed.ClosingTxConfirmed, alice3.currentBlockHeight, 44, mainTx))) assertIs(alice4.state) assertEquals(actionsAlice4.size, 1) actionsAlice4.has() - val (alice5, actionsAlice5) = alice4.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice4.channelId, WatchConfirmed.ClosingTxConfirmed, alice4.currentBlockHeight, 45, rvk.mainPenaltyTx.tx))) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice4.channelId, WatchConfirmed.ClosingTxConfirmed, alice4.currentBlockHeight, 45, penaltyTx))) assertIs(alice5.state) actionsAlice5.has() actionsAlice5.has() diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt index 3f82bace1..fddf45f9d 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt @@ -24,14 +24,14 @@ class SyncingTestsCommon : LightningTestSuite() { val (alice, bob) = init() disconnect(alice, bob) } - val aliceCommitTx = alice.commitments.latest.localCommit.publishableTxs.commitTx.tx + val aliceCommitTx = alice.signCommitTx() val (bob1, actions) = bob.process(ChannelCommand.WatchReceived(WatchSpentTriggered(bob.state.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), aliceCommitTx))) assertIs(bob1.state) // we published a tx to claim our main output val claimTx = actions.filterIsInstance().map { it.tx }.first() - Transaction.correctlySpends(claimTx, alice.commitments.latest.localCommit.publishableTxs.commitTx.tx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - val watches = actions.findWatches() - assertEquals(watches.map { it.txId }.toSet(), setOf(aliceCommitTx.txid, claimTx.txid)) + Transaction.correctlySpends(claimTx, aliceCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actions.hasWatchConfirmed(aliceCommitTx.txid) + actions.hasWatchOutputSpent(claimTx.txIn.first().outPoint) } @Test @@ -42,16 +42,15 @@ class SyncingTestsCommon : LightningTestSuite() { val (alice1, bob1) = nodes val (alice2, bob2) = TestsHelper.crossSign(alice1, bob1) val (alice3, bob3, _) = disconnect(alice2, bob2) - Triple(alice3, bob3, alice.commitments.latest.localCommit.publishableTxs.commitTx.tx) + Triple(alice3, bob3, alice.signCommitTx()) } val (bob1, actions) = bob.process(ChannelCommand.WatchReceived(WatchSpentTriggered(bob.state.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), revokedTx))) assertIs(bob1.state) val claimTxs = actions.filterIsInstance().map { it.tx } assertEquals(claimTxs.size, 2) claimTxs.forEach { Transaction.correctlySpends(it, revokedTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - val watches = actions.findWatches() - // we watch the revoked tx and our "claim main output tx" - assertEquals(watches.map { it.txId }.toSet(), setOf(revokedTx.txid, bob1.state.revokedCommitPublished.first().claimMainOutputTx!!.tx.txid)) + actions.hasWatchConfirmed(revokedTx.txid) + actions.hasWatchOutputsSpent(claimTxs.flatMap { tx -> tx.txIn.map { it.outPoint } }.toSet()) } @Test @@ -462,7 +461,7 @@ class SyncingTestsCommon : LightningTestSuite() { assertIs(bob5.state.rbfStatus) val commitSigAlice = actionsAlice5.hasOutgoingMessage() val commitSigBob = actionsBob5.hasOutgoingMessage() - return UnsignedRbfFixture(alice5, commitSigAlice, bob5, commitSigBob, (alice5.state.rbfStatus as RbfStatus.WaitingForSigs).session.fundingTx.txId) + return UnsignedRbfFixture(alice5, commitSigAlice, bob5, commitSigBob, alice5.state.rbfStatus.session.fundingTx.txId) } fun disconnect(alice: LNChannel, bob: LNChannel): Triple, LNChannel, Pair> { @@ -474,8 +473,8 @@ class SyncingTestsCommon : LightningTestSuite() { assertIs(bob1.state) assertTrue(actionsBob1.isEmpty()) - val aliceInit = Init(alice1.commitments.params.localParams.features) - val bobInit = Init(bob1.commitments.params.localParams.features) + val aliceInit = Init(alice1.commitments.channelParams.localParams.features) + val bobInit = Init(bob1.commitments.channelParams.localParams.features) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Connected(aliceInit, bobInit)) assertIs>(alice2) @@ -499,13 +498,13 @@ class SyncingTestsCommon : LightningTestSuite() { val aliceFeatures = when (alice.state) { is WaitForFundingSigned -> alice.state.channelParams.localParams.features - is ChannelStateWithCommitments -> alice.state.commitments.params.localParams.features + is ChannelStateWithCommitments -> alice.state.commitments.channelParams.localParams.features } val aliceInit = Init(aliceFeatures) assertTrue(aliceFeatures.hasFeature(Feature.ProvideStorage)) val bobFeatures = when (bob.state) { is WaitForFundingSigned -> bob.state.channelParams.localParams.features - is ChannelStateWithCommitments -> bob.state.commitments.params.localParams.features + is ChannelStateWithCommitments -> bob.state.commitments.channelParams.localParams.features } val bobInit = Init(bobFeatures) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt index 1a11ad2b8..a098cfc8b 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt @@ -12,6 +12,7 @@ import fr.acinq.lightning.channel.* import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.tests.utils.runSuspendTest +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.* import kotlin.test.* @@ -29,7 +30,8 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { val txAddInput = actions1.findOutgoingMessage() assertNotEquals(txAddInput.channelId, accept.temporaryChannelId) assertEquals(alice1.channelId, txAddInput.channelId) - assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) + assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, alice1.state.interactiveTxSession.fundingParams.commitmentFormat) } @Test @@ -51,7 +53,8 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { assertEquals(3, actions1.size) actions1.find() actions1.findOutgoingMessage() - assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) + assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, alice1.state.interactiveTxSession.fundingParams.commitmentFormat) assertEquals(ChannelEvents.Creating(alice1.state), actions1.find().event) } @@ -65,7 +68,8 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { actions1.find() assertEquals(ChannelEvents.Creating(alice1.state), actions1.find().event) actions1.findOutgoingMessage() - assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels, Feature.DualFunding))) + assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.ZeroReserveChannels, Feature.DualFunding))) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, alice1.state.interactiveTxSession.fundingParams.commitmentFormat) } @Test diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt index ceb22495d..787a7b711 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.Transaction import fr.acinq.lightning.ChannelEvents import fr.acinq.lightning.Features @@ -84,28 +85,34 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { val (alice, _, bob, _) = init() // bob publishes his commitment tx run { - val bobCommitTx = bob.commitments.latest.localCommit.publishableTxs.commitTx.tx + val bobCommitTx = bob.signCommitTx() val (alice1, actions1) = alice.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTx))) assertIs(alice1.state) assertNotNull(alice1.state.remoteCommitPublished) - assertEquals(1, actions1.findPublishTxs().size) - assertEquals(2, actions1.findWatches().size) // commit tx + main output + assertNotNull(alice1.state.remoteCommitPublished.localOutput) + val mainTx = actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + Transaction.correctlySpends(mainTx, listOf(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actions1.hasWatchConfirmed(bobCommitTx.txid) + actions1.hasWatchOutputSpent(mainTx.txIn.first().outPoint) } // alice publishes her commitment tx run { - val aliceCommitTx = alice.commitments.latest.localCommit.publishableTxs.commitTx.tx + val aliceCommitTx = alice.signCommitTx() val (bob1, actions1) = bob.process(ChannelCommand.WatchReceived(WatchSpentTriggered(bob.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), aliceCommitTx))) assertIs(bob1.state) assertNotNull(bob1.state.remoteCommitPublished) - assertEquals(1, actions1.findPublishTxs().size) - assertEquals(2, actions1.findWatches().size) // commit tx + main output + assertNotNull(bob1.state.remoteCommitPublished.localOutput) + val mainTx = actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + Transaction.correctlySpends(mainTx, listOf(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actions1.hasWatchConfirmed(aliceCommitTx.txid) + actions1.hasWatchOutputSpent(mainTx.txIn.first().outPoint) } } @Test fun `recv ChannelSpent -- other commit`() { val (alice, _, _) = init() - val aliceCommitTx = alice.commitments.latest.localCommit.publishableTxs.commitTx.tx + val aliceCommitTx = alice.signCommitTx() val unknownTx = Transaction(2, aliceCommitTx.txIn, listOf(), 0) val (alice1, actions1) = alice.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), unknownTx))) assertEquals(alice.state, alice1.state) @@ -116,13 +123,13 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { fun `recv Error`() { val (alice, _, bob, _) = init() listOf(alice, bob).forEach { state -> - val commitTx = state.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = state.signCommitTx() val (state1, actions1) = state.process(ChannelCommand.MessageReceived(Error(state.channelId, "no lightning for you sir"))) assertIs(state1.state) assertNotNull(state1.state.localCommitPublished) assertNull(actions1.findOutgoingMessageOpt()) actions1.hasPublishTx(commitTx) - actions1.hasWatch() + actions1.hasWatchConfirmed(commitTx.txid) } } @@ -143,14 +150,14 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Close_ForceClose`() { val (alice, _, bob, _) = init() listOf(alice, bob).forEach { state -> - val commitTx = state.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTx = state.signCommitTx() val (state1, actions1) = state.process(ChannelCommand.Close.ForceClose) assertIs(state1.state) assertNotNull(state1.state.localCommitPublished) val error = actions1.hasOutgoingMessage() assertEquals(ForcedLocalCommit(bob.channelId).message, error.toAscii()) actions1.hasPublishTx(commitTx) - actions1.hasWatch() + actions1.hasWatchConfirmed(commitTx.txid) } } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt index b0e407f46..142663938 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt @@ -1,7 +1,6 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* -import fr.acinq.lightning.Feature import fr.acinq.lightning.Features import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey @@ -44,7 +43,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val watch = actionsAlice1.hasWatch() assertIs(watch.event) assertEquals(watch.txId, fundingTx.txid) - assertEquals(watch.outputIndex.toLong(), alice.state.commitments.latest.commitInput.outPoint.index) + assertEquals(watch.outputIndex.toLong(), alice.state.commitments.latest.fundingInput.index) } run { val (bob1, actionsBob1) = bob.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob.state.channelId, WatchConfirmed.ChannelFundingDepthOk, 42, 0, fundingTx))) @@ -55,7 +54,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val watch = actionsBob1.hasWatch() assertIs(watch.event) assertEquals(watch.txId, fundingTx.txid) - assertEquals(watch.outputIndex.toLong(), bob.state.commitments.latest.commitInput.outPoint.index) + assertEquals(watch.outputIndex.toLong(), bob.state.commitments.latest.fundingInput.index) } } @@ -73,7 +72,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val watch = actionsBob2.hasWatch() assertIs(watch.event) assertEquals(watch.txId, fundingTx.txid) - assertEquals(watch.outputIndex.toLong(), bob.state.commitments.latest.commitInput.outPoint.index) + assertEquals(watch.outputIndex.toLong(), bob.state.commitments.latest.fundingInput.index) } @Test @@ -90,7 +89,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val watch = actionsBob2.hasWatch() assertIs(watch.event) assertEquals(watch.txId, previousFundingTx.txid) - assertEquals(watch.outputIndex.toLong(), bob.state.commitments.latest.commitInput.outPoint.index) + assertEquals(watch.outputIndex.toLong(), bob.state.commitments.latest.fundingInput.index) } run { val (alice2, actionsAlice2) = alice1.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice.state.channelId, WatchConfirmed.ChannelFundingDepthOk, 42, 0, previousFundingTx))) @@ -101,7 +100,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val watch = actionsAlice2.hasWatch() assertIs(watch.event) assertEquals(watch.txId, previousFundingTx.txid) - assertEquals(watch.outputIndex.toLong(), bob.state.commitments.latest.commitInput.outPoint.index) + assertEquals(watch.outputIndex.toLong(), bob.state.commitments.latest.fundingInput.index) } } @@ -265,18 +264,26 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val (bob1, actions1) = bob.process(ChannelCommand.MessageReceived(Error(bob.state.channelId, "oops"))) assertIs(bob1.state) assertNotNull(bob1.state.localCommitPublished) - actions1.hasPublishTx(bob.state.commitments.latest.localCommit.publishableTxs.commitTx.tx) - assertEquals(2, actions1.findWatches().size) // commit tx + main output + val commitTx = actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx) + Transaction.correctlySpends(commitTx, mapOf(bob.commitments.latest.fundingInput to bob.commitments.latest.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val mainTx = actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) + Transaction.correctlySpends(mainTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assertEquals(setOf(commitTx.txid), actions1.findWatches().map { it.txId }.toSet()) + actions1.hasWatchOutputSpent(mainTx.txIn.first().outPoint) } @Test fun `recv Error -- previous funding tx confirms`() { val (alice, bob, previousFundingTx, walletAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs) - val commitTxAlice1 = alice.state.commitments.latest.localCommit.publishableTxs.commitTx.tx - val commitTxBob1 = bob.state.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTxAlice1 = alice.signCommitTx() + Transaction.correctlySpends(commitTxAlice1, listOf(previousFundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val commitTxBob1 = bob.signCommitTx() + Transaction.correctlySpends(commitTxBob1, listOf(previousFundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val (alice1, bob1, fundingTx) = rbf(alice, bob, walletAlice) - val commitTxAlice2 = alice1.state.commitments.latest.localCommit.publishableTxs.commitTx.tx - val commitTxBob2 = bob1.state.commitments.latest.localCommit.publishableTxs.commitTx.tx + val commitTxAlice2 = alice1.signCommitTx() + Transaction.correctlySpends(commitTxAlice2, listOf(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val commitTxBob2 = bob1.signCommitTx() + Transaction.correctlySpends(commitTxBob2, listOf(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) assertNotEquals(previousFundingTx.txid, fundingTx.txid) assertNotEquals(commitTxAlice1.txid, commitTxAlice2.txid) assertNotEquals(commitTxBob1.txid, commitTxBob2.txid) @@ -351,9 +358,12 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val (state1, actions1) = state.process(ChannelCommand.Close.ForceClose) assertIs(state1.state) assertNotNull(state1.state.localCommitPublished) - actions1.hasPublishTx(state1.state.localCommitPublished.commitTx) - actions1.hasPublishTx(state1.state.localCommitPublished.claimMainDelayedOutputTx!!.tx) - assertEquals(2, actions1.findWatches().size) // commit tx + main output + val commitTx = state1.state.localCommitPublished.commitTx + actions1.hasPublishTx(commitTx) + val mainTx = actions1.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) + Transaction.correctlySpends(mainTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assertEquals(setOf(commitTx.txid), actions1.findWatches().map { it.txId }.toSet()) + actions1.hasWatchOutputSpent(mainTx.txIn.first().outPoint) } } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt index 1dbc75db3..93869269c 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt @@ -60,8 +60,8 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { actionsBob3.has() assertIs(alice2.state) assertIs(bob3.state) - assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) - assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) + assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) assertIs(alice.state.replyTo.await()).also { assertEquals(0, it.fundingTxIndex) } assertIs(bob.state.replyTo.await()).also { assertEquals(0, it.fundingTxIndex) } verifyCommits(alice2.state.signingSession, bob3.state.signingSession, TestConstants.aliceFundingAmount.toMilliSatoshi(), 0.msat) @@ -87,8 +87,8 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { actionsBob3.has() assertIs(alice2.state) assertIs(bob3.state) - assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) - assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) + assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) verifyCommits( alice2.state.signingSession, bob3.state.signingSession, @@ -118,8 +118,8 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { actionsBob3.has() assertIs(alice2.state) assertIs(bob3.state) - assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) - assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) + assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) verifyCommits(alice2.state.signingSession, bob3.state.signingSession, balanceAlice = 10_000_000.msat, balanceBob = 1_500_000_000.msat) } @@ -143,8 +143,8 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { actionsBob3.has() assertIs(alice2.state) assertIs(bob3.state) - assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels, Feature.DualFunding))) - assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels, Feature.DualFunding))) + assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.ZeroReserveChannels, Feature.DualFunding))) + assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.ZeroReserveChannels, Feature.DualFunding))) verifyCommits(alice2.state.signingSession, bob3.state.signingSession, TestConstants.aliceFundingAmount.toMilliSatoshi(), TestConstants.bobFundingAmount.toMilliSatoshi()) } @@ -170,12 +170,12 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { fun `recv CommitSig`() { val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) run { - val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(CommitSig(alice.channelId, ByteVector64.Zeroes, listOf()))) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(CommitSig(alice.channelId, ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes), listOf()))) assertEquals(actionsAlice1.findOutgoingMessage().toAscii(), UnexpectedCommitSig(alice.channelId).message) assertIs(alice1.state) } run { - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(CommitSig(bob.channelId, ByteVector64.Zeroes, listOf()))) + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(CommitSig(bob.channelId, ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes), listOf()))) assertEquals(actionsBob1.findOutgoingMessage().toAscii(), UnexpectedCommitSig(bob.channelId).message) assertIs(bob1.state) } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt index 95d7137fe..95a2217fc 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt @@ -22,7 +22,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { @Test fun `recv CommitSig`() { val (alice, commitSigAlice, bob, commitSigBob) = init() - val commitInput = alice.state.signingSession.commitInput + val commitInput = alice.state.signingSession.commitInput(alice.channelKeys) run { alice.process(ChannelCommand.MessageReceived(commitSigBob)).also { (state, actions) -> assertIs(state.state) @@ -116,14 +116,14 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { fun `recv CommitSig -- with invalid signature`() { val (alice, commitSigAlice, bob, commitSigBob) = init() run { - val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitSigBob.copy(signature = ByteVector64.Zeroes))) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitSigBob.copy(signature = ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes)))) assertEquals(actionsAlice1.size, 2) actionsAlice1.hasOutgoingMessage() actionsAlice1.find().also { assertEquals(alice.channelId, it.data.channelId) } assertIs(alice1.state) } run { - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice.copy(signature = ByteVector64.Zeroes))) + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice.copy(signature = ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes)))) assertEquals(actionsBob1.size, 2) actionsBob1.hasOutgoingMessage() actionsBob1.find().also { assertEquals(bob.channelId, it.data.channelId) } @@ -134,7 +134,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { @Test fun `recv TxSignatures`() { val (alice, commitSigAlice, bob, commitSigBob) = init() - val commitInput = alice.state.signingSession.commitInput + val commitInput = alice.state.signingSession.commitInput(alice.channelKeys) val txSigsBob = run { val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) assertIs(bob1.state) @@ -161,7 +161,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { @Test fun `recv TxSignatures -- liquidity ads`() { val (alice, commitSigAlice, bob, commitSigBob) = init(requestRemoteFunding = TestConstants.bobFundingAmount) - val commitInput = alice.state.signingSession.commitInput + val commitInput = alice.state.signingSession.commitInput(alice.channelKeys) val txSigsBob = run { val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) assertIs(bob1.state) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt index 16375836c..21400371a 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt @@ -7,6 +7,7 @@ import fr.acinq.lightning.channel.* import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.tests.utils.runSuspendTest +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.AcceptDualFundedChannel import fr.acinq.lightning.wire.ChannelTlv @@ -27,7 +28,8 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { assertIs>(bob1) assertEquals(3, actions.size) assertTrue(bob1.state.channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) - assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) + assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, bob1.state.interactiveTxSession.fundingParams.commitmentFormat) actions.hasOutgoingMessage() actions.has() assertEquals(ChannelEvents.Creating(bob1.state), actions.find().event) @@ -39,7 +41,8 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { val (bob1, actions) = bob.process(ChannelCommand.MessageReceived(open)) assertIs>(bob1) assertEquals(3, actions.size) - assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding))) + assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, bob1.state.interactiveTxSession.fundingParams.commitmentFormat) actions.hasOutgoingMessage() actions.has() assertEquals(ChannelEvents.Creating(bob1.state), actions.find().event) @@ -52,7 +55,8 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { assertIs>(bob1) assertEquals(3, actions.size) assertTrue(bob1.state.channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) - assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels, Feature.DualFunding))) + assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.ZeroReserveChannels, Feature.DualFunding))) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, bob1.state.interactiveTxSession.fundingParams.commitmentFormat) val accept = actions.hasOutgoingMessage() assertEquals(0, accept.minimumDepth) actions.has() diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt index 84f7ce0a8..4d021c487 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt @@ -119,7 +119,7 @@ class PeerTest : LightningTestSuite() { alice.forward(open3) alice2bob.expect() - assertEquals(3, alice.channels.values.filterIsInstance().map { it.localParams.fundingKeyPath }.toSet().size) + assertEquals(3, alice.channels.values.filterIsInstance().map { it.localChannelParams.fundingKeyPath }.toSet().size) } @Test @@ -303,9 +303,9 @@ class PeerTest : LightningTestSuite() { val syncState = syncChannels.first() assertIs(syncState.state) - val commitments = (syncState.state as Normal).commitments + val commitments = syncState.state.commitments val yourLastPerCommitmentSecret = ByteVector32.Zeroes - val myCurrentPerCommitmentPoint = peer.nodeParams.keyManager.channelKeys(commitments.params.localParams.fundingKeyPath).commitmentPoint(commitments.localCommitIndex) + val myCurrentPerCommitmentPoint = peer.nodeParams.keyManager.channelKeys(commitments.channelParams.localParams.fundingKeyPath).commitmentPoint(commitments.localCommitIndex) val channelReestablish = ChannelReestablish( channelId = syncState.state.channelId, diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt index 70ea23cf9..5ef6633ce 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt @@ -104,7 +104,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { assertEquals(100_000.msat, dbPayment.recipientAmount) assertEquals(invoice.nodeId, dbPayment.recipient) assertTrue(dbPayment.status is LightningOutgoingPayment.Status.Failed) - assertEquals(FinalFailure.ChannelNotConnected, (dbPayment.status as LightningOutgoingPayment.Status.Failed).reason) + assertEquals(FinalFailure.ChannelNotConnected, dbPayment.status.reason) assertTrue(dbPayment.parts.isEmpty()) } @@ -123,7 +123,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { assertNotNull(dbPayment) assertEquals(amount, dbPayment.recipientAmount) assertTrue(dbPayment.status is LightningOutgoingPayment.Status.Failed) - assertEquals(FinalFailure.InsufficientBalance, (dbPayment.status as LightningOutgoingPayment.Status.Failed).reason) + assertEquals(FinalFailure.InsufficientBalance, dbPayment.status.reason) assertTrue(dbPayment.parts.isEmpty()) } @@ -158,7 +158,8 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { @Test fun `channel restrictions -- maxAcceptedHtlcs`() = runSuspendTest { var (alice, _) = TestsHelper.reachNormal() - alice = alice.copy(state = alice.state.copy(commitments = alice.commitments.copy(params = alice.commitments.params.copy(remoteParams = alice.commitments.params.remoteParams.copy(maxAcceptedHtlcs = 1))))) + val remoteCommitParams = alice.state.commitments.latest.remoteCommitParams.copy(maxAcceptedHtlcs = 1) + alice = alice.copy(state = alice.state.copy(commitments = alice.state.commitments.copy(active = alice.state.commitments.active.map { it.copy(remoteCommitParams = remoteCommitParams) }))) val outgoingPaymentHandler = OutgoingPaymentHandler(alice.staticParams.nodeParams, defaultWalletParams, InMemoryPaymentsDb()) run { @@ -205,10 +206,8 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { @Test fun `channel restrictions -- maxHtlcValueInFlight`() = runSuspendTest { var (alice, _) = TestsHelper.reachNormal() - val maxHtlcValueInFlightMsat = 150_000L - alice = alice.copy( - state = alice.state.copy(commitments = alice.commitments.copy(params = alice.commitments.params.copy(remoteParams = alice.commitments.params.remoteParams.copy(maxHtlcValueInFlightMsat = maxHtlcValueInFlightMsat)))) - ) + val remoteCommitParams = alice.state.commitments.latest.remoteCommitParams.copy(maxHtlcValueInFlightMsat = 150_000L) + alice = alice.copy(state = alice.state.copy(commitments = alice.state.commitments.copy(active = alice.state.commitments.active.map { it.copy(remoteCommitParams = remoteCommitParams) }))) val outgoingPaymentHandler = OutgoingPaymentHandler(alice.staticParams.nodeParams, defaultWalletParams, InMemoryPaymentsDb()) run { @@ -244,7 +243,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { assertNotNull(addFailure) // Now the channel error gets sent back to the OutgoingPaymentHandler. val failure = outgoingPaymentHandler.processAddFailed(alice.channelId, addFailure) - val expected = OutgoingPaymentHandler.Failure(payment, OutgoingPaymentFailure(FinalFailure.NoAvailableChannels, listOf(Either.Left(HtlcValueTooHighInFlight(alice.channelId, maxHtlcValueInFlightMsat.toULong(), 200_000.msat))))) + val expected = OutgoingPaymentHandler.Failure(payment, OutgoingPaymentFailure(FinalFailure.NoAvailableChannels, listOf(Either.Left(HtlcValueTooHighInFlight(alice.channelId, 150_000UL, 200_000.msat))))) assertFailureEquals(failure as OutgoingPaymentHandler.Failure, expected) assertNull(outgoingPaymentHandler.getPendingPayment(payment.paymentId)) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt index 42c7401d4..fbb1acbc8 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt @@ -1,11 +1,14 @@ package fr.acinq.lightning.tests -import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.ByteVector +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.Chain +import fr.acinq.bitcoin.MnemonicCode import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.blockchain.fee.FeerateTolerance import fr.acinq.lightning.blockchain.fee.OnChainFeeConf -import fr.acinq.lightning.channel.LocalParams +import fr.acinq.lightning.channel.LocalChannelParams import fr.acinq.lightning.crypto.LocalKeyManager import fr.acinq.lightning.tests.utils.testLoggerFactory import fr.acinq.lightning.utils.msat @@ -103,7 +106,7 @@ object TestConstants { paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(0), CltvExpiryDelta(0)), ) - fun channelParams(payCommitTxFees: Boolean): LocalParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = payCommitTxFees) + fun channelParams(payCommitTxFees: Boolean): LocalChannelParams = LocalChannelParams(nodeParams, isChannelOpener = true, payCommitTxFees = payCommitTxFees) } object Bob { @@ -126,16 +129,16 @@ object TestConstants { feerateTolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 5.0) ), minDepthBlocks = 3, - maxHtlcValueInFlightMsat = 1_500_000_000L, - maxAcceptedHtlcs = 100, - toRemoteDelayBlocks = CltvExpiryDelta(144), + maxHtlcValueInFlightMsat = 2_000_000_000L, + maxAcceptedHtlcs = 80, + toRemoteDelayBlocks = CltvExpiryDelta(72), maxToLocalDelayBlocks = CltvExpiryDelta(1024), feeBase = 10.msat, feeProportionalMillionths = 10, paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(0), CltvExpiryDelta(0)), ) - fun channelParams(payCommitTxFees: Boolean): LocalParams = LocalParams(nodeParams, isChannelOpener = false, payCommitTxFees = payCommitTxFees) + fun channelParams(payCommitTxFees: Boolean): LocalChannelParams = LocalChannelParams(nodeParams, isChannelOpener = false, payCommitTxFees = payCommitTxFees) } } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt index e0e972335..0e7a02cad 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt @@ -156,7 +156,7 @@ suspend fun CoroutineScope.newPeer( } val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes - val myCurrentPerCommitmentPoint = peer.nodeParams.keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath).commitmentPoint(state.commitments.localCommitIndex) + val myCurrentPerCommitmentPoint = peer.nodeParams.keyManager.channelKeys(state.commitments.channelParams.localParams.fundingKeyPath).commitmentPoint(state.commitments.localCommitIndex) val channelReestablish = ChannelReestablish( channelId = state.channelId, diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt index bf1b62092..a6195a99b 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt @@ -14,8 +14,6 @@ import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.crypto.LocalCommitmentKeys import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.TestHelpers -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx -import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector @@ -23,6 +21,7 @@ import fr.acinq.lightning.wire.UpdateAddHtlc import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlin.test.assertTrue class AnchorOutputsTestsCommon { @@ -54,11 +53,7 @@ class AnchorOutputsTestsCommon { val funding_tx = Transaction.read( "0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000" ) - val commitTxInput = Transactions.InputInfo( - OutPoint(funding_tx, 0), - funding_tx.txOut[0], - Scripts.multiSig2of2(local_funding_pubkey, remote_funding_pubkey) - ) + val commitTxInput = Transactions.InputInfo(OutPoint(funding_tx, 0), funding_tx.txOut[0]) val preimages = listOf( ByteVector32("0000000000000000000000000000000000000000000000000000000000000000"), ByteVector32("0101010101010101010101010101010101010101010101010101010101010101"), @@ -76,21 +71,16 @@ class AnchorOutputsTestsCommon { // high level tests which calls Commitments methods to generate transactions private fun runHighLevelTest(testCase: TestCase) { - val localParams = LocalParams( + val localChannelParams = LocalChannelParams( TestConstants.Alice.nodeParams.nodeId, KeyPath.empty, - 546.sat, 1000000000L, 0.msat, CltvExpiryDelta(144), 1000, isChannelOpener = true, paysCommitTxFees = true, Script.write(Script.pay2wpkh(randomKey().publicKey())).toByteVector(), TestConstants.Alice.nodeParams.features, ) - val remoteParams = RemoteParams( + val localCommitParams = CommitParams(546.sat, 1_000_000_000L, 0.msat, CltvExpiryDelta(144), 1000) + val remoteChannelParams = RemoteChannelParams( TestConstants.Bob.nodeParams.nodeId, - 546.sat, - 1000000000L, - 0.msat, - CltvExpiryDelta(144), - 1000, remote_revocation_basepoint, remote_payment_privkey.publicKey(), PrivateKey.fromHex("444444444444444444444444444444444444444444444444444444444444444401").publicKey(), @@ -101,8 +91,8 @@ class AnchorOutputsTestsCommon { channelId = randomBytes32(), channelConfig = ChannelConfig.standard, channelFeatures = ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs)), - localParams = localParams, - remoteParams = remoteParams, + localParams = localChannelParams, + remoteParams = remoteChannelParams, channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false) ) val spec = CommitmentSpec( @@ -134,29 +124,34 @@ class AnchorOutputsTestsCommon { val (commitTx, htlcTxs) = Commitments.makeLocalTxs( channelParams, + localCommitParams, localCommitmentKeys, 42, local_funding_privkey, remote_funding_pubkey, - Transactions.InputInfo(OutPoint(funding_tx, 0), funding_tx.txOut[0], Scripts.multiSig2of2(local_funding_pubkey, remote_funding_pubkey)), + Transactions.InputInfo(OutPoint(funding_tx, 0), funding_tx.txOut[0]), + Transactions.CommitmentFormat.AnchorOutputs, spec ) - val localSig = Transactions.sign(commitTx, local_funding_privkey) - val remoteSig = Transactions.sign(commitTx, remote_funding_privkey) - val signedTx = Transactions.addSigs(commitTx, local_funding_pubkey, remote_funding_pubkey, localSig, remoteSig) - assertEquals(Transaction.read(testCase.ExpectedCommitmentTxHex), signedTx.tx) + val localSig = commitTx.sign(local_funding_privkey, remote_funding_pubkey) + val remoteSig = commitTx.sign(remote_funding_privkey, local_funding_pubkey) + val signedTx = commitTx.aggregateSigs(local_funding_pubkey, remote_funding_pubkey, localSig, remoteSig) + assertEquals(Transaction.read(testCase.ExpectedCommitmentTxHex), signedTx) val txs = testCase.HtlcDescs.associate { Transaction.read(it.ResolutionTxHex).txid to Transaction.read(it.ResolutionTxHex) } val remoteHtlcSigs = testCase.HtlcDescs.associate { Transaction.read(it.ResolutionTxHex).txid to ByteVector(it.RemoteSigHex) } assertTrue { remoteHtlcSigs.keys.containsAll(htlcTxs.map { it.tx.txid }) } htlcTxs.forEach { htlcTx -> - val localHtlcSig = Transactions.sign(htlcTx, local_htlc_privkey, SigHash.SIGHASH_ALL) val remoteHtlcSig = Crypto.der2compact(remoteHtlcSigs[htlcTx.tx.txid]!!.toByteArray()) - val expectedTx = txs[htlcTx.tx.txid] val signed = when (htlcTx) { - is HtlcSuccessTx -> Transactions.addSigs(htlcTx, localHtlcSig, remoteHtlcSig, preimages.find { it.sha256() == htlcTx.paymentHash }!!) - is HtlcTimeoutTx -> Transactions.addSigs(htlcTx, localHtlcSig, remoteHtlcSig) + is Transactions.HtlcSuccessTx -> { + val preimage = preimages.find { it.sha256() == htlcTx.paymentHash } + assertNotNull(preimage) + htlcTx.sign(localCommitmentKeys, remoteHtlcSig, preimage) + } + is Transactions.HtlcTimeoutTx -> htlcTx.sign(localCommitmentKeys, remoteHtlcSig) } + val expectedTx = txs[htlcTx.tx.txid] assertEquals(expectedTx, signed.tx) } } @@ -176,6 +171,7 @@ class AnchorOutputsTestsCommon { true, 546.sat, CltvExpiryDelta(144), + Transactions.CommitmentFormat.AnchorOutputs, spec ) val commitTx = Transactions.makeCommitTx( @@ -186,23 +182,26 @@ class AnchorOutputsTestsCommon { true, outputs ) - val localSig = Transactions.sign(commitTx, local_funding_privkey) - val remoteSig = Transactions.sign(commitTx, remote_funding_privkey) - val signedTx = Transactions.addSigs(commitTx, local_funding_pubkey, remote_funding_pubkey, localSig, remoteSig) - assertEquals(testCase.ExpectedCommitmentTx, signedTx.tx) + val localSig = commitTx.sign(local_funding_privkey, remote_funding_pubkey) + val remoteSig = commitTx.sign(remote_funding_privkey, local_funding_pubkey) + val signedTx = commitTx.aggregateSigs(local_funding_pubkey, remote_funding_pubkey, localSig, remoteSig) + assertEquals(testCase.ExpectedCommitmentTx, signedTx) val txs = testCase.HtlcDescs.associate { it.ResolutionTx.txid to it.ResolutionTx } val remoteHtlcSigs = testCase.HtlcDescs.associate { it.ResolutionTx.txid to ByteVector(it.RemoteSigHex) } - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localCommitmentKeys.publicKeys, 546.sat, CltvExpiryDelta(144), spec.feerate, outputs) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, outputs, Transactions.CommitmentFormat.AnchorOutputs) assertTrue { remoteHtlcSigs.keys.containsAll(htlcTxs.map { it.tx.txid }) } htlcTxs.forEach { htlcTx -> - val localHtlcSig = Transactions.sign(htlcTx, local_htlc_privkey, SigHash.SIGHASH_ALL) val remoteHtlcSig = Crypto.der2compact(remoteHtlcSigs[htlcTx.tx.txid]!!.toByteArray()) - val expectedTx = txs[htlcTx.tx.txid] val signed = when (htlcTx) { - is HtlcSuccessTx -> Transactions.addSigs(htlcTx, localHtlcSig, remoteHtlcSig, preimages.find { it.sha256() == htlcTx.paymentHash }!!) - is HtlcTimeoutTx -> Transactions.addSigs(htlcTx, localHtlcSig, remoteHtlcSig) + is Transactions.HtlcSuccessTx -> { + val preimage = preimages.find { it.sha256() == htlcTx.paymentHash } + assertNotNull(preimage) + htlcTx.sign(localCommitmentKeys, remoteHtlcSig, preimage) + } + is Transactions.HtlcTimeoutTx -> htlcTx.sign(localCommitmentKeys, remoteHtlcSig) } + val expectedTx = txs[htlcTx.tx.txid] assertEquals(expectedTx, signed.tx) } } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index 2ff413e07..14ee6029b 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -1,62 +1,27 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* -import fr.acinq.bitcoin.Crypto.ripemd160 import fr.acinq.bitcoin.Crypto.sha256 import fr.acinq.bitcoin.Script.pay2wpkh -import fr.acinq.bitcoin.Script.pay2wsh import fr.acinq.bitcoin.Script.write -import fr.acinq.bitcoin.crypto.Pack import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Lightning.randomBytes32 +import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.Lightning.randomKey -import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.Commitments -import fr.acinq.lightning.channel.Helpers.Funding +import fr.acinq.lightning.channel.ChannelSpendSignature import fr.acinq.lightning.crypto.LocalCommitmentKeys import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.io.AddLiquidityForIncomingPayment import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.transactions.CommitmentOutput.OutHtlc -import fr.acinq.lightning.transactions.Scripts.htlcOffered -import fr.acinq.lightning.transactions.Scripts.htlcReceived -import fr.acinq.lightning.transactions.Scripts.toLocalDelayed -import fr.acinq.lightning.transactions.Transactions.PlaceHolderPubKey -import fr.acinq.lightning.transactions.Transactions.PlaceHolderSig -import fr.acinq.lightning.transactions.Transactions.TxGenerationSkipped.AmountBelowDustLimit -import fr.acinq.lightning.transactions.Transactions.TxGenerationSkipped.OutputNotFound -import fr.acinq.lightning.transactions.Transactions.TxResult.Skipped -import fr.acinq.lightning.transactions.Transactions.TxResult.Success -import fr.acinq.lightning.transactions.Transactions.addSigs -import fr.acinq.lightning.transactions.Transactions.checkSig -import fr.acinq.lightning.transactions.Transactions.checkSpendable -import fr.acinq.lightning.transactions.Transactions.claimHtlcDelayedWeight -import fr.acinq.lightning.transactions.Transactions.claimHtlcSuccessWeight -import fr.acinq.lightning.transactions.Transactions.claimHtlcTimeoutWeight import fr.acinq.lightning.transactions.Transactions.commitTxFee import fr.acinq.lightning.transactions.Transactions.decodeTxNumber import fr.acinq.lightning.transactions.Transactions.encodeTxNumber -import fr.acinq.lightning.transactions.Transactions.fee2rate -import fr.acinq.lightning.transactions.Transactions.getCommitTxNumber -import fr.acinq.lightning.transactions.Transactions.htlcPenaltyWeight -import fr.acinq.lightning.transactions.Transactions.mainPenaltyWeight -import fr.acinq.lightning.transactions.Transactions.makeClaimDelayedOutputPenaltyTxs -import fr.acinq.lightning.transactions.Transactions.makeClaimHtlcSuccessTx -import fr.acinq.lightning.transactions.Transactions.makeClaimHtlcTimeoutTx -import fr.acinq.lightning.transactions.Transactions.makeClaimLocalDelayedOutputTx -import fr.acinq.lightning.transactions.Transactions.makeClaimRemoteDelayedOutputTx -import fr.acinq.lightning.transactions.Transactions.makeClosingTxs -import fr.acinq.lightning.transactions.Transactions.makeCommitTx -import fr.acinq.lightning.transactions.Transactions.makeCommitTxOutputs -import fr.acinq.lightning.transactions.Transactions.makeHtlcPenaltyTx -import fr.acinq.lightning.transactions.Transactions.makeHtlcTxs -import fr.acinq.lightning.transactions.Transactions.makeMainPenaltyTx -import fr.acinq.lightning.transactions.Transactions.sign import fr.acinq.lightning.transactions.Transactions.swapInputWeight import fr.acinq.lightning.transactions.Transactions.swapInputWeightLegacy import fr.acinq.lightning.transactions.Transactions.weight2fee @@ -94,7 +59,6 @@ class TransactionsTestsCommon : LightningTestSuite() { theirHtlcPublicKey = localHtlcPriv.publicKey(), revocationPublicKey = localRevocationPriv.publicKey(), ) - private val commitInput = Funding.makeFundingInputInfo(TxId(randomBytes32()), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey()) private val toLocalDelay = CltvExpiryDelta(144) private val localDustLimit = 546.sat private val feerate = FeeratePerKw(22_000.sat) @@ -131,329 +95,261 @@ class TransactionsTestsCommon : LightningTestSuite() { IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000.msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket)) ) val spec = CommitmentSpec(htlcs, feerate = FeeratePerKw(5_000.sat), toLocal = 0.msat, toRemote = 0.msat) - val fee = commitTxFee(546.sat, spec) + val fee = commitTxFee(546.sat, spec, Transactions.CommitmentFormat.AnchorOutputs) assertEquals(8000.sat, fee) } - @Test - fun `check pre-computed transaction weights`() { - val finalPubKeyScript = write(pay2wpkh(PrivateKey(randomBytes32()).publicKey())) - val localDustLimit = 546.sat - val toLocalDelay = CltvExpiryDelta(144) - val feeratePerKw = FeeratePerKw.MinimumFeeratePerKw - val blockHeight = 400_000 - - run { - // ClaimHtlcDelayedTx - // first we create a fake htlcSuccessOrTimeoutTx tx, containing only the output that will be spent by the ClaimDelayedOutputTx - val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey()))) - val htlcSuccessOrTimeoutTx = Transaction(version = 2, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = listOf(TxOut(20000.sat, pubKeyScript)), lockTime = 0) - val claimHtlcDelayedTx = makeClaimLocalDelayedOutputTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey(), finalPubKeyScript, feeratePerKw) - assertTrue(claimHtlcDelayedTx is Success, "is $claimHtlcDelayedTx") - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(claimHtlcDelayedTx.result, PlaceHolderSig).tx) - assertEquals(claimHtlcDelayedWeight, weight) - assertEquals(FeeratePerByte(fee2rate(claimHtlcDelayedTx.result.fee, weight)), FeeratePerByte(1.sat)) - } - run { - // MainPenaltyTx - // first we create a fake commitTx tx, containing only the output that will be spent by the MainPenaltyTx - val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey()))) - val commitTx = Transaction(version = 2, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = listOf(TxOut(20000.sat, pubKeyScript)), lockTime = 0) - val mainPenaltyTx = makeMainPenaltyTx(commitTx, localDustLimit, localRevocationPriv.publicKey(), finalPubKeyScript, toLocalDelay, localPaymentPriv.publicKey(), feeratePerKw) - assertTrue(mainPenaltyTx is Success, "is $mainPenaltyTx") - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(mainPenaltyTx.result, PlaceHolderSig).tx) - assertEquals(mainPenaltyWeight, weight) - assertEquals(FeeratePerByte(fee2rate(mainPenaltyTx.result.fee, weight)), FeeratePerByte(1.sat)) - } - run { - // HtlcPenaltyTx - // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx - val paymentPreimage = randomBytes32() - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 20_000_000.msat, ByteVector32(sha256(paymentPreimage)), CltvExpiryDelta(144).toCltvExpiry(blockHeight.toLong()), TestConstants.emptyOnionPacket) - val redeemScript = htlcReceived(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), localRevocationPriv.publicKey(), ripemd160(htlc.paymentHash), htlc.cltvExpiry) - val pubKeyScript = write(pay2wsh(redeemScript)) - val commitTx = Transaction(version = 2, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = listOf(TxOut(htlc.amountMsat.truncateToSatoshi(), pubKeyScript)), lockTime = 0) - val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx, 0, write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw) - assertTrue(htlcPenaltyTx is Success, "is $htlcPenaltyTx") - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(htlcPenaltyTx.result, PlaceHolderSig, localRevocationPriv.publicKey()).tx) - assertEquals(htlcPenaltyWeight, weight) - assertEquals(FeeratePerByte(fee2rate(htlcPenaltyTx.result.fee, weight)), FeeratePerByte(1.sat)) - } - run { - // ClaimHtlcSuccessTx - // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx - val paymentPreimage = randomBytes32() - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000).msat, ByteVector32(sha256(paymentPreimage)), CltvExpiryDelta(144).toCltvExpiry(blockHeight.toLong()), TestConstants.emptyOnionPacket) - val spec = CommitmentSpec(setOf(OutgoingHtlc(htlc)), feeratePerKw, toLocal = 0.msat, toRemote = 0.msat) - val outputs = makeCommitTxOutputs( - localFundingPriv.publicKey(), - remoteFundingPriv.publicKey(), - localKeys.publicKeys, - true, - localDustLimit, - toLocalDelay, - spec - ) - val commitTx = Transaction(version = 2, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = outputs.map { it.output }, lockTime = 0) - val claimHtlcSuccessTx = - makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey(), localHtlcPriv.publicKey(), localRevocationPriv.publicKey(), finalPubKeyScript, htlc, feeratePerKw) - assertTrue(claimHtlcSuccessTx is Success, "is $claimHtlcSuccessTx") - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(claimHtlcSuccessTx.result, PlaceHolderSig, paymentPreimage).tx) - assertEquals(claimHtlcSuccessWeight, weight) - assertEquals(FeeratePerByte(fee2rate(claimHtlcSuccessTx.result.fee, weight)), FeeratePerByte(1.sat)) - } - run { - // ClaimHtlcTimeoutTx - // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcTimeoutTx - val paymentPreimage = randomBytes32() - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000).msat, ByteVector32(sha256(paymentPreimage)), toLocalDelay.toCltvExpiry(blockHeight.toLong()), TestConstants.emptyOnionPacket) - val spec = CommitmentSpec(setOf(IncomingHtlc(htlc)), feeratePerKw, toLocal = 0.msat, toRemote = 0.msat) - val outputs = makeCommitTxOutputs( - localFundingPriv.publicKey(), - remoteFundingPriv.publicKey(), - localKeys.publicKeys, - true, - localDustLimit, - toLocalDelay, - spec - ) - val commitTx = Transaction(version = 2, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = outputs.map { it.output }, lockTime = 0) - val claimHtlcTimeoutTx = - makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey(), localHtlcPriv.publicKey(), localRevocationPriv.publicKey(), finalPubKeyScript, htlc, feeratePerKw) - assertTrue(claimHtlcTimeoutTx is Success, "is $claimHtlcTimeoutTx") - // we use dummy signatures to compute the weight - val weight = Transaction.weight(addSigs(claimHtlcTimeoutTx.result, PlaceHolderSig).tx) - assertEquals(claimHtlcTimeoutWeight, weight) - assertEquals(FeeratePerByte(fee2rate(claimHtlcTimeoutTx.result.fee, weight)), FeeratePerByte(1.sat)) + private fun checkExpectedWeight(actual: Int, expected: Int, commitmentFormat: Transactions.CommitmentFormat) { + when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> { + // ECDSA signatures are der-encoded, which creates some variability in signature size compared to the baseline. + assertTrue(actual <= expected + 2, "actual=$actual, expected=$expected") + assertTrue(actual >= expected - 2, "actual=$actual, expected=$expected") + } } } - @Test - fun `generate valid commitment and htlc transactions`() { - val finalPubKeyScript = write(pay2wpkh(PrivateKey(ByteVector32("01".repeat(32))).publicKey())) - val commitInput = Funding.makeFundingInputInfo(TxId(ByteVector32("02".repeat(32))), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey()) - - // htlc1 and htlc2 are regular IN/OUT htlcs - val paymentPreimage1 = ByteVector32("03".repeat(32)) - val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, 100.mbtc.toMilliSatoshi(), ByteVector32(sha256(paymentPreimage1)), CltvExpiry(300), TestConstants.emptyOnionPacket) - val paymentPreimage2 = ByteVector32("04".repeat(32)) - val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 200.mbtc.toMilliSatoshi(), ByteVector32(sha256(paymentPreimage2)), CltvExpiry(300), TestConstants.emptyOnionPacket) - // htlc3 and htlc4 are dust htlcs IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage - val paymentPreimage3 = ByteVector32("05".repeat(32)) - val htlc3 = UpdateAddHtlc( - ByteVector32.Zeroes, - 2, - (localDustLimit + weight2fee(feerate, Commitments.HTLC_TIMEOUT_WEIGHT)).toMilliSatoshi(), - ByteVector32(sha256(paymentPreimage3)), - CltvExpiry(300), - TestConstants.emptyOnionPacket - ) - val paymentPreimage4 = ByteVector32("06".repeat(32)) - val htlc4 = UpdateAddHtlc( - ByteVector32.Zeroes, - 3, - (localDustLimit + weight2fee(feerate, Commitments.HTLC_SUCCESS_WEIGHT)).toMilliSatoshi(), - ByteVector32(sha256(paymentPreimage4)), - CltvExpiry(300), - TestConstants.emptyOnionPacket - ) + private fun testCommitAndHtlcTxs(commitmentFormat: Transactions.CommitmentFormat) { + val fundingInfo = Transactions.makeFundingScript(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), commitmentFormat) + val fundingTx = Transaction(2, listOf(), listOf(TxOut(1.btc, fundingInfo.pubkeyScript)), 0) + val commitInput = Transactions.makeFundingInputInfo(fundingTx.txid, 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), commitmentFormat) + val finalScript = write(pay2wpkh(randomKey().publicKey())).byteVector() + + val paymentPreimages = listOf(randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32()) + val paymentPreimageMap = paymentPreimages.associateBy { p -> sha256(p).byteVector32() } + + // htlc1, htlc2a and htlc2b are regular IN/OUT htlcs + val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, 100.mbtc.toMilliSatoshi(), sha256(paymentPreimages[0]).byteVector32(), CltvExpiry(300), TestConstants.emptyOnionPacket) + val htlc2a = UpdateAddHtlc(ByteVector32.Zeroes, 1, 50.mbtc.toMilliSatoshi(), sha256(paymentPreimages[1]).byteVector32(), CltvExpiry(310), TestConstants.emptyOnionPacket) + val htlc2b = UpdateAddHtlc(ByteVector32.Zeroes, 2, 150.mbtc.toMilliSatoshi(), sha256(paymentPreimages[1]).byteVector32(), CltvExpiry(310), TestConstants.emptyOnionPacket) + // htlc3 and htlc4 are dust IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage + val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feerate, commitmentFormat.htlcTimeoutWeight)).toMilliSatoshi(), sha256(paymentPreimages[2]).byteVector32(), CltvExpiry(295), TestConstants.emptyOnionPacket) + val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit + weight2fee(feerate, commitmentFormat.htlcSuccessWeight)).toMilliSatoshi(), sha256(paymentPreimages[3]).byteVector32(), CltvExpiry(300), TestConstants.emptyOnionPacket) + // htlc5 and htlc6 are dust IN/OUT htlcs + val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi(), sha256(paymentPreimages[4]).byteVector32(), CltvExpiry(295), TestConstants.emptyOnionPacket) + val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 6, (localDustLimit * 0.9).toMilliSatoshi(), sha256(paymentPreimages[5]).byteVector32(), CltvExpiry(305), TestConstants.emptyOnionPacket) + // htlc7 and htlc8 are at the dust limit when we ignore 2nd-stage tx fees + val htlc7 = UpdateAddHtlc(ByteVector32.Zeroes, 7, localDustLimit.toMilliSatoshi(), sha256(paymentPreimages[6]).byteVector32(), CltvExpiry(300), TestConstants.emptyOnionPacket) + val htlc8 = UpdateAddHtlc(ByteVector32.Zeroes, 8, localDustLimit.toMilliSatoshi(), sha256(paymentPreimages[7]).byteVector32(), CltvExpiry(302), TestConstants.emptyOnionPacket) val spec = CommitmentSpec( htlcs = setOf( OutgoingHtlc(htlc1), - IncomingHtlc(htlc2), + IncomingHtlc(htlc2a), + IncomingHtlc(htlc2b), OutgoingHtlc(htlc3), - IncomingHtlc(htlc4) + IncomingHtlc(htlc4), + OutgoingHtlc(htlc5), + IncomingHtlc(htlc6), + OutgoingHtlc(htlc7), + IncomingHtlc(htlc8), ), feerate = feerate, toLocal = 400.mbtc.toMilliSatoshi(), toRemote = 300.mbtc.toMilliSatoshi() ) - val outputs = makeCommitTxOutputs( - localFundingPriv.publicKey(), - remoteFundingPriv.publicKey(), - localKeys.publicKeys, - true, - localDustLimit, - toLocalDelay, - spec - ) - val commitTxNumber = 0x404142434445L - val commitTx = run { - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey(), remotePaymentPriv.publicKey(), true, outputs) - val localSig = sign(txInfo, localPaymentPriv) - val remoteSig = sign(txInfo, remotePaymentPriv) - addSigs(txInfo, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localSig, remoteSig) + val commitTxOutputs = Transactions.makeCommitTxOutputs(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, commitmentFormat, spec) + val (commitTx, htlcTimeoutTxs, htlcSuccessTxs) = run { + val txInfo = Transactions.makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey(), remotePaymentPriv.publicKey(), localIsChannelOpener = true, commitTxOutputs) + val commitTx = when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> { + val localSig = txInfo.sign(localFundingPriv, remoteFundingPriv.publicKey()) + val remoteSig = txInfo.sign(remoteFundingPriv, localFundingPriv.publicKey()) + assertTrue(txInfo.checkRemoteSig(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), remoteSig)) + val invalidRemoteSig = ChannelSpendSignature.IndividualSignature(randomBytes64()) + assertFalse(txInfo.checkRemoteSig(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), invalidRemoteSig)) + txInfo.aggregateSigs(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localSig, remoteSig) + } + } + Transaction.correctlySpends(commitTx, listOf(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // We check the expected weight of the commit input: + val commitInputWeight = commitTx.copy(txIn = listOf(commitTx.txIn.first(), commitTx.txIn.first())).weight() - commitTx.weight() + checkExpectedWeight(commitInputWeight, commitmentFormat.fundingInputWeight, commitmentFormat) + val htlcTxs = Transactions.makeHtlcTxs(commitTx, commitTxOutputs, commitmentFormat) + val expiries = htlcTxs.associate { it.htlcId to it.htlcExpiry.toLong() } + val htlcSuccessTxs = htlcTxs.filterIsInstance() + val htlcTimeoutTxs = htlcTxs.filterIsInstance() + when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> { + assertEquals(5, htlcTxs.size) + assertEquals(mapOf(0L to 300L, 1L to 310L, 2L to 310L, 3L to 295L, 4L to 300L), expiries) + assertEquals(2, htlcTimeoutTxs.size) + assertEquals(setOf(0L, 3L), htlcTimeoutTxs.map { it.htlcId }.toSet()) + assertEquals(3, htlcSuccessTxs.size) + assertEquals(setOf(1L, 2L, 4L), htlcSuccessTxs.map { it.htlcId }.toSet()) + } + } + Triple(commitTx, htlcTimeoutTxs, htlcSuccessTxs) } run { - assertEquals(commitTxNumber, getCommitTxNumber(commitTx.tx, true, localPaymentPriv.publicKey(), remotePaymentPriv.publicKey())) - val hash = sha256(localPaymentPriv.publicKey().value + remotePaymentPriv.publicKey().value) - val num = Pack.int64BE(hash.takeLast(8).toByteArray()) and 0xffffffffffffL - val check = ((commitTx.tx.txIn.first().sequence and 0xffffffL) shl 24) or (commitTx.tx.lockTime and 0xffffffL) - assertEquals(commitTxNumber, check xor num) + // local spends main delayed output + val localMain = Transactions.ClaimLocalDelayedOutputTx.createUnsignedTx(localKeys, commitTx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat).map { it.sign().tx }.right + assertNotNull(localMain) + checkExpectedWeight(localMain.weight(), commitmentFormat.toLocalDelayedWeight, commitmentFormat) + Transaction.correctlySpends(localMain, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - val htlcTxs = makeHtlcTxs(commitTx.tx, localKeys.publicKeys, localDustLimit, toLocalDelay, spec.feerate, outputs) - assertEquals(4, htlcTxs.size) - val htlcSuccessTxs = htlcTxs.filterIsInstance() - assertEquals(2, htlcSuccessTxs.size) // htlc2 and htlc4 - assertEquals(setOf(1L, 3L), htlcSuccessTxs.map { it.htlcId }.toSet()) - val htlcTimeoutTxs = htlcTxs.filterIsInstance() - assertEquals(2, htlcTimeoutTxs.size) // htlc1 and htlc3 - assertEquals(setOf(0L, 2L), htlcTimeoutTxs.map { it.htlcId }.toSet()) - run { - // either party spends local->remote htlc output with htlc timeout tx - for (htlcTimeoutTx in htlcTimeoutTxs) { - val localSig = sign(htlcTimeoutTx, localHtlcPriv) - val remoteSig = sign(htlcTimeoutTx, remoteHtlcPriv, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) - val signed = addSigs(htlcTimeoutTx, localSig, remoteSig) - val csResult = checkSpendable(signed) - assertTrue(csResult.isSuccess, "is $csResult") - } + // remote spends main delayed output + val remoteMain = Transactions.ClaimRemoteDelayedOutputTx.createUnsignedTx(remoteKeys, commitTx, localDustLimit, finalScript, feerate, commitmentFormat).map { it.sign().tx }.right + assertNotNull(remoteMain) + checkExpectedWeight(remoteMain.weight(), commitmentFormat.toRemoteWeight, commitmentFormat) + Transaction.correctlySpends(remoteMain, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } run { - // local spends delayed output of htlc1 timeout tx - val claimHtlcDelayed = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) - assertTrue(claimHtlcDelayed is Success, "is $claimHtlcDelayed") - val localSig = sign(claimHtlcDelayed.result, localDelayedPaymentPriv) - val signedTx = addSigs(claimHtlcDelayed.result, localSig) - assertTrue(checkSpendable(signedTx).isSuccess) - // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit - val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey(), finalPubKeyScript, feerate) - assertEquals(Skipped(OutputNotFound), claimHtlcDelayed1) + // remote spends local main delayed output with revocation key + val mainPenalty = Transactions.MainPenaltyTx.createUnsignedTx(remoteKeys, localRevocationPriv, commitTx, localDustLimit, finalScript, toLocalDelay, feerate, commitmentFormat).map { it.sign().tx }.right + assertNotNull(mainPenalty) + checkExpectedWeight(mainPenalty.weight(), commitmentFormat.mainPenaltyWeight, commitmentFormat) + Transaction.correctlySpends(mainPenalty, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } run { - // remote spends local->remote htlc1/htlc3 output directly in case of success - for ((htlc, paymentPreimage) in listOf(htlc1 to paymentPreimage1, htlc3 to paymentPreimage3)) { - val claimHtlcSuccessTx = - makeClaimHtlcSuccessTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey(), localHtlcPriv.publicKey(), localRevocationPriv.publicKey(), finalPubKeyScript, htlc, feerate) - assertTrue(claimHtlcSuccessTx is Success, "is $claimHtlcSuccessTx") - val localSig = sign(claimHtlcSuccessTx.result, remoteHtlcPriv) - val signed = addSigs(claimHtlcSuccessTx.result, localSig, paymentPreimage) - val csResult = checkSpendable(signed) - assertTrue(csResult.isSuccess, "is $csResult") + // local spends received htlc with HTLC-timeout tx + htlcTimeoutTxs.forEach { htlcTimeoutTx -> + val remoteSig = htlcTimeoutTx.localSig(remoteKeys) + assertTrue(htlcTimeoutTx.checkRemoteSig(localKeys, remoteSig)) + val signedTx = htlcTimeoutTx.sign(localKeys, remoteSig).tx + Transaction.correctlySpends(signedTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // local detects when remote doesn't use the right sighash flags + val invalidSighash = when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> listOf(SigHash.SIGHASH_ALL, SigHash.SIGHASH_ALL or SigHash.SIGHASH_ANYONECANPAY, SigHash.SIGHASH_SINGLE, SigHash.SIGHASH_NONE) + } + invalidSighash.forEach { sighash -> + val invalidRemoteSig = htlcTimeoutTx.sign(remoteKeys.ourHtlcKey, sighash, htlcTimeoutTx.redeemInfo(remoteKeys.publicKeys), mapOf()) + assertFalse(htlcTimeoutTx.checkRemoteSig(localKeys, invalidRemoteSig)) + } } } run { - // local spends remote->local htlc2/htlc4 output with htlc success tx using payment preimage - for ((htlcSuccessTx, paymentPreimage) in listOf(htlcSuccessTxs[1] to paymentPreimage2, htlcSuccessTxs[0] to paymentPreimage4)) { - val localSig = sign(htlcSuccessTx, localHtlcPriv) - val remoteSig = sign(htlcSuccessTx, remoteHtlcPriv, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) - val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage) - val csResult = checkSpendable(signedTx) - assertTrue(csResult.isSuccess, "is $csResult") - // check remote sig - assertTrue(checkSig(htlcSuccessTx, remoteSig, remoteHtlcPriv.publicKey(), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) + // local spends delayed output of htlc1 timeout tx + val htlcDelayed = Transactions.HtlcDelayedTx.createUnsignedTx(localKeys, htlcTimeoutTxs[1].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat).map { it.sign().tx }.right + assertNotNull(htlcDelayed) + checkExpectedWeight(htlcDelayed.weight(), commitmentFormat.htlcDelayedWeight, commitmentFormat) + Transaction.correctlySpends(htlcDelayed, listOf(htlcTimeoutTxs[1].tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit + val htlcDelayed1 = Transactions.HtlcDelayedTx.createUnsignedTx(localKeys, htlcTimeoutTxs[0].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat) + assertEquals(Either.Left(Transactions.TxGenerationSkipped.AmountBelowDustLimit), htlcDelayed1) + } + run { + // local spends offered htlc with HTLC-success tx + htlcSuccessTxs.take(3).forEach { htlcSuccessTx -> + val preimage = paymentPreimageMap[htlcSuccessTx.paymentHash]!! + val remoteSig = htlcSuccessTx.localSig(remoteKeys) + val signedTx = htlcSuccessTx.sign(localKeys, remoteSig, preimage).tx + Transaction.correctlySpends(signedTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assertTrue(htlcSuccessTx.checkRemoteSig(localKeys, remoteSig)) + // local detects when remote doesn't use the right sighash flags + val invalidSighash = when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> listOf(SigHash.SIGHASH_ALL, SigHash.SIGHASH_ALL or SigHash.SIGHASH_ANYONECANPAY, SigHash.SIGHASH_SINGLE, SigHash.SIGHASH_NONE) + } + invalidSighash.forEach { sighash -> + val invalidRemoteSig = htlcSuccessTx.sign(remoteKeys.ourHtlcKey, sighash, htlcSuccessTx.redeemInfo(remoteKeys.publicKeys), mapOf()) + assertFalse(htlcSuccessTx.checkRemoteSig(localKeys, invalidRemoteSig)) + } } } run { - // local spends delayed output of htlc2 success tx - val claimHtlcDelayed = makeClaimLocalDelayedOutputTx(htlcSuccessTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) - assertTrue(claimHtlcDelayed is Success, "is $claimHtlcDelayed") - val localSig = sign(claimHtlcDelayed.result, localDelayedPaymentPriv) - val signedTx = addSigs(claimHtlcDelayed.result, localSig) - val csResult = checkSpendable(signedTx) - assertTrue(csResult.isSuccess, "is $csResult") - // local can't claim delayed output of htlc4 timeout tx because it is below the dust limit - val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcSuccessTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) - assertEquals(Skipped(AmountBelowDustLimit), claimHtlcDelayed1) + // local spends delayed output of htlc2a and htlc2b success txs + val htlcDelayedA = Transactions.HtlcDelayedTx.createUnsignedTx(localKeys, htlcSuccessTxs[1].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat).map { it.sign().tx }.right + assertNotNull(htlcDelayedA) + val htlcDelayedB = Transactions.HtlcDelayedTx.createUnsignedTx(localKeys, htlcSuccessTxs[2].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat).map { it.sign().tx }.right + assertNotNull(htlcDelayedB) + listOf(htlcDelayedA, htlcDelayedB).forEach { checkExpectedWeight(it.weight(), commitmentFormat.htlcDelayedWeight, commitmentFormat) } + Transaction.correctlySpends(htlcDelayedA, listOf(htlcSuccessTxs[1].tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(htlcDelayedB, listOf(htlcSuccessTxs[2].tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // local can't claim delayed output of htlc4 success tx because it is below the dust limit + val htlcDelayedC = Transactions.HtlcDelayedTx.createUnsignedTx(localKeys, htlcSuccessTxs[0].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat) + assertEquals(Either.Left(Transactions.TxGenerationSkipped.AmountBelowDustLimit), htlcDelayedC) } run { - // remote spends main output - val claimP2WPKHOutputTx = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey(), finalPubKeyScript.toByteVector(), feerate) - assertTrue(claimP2WPKHOutputTx is Success, "is $claimP2WPKHOutputTx") - val localSig = sign(claimP2WPKHOutputTx.result, remotePaymentPriv) - val signedTx = addSigs(claimP2WPKHOutputTx.result, localSig) - val csResult = checkSpendable(signedTx) - assertTrue(csResult.isSuccess, "is $csResult") + // remote spends local->remote htlc outputs directly in case of success + listOf(htlc1, htlc3).forEach { htlc -> + val preimage = paymentPreimageMap[htlc.paymentHash]!! + val htlcTx = Transactions.ClaimHtlcSuccessTx.createUnsignedTx(remoteKeys, commitTx, localDustLimit, commitTxOutputs, finalScript, htlc, preimage, feerate, commitmentFormat).map { it.sign().tx }.right + assertNotNull(htlcTx) + checkExpectedWeight(htlcTx.weight(), commitmentFormat.claimHtlcSuccessWeight, commitmentFormat) + Transaction.correctlySpends(htlcTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } } run { // remote spends htlc1's htlc-timeout tx with revocation key - val claimHtlcDelayedPenaltyTxs = makeClaimDelayedOutputPenaltyTxs(htlcTimeoutTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) - assertEquals(1, claimHtlcDelayedPenaltyTxs.size) - val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() - assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") - val sig = sign(claimHtlcDelayedPenaltyTx.result, localRevocationPriv) - val signed = addSigs(claimHtlcDelayedPenaltyTx.result, sig) - val csResult = checkSpendable(signed) - assertTrue(csResult.isSuccess, "is $csResult") + val penaltyTx = Transactions.ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs[1].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat).first().right?.sign()?.tx + assertNotNull(penaltyTx) + checkExpectedWeight(penaltyTx.weight(), commitmentFormat.claimHtlcPenaltyWeight, commitmentFormat) + Transaction.correctlySpends(penaltyTx, listOf(htlcTimeoutTxs[1].tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit - val claimHtlcDelayedPenaltyTxsSkipped = makeClaimDelayedOutputPenaltyTxs(htlcTimeoutTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) - assertEquals(listOf(Skipped(AmountBelowDustLimit)), claimHtlcDelayedPenaltyTxsSkipped) + val penaltyTx1 = Transactions.ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcTimeoutTxs[0].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat) + assertEquals(1, penaltyTx1.size) + assertEquals(Either.Left(Transactions.TxGenerationSkipped.AmountBelowDustLimit), penaltyTx1.first()) } run { // remote spends remote->local htlc output directly in case of timeout - val claimHtlcTimeoutTx = - makeClaimHtlcTimeoutTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey(), localHtlcPriv.publicKey(), localRevocationPriv.publicKey(), finalPubKeyScript, htlc2, feerate) - assertTrue(claimHtlcTimeoutTx is Success, "is $claimHtlcTimeoutTx") - val remoteSig = sign(claimHtlcTimeoutTx.result, remoteHtlcPriv) - val signed = addSigs(claimHtlcTimeoutTx.result, remoteSig) - val csResult = checkSpendable(signed) - assertTrue(csResult.isSuccess, "is $csResult") + listOf(htlc2a, htlc2b).forEach { htlc -> + val htlcTx = Transactions.ClaimHtlcTimeoutTx.createUnsignedTx(remoteKeys, commitTx, localDustLimit, commitTxOutputs, finalScript, htlc, feerate, commitmentFormat).map { it.sign().tx }.right + assertNotNull(htlcTx) + checkExpectedWeight(htlcTx.weight(), commitmentFormat.claimHtlcTimeoutWeight, commitmentFormat) + Transaction.correctlySpends(htlcTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } } run { - // remote spends htlc2's htlc-success tx with revocation key - val claimHtlcDelayedPenaltyTxs = makeClaimDelayedOutputPenaltyTxs(htlcSuccessTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) - assertEquals(1, claimHtlcDelayedPenaltyTxs.size) - val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() - assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") - val sig = sign(claimHtlcDelayedPenaltyTx.result, localRevocationPriv) - val signed = addSigs(claimHtlcDelayedPenaltyTx.result, sig) - val csResult = checkSpendable(signed) - assertTrue(csResult.isSuccess, "is $csResult") + // remote spends htlc2a/htlc2b's htlc-success tx with revocation key + val penaltyTxA = Transactions.ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs[1].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat).first().right?.sign()?.tx + assertNotNull(penaltyTxA) + val penaltyTxB = Transactions.ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs[2].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat).first().right?.sign()?.tx + assertNotNull(penaltyTxB) + listOf(penaltyTxA, penaltyTxB).forEach { checkExpectedWeight(it.weight(), commitmentFormat.claimHtlcPenaltyWeight, commitmentFormat) } + Transaction.correctlySpends(penaltyTxA, listOf(htlcSuccessTxs[1].tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(penaltyTxB, listOf(htlcSuccessTxs[2].tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit - val claimHtlcDelayedPenaltyTxsSkipped = makeClaimDelayedOutputPenaltyTxs(htlcSuccessTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) - assertEquals(listOf(Skipped(AmountBelowDustLimit)), claimHtlcDelayedPenaltyTxsSkipped) + val penaltyTx1 = Transactions.ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, htlcSuccessTxs[0].tx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat) + assertEquals(1, penaltyTx1.size) + assertEquals(Either.Left(Transactions.TxGenerationSkipped.AmountBelowDustLimit), penaltyTx1.first()) } run { // remote spends all htlc txs aggregated in a single tx val txIn = htlcTimeoutTxs.flatMap { it.tx.txIn } + htlcSuccessTxs.flatMap { it.tx.txIn } val txOut = htlcTimeoutTxs.flatMap { it.tx.txOut } + htlcSuccessTxs.flatMap { it.tx.txOut } val aggregatedHtlcTx = Transaction(2, txIn, txOut, 0) - val claimHtlcDelayedPenaltyTxs = makeClaimDelayedOutputPenaltyTxs(aggregatedHtlcTx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) - assertEquals(4, claimHtlcDelayedPenaltyTxs.size) - val skipped = claimHtlcDelayedPenaltyTxs.filterIsInstance>() - assertEquals(2, skipped.size) - val claimed = claimHtlcDelayedPenaltyTxs.filterIsInstance>() - assertEquals(2, claimed.size) - assertEquals(2, claimed.map { it.result.input.outPoint }.toSet().size) - } - run { - // remote spends offered HTLC output with revocation key - val script = write(htlcOffered(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), localRevocationPriv.publicKey(), ripemd160(htlc1.paymentHash))) - val htlcOutputIndex = outputs.indexOfFirst { - val outHtlc = (it.commitmentOutput as? OutHtlc)?.outgoingHtlc?.add - outHtlc != null && outHtlc.id == htlc1.id + val penaltyTxs = Transactions.ClaimHtlcDelayedOutputPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, aggregatedHtlcTx, localDustLimit, toLocalDelay, finalScript, feerate, commitmentFormat) + val skipped = penaltyTxs.mapNotNull { it.left } + val claimed = penaltyTxs.mapNotNull { it.right?.sign()?.tx } + when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> { + assertEquals(5, penaltyTxs.size) + assertEquals(2, skipped.size) + assertEquals(setOf(Transactions.TxGenerationSkipped.AmountBelowDustLimit), skipped.toSet()) + assertEquals(3, claimed.size) + assertEquals(3, claimed.flatMap { tx -> tx.txIn.map { txIn -> txIn.outPoint } }.toSet().size) + } + } + claimed.forEach { penaltyTx -> + checkExpectedWeight(penaltyTx.weight(), commitmentFormat.claimHtlcPenaltyWeight, commitmentFormat) + Transaction.correctlySpends(penaltyTx, listOf(aggregatedHtlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feerate) - assertTrue(htlcPenaltyTx is Success, "is $htlcPenaltyTx") - val sig = sign(htlcPenaltyTx.result, localRevocationPriv) - val signed = addSigs(htlcPenaltyTx.result, sig, localRevocationPriv.publicKey()) - val csResult = checkSpendable(signed) - assertTrue(csResult.isSuccess, "is $csResult") } run { - // remote spends received HTLC output with revocation key - val script = write(htlcReceived(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), localRevocationPriv.publicKey(), ripemd160(htlc2.paymentHash), htlc2.cltvExpiry)) - val htlcOutputIndex = outputs.indexOfFirst { - val inHtlc = (it.commitmentOutput as? CommitmentOutput.InHtlc)?.incomingHtlc?.add - inHtlc != null && inHtlc.id == htlc2.id + // remote spends htlc outputs with revocation key + val htlcs = spec.htlcs.map { it.add }.map { Pair(it.paymentHash, it.cltvExpiry) } + val penaltyTxs = Transactions.HtlcPenaltyTx.createUnsignedTxs(remoteKeys, localRevocationPriv, commitTx, htlcs, localDustLimit, finalScript, feerate, commitmentFormat) + assertEquals(setOf(htlc1, htlc2a, htlc2b, htlc3, htlc4).map { it.paymentHash }.toSet(), penaltyTxs.mapNotNull { it.right?.paymentHash }.toSet()) // the first 5 htlcs are above the dust limit + penaltyTxs.mapNotNull { it.right?.sign() }.forEach { penaltyTx -> + val expectedWeight = if (htlcTimeoutTxs.map { it.input.outPoint }.toSet().contains(penaltyTx.input.outPoint)) { + commitmentFormat.htlcOfferedPenaltyWeight + } else { + commitmentFormat.htlcReceivedPenaltyWeight + } + checkExpectedWeight(penaltyTx.tx.weight(), expectedWeight, commitmentFormat) + Transaction.correctlySpends(penaltyTx.tx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feerate) - assertTrue(htlcPenaltyTx is Success, "is $htlcPenaltyTx") - val sig = sign(htlcPenaltyTx.result, localRevocationPriv) - val signed = addSigs(htlcPenaltyTx.result, sig, localRevocationPriv.publicKey()) - val csResult = checkSpendable(signed) - assertTrue(csResult.isSuccess, "is $csResult") } } + @Test + fun `generate valid commitment and htlc transactions -- anchor outputs`() { + testCommitAndHtlcTxs(Transactions.CommitmentFormat.AnchorOutputs) + } + @Test fun `spend 2-of-2 legacy swap-in`() { val userWallet = TestConstants.Alice.keyManager.swapInOnChainWallet @@ -578,12 +474,12 @@ class TransactionsTestsCommon : LightningTestSuite() { @Test fun `adjust feerate when purchasing liquidity for on-the-fly payment`() { - val fundingScript = write(Scripts.multiSig2of2(PlaceHolderPubKey, PlaceHolderPubKey)) - val fundingWitness = Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, PlaceHolderPubKey, PlaceHolderPubKey) - val changeOutput = TxOut(150_000.sat, pay2wpkh(PlaceHolderPubKey)) + val fundingScript = write(Scripts.multiSig2of2(localFundingPriv.publicKey(), remoteFundingPriv.publicKey())) + val fundingWitness = Scripts.witness2of2(randomBytes64(), randomBytes64(), localFundingPriv.publicKey(), remoteFundingPriv.publicKey()) + val changeOutput = TxOut(150_000.sat, pay2wpkh(randomKey().publicKey())) fun createSignedWalletInput(): TxIn { - val witness = Script.witnessPay2wpkh(PlaceHolderPubKey, Scripts.der(PlaceHolderSig, SigHash.SIGHASH_ALL)) + val witness = Script.witnessPay2wpkh(randomKey().publicKey(), Scripts.der(randomBytes64(), SigHash.SIGHASH_ALL)) return TxIn(OutPoint(TxId(randomBytes32()), 3), ByteVector.empty, 0, witness) } @@ -676,8 +572,8 @@ class TransactionsTestsCommon : LightningTestSuite() { theirHtlcPublicKey = remoteHtlcPriv.publicKey(), revocationPublicKey = localRevocationPriv.publicKey(), ) - val commitInput = Funding.makeFundingInputInfo(TxId("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey()) - + val commitInput = + Transactions.makeFundingInputInfo(TxId("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), Transactions.CommitmentFormat.AnchorOutputs) // htlc1 and htlc2 are two regular incoming HTLCs with different amounts. // htlc2 and htlc3 have the same amounts and should be sorted according to their scriptPubKey // htlc4 is identical to htlc3 and htlc5 has same payment_hash/amount but different CLTV @@ -689,7 +585,6 @@ class TransactionsTestsCommon : LightningTestSuite() { val htlc3 = UpdateAddHtlc(randomBytes32(), 3, 200.mbtc.toMilliSatoshi(), paymentPreimage3.sha256(), CltvExpiry(300), TestConstants.emptyOnionPacket) val htlc4 = UpdateAddHtlc(randomBytes32(), 4, 200.mbtc.toMilliSatoshi(), paymentPreimage3.sha256(), CltvExpiry(300), TestConstants.emptyOnionPacket) val htlc5 = UpdateAddHtlc(randomBytes32(), 5, 200.mbtc.toMilliSatoshi(), paymentPreimage3.sha256(), CltvExpiry(301), TestConstants.emptyOnionPacket) - val spec = CommitmentSpec( htlcs = setOf( OutgoingHtlc(htlc1), @@ -702,58 +597,56 @@ class TransactionsTestsCommon : LightningTestSuite() { toLocal = 400.mbtc.toMilliSatoshi(), toRemote = 300.mbtc.toMilliSatoshi() ) - val commitTxNumber = 0x404142434446L val (commitTx, outputs, htlcTxs) = run { - val outputs = makeCommitTxOutputs( + val outputs = Transactions.makeCommitTxOutputs( localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localKeys.publicKeys, true, localDustLimit, toLocalDelay, + Transactions.CommitmentFormat.AnchorOutputs, spec ) - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey(), remotePaymentPriv.publicKey(), true, outputs) - val localSig = sign(txInfo, localPaymentPriv) - val remoteSig = sign(txInfo, remotePaymentPriv) - val commitTx = addSigs(txInfo, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localSig, remoteSig) - val htlcTxs = makeHtlcTxs(commitTx.tx, localKeys.publicKeys, localDustLimit, toLocalDelay, feerate, outputs) + val txInfo = Transactions.makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey(), remotePaymentPriv.publicKey(), true, outputs) + val localSig = txInfo.sign(localFundingPriv, remoteFundingPriv.publicKey()) + val remoteSig = txInfo.sign(remoteFundingPriv, localFundingPriv.publicKey()) + val commitTx = txInfo.aggregateSigs(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localSig, remoteSig) + val htlcTxs = Transactions.makeHtlcTxs(commitTx, outputs, Transactions.CommitmentFormat.AnchorOutputs) Triple(commitTx, outputs, htlcTxs) } - // htlc1 comes before htlc2 because of the smaller amount (BIP69) // htlc3, htlc4 and htlc5 have the same pubKeyScript but htlc5 comes after because it has a higher CLTV // htlc2 and htlc3/4/5 have the same amount but htlc2 comes last because its pubKeyScript is lexicographically greater than htlc3/4/5 - val (htlcOut1, htlcOut2, htlcOut3, htlcOut4, htlcOut5) = commitTx.tx.txOut.drop(2) + val (htlcOut1, htlcOut2, htlcOut3, htlcOut4, htlcOut5) = commitTx.txOut.drop(2) assertEquals(10_000_000.sat, htlcOut1.amount) for (htlcOut in listOf(htlcOut2, htlcOut3, htlcOut4, htlcOut5)) { assertEquals(20_000_000.sat, htlcOut.amount) } - // htlc3 and htlc4 are completely identical, their relative order can't be enforced. assertEquals(5, htlcTxs.size) - htlcTxs.forEach { tx -> assertTrue(tx is Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx) } + htlcTxs.forEach { tx -> assertIs(tx) } val htlcIds = htlcTxs.map { it.htlcId } assertTrue(htlcIds == listOf(1L, 3L, 4L, 5L, 2L) || htlcIds == listOf(1L, 4L, 3L, 5L, 2L)) - assertTrue(htlcOut4.publicKeyScript.toHex() < htlcOut5.publicKeyScript.toHex()) - assertEquals(htlcOut1.publicKeyScript, outputs.find { it.commitmentOutput == OutHtlc(OutgoingHtlc(htlc1)) }?.output?.publicKeyScript) - assertEquals(htlcOut5.publicKeyScript, outputs.find { it.commitmentOutput == OutHtlc(OutgoingHtlc(htlc2)) }?.output?.publicKeyScript) - assertEquals(htlcOut3.publicKeyScript, outputs.find { it.commitmentOutput == OutHtlc(OutgoingHtlc(htlc3)) }?.output?.publicKeyScript) - assertEquals(htlcOut4.publicKeyScript, outputs.find { it.commitmentOutput == OutHtlc(OutgoingHtlc(htlc4)) }?.output?.publicKeyScript) - assertEquals(htlcOut2.publicKeyScript, outputs.find { it.commitmentOutput == OutHtlc(OutgoingHtlc(htlc5)) }?.output?.publicKeyScript) + assertEquals(htlcOut1.publicKeyScript, outputs.find { it is OutHtlc && it.htlc.add == htlc1 }?.txOut?.publicKeyScript) + assertEquals(htlcOut5.publicKeyScript, outputs.find { it is OutHtlc && it.htlc.add == htlc2 }?.txOut?.publicKeyScript) + assertEquals(htlcOut3.publicKeyScript, outputs.find { it is OutHtlc && it.htlc.add == htlc3 }?.txOut?.publicKeyScript) + assertEquals(htlcOut4.publicKeyScript, outputs.find { it is OutHtlc && it.htlc.add == htlc4 }?.txOut?.publicKeyScript) + assertEquals(htlcOut2.publicKeyScript, outputs.find { it is OutHtlc && it.htlc.add == htlc5 }?.txOut?.publicKeyScript) } @Test fun `find our output in closing tx`() { val localPubKeyScript = write(pay2wpkh(PrivateKey(randomBytes32()).publicKey())).byteVector() val remotePubKeyScript = write(pay2wpkh(PrivateKey(randomBytes32()).publicKey())).byteVector() + val commitInput = Transactions.makeFundingInputInfo(TxId(randomBytes32()), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), Transactions.CommitmentFormat.AnchorOutputs) run { // Different amounts, both outputs untrimmed, local is closer: val spec = CommitmentSpec(setOf(), feerate, 150_000_000.msat, 250_000_000.msat) - val closingTxs = makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByUs(5_000.sat), 0, localPubKeyScript, remotePubKeyScript) + val closingTxs = Transactions.makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByUs(5_000.sat), 0, localPubKeyScript, remotePubKeyScript) assertNotNull(closingTxs.localAndRemote) assertNotNull(closingTxs.localOnly) assertNull(closingTxs.remoteOnly) @@ -767,7 +660,7 @@ class TransactionsTestsCommon : LightningTestSuite() { run { // Same amounts, both outputs untrimmed, remote is closer: val spec = CommitmentSpec(setOf(), feerate, 150_000_000.msat, 250_000_000.msat) - val closingTxs = makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByThem(5_000.sat), 0, localPubKeyScript, remotePubKeyScript) + val closingTxs = Transactions.makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByThem(5_000.sat), 0, localPubKeyScript, remotePubKeyScript) assertNotNull(closingTxs.localAndRemote) assertNotNull(closingTxs.localOnly) assertNull(closingTxs.remoteOnly) @@ -781,7 +674,7 @@ class TransactionsTestsCommon : LightningTestSuite() { run { // Their output is trimmed: val spec = CommitmentSpec(setOf(), feerate, 150_000_000.msat, 1_000_000.msat) - val closingTxs = makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByThem(800.sat), 0, localPubKeyScript, remotePubKeyScript) + val closingTxs = Transactions.makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByThem(800.sat), 0, localPubKeyScript, remotePubKeyScript) assertEquals(1, closingTxs.all.size) assertNotNull(closingTxs.localOnly) assertEquals(1, closingTxs.localOnly.tx.txOut.size) @@ -792,7 +685,7 @@ class TransactionsTestsCommon : LightningTestSuite() { run { // Our output is trimmed: val spec = CommitmentSpec(setOf(), feerate, 1_000_000.msat, 150_000_000.msat) - val closingTxs = makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByUs(800.sat), 0, localPubKeyScript, remotePubKeyScript) + val closingTxs = Transactions.makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByUs(800.sat), 0, localPubKeyScript, remotePubKeyScript) assertEquals(1, closingTxs.all.size) assertNotNull(closingTxs.remoteOnly) assertNull(closingTxs.remoteOnly.toLocalOutput) @@ -800,7 +693,7 @@ class TransactionsTestsCommon : LightningTestSuite() { run { // Both outputs are trimmed: val spec = CommitmentSpec(setOf(), feerate, 50_000.msat, 10_000.msat) - val closingTxs = makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByUs(10.sat), 0, localPubKeyScript, remotePubKeyScript) + val closingTxs = Transactions.makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByUs(10.sat), 0, localPubKeyScript, remotePubKeyScript) assertNull(closingTxs.localAndRemote) assertNull(closingTxs.localOnly) assertNull(closingTxs.remoteOnly) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 2b68908b6..31baaa6cb 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -11,11 +11,12 @@ import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelFlags +import fr.acinq.lightning.channel.ChannelSpendSignature import fr.acinq.lightning.channel.ChannelType -import fr.acinq.lightning.channel.Helpers import fr.acinq.lightning.message.OnionMessages import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector @@ -402,9 +403,11 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val nodeKey = PrivateKey.fromHex("57ac961f1b80ebfb610037bf9c96c6333699bde42257919a53974811c34649e3") val fundingLease = LiquidityAds.FundingRate(500_000.sat, 5_000_000.sat, 1100, 75, 0.sat, 1_500.sat) val requestFunds = LiquidityAds.RequestFunding(750_000.sat, fundingLease, LiquidityAds.PaymentDetails.FromChannelBalance) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(publicKey(1), publicKey(1)) - val willFund = - LiquidityAds.WillFundRates(listOf(fundingLease), setOf(LiquidityAds.PaymentType.FromChannelBalance)).validateRequest(nodeKey, fundingScript, FeeratePerKw(5000.sat), requestFunds, isChannelCreation = true, 0.msat)!!.willFund + val fundingScript = Transactions.makeFundingScript(publicKey(1), publicKey(1), Transactions.CommitmentFormat.AnchorOutputs).pubkeyScript + val willFund = LiquidityAds.WillFundRates( + fundingRates = listOf(fundingLease), + paymentTypes = setOf(LiquidityAds.PaymentType.FromChannelBalance), + ).validateRequest(nodeKey, fundingScript, FeeratePerKw(5000.sat), requestFunds, isChannelCreation = true, 0.msat)!!.willFund // @formatter:off val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000.sat, 473.sat, 100_000_000, 1.msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), publicKey(7)) val defaultEncoded = ByteVector("0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f") @@ -447,7 +450,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { @Test fun `encode - decode commit_sig`() { val channelId = ByteVector32.fromValidHex("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25") - val signature = ByteVector64.fromValidHex("05e06d9a8fdfbb3625051ff2e3cdf82679cc2268beee6905941d6dd8a067cd62711e04b119a836aa0eebe07545172cefb228860fea6c797178453a319169bed7") + val signature = ChannelSpendSignature.IndividualSignature(ByteVector64.fromValidHex("05e06d9a8fdfbb3625051ff2e3cdf82679cc2268beee6905941d6dd8a067cd62711e04b119a836aa0eebe07545172cefb228860fea6c797178453a319169bed7")) val alternateSigs = listOf( CommitSigTlv.AlternativeFeerateSig(FeeratePerKw(253.sat), ByteVector64.fromValidHex("c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3")), CommitSigTlv.AlternativeFeerateSig(FeeratePerKw(500.sat), ByteVector64.fromValidHex("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5")), @@ -736,7 +739,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { fun `nonreg backup channel data`() { val channelId = randomBytes32() val txHash = TxHash(randomBytes32()) - val signature = randomBytes64() + val signature = ChannelSpendSignature.IndividualSignature(randomBytes64()) val key = randomKey() val point = randomKey().publicKey() val randomData = randomBytes(42) @@ -754,8 +757,8 @@ class LightningCodecsTestsCommon : LightningTestSuite() { Hex.decode("0047") + channelId.toByteArray() + txHash.value.toByteArray() + Hex.decode("0000") to TxSignatures(channelId, TxId(txHash), listOf()), Hex.decode("0047") + channelId.toByteArray() + txHash.value.toByteArray() + Hex.decode("0000 2b012a") to TxSignatures(channelId, TxId(txHash), listOf(), TlvStream(setOf(), setOf(GenericTlv(43, ByteVector("2a"))))), // commit_sig - Hex.decode("0084") + channelId.toByteArray() + signature.toByteArray() + Hex.decode("0000") to CommitSig(channelId, signature, listOf()), - Hex.decode("0084") + channelId.toByteArray() + signature.toByteArray() + Hex.decode("0000") + Hex.decode("01 02 0102") to CommitSig(channelId, signature, listOf(), TlvStream(setOf(), setOf(GenericTlv(1, ByteVector("0102"))))), + Hex.decode("0084") + channelId.toByteArray() + signature.sig.toByteArray() + Hex.decode("0000") to CommitSig(channelId, signature, listOf()), + Hex.decode("0084") + channelId.toByteArray() + signature.sig.toByteArray() + Hex.decode("0000") + Hex.decode("01 02 0102") to CommitSig(channelId, signature, listOf(), TlvStream(setOf(), setOf(GenericTlv(1, ByteVector("0102"))))), // revoke_and_ack Hex.decode("0085") + channelId.toByteArray() + key.value.toByteArray() + point.value.toByteArray() to RevokeAndAck(channelId, key, point), Hex.decode("0085") + channelId.toByteArray() + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("01 02 0102") to RevokeAndAck(channelId, key, point, TlvStream(setOf(), setOf(GenericTlv(1, ByteVector("0102"))))), diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Local_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Local_ebb9087c/data.json index 88740f2d3..16a7c23ce 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Local_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Local_ebb9087c/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.Closing", "commitments": { - "params": { + "channelParams": { "channelId": "abd6f05f60a686f8e2754cd4a5e293e3b99b0cf9dda7fed87190d3b04b5b2c1e", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/1025482412/1929474787/987346171'/931237647'/659857586/1516206615'/128221046'/2067467114/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 0, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -45,11 +40,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "021a7f0907f923519d4da0e4bdcb344107ee2d4b47b5e93f27014e3a5aefb187b4", "paymentBasepoint": "02f1c20fe4c931170a0c17f4991727ebf6b3c48738f0fe0dbb463eac3a58512566", "delayedPaymentBasepoint": "03c23a68c8cfe4b551ffaaa6a4edeee0ac20c7ddb5e5db8711b9b20620b3dd7018", @@ -101,6 +91,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "9b2a64072912064cdbfbc9f79a36d47af48b458f9ea133c4588f53f59d3cc307:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "0262927c7f186107ea2b04522abda2e12dad48ff5d58646dda882b61f65cf6678f", "localFundingStatus": { "status": "confirmed", @@ -109,6 +101,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 0, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 1, "spec": { @@ -135,38 +137,20 @@ "toLocal": 748900100, "toRemote": 200000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "9b2a64072912064cdbfbc9f79a36d47af48b458f9ea133c4588f53f59d3cc307:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "0020479c37a0e82b2fc0e0429b354c6a7a404ec4421c24ec04697691701ce841eff9" - }, - "redeemScript": "52210262927c7f186107ea2b04522abda2e12dad48ff5d58646dda882b61f65cf6678f2103b34007686c9f4652ecce8495a1d47591f322289903749e03f902ef741106bafc52ae" - }, - "tx": "0200000000010107c33c9df5538f58c433a19e8f458bf47ad4369af7c9fbdb4c06122907642a9b0000000000d1489680054a01000000000000220020cca63a05053e3136be80fdff0b910964921e9a86abf2f6c8a5a2572c0b9ec0874a01000000000000220020f6c0da7bcacad5eae05b637a064e0b50b9509ae8605b7935529abfbda3d5d02150c30000000000002200203b089cd6e67ed390b14fc4f427e31096e57d0255970f2a320711324cb978b86d400d03000000000022002027e4847ebe39f77ddb74b6f8db707dddc0bb390c67a87ad2650a128b7d500bc680510b0000000000220020698a6b11e81fdaa28e0efff32d4d93e781e4075e1c6c381e7ae35ab16a6a40260400473044022058555dff0574ce320e281e1ff9d945674f710e5a33aeffcd2c0a105bbe1c2bad02207389c84098615ab0f12dd7e9f951ede0000f205fda0f03fe958a42bfb59c9726014730440220256b21466dd455baaf05e9d650e1e0e2f6af821f40a6ad60af6792473cb277fa022068944c1ec29b2a7d08ec497258472474e292ab1dfaaf3d4abc5a1b409604c2d4014752210262927c7f186107ea2b04522abda2e12dad48ff5d58646dda882b61f65cf6678f2103b34007686c9f4652ecce8495a1d47591f322289903749e03f902ef741106bafc52ae12ef0320" - }, - "htlcTxsAndSigs": [ - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "24f0d946a87c12c7fa345d04f2f2e9623074bd093d44229e8d00e1422257e015:2", - "txOut": { - "amount": 50000, - "publicKeyScript": "00203b089cd6e67ed390b14fc4f427e31096e57d0255970f2a320711324cb978b86d" - }, - "redeemScript": "76a914d5e58503b9c9e98517f4acd6fdf3c55bdbc678478763ac67210295384b0902700fcb0dca94e5df1ec0b8fe67fdc8668c54339baa1c0e24db36887c820120876475527c2102207023f99fd0b891f484202383085203fcb240ad8712ce73c4920da26ee4e31852ae67a914fafabc04f1544f6d4e88eba2fc1d236c3e9d729488ac6851b27568" - }, - "tx": "020000000115e0572242e1008d9e22443d09bd743062e9f2f2045d34fac7127ca846d9f024020000000001000000014eb6000000000000220020698a6b11e81fdaa28e0efff32d4d93e781e4075e1c6c381e7ae35ab16a6a4026101b0600", - "htlcId": 0 - }, - "localSig": "7b01095b2cf2bf7311309280316f1528359efe79f2b352d37f8b5a28dd0b0a3a7a0f4228181436563ba31a71bdfb6d7470600df6033263cf209374d2f8fb2263", - "remoteSig": "29503e87f9b949d66cdbaef5a011d52d3f01fda29912c342f2e6ed69d74755bd029544a42cae34e45aaf92628b9eca98ae2441ec4433f8bf8714224f82cc852c" - } - ] - } + "txId": "24f0d946a87c12c7fa345d04f2f2e9623074bd093d44229e8d00e1422257e015", + "remoteSig": { + "sig": "58555dff0574ce320e281e1ff9d945674f710e5a33aeffcd2c0a105bbe1c2bad7389c84098615ab0f12dd7e9f951ede0000f205fda0f03fe958a42bfb59c9726" + }, + "htlcRemoteSigs": [ + "29503e87f9b949d66cdbaef5a011d52d3f01fda29912c342f2e6ed69d74755bd029544a42cae34e45aaf92628b9eca98ae2441ec4433f8bf8714224f82cc852c" + ] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 1, @@ -214,44 +198,15 @@ "waitingSinceBlock": 400000, "localCommitPublished": { "commitTx": "0200000000010107c33c9df5538f58c433a19e8f458bf47ad4369af7c9fbdb4c06122907642a9b0000000000d1489680054a01000000000000220020cca63a05053e3136be80fdff0b910964921e9a86abf2f6c8a5a2572c0b9ec0874a01000000000000220020f6c0da7bcacad5eae05b637a064e0b50b9509ae8605b7935529abfbda3d5d02150c30000000000002200203b089cd6e67ed390b14fc4f427e31096e57d0255970f2a320711324cb978b86d400d03000000000022002027e4847ebe39f77ddb74b6f8db707dddc0bb390c67a87ad2650a128b7d500bc680510b0000000000220020698a6b11e81fdaa28e0efff32d4d93e781e4075e1c6c381e7ae35ab16a6a40260400473044022058555dff0574ce320e281e1ff9d945674f710e5a33aeffcd2c0a105bbe1c2bad02207389c84098615ab0f12dd7e9f951ede0000f205fda0f03fe958a42bfb59c9726014730440220256b21466dd455baaf05e9d650e1e0e2f6af821f40a6ad60af6792473cb277fa022068944c1ec29b2a7d08ec497258472474e292ab1dfaaf3d4abc5a1b409604c2d4014752210262927c7f186107ea2b04522abda2e12dad48ff5d58646dda882b61f65cf6678f2103b34007686c9f4652ecce8495a1d47591f322289903749e03f902ef741106bafc52ae12ef0320", - "claimMainDelayedOutputTx": { - "input": { - "outPoint": "24f0d946a87c12c7fa345d04f2f2e9623074bd093d44229e8d00e1422257e015:4", - "txOut": { - "amount": 741760, - "publicKeyScript": "0020698a6b11e81fdaa28e0efff32d4d93e781e4075e1c6c381e7ae35ab16a6a4026" - }, - "redeemScript": "632102f1ffc5973b847e3ac8b21a0aa624de4fa0c68c272ebe6f70c3630ba67f4f7df867029000b275210359cc842b0b3570536c4d8e3ddf6d6ed84c826369cbf79c2af8cd46c25034f31968ac" - }, - "tx": "0200000000010115e0572242e1008d9e22443d09bd743062e9f2f2045d34fac7127ca846d9f0240400000000900000000111480b000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c03483045022100dd20026096adec36aaad856c4f1abe398e8d6888311c665cad2da734439ddf89022006680b414b9cd31f10b5e5ad3d2bf92af73f7315db17db242a3d1aa17007833f01004d632102f1ffc5973b847e3ac8b21a0aa624de4fa0c68c272ebe6f70c3630ba67f4f7df867029000b275210359cc842b0b3570536c4d8e3ddf6d6ed84c826369cbf79c2af8cd46c25034f31968ac00000000" + "localOutput": "24f0d946a87c12c7fa345d04f2f2e9623074bd093d44229e8d00e1422257e015:4", + "anchorOutput": null, + "incomingHtlcs": {}, + "outgoingHtlcs": { + "24f0d946a87c12c7fa345d04f2f2e9623074bd093d44229e8d00e1422257e015:2": 0 }, - "htlcTxs": { - "24f0d946a87c12c7fa345d04f2f2e9623074bd093d44229e8d00e1422257e015:2": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "24f0d946a87c12c7fa345d04f2f2e9623074bd093d44229e8d00e1422257e015:2", - "txOut": { - "amount": 50000, - "publicKeyScript": "00203b089cd6e67ed390b14fc4f427e31096e57d0255970f2a320711324cb978b86d" - }, - "redeemScript": "76a914d5e58503b9c9e98517f4acd6fdf3c55bdbc678478763ac67210295384b0902700fcb0dca94e5df1ec0b8fe67fdc8668c54339baa1c0e24db36887c820120876475527c2102207023f99fd0b891f484202383085203fcb240ad8712ce73c4920da26ee4e31852ae67a914fafabc04f1544f6d4e88eba2fc1d236c3e9d729488ac6851b27568" - }, - "tx": "0200000000010115e0572242e1008d9e22443d09bd743062e9f2f2045d34fac7127ca846d9f024020000000001000000014eb6000000000000220020698a6b11e81fdaa28e0efff32d4d93e781e4075e1c6c381e7ae35ab16a6a40260500473044022029503e87f9b949d66cdbaef5a011d52d3f01fda29912c342f2e6ed69d74755bd0220029544a42cae34e45aaf92628b9eca98ae2441ec4433f8bf8714224f82cc852c8347304402207b01095b2cf2bf7311309280316f1528359efe79f2b352d37f8b5a28dd0b0a3a02207a0f4228181436563ba31a71bdfb6d7470600df6033263cf209374d2f8fb226301008876a914d5e58503b9c9e98517f4acd6fdf3c55bdbc678478763ac67210295384b0902700fcb0dca94e5df1ec0b8fe67fdc8668c54339baa1c0e24db36887c820120876475527c2102207023f99fd0b891f484202383085203fcb240ad8712ce73c4920da26ee4e31852ae67a914fafabc04f1544f6d4e88eba2fc1d236c3e9d729488ac6851b27568101b0600", - "htlcId": 0 - } - }, - "claimHtlcDelayedTxs": [ - { - "input": { - "outPoint": "4ed5f44594cb4afd61028a65555aa68ac19f11e87f757e8dbcf3a83e31f9deeb:0", - "txOut": { - "amount": 46670, - "publicKeyScript": "0020698a6b11e81fdaa28e0efff32d4d93e781e4075e1c6c381e7ae35ab16a6a4026" - }, - "redeemScript": "632102f1ffc5973b847e3ac8b21a0aa624de4fa0c68c272ebe6f70c3630ba67f4f7df867029000b275210359cc842b0b3570536c4d8e3ddf6d6ed84c826369cbf79c2af8cd46c25034f31968ac" - }, - "tx": "02000000000101ebdef9313ea8f3bc8d7e757fe8119fc18aa65a55658a0261fd4acb9445f4d54e00000000009000000001dfac00000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c0348304502210097f93588410f56376fd2280b480bea153d32a7ccb7dc7008310e928b10fc27bf022064136888e322578b62234572e645fc8ec13b1ad27559d65cd695a498712ac1f801004d632102f1ffc5973b847e3ac8b21a0aa624de4fa0c68c272ebe6f70c3630ba67f4f7df867029000b275210359cc842b0b3570536c4d8e3ddf6d6ed84c826369cbf79c2af8cd46c25034f31968ac00000000" - } - ] + "htlcDelayedOutputs": [ + "4ed5f44594cb4afd61028a65555aa68ac19f11e87f757e8dbcf3a83e31f9deeb:0" + ], + "irrevocablySpent": {} } } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Mutual_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Mutual_ebb9087c/data.json index dadf6db40..a151bbda5 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Mutual_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Mutual_ebb9087c/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.Closing", "commitments": { - "params": { + "channelParams": { "channelId": "d469132750e7d041f2fd990e8f9da39810e931c6e43a74d9cd4fe462a8376627", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/464799359'/1844920236/1205338618'/1049864060'/776185252'/1994944773'/188152197/1162311989'/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 0, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -45,11 +40,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "033d473f96bd80933245a7b9ff4c3ee3045b9293b7bf5b98eb72197057ed8ec34a", "paymentBasepoint": "034584173fcd85590bb878bc0833c8b0b200ea2fb0f353d8046ad37717f1a783c4", "delayedPaymentBasepoint": "02a5764c14a0371a53391d3d7a84faaa0b0922cb8fb1194e67482a6467be37d004", @@ -101,6 +91,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "fb3eaf9e0d57440b02c0a7f8b221e5628c567651e14912ad32f478d795c4e33b:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "02a73d5523148f33b473419816e41c58bfec5cb332ec86b9a80ce6f1825bca170c", "localFundingStatus": { "status": "confirmed", @@ -109,6 +101,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 0, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 0, "spec": { @@ -118,20 +120,18 @@ "toLocal": 800000000, "toRemote": 200000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "fb3eaf9e0d57440b02c0a7f8b221e5628c567651e14912ad32f478d795c4e33b:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "002057ca65f637bc83aaa046335b1aa7ef3b5347c2b40aef1c83c7831202a32cf73e" - }, - "redeemScript": "522102a73d5523148f33b473419816e41c58bfec5cb332ec86b9a80ce6f1825bca170c2102ef967c443519718449a9c9b60a85def8c99e0ea8a0da33273a7c18775e7eb91b52ae" - }, - "tx": "020000000001013be3c495d778f432ad1249e15176568c62e521b2f8a7c0020b44570d9eaf3efb00000000000abbb380044a01000000000000220020a0ca67609d8f4d1c580f41451ac9f40ac8e4942f2ee1fb39253c89dec7f830f34a01000000000000220020c8809c3b9a97354b8c155d027966ef185e86c19bde823ab9429b5d02b2000650400d0300000000002200205311eef80c15717d4dbd16822f8bee49d1ddc5707f9240f1ff20c11091c1dfa3781c0c0000000000220020c1f2b1ac55b4bd727e85af80fdcdbfff0ea17156126175034b784878cb0bb80f0400483045022100dc0d6153f56f4fb1238325ce8b38de8d2196f015e337502198ffaa43e15c3ff602205ac254766dcab049e16bbd40bac3438fc3e0274c853bfc1eb88e7260b0f7fe1101473044022006483d50cd4ea6ada8d6bb059cfeba406d5ba0c8e2edfe6b56996ef1ab61cacf022019ece48b729dc967c7c20c3299781641f3c38e1870b82ec3f17efd6fbe4221570147522102a73d5523148f33b473419816e41c58bfec5cb332ec86b9a80ce6f1825bca170c2102ef967c443519718449a9c9b60a85def8c99e0ea8a0da33273a7c18775e7eb91b52ae106ace20" - }, - "htlcTxsAndSigs": [] - } + "txId": "0f9ad08d4eddee3c7d91464eb1dcca37548f48d95863ff67a0ef11b5ef8cd312", + "remoteSig": { + "sig": "dc0d6153f56f4fb1238325ce8b38de8d2196f015e337502198ffaa43e15c3ff65ac254766dcab049e16bbd40bac3438fc3e0274c853bfc1eb88e7260b0f7fe11" + }, + "htlcRemoteSigs": [] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 0, @@ -164,11 +164,10 @@ "txOut": { "amount": 1000000, "publicKeyScript": "002057ca65f637bc83aaa046335b1aa7ef3b5347c2b40aef1c83c7831202a32cf73e" - }, - "redeemScript": "522102a73d5523148f33b473419816e41c58bfec5cb332ec86b9a80ce6f1825bca170c2102ef967c443519718449a9c9b60a85def8c99e0ea8a0da33273a7c18775e7eb91b52ae" + } }, "tx": "02000000013be3c495d778f432ad1249e15176568c62e521b2f8a7c0020b44570d9eaf3efb0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1d6270c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 + "toLocalOutputIndex": 1 } ], "mutualClosePublished": [ @@ -178,11 +177,10 @@ "txOut": { "amount": 1000000, "publicKeyScript": "002057ca65f637bc83aaa046335b1aa7ef3b5347c2b40aef1c83c7831202a32cf73e" - }, - "redeemScript": "522102a73d5523148f33b473419816e41c58bfec5cb332ec86b9a80ce6f1825bca170c2102ef967c443519718449a9c9b60a85def8c99e0ea8a0da33273a7c18775e7eb91b52ae" + } }, "tx": "020000000001013be3c495d778f432ad1249e15176568c62e521b2f8a7c0020b44570d9eaf3efb0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1d6270c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c0400483045022100832a2fef23d4aa781be1ccd6658a49265bac6b70698e21ec675229ae3c0c2f2e02205510f59e68438fe94ec559917bceae19456af6c65609f4f8c4e435374515f3e9014730440220169b63c09d0f0e6ea76740fa024c327886e6f4b4c19c7ec79e535523e71bbc55022061d9ac0db36cbd6af8703846f49a2d5850d4a8a68e6eef306e5bdb1ab3d31b280147522102a73d5523148f33b473419816e41c58bfec5cb332ec86b9a80ce6f1825bca170c2102ef967c443519718449a9c9b60a85def8c99e0ea8a0da33273a7c18775e7eb91b52ae00000000", - "toLocalIndex": 1 + "toLocalOutputIndex": 1 } ] } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Remote_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Remote_ebb9087c/data.json index c4b6d14fb..192b5e573 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Remote_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Remote_ebb9087c/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.Closing", "commitments": { - "params": { + "channelParams": { "channelId": "7586b3458997d0776aa1de435d23fe659dbabc7b4fe1ebf3efaa7b6f253ef797", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/917204307'/1843072245/1110453303/1285396001'/1746678994/772884323'/1589666567'/681064086'/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 0, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -45,11 +40,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "02ae827a31dd89c6b3b03f79e7d5a1c607d5abc40831c40b7957840eed4eae07c6", "paymentBasepoint": "0375a040260f0129d1e0cd48c2a97393d9d1c7947bcdec77be2a525ff47b17a7a1", "delayedPaymentBasepoint": "02943dc0608ef409f4ed82a6c5a8a9d3c5692862e1eea8eebc53ea06e9f0a2d343", @@ -101,6 +91,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "f57bf9405340cc20cb8528d32541419fe7c1814757ae8de987a9205a416249d0:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "024d0f54a8ff41d13f936150158788115fe19868b592623b60be6c82eb561b30e6", "localFundingStatus": { "status": "confirmed", @@ -109,6 +101,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 0, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 1, "spec": { @@ -143,72 +145,22 @@ "toLocal": 725000000, "toRemote": 200000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "f57bf9405340cc20cb8528d32541419fe7c1814757ae8de987a9205a416249d0:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "002006e284d14b0f9788c0badf23c52f2ea218bc3a57c63d8f85bf79a678949fba92" - }, - "redeemScript": "5221024d0f54a8ff41d13f936150158788115fe19868b592623b60be6c82eb561b30e62103cab05a7938caac126f86783c68fed9ab90062bc323db580e555f4af79dae0c6952ae" - }, - "tx": "02000000000101d04962415a20a987e98dae574781c1e79f414125d32885cb20cc405340f97bf50000000000cdb6f080074a01000000000000220020a92010dbbc2b70b39d2c9d1911ee2e5da1f294196338c06ede340c87f4923f714a01000000000000220020b1e710db2b94f2557d1f9e446ff57980fc978f77403b49f3103432b82264ad9d204e00000000000022002077fd42515fc3e36f9e7eb7f9cb4daa64439e3337eaeadce4072312355c039fb1a86100000000000022002077fd42515fc3e36f9e7eb7f9cb4daa64439e3337eaeadce4072312355c039fb1307500000000000022002077fd42515fc3e36f9e7eb7f9cb4daa64439e3337eaeadce4072312355c039fb1400d03000000000022002014e113c73d2ca978e2b1f9643608c0bded0bb02851557a678a07a2fb41eec3516ced0a000000000022002086e17ba679d2568559974707e9eff99981d5fd7d8a9fae29b68a8f9a14ed883604004730440220125de9d83e8005bd73a01e24aadf5f387d15c4cc40990a3d5ccc42dc446461d102201ce47aa07dc35658cca00f1c092c8f8273e6b11eca8f40e360b4f9ee0955f74a0148304502210086875271a6242cf86f5d3b0f0e35ad49925e811380b1fd50336dc07067d3f95e0220550f1169a157b996cc944041255075549e5981839f5dc5d507b08aa83f2e129e01475221024d0f54a8ff41d13f936150158788115fe19868b592623b60be6c82eb561b30e62103cab05a7938caac126f86783c68fed9ab90062bc323db580e555f4af79dae0c6952aeeeca3f20" - }, - "htlcTxsAndSigs": [ - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "fed55c7986d149ca8432c171cd163ab78658e4e0a7b70234218ee74644015f2a:2", - "txOut": { - "amount": 20000, - "publicKeyScript": "002077fd42515fc3e36f9e7eb7f9cb4daa64439e3337eaeadce4072312355c039fb1" - }, - "redeemScript": "76a914a644af4571f92ffba15fb2d36c089b405c1f05a58763ac672102bf0b1a0d169ea9e1a9aac4b0cbb66386aae755c935bf137cf621b708bfd1f4a97c820120876475527c2102ae57e0cbcb6d0a3734244a5090396a5f8aba51f1c288a9da84b2f3daaa887aa852ae67a914ed8f56a1a95950080a822f6e68d7d99ff4ad6a7688ac6851b27568" - }, - "tx": "02000000012a5f014446e78e213402b7a7e0e45886b73a16cd71c13284ca49d186795cd5fe020000000001000000011e4100000000000022002086e17ba679d2568559974707e9eff99981d5fd7d8a9fae29b68a8f9a14ed88360f1b0600", - "htlcId": 2 - }, - "localSig": "8ea6861abecb3b5cc37b9b8dfa44899557e315ff75a9524dacaed1f7a5ba6f8d77deef3c7846c07c1b0982ef3447727eb4ee6cab710d7cc35feb8805f55731de", - "remoteSig": "e8f3c941cc5ec88b6f03ea64085742d99d8c1403f0fc78fc67d56271c07ef6e42776b1100b32062a087cf86acaeb7cd280676e792986dd6fb5b640f64d22694e" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "fed55c7986d149ca8432c171cd163ab78658e4e0a7b70234218ee74644015f2a:3", - "txOut": { - "amount": 25000, - "publicKeyScript": "002077fd42515fc3e36f9e7eb7f9cb4daa64439e3337eaeadce4072312355c039fb1" - }, - "redeemScript": "76a914a644af4571f92ffba15fb2d36c089b405c1f05a58763ac672102bf0b1a0d169ea9e1a9aac4b0cbb66386aae755c935bf137cf621b708bfd1f4a97c820120876475527c2102ae57e0cbcb6d0a3734244a5090396a5f8aba51f1c288a9da84b2f3daaa887aa852ae67a914ed8f56a1a95950080a822f6e68d7d99ff4ad6a7688ac6851b27568" - }, - "tx": "02000000012a5f014446e78e213402b7a7e0e45886b73a16cd71c13284ca49d186795cd5fe03000000000100000001a65400000000000022002086e17ba679d2568559974707e9eff99981d5fd7d8a9fae29b68a8f9a14ed8836101b0600", - "htlcId": 1 - }, - "localSig": "a06d47536c7bce2fca67a1af4e3257835de0014761d9a437eae47c9d5daa36c91a00573297c1176e742d3d2372c41d3a1b3a27a991351b575a17f158b5c29a02", - "remoteSig": "59adae2c05ac5834aa4eaae067126c977fb30d17c80f6b65a29b575b8f82149f4f62d7f6ba04673ae8172d4dd865d9f83837a16c624d8c659743bd6e9c701489" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "fed55c7986d149ca8432c171cd163ab78658e4e0a7b70234218ee74644015f2a:4", - "txOut": { - "amount": 30000, - "publicKeyScript": "002077fd42515fc3e36f9e7eb7f9cb4daa64439e3337eaeadce4072312355c039fb1" - }, - "redeemScript": "76a914a644af4571f92ffba15fb2d36c089b405c1f05a58763ac672102bf0b1a0d169ea9e1a9aac4b0cbb66386aae755c935bf137cf621b708bfd1f4a97c820120876475527c2102ae57e0cbcb6d0a3734244a5090396a5f8aba51f1c288a9da84b2f3daaa887aa852ae67a914ed8f56a1a95950080a822f6e68d7d99ff4ad6a7688ac6851b27568" - }, - "tx": "02000000012a5f014446e78e213402b7a7e0e45886b73a16cd71c13284ca49d186795cd5fe040000000001000000012e6800000000000022002086e17ba679d2568559974707e9eff99981d5fd7d8a9fae29b68a8f9a14ed8836101b0600", - "htlcId": 0 - }, - "localSig": "a876af2b2a5220aca6cbe62b8af321daade068c913872833fe070db1674ccb3b621bdc987ff9482cffde7b0152116ec148e7371342c18415ab538e888caef24e", - "remoteSig": "df656b2cd059046b55cb29df2b91232738254f6200230dd1f40c1cf69b6dc64f10ddb9ff6a543b8e25cc189d6c458c482ff18f8e1766f619748aa883bc974258" - } - ] - } + "txId": "fed55c7986d149ca8432c171cd163ab78658e4e0a7b70234218ee74644015f2a", + "remoteSig": { + "sig": "125de9d83e8005bd73a01e24aadf5f387d15c4cc40990a3d5ccc42dc446461d11ce47aa07dc35658cca00f1c092c8f8273e6b11eca8f40e360b4f9ee0955f74a" + }, + "htlcRemoteSigs": [ + "e8f3c941cc5ec88b6f03ea64085742d99d8c1403f0fc78fc67d56271c07ef6e42776b1100b32062a087cf86acaeb7cd280676e792986dd6fb5b640f64d22694e", + "59adae2c05ac5834aa4eaae067126c977fb30d17c80f6b65a29b575b8f82149f4f62d7f6ba04673ae8172d4dd865d9f83837a16c624d8c659743bd6e9c701489", + "df656b2cd059046b55cb29df2b91232738254f6200230dd1f40c1cf69b6dc64f10ddb9ff6a543b8e25cc189d6c458c482ff18f8e1766f619748aa883bc974258" + ] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 1, @@ -265,58 +217,14 @@ "waitingSinceBlock": 400000, "remoteCommitPublished": { "commitTx": "02000000000101d04962415a20a987e98dae574781c1e79f414125d32885cb20cc405340f97bf50000000000cdb6f080074a01000000000000220020a92010dbbc2b70b39d2c9d1911ee2e5da1f294196338c06ede340c87f4923f714a01000000000000220020b1e710db2b94f2557d1f9e446ff57980fc978f77403b49f3103432b82264ad9d204e0000000000002200209afe5ca4059c0ff9742b405583beab6a037e28af41ed9a09ca17aef40444e3f0a86100000000000022002030e1b25c30802faa6380c3a0bc8a9aee6aa20bba145ae0dc57c17799611aa828307500000000000022002030e1b25c30802faa6380c3a0bc8a9aee6aa20bba145ae0dc57c17799611aa828400d030000000000220020a967531f63bc31478dd0bef66b2a562e60315884aba98fec7ec93ad5f765801c6ced0a00000000002200205550759c1f7f8d8dbb21260139b729dc80f8b68177ab34fc781c85c7c0b1649c040047304402204975adf09a929cd238dac6950784c04609f64e1051bc624e4adb3744fe1d4f7c0220018d03f708d5630eb5761d8a6ee300ecf62ce64fd1f7f7b8c3d876c3859203220147304402206b695adf0d2e788d5db9ff730c9647713bcae4c0d5ba8776639e317500b400a90220315bfa720c5135cfe70bf2323bdf71405bd667482aaf065fa61933800e69c7c801475221024d0f54a8ff41d13f936150158788115fe19868b592623b60be6c82eb561b30e62103cab05a7938caac126f86783c68fed9ab90062bc323db580e555f4af79dae0c6952aeeeca3f20", - "claimMainOutputTx": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", - "input": { - "outPoint": "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:6", - "txOut": { - "amount": 716140, - "publicKeyScript": "00205550759c1f7f8d8dbb21260139b729dc80f8b68177ab34fc781c85c7c0b1649c" - }, - "redeemScript": "21020e0b6762b23d995a4dbdd8bb91e42269c56926686939edc499a3fc63593cf286ad51b2" - }, - "tx": "020000000001014e1f0bb21b36e7653677d49d46bd7aef268de50590b5c8284fb518cb718d0ed906000000000100000001cae40a000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c0247304402202588c1131fe0bf10d2cd950033d8c40406a33b9637c1fd15a20506b499881141022068e7d7846dfec8e101416e639d2293882a1301eda3ab9053d010f3aa5db67d04012521020e0b6762b23d995a4dbdd8bb91e42269c56926686939edc499a3fc63593cf286ad51b200000000" + "localOutput": "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:6", + "anchorOutput": null, + "incomingHtlcs": {}, + "outgoingHtlcs": { + "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:4": 0, + "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:3": 1, + "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:2": 2 }, - "claimHtlcTxs": { - "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:4": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", - "input": { - "outPoint": "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:4", - "txOut": { - "amount": 30000, - "publicKeyScript": "002030e1b25c30802faa6380c3a0bc8a9aee6aa20bba145ae0dc57c17799611aa828" - }, - "redeemScript": "76a91460ae1185fcc4b87b10a117b5d3bc9f73af9fcd408763ac6721028d935b11d497d0d4555afaf1da310f1823d1c5263fdceb9a372bee1fd93b35557c8201208763a914ed8f56a1a95950080a822f6e68d7d99ff4ad6a7688527c21023e075e0e994a87f2dee45697ee98ca259f95d6f524b2314b6c5195d4c850365852ae677503101b06b175ac6851b27568" - }, - "tx": "020000000001014e1f0bb21b36e7653677d49d46bd7aef268de50590b5c8284fb518cb718d0ed9040000000001000000017c6a00000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c03483045022100c215676aca76b1efceb8198a435859c6a8b97f3c1be41889138e346d884495590220628f64cf3a9306fd504e94604f0e5a027e8bb26b26d12204f2e319a6ba2bb88901008e76a91460ae1185fcc4b87b10a117b5d3bc9f73af9fcd408763ac6721028d935b11d497d0d4555afaf1da310f1823d1c5263fdceb9a372bee1fd93b35557c8201208763a914ed8f56a1a95950080a822f6e68d7d99ff4ad6a7688527c21023e075e0e994a87f2dee45697ee98ca259f95d6f524b2314b6c5195d4c850365852ae677503101b06b175ac6851b27568101b0600", - "htlcId": 0 - }, - "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:3": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", - "input": { - "outPoint": "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:3", - "txOut": { - "amount": 25000, - "publicKeyScript": "002030e1b25c30802faa6380c3a0bc8a9aee6aa20bba145ae0dc57c17799611aa828" - }, - "redeemScript": "76a91460ae1185fcc4b87b10a117b5d3bc9f73af9fcd408763ac6721028d935b11d497d0d4555afaf1da310f1823d1c5263fdceb9a372bee1fd93b35557c8201208763a914ed8f56a1a95950080a822f6e68d7d99ff4ad6a7688527c21023e075e0e994a87f2dee45697ee98ca259f95d6f524b2314b6c5195d4c850365852ae677503101b06b175ac6851b27568" - }, - "tx": "020000000001014e1f0bb21b36e7653677d49d46bd7aef268de50590b5c8284fb518cb718d0ed903000000000100000001f45600000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c0347304402201f6a2fbba5f10ab041f1f9bbb5dd546e56d7bebdd1f7bf18834b7d6df1bb93bc022009c313c1dec146590efb89d848995026ca56791b104a6478b985630d0e14de4a01008e76a91460ae1185fcc4b87b10a117b5d3bc9f73af9fcd408763ac6721028d935b11d497d0d4555afaf1da310f1823d1c5263fdceb9a372bee1fd93b35557c8201208763a914ed8f56a1a95950080a822f6e68d7d99ff4ad6a7688527c21023e075e0e994a87f2dee45697ee98ca259f95d6f524b2314b6c5195d4c850365852ae677503101b06b175ac6851b27568101b0600", - "htlcId": 1 - }, - "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:2": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", - "input": { - "outPoint": "d90e8d71cb18b54f28c8b59005e58d26ef7abd469dd4773665e7361bb20b1f4e:2", - "txOut": { - "amount": 20000, - "publicKeyScript": "00209afe5ca4059c0ff9742b405583beab6a037e28af41ed9a09ca17aef40444e3f0" - }, - "redeemScript": "76a91460ae1185fcc4b87b10a117b5d3bc9f73af9fcd408763ac6721028d935b11d497d0d4555afaf1da310f1823d1c5263fdceb9a372bee1fd93b35557c8201208763a914ed8f56a1a95950080a822f6e68d7d99ff4ad6a7688527c21023e075e0e994a87f2dee45697ee98ca259f95d6f524b2314b6c5195d4c850365852ae6775030f1b06b175ac6851b27568" - }, - "tx": "020000000001014e1f0bb21b36e7653677d49d46bd7aef268de50590b5c8284fb518cb718d0ed9020000000001000000016c4300000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c034730440220641d3771a1f18ad80bb371684db8a502d37b67a7ad8b8f09511c08968ddff9af0220691b14a463bd42d39b21116dc7dab25443c844758932e866deb8b89fa495297801008e76a91460ae1185fcc4b87b10a117b5d3bc9f73af9fcd408763ac6721028d935b11d497d0d4555afaf1da310f1823d1c5263fdceb9a372bee1fd93b35557c8201208763a914ed8f56a1a95950080a822f6e68d7d99ff4ad6a7688527c21023e075e0e994a87f2dee45697ee98ca259f95d6f524b2314b6c5195d4c850365852ae6775030f1b06b175ac6851b275680f1b0600", - "htlcId": 2 - } - } + "irrevocablySpent": {} } } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Revoked_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Revoked_ebb9087c/data.json index f6a95f2a7..7e5416191 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Revoked_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Revoked_ebb9087c/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.Closing", "commitments": { - "params": { + "channelParams": { "channelId": "17349e4aedef0df55786bb7c898ed26f9efb0e9137e39842305225abbac15de8", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/988333220/1233482617/73416351/822863940/1638212389'/107957142/841980396'/1796858398/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 0, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -45,11 +40,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "03b33355c6736888c21140c7cc917a513e35e4b16809b38aca76c24f51ee10fcc5", "paymentBasepoint": "03e319244d04f711041512e69b07bd1b9788c940e72686ab2d850bd2c3e8a9ce56", "delayedPaymentBasepoint": "03fa7444cb2c4c91144ff348305a7c6b6097a0d9e930f20dc4281437fe9426493a", @@ -101,6 +91,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "91e599d9edf2d5f111d7e626424e9f70836c4a742298f4340082f536a311c492:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "03e5ca03e7ca5de071fe7d6d3f0608bb00df32416942af27ffa946b9904af31fdd", "localFundingStatus": { "status": "confirmed", @@ -109,6 +101,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 0, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 5, "spec": { @@ -118,20 +120,18 @@ "toLocal": 800000000, "toRemote": 200000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "91e599d9edf2d5f111d7e626424e9f70836c4a742298f4340082f536a311c492:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "0020fc81f1c848101539be21ca416a5ae4484d712e1d28102306abd861637bf16bec" - }, - "redeemScript": "52210200657a0595392c568dc82fb40a9353e903d3631621f9cda522e793a21e3f582c2103e5ca03e7ca5de071fe7d6d3f0608bb00df32416942af27ffa946b9904af31fdd52ae" - }, - "tx": "0200000000010192c411a336f5820034f49822744a6c83709f4e4226e6d711f1d5f2edd999e59100000000001703a580044a01000000000000220020ae3699b4009a977bc9f6ddfdb01acd73a50a7df45d8fad51a92c1da8f2a1adf94a01000000000000220020cdf1f91af81ba62f260048245896f15fdba08490d1f99ce469c5868c16169b13400d0300000000002200207e95cbde18069fe70f6644c8a32ee72d36cc7a472f0130513ebf2c35a26a6501781c0c000000000022002023cda1884857117b9680b6fc3ebd0d04b498527107581f596b42c6fcb1dc404f040047304402202e35c0a4eb65e452a6614bc46b99e1066462f2a4c056bb08136f5c7d52bc3d5f02205715b2e88af9f1ce02746b8b9b64221010437f76d6f5b4cae0041661ee14a568014730440220716bbeadfc5a323082f8c2e4c9b401f2cf07bd71d7d47fa31effa54ef1c6ff5f0220463be28b9beecba893f0c1df47610a0c201605391d312e8607a647db13938cf6014752210200657a0595392c568dc82fb40a9353e903d3631621f9cda522e793a21e3f582c2103e5ca03e7ca5de071fe7d6d3f0608bb00df32416942af27ffa946b9904af31fdd52ae0bbf8120" - }, - "htlcTxsAndSigs": [] - } + "txId": "1f369b753124adac5fedce5d6eae5539c08261b29fbc291f4e365c295f68dbb5", + "remoteSig": { + "sig": "716bbeadfc5a323082f8c2e4c9b401f2cf07bd71d7d47fa31effa54ef1c6ff5f463be28b9beecba893f0c1df47610a0c201605391d312e8607a647db13938cf6" + }, + "htlcRemoteSigs": [] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 6, @@ -161,99 +161,19 @@ { "commitTx": "0200000000010192c411a336f5820034f49822744a6c83709f4e4226e6d711f1d5f2edd999e59100000000001703a580084a01000000000000220020ae3699b4009a977bc9f6ddfdb01acd73a50a7df45d8fad51a92c1da8f2a1adf94a01000000000000220020cdf1f91af81ba62f260048245896f15fdba08490d1f99ce469c5868c16169b1350460000000000002200200c9697ffb9f0f0b4b602851122e042ff2e1ced15687653fce95fbd4f7c8c9fff204e00000000000022002062a03a86a6b1c7053ed40b9b6d5aa6628b19280f71ded04ff0b635210c77a5b0a861000000000000220020e2021599a4f215f570db24fc3386b93e6fdd61acceac359057a8250f9fce2982b888000000000000220020d4c8f54ce363c3706dc3f4ec5f294aaab8b552fb48226157f3db6793ad48f1e4d07802000000000022002029ce54f9eff2ada570202d729bb514966271166a3cbd97e6a9a00c54bae34b6ea8240b0000000000220020e9ac2543d600156674d6a8f9bc7a98c205f187b948a56d5bd24fc564c1831465040047304402205d899dfb1007e4bfd2b766522c357a3bc93239091aec6f3545245b8576ce1a2702207b6918175806b3864c8f255c898269505c8614fa7f6eff67e8d3478ab71eadaf01483045022100ec65efa8a323db6013242fb9bcaf2f661fccab60a6ffdc02fca82a855744f1fd02202799d0bd3f39a6042bbe4b194ad1c167099ede267d2656ec6d17cb9991d57be4014752210200657a0595392c568dc82fb40a9353e903d3631621f9cda522e793a21e3f582c2103e5ca03e7ca5de071fe7d6d3f0608bb00df32416942af27ffa946b9904af31fdd52ae0abf8120", "remotePerCommitmentSecret": "", - "claimMainOutputTx": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", - "input": { - "outPoint": "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:7", - "txOut": { - "amount": 730280, - "publicKeyScript": "0020e9ac2543d600156674d6a8f9bc7a98c205f187b948a56d5bd24fc564c1831465" - }, - "redeemScript": "21026cda5cf8658fbe76136b0369cbbdca3fcdad28156cbf2d33f019a1dd768c40e9ad51b2" - }, - "tx": "020000000001019c37bccf0354914691b8f5ca7472055117f8e61143f783cae79370adfdeb612307000000000100000001061c0b000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c02473044022077aebad2c1c4cd33926d0a9e5a9bb4b737f1c952137369ff0eeed43e960d3d4702204d3ed12a664c95e44a46956c4488c96e07cf0ec170bcff7befccb01fbf77d341012521026cda5cf8658fbe76136b0369cbbdca3fcdad28156cbf2d33f019a1dd768c40e9ad51b200000000" - }, - "mainPenaltyTx": { - "input": { - "outPoint": "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:6", - "txOut": { - "amount": 162000, - "publicKeyScript": "002029ce54f9eff2ada570202d729bb514966271166a3cbd97e6a9a00c54bae34b6e" - }, - "redeemScript": "632103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb0182467029000b2752103fe7ad7666963051b72ee189434ad5c6c3e8c957028d9099ef934c46b1bbcca2d68ac" - }, - "tx": "020000000001019c37bccf0354914691b8f5ca7472055117f8e61143f783cae79370adfdeb61230600000000ffffffff015c6f02000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c03483045022100fad6df8469266bf75a52a9736fc222fe439ffb1718c2d62a30011581365df7c50220174875931558a27c3287b462d854876701531201542d122b12b5334edb5112a90101014d632103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb0182467029000b2752103fe7ad7666963051b72ee189434ad5c6c3e8c957028d9099ef934c46b1bbcca2d68ac00000000" - }, - "htlcPenaltyTxs": [ - { - "input": { - "outPoint": "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:2", - "txOut": { - "amount": 18000, - "publicKeyScript": "00200c9697ffb9f0f0b4b602851122e042ff2e1ced15687653fce95fbd4f7c8c9fff" - }, - "redeemScript": "76a91486b48a02d036113b6b80597633e7ca4e7a4523628763ac672103b1fa0fdca5c7b8c5e13317e9f7a63f453421ad83a1e8938f38dfbc08a6fd112b7c820120876475527c2102167dea8ff4caca0032f2898afb73bb756cf8fbb7f4cbe833fa2edf676b07e87e52ae67a914aabaafe106e28c053668b0fb2dba56914f4333e888ac6851b27568" - }, - "tx": "020000000001019c37bccf0354914691b8f5ca7472055117f8e61143f783cae79370adfdeb61230200000000ffffffff01b53b00000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c0347304402203359b132fc136ed2a82496ce6d40c7c907beabdb0f521a8094ecccdfc7b2d63202204ee87c11efbcd7ddf2095234367dc4c2f7e200f49e5052fe592131fccdeab579012103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb018248876a91486b48a02d036113b6b80597633e7ca4e7a4523628763ac672103b1fa0fdca5c7b8c5e13317e9f7a63f453421ad83a1e8938f38dfbc08a6fd112b7c820120876475527c2102167dea8ff4caca0032f2898afb73bb756cf8fbb7f4cbe833fa2edf676b07e87e52ae67a914aabaafe106e28c053668b0fb2dba56914f4333e888ac6851b2756800000000" - }, - { - "input": { - "outPoint": "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:3", - "txOut": { - "amount": 20000, - "publicKeyScript": "002062a03a86a6b1c7053ed40b9b6d5aa6628b19280f71ded04ff0b635210c77a5b0" - }, - "redeemScript": "76a91486b48a02d036113b6b80597633e7ca4e7a4523628763ac672103b1fa0fdca5c7b8c5e13317e9f7a63f453421ad83a1e8938f38dfbc08a6fd112b7c820120876475527c2102167dea8ff4caca0032f2898afb73bb756cf8fbb7f4cbe833fa2edf676b07e87e52ae67a914e425cd14d1517915dc36b0b27cb937c07dd05bd288ac6851b27568" - }, - "tx": "020000000001019c37bccf0354914691b8f5ca7472055117f8e61143f783cae79370adfdeb61230300000000ffffffff01854300000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c03483045022100c9c9dfeb598c1d2d6ac4e79d7e5b99a957ef642d3852319d2ec5ade0a56e878702200f8e9358a4298e919a87f2f323eb6224bfce74839d6a956ad35c1f974b4f2bfe012103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb018248876a91486b48a02d036113b6b80597633e7ca4e7a4523628763ac672103b1fa0fdca5c7b8c5e13317e9f7a63f453421ad83a1e8938f38dfbc08a6fd112b7c820120876475527c2102167dea8ff4caca0032f2898afb73bb756cf8fbb7f4cbe833fa2edf676b07e87e52ae67a914e425cd14d1517915dc36b0b27cb937c07dd05bd288ac6851b2756800000000" - }, - { - "input": { - "outPoint": "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:4", - "txOut": { - "amount": 25000, - "publicKeyScript": "0020e2021599a4f215f570db24fc3386b93e6fdd61acceac359057a8250f9fce2982" - }, - "redeemScript": "76a91486b48a02d036113b6b80597633e7ca4e7a4523628763ac672103b1fa0fdca5c7b8c5e13317e9f7a63f453421ad83a1e8938f38dfbc08a6fd112b7c8201208763a91450256d581171efda65d11c9bfaec3291101ea51688527c2102167dea8ff4caca0032f2898afb73bb756cf8fbb7f4cbe833fa2edf676b07e87e52ae677503101b06b175ac6851b27568" - }, - "tx": "020000000001019c37bccf0354914691b8f5ca7472055117f8e61143f783cae79370adfdeb61230400000000ffffffff01ef5600000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c03483045022100b04e61783b161bf4b2ed713336c8e7113f85d64de5b02bd8b63de4b5a81a5fe302206914fb263c8a3756e2c39400a6f8ef16adc011269041e86f7aa23e8b748111b2012103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb018248e76a91486b48a02d036113b6b80597633e7ca4e7a4523628763ac672103b1fa0fdca5c7b8c5e13317e9f7a63f453421ad83a1e8938f38dfbc08a6fd112b7c8201208763a91450256d581171efda65d11c9bfaec3291101ea51688527c2102167dea8ff4caca0032f2898afb73bb756cf8fbb7f4cbe833fa2edf676b07e87e52ae677503101b06b175ac6851b2756800000000" - }, - { - "input": { - "outPoint": "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:5", - "txOut": { - "amount": 35000, - "publicKeyScript": "0020d4c8f54ce363c3706dc3f4ec5f294aaab8b552fb48226157f3db6793ad48f1e4" - }, - "redeemScript": "76a91486b48a02d036113b6b80597633e7ca4e7a4523628763ac672103b1fa0fdca5c7b8c5e13317e9f7a63f453421ad83a1e8938f38dfbc08a6fd112b7c8201208763a91490272d83f3f0f695f20d515925c67a93ce4b6af888527c2102167dea8ff4caca0032f2898afb73bb756cf8fbb7f4cbe833fa2edf676b07e87e52ae677503101b06b175ac6851b27568" - }, - "tx": "020000000001019c37bccf0354914691b8f5ca7472055117f8e61143f783cae79370adfdeb61230500000000ffffffff01ff7d00000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c03483045022100d9782e0c2c1fe069329791d917847b1c03081219f23a353952cc41ca9bc0937b022064a59867560c0007935b36633b7154217eba36e7751c31f71b6ef78afeb6857c012103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb018248e76a91486b48a02d036113b6b80597633e7ca4e7a4523628763ac672103b1fa0fdca5c7b8c5e13317e9f7a63f453421ad83a1e8938f38dfbc08a6fd112b7c8201208763a91490272d83f3f0f695f20d515925c67a93ce4b6af888527c2102167dea8ff4caca0032f2898afb73bb756cf8fbb7f4cbe833fa2edf676b07e87e52ae677503101b06b175ac6851b2756800000000" - } + "localOutput": "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:7", + "remoteOutput": "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:6", + "htlcOutputs": [ + "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:2", + "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:3", + "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:4", + "2361ebfdad7093e7ca83f74311e6f81751057274caf5b89146915403cfbc379c:5" ], - "claimHtlcDelayedPenaltyTxs": [ - { - "input": { - "outPoint": "587b2eb17f9fb99cdd9dafc0805f4db35b15699542ff30a64d30eee39e0dd91f:0", - "txOut": { - "amount": 31470, - "publicKeyScript": "002029ce54f9eff2ada570202d729bb514966271166a3cbd97e6a9a00c54bae34b6e" - }, - "redeemScript": "632103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb0182467029000b2752103fe7ad7666963051b72ee189434ad5c6c3e8c957028d9099ef934c46b1bbcca2d68ac" - }, - "tx": "020000000001011fd90d9ee3ee304da630ff429569155bb34d5f80c0af9ddd9cb99f7fb12e7b580000000000ffffffff017a7100000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c03483045022100d84fd7d4a1b85c5e90b18710f160bcff0ec40f0cc29f2548da770d5a74f2c8f902203a1e9e0e3bc8c2428075164c6d6331dbd56c1af17371a6e32b9795514b29d0a10101014d632103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb0182467029000b2752103fe7ad7666963051b72ee189434ad5c6c3e8c957028d9099ef934c46b1bbcca2d68ac00000000" - }, - { - "input": { - "outPoint": "283f70774fd61e202a54d7f05bfdd5094e66ccd4deb2eed6f7303b95f55d7a54:0", - "txOut": { - "amount": 14670, - "publicKeyScript": "002029ce54f9eff2ada570202d729bb514966271166a3cbd97e6a9a00c54bae34b6e" - }, - "redeemScript": "632103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb0182467029000b2752103fe7ad7666963051b72ee189434ad5c6c3e8c957028d9099ef934c46b1bbcca2d68ac" - }, - "tx": "02000000000101547a5df5953b30f7d6eeb2ded4cc664e09d5fd5bf0d7542a201ed64f77703f280000000000ffffffff01da2f00000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c0347304402201bd91ba3a9f24f54f75af3f8db7860c6251a047472f5e855690a5d18dafcff350220009fc5412a203e4b0d531bc0053ee5e2b68752d36e615cb33ca14b5a4228e7910101014d632103a85952bcb47e5d731bcf1929b325ed9820a90d13c72e5b226ab1454cadb0182467029000b2752103fe7ad7666963051b72ee189434ad5c6c3e8c957028d9099ef934c46b1bbcca2d68ac00000000" - } - ] + "htlcDelayedOutputs": [ + "587b2eb17f9fb99cdd9dafc0805f4db35b15699542ff30a64d30eee39e0dd91f:0", + "283f70774fd61e202a54d7f05bfdd5094e66ccd4deb2eed6f7303b95f55d7a54:0" + ], + "irrevocablySpent": {} } ] } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Negotiating_fac54067/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Negotiating_fac54067/data.json index 4ac975630..99ebc51ae 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Negotiating_fac54067/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Negotiating_fac54067/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.Negotiating", "commitments": { - "params": { + "channelParams": { "channelId": "6757ef5a110b55598548ef487267023694da8d0d18717d6483021c3d711e8f09", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/546955352/1084351973'/1988732683'/878120005/2080028567/525315766'/2072496838/2011076179'/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 100, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -48,11 +43,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "03f90bd70f015e8254f2a8849fea8db440c84a3a9171593ba50d11e3fd8d23c8c7", "paymentBasepoint": "03f83f37d769d6e867fee337539ce3c4ed2454b09076442f9624b8de6cea1fcc3a", "delayedPaymentBasepoint": "026226d9d3a530de8e918136c0686a6c89681cf8c3b890e826defb401b6cb1c1ad", @@ -104,6 +94,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "5d9347f6e9298b5eb18c7647fec1373b127ec4ae02c63affb221acb3f91dba17:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "03441263f4dff5cceec2c817cc00ab6b27e65128259b4d1aa018189d5ac779c611", "localFundingStatus": { "status": "confirmed", @@ -112,6 +104,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 100, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 0, "spec": { @@ -121,20 +123,18 @@ "toLocal": 850000000, "toRemote": 150000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "5d9347f6e9298b5eb18c7647fec1373b127ec4ae02c63affb221acb3f91dba17:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "002065cf56cc050f23840574a042319fb16a3e1f5c01a778776b23935f4dd3ac4bec" - }, - "redeemScript": "52210224d74c9af107db3c470af70997980868c5f903f36a3cc9afbfd966e1097b92922103441263f4dff5cceec2c817cc00ab6b27e65128259b4d1aa018189d5ac779c61152ae" - }, - "tx": "0200000000010117ba1df9b3ac21b2ff3ac602aec47e123b37c1fe47768cb15e8b29e9f647935d000000000026d3b180044a01000000000000220020744905c5179f9a00982fb5fc86cdd360d2426d116639891515d40948b4536f364a01000000000000220020875bdfa81e22db83e7cfc9fcddd2835844d3c611712eb5938e4e47d4657cd77bf0490200000000002200206bb6c3968afb5d2c68afe9a41020d6cb888490ce808cfee385884eb4b0ea840ac8df0c0000000000220020aec0e8dee761c27f97eb743e976dd3cd93c239ff53f7663c14cdbd0b6314fc53040047304402205af40a1429ae8a01cb76051aac6904e3fab1f0031aa9d5e5e129aaf202ebe43802202bb40423348487e97153a1f2eba466af3ce5cbefcae582672318ba3dc0e3069d0147304402201d69ef3d27a4ee4ea170b827e40255df0d335573bd4a856e702f6beefefd0b2702203dd8c5438bf26f5888efbd8917f4b9d94bc1fd769cd7d5dc844f9bad4c0174f7014752210224d74c9af107db3c470af70997980868c5f903f36a3cc9afbfd966e1097b92922103441263f4dff5cceec2c817cc00ab6b27e65128259b4d1aa018189d5ac779c61152ae395ba520" - }, - "htlcTxsAndSigs": [] - } + "txId": "a3cac82072d07b57f3b2b4246fa4d3e2f903e627b7dd1ee6b4b2a571cc4e34d8", + "remoteSig": { + "sig": "1d69ef3d27a4ee4ea170b827e40255df0d335573bd4a856e702f6beefefd0b273dd8c5438bf26f5888efbd8917f4b9d94bc1fd769cd7d5dc844f9bad4c0174f7" + }, + "htlcRemoteSigs": [] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 0, @@ -169,11 +169,10 @@ "txOut": { "amount": 1000000, "publicKeyScript": "002065cf56cc050f23840574a042319fb16a3e1f5c01a778776b23935f4dd3ac4bec" - }, - "redeemScript": "52210224d74c9af107db3c470af70997980868c5f903f36a3cc9afbfd966e1097b92922103441263f4dff5cceec2c817cc00ab6b27e65128259b4d1aa018189d5ac779c61152ae" + } }, "tx": "020000000117ba1df9b3ac21b2ff3ac602aec47e123b37c1fe47768cb15e8b29e9f647935d0000000000fdffffff02f04902000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d126eb0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c801a0600", - "toLocalIndex": 1 + "toLocalOutputIndex": 1 }, "localOnly": { "input": { @@ -181,11 +180,10 @@ "txOut": { "amount": 1000000, "publicKeyScript": "002065cf56cc050f23840574a042319fb16a3e1f5c01a778776b23935f4dd3ac4bec" - }, - "redeemScript": "52210224d74c9af107db3c470af70997980868c5f903f36a3cc9afbfd966e1097b92922103441263f4dff5cceec2c817cc00ab6b27e65128259b4d1aa018189d5ac779c61152ae" + } }, "tx": "020000000117ba1df9b3ac21b2ff3ac602aec47e123b37c1fe47768cb15e8b29e9f647935d0000000000fdffffff0126eb0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c801a0600", - "toLocalIndex": 0 + "toLocalOutputIndex": 0 }, "remoteOnly": null } @@ -197,11 +195,10 @@ "txOut": { "amount": 1000000, "publicKeyScript": "002065cf56cc050f23840574a042319fb16a3e1f5c01a778776b23935f4dd3ac4bec" - }, - "redeemScript": "52210224d74c9af107db3c470af70997980868c5f903f36a3cc9afbfd966e1097b92922103441263f4dff5cceec2c817cc00ab6b27e65128259b4d1aa018189d5ac779c61152ae" + } }, "tx": "0200000000010117ba1df9b3ac21b2ff3ac602aec47e123b37c1fe47768cb15e8b29e9f647935d0000000000fdffffff02c63c02000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d150f80c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c040047304402207297468cbcd9413a6dcba4c237000f70c791bdf2709cbe6d59be4c451141d95a02202c025e230df2234c16914976b798787c7d9efc6b7fc62bc391f4d9bd85bc6db801483045022100cbd75c5a9bf58e11d323ad0bcd2e64e59f665b04e27a854a6dbc0a5a92ea250d022046a626184954a8bdcc14a15d6eaa961bfe262c31fc006674e7bdbea893ac3f95014752210224d74c9af107db3c470af70997980868c5f903f36a3cc9afbfd966e1097b92922103441263f4dff5cceec2c817cc00ab6b27e65128259b4d1aa018189d5ac779c61152ae801a0600", - "toLocalIndex": 1 + "toLocalOutputIndex": 1 }, { "input": { @@ -209,11 +206,10 @@ "txOut": { "amount": 1000000, "publicKeyScript": "002065cf56cc050f23840574a042319fb16a3e1f5c01a778776b23935f4dd3ac4bec" - }, - "redeemScript": "52210224d74c9af107db3c470af70997980868c5f903f36a3cc9afbfd966e1097b92922103441263f4dff5cceec2c817cc00ab6b27e65128259b4d1aa018189d5ac779c61152ae" + } }, "tx": "0200000000010117ba1df9b3ac21b2ff3ac602aec47e123b37c1fe47768cb15e8b29e9f647935d0000000000fdffffff02f04902000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d126eb0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c0400473044022035d816dc7a9421db14298f4c95479fde441aba3ad1205736d88fe8626c9add1902204f5fa4f51dcde6d6ef333d5f2cf1092cd3d35bea11683ee0270f95e30087a7be014730440220426bd871033060ce0bfc9d10384243d199a1c9aae149a184b41678996f6eba9d0220144dc180d3bf05bc1d10e8c57d55b30f3fc5ac7bdfbd1e975a7ca59a18734d75014752210224d74c9af107db3c470af70997980868c5f903f36a3cc9afbfd966e1097b92922103441263f4dff5cceec2c817cc00ab6b27e65128259b4d1aa018189d5ac779c61152ae801a0600", - "toLocalIndex": 1 + "toLocalOutputIndex": 1 } ], "waitingSinceBlock": 400000, diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Normal_77f198a3/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Normal_77f198a3/data.json index 38e634f2d..5635afc14 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Normal_77f198a3/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Normal_77f198a3/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.Normal", "commitments": { - "params": { + "channelParams": { "channelId": "77f198a3b7e0e391aad094aaa0dd33e19a8aabdeaed43835faa0c8863b539980", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", "fundingKeyPath": "m/1142459463'/2108219405'/1315441450/809265161'/2018794703'/588334649'/420596645'/1350775054/0'", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": false, "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", @@ -49,11 +44,6 @@ }, "remoteParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 100, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "020abca8b0807e5d080dab37c3acf903e0427d23e7d9fa4bd0e5690e18fcd3f814", "paymentBasepoint": "0386435198ac1b025324942098a40a4f68a66f429eaad3237166fc44c9f4c39f7b", "delayedPaymentBasepoint": "031302c7e4b8ffac75a8b33869061b0fc5084d62e2c36adddaba47c822461bf910", @@ -104,6 +94,8 @@ "active": [ { "fundingTxIndex": 1, + "fundingInput": "cbba20bf8295e9f3dfa16c566f4ce7c24529fc3493b5b17151cb55a30efb161c:0", + "fundingAmount": 999718, "remoteFundingPubkey": "02a75222c92bc2d0a86273f438605733432d493d4ee6e2d0ee10052b60ae59919b", "localFundingStatus": { "status": "unconfirmed", @@ -112,6 +104,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 2, "spec": { @@ -219,231 +221,31 @@ "toLocal": 114000000, "toRemote": 813718000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "cbba20bf8295e9f3dfa16c566f4ce7c24529fc3493b5b17151cb55a30efb161c:0", - "txOut": { - "amount": 999718, - "publicKeyScript": "0020d4224e31e577a1454732f40e829d22ad058f515158690b36e8136ff63925ff58" - }, - "redeemScript": "522102a75222c92bc2d0a86273f438605733432d493d4ee6e2d0ee10052b60ae59919b2102c9c50f0cc5483cefa52a789eaa508dde154e0fa20fd8c826ee455b1e6df11c4252ae" - }, - "tx": "020000000001011c16fb0ea355cb5171b1b59334fc2945c2e74c6f566ca1dff3e99582bf20bacb000000000018cd6080104a010000000000002200203ee39fa6b07ace779112e47c7207c12f5dac0287b74dc2f2b5ee821562a0ffc24a01000000000000220020cacff39916a5c22536dfc8b470f2b87b1bed4bf2725fc232da671e52061c2cb370170000000000002200201b1285c3f547efe59007aa7893f551b7e2a3150f7de8081da2b6e5b32206bdc57017000000000000220020215d17ce5fdf6f92ed501e72d013eb52e8f6cc88ed6ae5bd880a6b4820b9a8227017000000000000220020276c2981da3809b5aa79313a3646da6afc53dbbea5d6f6f1ba04c3320cc4c9317017000000000000220020475e212a25c5be55395be9f0fe6a25863c4b6997c3588bac8653d44c48d98da2701700000000000022002050a2fd62a4b20eb0fdd1b5c1c07f45b96ef8ff145fed7223dfdccc267ced6ea970170000000000002200206e165030f8a1e8a8daac207a43302a0f834284349c122b6a569e10150d6fd829701700000000000022002084f1d5fa2614671661d7d4faa992939280e64b688f80c7e9a401d87ddb5196927017000000000000220020a0d32e984c2d9790165b3b1c8fc426ba6f3ebeb066879561184cfe34032c31137017000000000000220020a6cd1e408296f9717d423c942a72bb96edc570a6f795c5c3a004905b6b81c4547017000000000000220020c04e8ba0a096777552a15d0bffd3e8b3e5ca25e8c205f693c6c8e082d82147f97017000000000000220020c72184dac180911bebc44365a7c2d496fa3bd61788fe29b8c810646ee5b9e4ee7017000000000000220020e65a8e5c40a36330ef15774d15d2093d30b73e7f3c7e7560a2fe49abaeb82ee850bd010000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578ebe290c00000000002200201a9ff85d351568615c9602aef7cf8e4b9c8aa4da3816339c82c1a7544394e5410400473044022030b7c66f20b7ac100387047a159c1045364fc1fce5f1996f1230618a7688197602202f4d20baf053ab8058fcb9b8e81e931a7d4f9c31f5f1cd0b16791af1e54046c00147304402205d439e55e1a8ce57bd909b62bcd4bbced484e97412e0f7748e8e07abcad2fa8d0220646ea58cea10853dca45461ee41a21d5cdd65da7590b035de77b525af044b2b20147522102a75222c92bc2d0a86273f438605733432d493d4ee6e2d0ee10052b60ae59919b2102c9c50f0cc5483cefa52a789eaa508dde154e0fa20fd8c826ee455b1e6df11c4252ae6d0ac920" - }, - "htlcTxsAndSigs": [ - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:2", - "txOut": { - "amount": 6000, - "publicKeyScript": "00201b1285c3f547efe59007aa7893f551b7e2a3150f7de8081da2b6e5b32206bdc5" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a9142a86feb35c99a59ca5a5524eab9cad4441806cb388527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb02000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "7054a5038cc0aadf346ae72a03c8447eee5e99183a98b00c84c87bc5ed4a32b4", - "htlcId": 1 - }, - "localSig": "b3a53f5ca43492a2e185cd3ed9533424297a18cb187bbe95d60b6d34e6d02ce65c1c6a7f5524d8ce0db8e5fbace75526af4b3600d1e36ffa3c51d5fda2de776b", - "remoteSig": "700471e0f8758cb5fde4c66a365feb2864ee80cb7946abfc2c1a15c94c9f6e9d00796b8f818a4e263112b4a04c6a904e8db076960549c22c8e9eb728367c24fb" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:3", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020215d17ce5fdf6f92ed501e72d013eb52e8f6cc88ed6ae5bd880a6b4820b9a822" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a9148c5b3deafcd847295ac9e6bc169a743f9b27657388ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb030000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 0 - }, - "localSig": "551ac72758de51b031963999d04f9649be446dac99a55b343026293c1f6b3aa90dbcf6ccdc19576c1a6da22f50e480504b9d2989fe2f3924698682139104ccf4", - "remoteSig": "87192d1a30b8257a8caaea09a84ca25979479c3adb964da8900a18dfbf5692c634f5e6582eb2f163a310e4105728dfc38b70135dd8675190b59f1f54a91bb158" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:4", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020276c2981da3809b5aa79313a3646da6afc53dbbea5d6f6f1ba04c3320cc4c931" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a9147400ce9668d9f3a266f86cf6ede338b785e6867d88527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb04000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "13c4adec65b394ff42db34e35869e7c06be06a6f0774a7243ed2e118bc29d0db", - "htlcId": 4 - }, - "localSig": "2005453f20fb2e167a6745329519b1db34c352601fac6437ab805b4fd936a91c67a5b4d6e16f25d460abfc3bdc9bdc9c9aa89ae5db4612d8f86d81da7fc5acab", - "remoteSig": "314222697c092f53573d3b13b234d35b37fc644e1a4c26da7cf2198c3c7720f22ebdf64b0a7eb53cf3c672561c63458be6e06b73fb61f865e9c97d4e1a33704a" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:5", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020475e212a25c5be55395be9f0fe6a25863c4b6997c3588bac8653d44c48d98da2" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a914d2fe82fcd7b7f93cad23949a1b3b6dad55af35f588ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb050000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 3 - }, - "localSig": "e4d0672bf25efaea67653c10f6567e4526b7741a7160f21852b52f116a4c499c3306a4d29e399da220e9cccb7993c00e87ba7df07fe04b69a77f74fc41e695fd", - "remoteSig": "62fabac6a07e9100f65293572a1d20deb99d451f002d3931d6872ca0833101f805cb4c8703253130060a40d764ba33ece1fd7ead253d968da4cfe3dc93cf5f60" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:6", - "txOut": { - "amount": 6000, - "publicKeyScript": "002050a2fd62a4b20eb0fdd1b5c1c07f45b96ef8ff145fed7223dfdccc267ced6ea9" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a91404749a395f5829563c37f3597c5d78bb9e8f975688ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb060000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 4 - }, - "localSig": "4d869f14c0e2064670ae2f290a4cff7f5691a5248ccdce6c132b0cf930f4835f04d2974334aef647a1efdce637aa805e633af8a93731c2b314bf9a1bfa3686d7", - "remoteSig": "d0e25ede01e9db0b3e0be85aa4e2685b23763c6cbfeeb52e276e38898e8108645a0bba747fca6fc32c8b8518b26ceec03a75871b706e46075fb21c20731c6fd9" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:7", - "txOut": { - "amount": 6000, - "publicKeyScript": "00206e165030f8a1e8a8daac207a43302a0f834284349c122b6a569e10150d6fd829" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a91476ecf44091d89fa0ef49d2cc0accc1856e07bac888ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb070000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 1 - }, - "localSig": "f65c1e5a2a71997e9b23cde77762678bcb28b7d616954920468fc45b8e08505a27a66efe01e11871895cca6c02cd62704299332a9c6a635aedfb8d0a18d4760f", - "remoteSig": "6b01b12563d6af66ba56995732919d23315b53c56e39cb4057f290c98deedf355737f7df1342ccae8aec8906e5dece5fa0574ea99d250298042f676d14386d6a" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:8", - "txOut": { - "amount": 6000, - "publicKeyScript": "002084f1d5fa2614671661d7d4faa992939280e64b688f80c7e9a401d87ddb519692" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a914d64799a281d6f8adaaf37633f51d36e0549b51de88527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb08000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "f60a26e96193bc41dbbe266c12cec8443703d0c8dbbefd781911a62b54741f73", - "htlcId": 3 - }, - "localSig": "8953aac0857146bd494176eb68fc318f789d93928c3388dad07c23d5f2f37803333543ebea3a22d74e92a6e684014a2f12a6e482cb28e13ffda550dfb886076b", - "remoteSig": "3e650dc3fc131b7c6252db7cc4aa243dafbee496e14ed56a0c4358b6c651a6350b9b76629be47a5fa6e722aa60341990ea2febc12097780dfede11772e6d01cf" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:9", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020a0d32e984c2d9790165b3b1c8fc426ba6f3ebeb066879561184cfe34032c3113" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a91488c876c0915ebcd773556335728a5a48876264a288527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb09000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "44365288ddbebf5f87cff9564103333802e3c317d3da4ba7c21f7e03dde75b0d", - "htlcId": 0 - }, - "localSig": "2459644ed739fb9f82b2d92207e69ec68136600eb1e36cc64603c109ca2831075fd099bcc68900201c7ca66efd2e26918afb2f6ec200de77ac158b86e36909be", - "remoteSig": "c96e97b3e98700fe1f9996c16f26d853f5ef0b4f70c8dc8928c835a35cb29ed71fbd520dc35e8498e18dc7fe3914cfd835c4895f90796f661f9e7c10e973eb35" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:10", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020a6cd1e408296f9717d423c942a72bb96edc570a6f795c5c3a004905b6b81c454" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a914cf6cc07931ac7ac7331d67b32b3e34ec1ea8a5ec88ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb0a0000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 2 - }, - "localSig": "b36822940556665e28cb93906b0e8434ad3547e6f1cacbd811e278fff8d4d04f192f985a754f898dcadbd88f02744d7baa04946032fae9cff14e93c3441d965b", - "remoteSig": "4df6539e5e528215a36bbd77a4173124a62cecef65235caa06f2a05d6fc8a94b5c81212aca22b14f5eb3e50253d448bdf7c9d28b7825856b1ecd21a4ee02b73f" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:11", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020c04e8ba0a096777552a15d0bffd3e8b3e5ca25e8c205f693c6c8e082d82147f9" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a91480e8d3c674125d9f2fc2f0e665b6d54c6f1a9bf088ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb0b0000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 5 - }, - "localSig": "9053b8b9d3dc5a446b28cf2b981ad5f85d5968eac94de69861caff8dbc46b5ae1d01a8ca62686a83440e081aa75252463fddd784a7e7916fdd2d0044e3327f48", - "remoteSig": "74fa7ea8f71c574c7cec00751f82086426425fe014aecdd29395db6610f0a80c052e9ae21eedfbbbad56d6be3539b3666ececcce8273c5763846bd6f8443fb33" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:12", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020c72184dac180911bebc44365a7c2d496fa3bd61788fe29b8c810646ee5b9e4ee" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a914eca8f44ad64db75fe9bda836657d60586c862d2788527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb0c000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "f0cdc25d0320482e52be55570a8a1ddaf76d70a960d9ad3100e5e71371db2622", - "htlcId": 2 - }, - "localSig": "44bceeca29f6f87be5888225716a2ec83db4fa21cfef5ad0a8b7cbab549b813d433dabd78fb759dcf429bbe4d003e251c7d5fd32f3eb2190196ebd58b8bcc7cc", - "remoteSig": "566fdb58af5335265b0fd710538fa40e9f19baa4df9877c86ec43e6aac7039dd28f8e318153e260354cc2b0268b8fcd132bb70a6423071a4546cc3b0e60fd460" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff:13", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020e65a8e5c40a36330ef15774d15d2093d30b73e7f3c7e7560a2fe49abaeb82ee8" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a914a17a9f109c95c5874dd632bd3358bc24200a61ea88527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001ff1177181c7768c441a2b3ebd0716909be2da70ce0334cf7a39c14ea64fa7ebb0d000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "5b311ca6f8e3df3d22c9741b8eff42925c34e1c8d83757a888a7218b9914ff3d", - "htlcId": 5 - }, - "localSig": "986453ea20acd4105e5b92de4ba4f48f6cdc1e848c4de33e6d68ccfab15254d1418831b6aced7fe4eaa80a853838bd586ed03d0d80d0ab4f112d097eaec9507e", - "remoteSig": "e657b0a16c7dc5f08ea1f63e88b7759315882d81cb31a25a59a58f88430df1a07aaae269cf80465a729da35b90e7a328a2d332a4c84cb90d66fb7f735862fe16" - } - ] - } + "txId": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff", + "remoteSig": { + "sig": "30b7c66f20b7ac100387047a159c1045364fc1fce5f1996f1230618a768819762f4d20baf053ab8058fcb9b8e81e931a7d4f9c31f5f1cd0b16791af1e54046c0" + }, + "htlcRemoteSigs": [ + "700471e0f8758cb5fde4c66a365feb2864ee80cb7946abfc2c1a15c94c9f6e9d00796b8f818a4e263112b4a04c6a904e8db076960549c22c8e9eb728367c24fb", + "87192d1a30b8257a8caaea09a84ca25979479c3adb964da8900a18dfbf5692c634f5e6582eb2f163a310e4105728dfc38b70135dd8675190b59f1f54a91bb158", + "314222697c092f53573d3b13b234d35b37fc644e1a4c26da7cf2198c3c7720f22ebdf64b0a7eb53cf3c672561c63458be6e06b73fb61f865e9c97d4e1a33704a", + "62fabac6a07e9100f65293572a1d20deb99d451f002d3931d6872ca0833101f805cb4c8703253130060a40d764ba33ece1fd7ead253d968da4cfe3dc93cf5f60", + "d0e25ede01e9db0b3e0be85aa4e2685b23763c6cbfeeb52e276e38898e8108645a0bba747fca6fc32c8b8518b26ceec03a75871b706e46075fb21c20731c6fd9", + "6b01b12563d6af66ba56995732919d23315b53c56e39cb4057f290c98deedf355737f7df1342ccae8aec8906e5dece5fa0574ea99d250298042f676d14386d6a", + "3e650dc3fc131b7c6252db7cc4aa243dafbee496e14ed56a0c4358b6c651a6350b9b76629be47a5fa6e722aa60341990ea2febc12097780dfede11772e6d01cf", + "c96e97b3e98700fe1f9996c16f26d853f5ef0b4f70c8dc8928c835a35cb29ed71fbd520dc35e8498e18dc7fe3914cfd835c4895f90796f661f9e7c10e973eb35", + "4df6539e5e528215a36bbd77a4173124a62cecef65235caa06f2a05d6fc8a94b5c81212aca22b14f5eb3e50253d448bdf7c9d28b7825856b1ecd21a4ee02b73f", + "74fa7ea8f71c574c7cec00751f82086426425fe014aecdd29395db6610f0a80c052e9ae21eedfbbbad56d6be3539b3666ececcce8273c5763846bd6f8443fb33", + "566fdb58af5335265b0fd710538fa40e9f19baa4df9877c86ec43e6aac7039dd28f8e318153e260354cc2b0268b8fcd132bb70a6423071a4546cc3b0e60fd460", + "e657b0a16c7dc5f08ea1f63e88b7759315882d81cb31a25a59a58f88430df1a07aaae269cf80465a729da35b90e7a328a2d332a4c84cb90d66fb7f735862fe16" + ] + }, + "remoteCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 100, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 1, @@ -559,6 +361,8 @@ }, { "fundingTxIndex": 0, + "fundingInput": "0c6d552e83b50a317c4d8f04ef1ff07011d690ea53c10a2cce52d6e662ff3915:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "03af266b0697e34895ba6f9f66fa172b50b45599b8bc6368db67deeb39a3195170", "localFundingStatus": { "status": "confirmed", @@ -567,6 +371,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 2, "spec": { @@ -674,231 +488,31 @@ "toLocal": 114000000, "toRemote": 814000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "0c6d552e83b50a317c4d8f04ef1ff07011d690ea53c10a2cce52d6e662ff3915:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "0020a8f38fd6f321a6755966bcb34a0f8a990e22343d39e8795b69e459d3909b6949" - }, - "redeemScript": "522102e50c061be524d3af336e859fc753c8eb8a195e85554a0c56ab88f34772e48deb2103af266b0697e34895ba6f9f66fa172b50b45599b8bc6368db67deeb39a319517052ae" - }, - "tx": "020000000001011539ff62e6d652ce2c0ac153ea90d61170f01fef048f4d7c310ab5832e556d0c000000000018cd6080104a010000000000002200200129e12f46364d431e08e7ce22b6dd2868e2398fa9d37910e600126eb1839e284a01000000000000220020e44463d678ddfca33a5603e563e4b8d129e21201badc0fffff53cbdf3854e1a970170000000000002200201b1285c3f547efe59007aa7893f551b7e2a3150f7de8081da2b6e5b32206bdc57017000000000000220020215d17ce5fdf6f92ed501e72d013eb52e8f6cc88ed6ae5bd880a6b4820b9a8227017000000000000220020276c2981da3809b5aa79313a3646da6afc53dbbea5d6f6f1ba04c3320cc4c9317017000000000000220020475e212a25c5be55395be9f0fe6a25863c4b6997c3588bac8653d44c48d98da2701700000000000022002050a2fd62a4b20eb0fdd1b5c1c07f45b96ef8ff145fed7223dfdccc267ced6ea970170000000000002200206e165030f8a1e8a8daac207a43302a0f834284349c122b6a569e10150d6fd829701700000000000022002084f1d5fa2614671661d7d4faa992939280e64b688f80c7e9a401d87ddb5196927017000000000000220020a0d32e984c2d9790165b3b1c8fc426ba6f3ebeb066879561184cfe34032c31137017000000000000220020a6cd1e408296f9717d423c942a72bb96edc570a6f795c5c3a004905b6b81c4547017000000000000220020c04e8ba0a096777552a15d0bffd3e8b3e5ca25e8c205f693c6c8e082d82147f97017000000000000220020c72184dac180911bebc44365a7c2d496fa3bd61788fe29b8c810646ee5b9e4ee7017000000000000220020e65a8e5c40a36330ef15774d15d2093d30b73e7f3c7e7560a2fe49abaeb82ee850bd010000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578ed82a0c00000000002200201a9ff85d351568615c9602aef7cf8e4b9c8aa4da3816339c82c1a7544394e5410400483045022100d51dc0c1e8760eed830c001147a3e15f87805ba81a900aeb219df29275d320a4022019cd34b73850ca6d9756324b96560c1675c245b50a2d3102a8cf73f5b40588f3014730440220182390e02cd28be5673c229bc99c39033e9dbd1942616d30b69efa9cd4fc256902204df27bd1aa9c2fe5e2901e6ecab2479417ddb277d201880968c0f46212e16c740147522102e50c061be524d3af336e859fc753c8eb8a195e85554a0c56ab88f34772e48deb2103af266b0697e34895ba6f9f66fa172b50b45599b8bc6368db67deeb39a319517052ae6d0ac920" - }, - "htlcTxsAndSigs": [ - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:2", - "txOut": { - "amount": 6000, - "publicKeyScript": "00201b1285c3f547efe59007aa7893f551b7e2a3150f7de8081da2b6e5b32206bdc5" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a9142a86feb35c99a59ca5a5524eab9cad4441806cb388527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc02000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "7054a5038cc0aadf346ae72a03c8447eee5e99183a98b00c84c87bc5ed4a32b4", - "htlcId": 1 - }, - "localSig": "61a778ac338671f1b4345187a46ea84eee1779f2b6c8a7433ac130dda46fae222d1f70c59d112bc772f444857507f4b3c653a5537638a53aa423a0d63ab4e7cb", - "remoteSig": "3229c56a2efa5d18d72805abfd61bbc0f6dfcd3cbbbaa1bf9e64061468518af86959a247ae8e3e60f964cc5e9d824bc7c35f24c8e0e40aabd36e0642806615e1" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:3", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020215d17ce5fdf6f92ed501e72d013eb52e8f6cc88ed6ae5bd880a6b4820b9a822" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a9148c5b3deafcd847295ac9e6bc169a743f9b27657388ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc030000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 0 - }, - "localSig": "0fcd82c60e36c5174fcbd27350086f733347a052c34e4f423c38f043c2bbd85a30c15350fa67130b013a424b3d69472c77688460cc669b76867664dbea5b289e", - "remoteSig": "de14327aab5553f478e7d89cb58fcd2e2d2070fca8780ad8e1387c8ffad393d350a2a99598c4cff3dae259c5080cc20deea723413a87ed30694dd33990abfdd3" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:4", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020276c2981da3809b5aa79313a3646da6afc53dbbea5d6f6f1ba04c3320cc4c931" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a9147400ce9668d9f3a266f86cf6ede338b785e6867d88527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc04000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "13c4adec65b394ff42db34e35869e7c06be06a6f0774a7243ed2e118bc29d0db", - "htlcId": 4 - }, - "localSig": "ebda05866e6198094438699c82e848e8c992273bf6306a8b755a0fa6ad76c0ad0b4943e388437e88ae209bc9125572fed6fcf593dc02dfa337b247c860cca792", - "remoteSig": "1bbd6a513bdae2aa70b0953ec1b1a54c849f59ae6fc7890f7eaa99c42da70dec015d5f90aec18e717711265c074ab14d8668cbe7fe08d498ebe2cf229b053eed" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:5", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020475e212a25c5be55395be9f0fe6a25863c4b6997c3588bac8653d44c48d98da2" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a914d2fe82fcd7b7f93cad23949a1b3b6dad55af35f588ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc050000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 3 - }, - "localSig": "f043fb8d73d062b2e1dfaf04aec170a548dc5c4ce8d239cfd6b449d0fccf167039fcad79be1cc495c8179e1ecdccff9b2d304e655b6bc3ad6a7221bd16f35637", - "remoteSig": "e6adaf8de1b8db9b005af625cfc2b7ac337bb95f3a87ac6f7468dd96149916ed776e6b900fd32c80708827a45ee22561c5bb2d922e3c29e40b314d94f4dd38df" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:6", - "txOut": { - "amount": 6000, - "publicKeyScript": "002050a2fd62a4b20eb0fdd1b5c1c07f45b96ef8ff145fed7223dfdccc267ced6ea9" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a91404749a395f5829563c37f3597c5d78bb9e8f975688ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc060000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 4 - }, - "localSig": "6bc8cf71358ff642b79541869c585d8629c3bdd84e678f7b40a283cd05fdcec46d6e5a03e2dc668db2d521b22c1d0ef5203f30e066f2819784cb17d45e6539e5", - "remoteSig": "3b47f36698acb540a4a953012e2aa418e5fabae2fd673823b070837e919ba4d64b7817b087094e5c0111777d078d9c2fc481281c3f2020ef8329159d99ff4647" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:7", - "txOut": { - "amount": 6000, - "publicKeyScript": "00206e165030f8a1e8a8daac207a43302a0f834284349c122b6a569e10150d6fd829" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a91476ecf44091d89fa0ef49d2cc0accc1856e07bac888ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc070000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 1 - }, - "localSig": "657dae2f1ae28625b7131405284f0930fd9c24385ee4a48761da436a1c60eb996dac762ffc977504fe0562fadc84222b5a78c052f1688241cac78eab3b2ba631", - "remoteSig": "874e20962e914f8262dbc154148d938b757d06979c2f7cac146da120dc60736d753efb27a398df56f472a98437d1fc2169dd7451f93bf87e03eeccfe6936853b" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:8", - "txOut": { - "amount": 6000, - "publicKeyScript": "002084f1d5fa2614671661d7d4faa992939280e64b688f80c7e9a401d87ddb519692" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a914d64799a281d6f8adaaf37633f51d36e0549b51de88527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc08000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "f60a26e96193bc41dbbe266c12cec8443703d0c8dbbefd781911a62b54741f73", - "htlcId": 3 - }, - "localSig": "c82b27f670983db07b17e21ec92d3d78edc10e3dc854ea74f29ee883f0eb34a4655d6500a030a841ae13ec5d83d7c577d77217e4a9f71894d471b6174945ebb9", - "remoteSig": "7f61d1ac74b2903264b3abb2ed9023d8c43d7a34cbb97cd3ff26235253ab23ae4843d900164bb42b55f0b6438a858fbdac1d445d98172e0d518af6d2a21fd737" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:9", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020a0d32e984c2d9790165b3b1c8fc426ba6f3ebeb066879561184cfe34032c3113" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a91488c876c0915ebcd773556335728a5a48876264a288527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc09000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "44365288ddbebf5f87cff9564103333802e3c317d3da4ba7c21f7e03dde75b0d", - "htlcId": 0 - }, - "localSig": "d99c61ef05db0761c607b31e4d098dafb14ef735841954ad7039fba340a8c0b80a45aa42320079f73bcb56521341183c28ad2e84d1074769b75b8eaefe267201", - "remoteSig": "b9dd93a03db61aebeb5ccfaf9121996d69545ba8e21cc0424df80b5953f8f50c2015c5267b858bfa93eb5322ef77a28611037b8266317dfe3a9cb573fa920c70" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:10", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020a6cd1e408296f9717d423c942a72bb96edc570a6f795c5c3a004905b6b81c454" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a914cf6cc07931ac7ac7331d67b32b3e34ec1ea8a5ec88ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc0a0000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 2 - }, - "localSig": "02d17d0f4d7a35e05a7c2fc37b9e6c1936d71ae56c6a41757a0df510d5fc1e3777534c1580338fe3bba558fa144d21e98dce5aa68367c00f419bebc6315edbc6", - "remoteSig": "6c672b28066111bb135ece54f4e78b938d38657c33eb9e83b4acee0b09aa517f0e0e78272badf06c0dcdd46605d6519ad4fede9c411399bc37f7e3cebfb4b6f5" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:11", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020c04e8ba0a096777552a15d0bffd3e8b3e5ca25e8c205f693c6c8e082d82147f9" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c820120876475527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae67a91480e8d3c674125d9f2fc2f0e665b6d54c6f1a9bf088ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc0b0000000001000000016e0a000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e101b0600", - "htlcId": 5 - }, - "localSig": "4367afd8021512733092ee116f5794c967fe27ecd28e21707741c02821e3a9d428eef3b35ce6621f6c0033c515ac5b65017ca2efa55362fe5187dc44aa03fa35", - "remoteSig": "39214f82af81d41da5c09f204eb192ada71a5bdddd5d5913fd32793e4f4587ff2e74da9328e876e9057f6344aec453f60b3b947ef5dead6d6c3facb28906ec0c" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:12", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020c72184dac180911bebc44365a7c2d496fa3bd61788fe29b8c810646ee5b9e4ee" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a914eca8f44ad64db75fe9bda836657d60586c862d2788527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc0c000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "f0cdc25d0320482e52be55570a8a1ddaf76d70a960d9ad3100e5e71371db2622", - "htlcId": 2 - }, - "localSig": "3b16bc78a690163adfb49a4e61a476cf337164f1e548c5aef9989bf0c16302d040f14894639623bff5520db468929761fdb5ce8ef65752c3eaddcd53e576bf09", - "remoteSig": "bda2aee299506be5bb34f3fc5bda4f5c6e47de453caed23c3cc5cd30f048147309a2439b3ec12763fc19e8ca6a28f938d7e5726d9f7afe194ee171fe64706921" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266:13", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020e65a8e5c40a36330ef15774d15d2093d30b73e7f3c7e7560a2fe49abaeb82ee8" - }, - "redeemScript": "76a914a722f2303496be25d72a0563960a3ea25f56c36b8763ac67210369425a0154c0b8217b3506c775431e3abe93d112709c2c528c75b15dfb3ce4e07c8201208763a914a17a9f109c95c5874dd632bd3358bc24200a61ea88527c2103b4d2ca8e9acaf7b8f36696588c616347f0dd3e54c5e417a9a77d67052885375852ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001662287464194fd5642196169bebd6491d9a388fd7a409a8058301753f80efbdc0d000000000100000001a609000000000000220020ad64b0094b2f2f03192d321309fb5b1320a8ee90bd8b34737c8a69e716b0578e00000000", - "paymentHash": "5b311ca6f8e3df3d22c9741b8eff42925c34e1c8d83757a888a7218b9914ff3d", - "htlcId": 5 - }, - "localSig": "60b43bba26497c66264b627c665d1528e243b634ab4e3842f179c33bcf05d5e067efc44edf3948cda69b8fddd1969652f28a6570d1065dabf0902a8a36c1ec13", - "remoteSig": "e09c9ffc6935280a4f10e59bd2b5e93106db170d72927af8bbc33f7701396f4602273b9ee917134f3336f446e722a21c7b7301127379d5a6b42f647d05738e77" - } - ] - } + "txId": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266", + "remoteSig": { + "sig": "182390e02cd28be5673c229bc99c39033e9dbd1942616d30b69efa9cd4fc25694df27bd1aa9c2fe5e2901e6ecab2479417ddb277d201880968c0f46212e16c74" + }, + "htlcRemoteSigs": [ + "3229c56a2efa5d18d72805abfd61bbc0f6dfcd3cbbbaa1bf9e64061468518af86959a247ae8e3e60f964cc5e9d824bc7c35f24c8e0e40aabd36e0642806615e1", + "de14327aab5553f478e7d89cb58fcd2e2d2070fca8780ad8e1387c8ffad393d350a2a99598c4cff3dae259c5080cc20deea723413a87ed30694dd33990abfdd3", + "1bbd6a513bdae2aa70b0953ec1b1a54c849f59ae6fc7890f7eaa99c42da70dec015d5f90aec18e717711265c074ab14d8668cbe7fe08d498ebe2cf229b053eed", + "e6adaf8de1b8db9b005af625cfc2b7ac337bb95f3a87ac6f7468dd96149916ed776e6b900fd32c80708827a45ee22561c5bb2d922e3c29e40b314d94f4dd38df", + "3b47f36698acb540a4a953012e2aa418e5fabae2fd673823b070837e919ba4d64b7817b087094e5c0111777d078d9c2fc481281c3f2020ef8329159d99ff4647", + "874e20962e914f8262dbc154148d938b757d06979c2f7cac146da120dc60736d753efb27a398df56f472a98437d1fc2169dd7451f93bf87e03eeccfe6936853b", + "7f61d1ac74b2903264b3abb2ed9023d8c43d7a34cbb97cd3ff26235253ab23ae4843d900164bb42b55f0b6438a858fbdac1d445d98172e0d518af6d2a21fd737", + "b9dd93a03db61aebeb5ccfaf9121996d69545ba8e21cc0424df80b5953f8f50c2015c5267b858bfa93eb5322ef77a28611037b8266317dfe3a9cb573fa920c70", + "6c672b28066111bb135ece54f4e78b938d38657c33eb9e83b4acee0b09aa517f0e0e78272badf06c0dcdd46605d6519ad4fede9c411399bc37f7e3cebfb4b6f5", + "39214f82af81d41da5c09f204eb192ada71a5bdddd5d5913fd32793e4f4587ff2e74da9328e876e9057f6344aec453f60b3b947ef5dead6d6c3facb28906ec0c", + "bda2aee299506be5bb34f3fc5bda4f5c6e47de453caed23c3cc5cd30f048147309a2439b3ec12763fc19e8ca6a28f938d7e5726d9f7afe194ee171fe64706921", + "e09c9ffc6935280a4f10e59bd2b5e93106db170d72927af8bbc33f7701396f4602273b9ee917134f3336f446e722a21c7b7301127379d5a6b42f647d05738e77" + ] + }, + "remoteCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 100, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 1, diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Normal_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Normal_ebb9087c/data.json index 0042dc316..f85cc2c72 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Normal_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Normal_ebb9087c/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.Normal", "commitments": { - "params": { + "channelParams": { "channelId": "e1545b51a8a33aea2d23dd4a6dbd4201d802b0b655ee597bf53b4b14f6e81232", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", "fundingKeyPath": "m/18154681'/2008065081/2055474196/26657562'/763527636'/1489084225/775368589'/1778659862/0'", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": false, "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", @@ -49,11 +44,6 @@ }, "remoteParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 0, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "02dd60d898d87bf38b29df1e2dc751851c9d2317e7c54d97cb070739ab2d873f8a", "paymentBasepoint": "03ea1f8aa732d7bb3c78db9fbebe6bc8e1d8c636e6525eeb1103f6997f6b1893ce", "delayedPaymentBasepoint": "02bab6073cf7c3e375784cbca743e055eb3ef5d7c21f2795f76aa753274f579e95", @@ -111,6 +101,8 @@ "active": [ { "fundingTxIndex": 1, + "fundingInput": "6be60e35b4774be2f51df14c08171d280a292ded4f2ff55dc0d8954721bb7dda:1", + "fundingAmount": 949817, "remoteFundingPubkey": "0380092c5800eef8460d958b053d0a512b70a501c13e4747d01788f8fb1c58ee2d", "localFundingStatus": { "status": "unconfirmed", @@ -119,6 +111,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 1, "spec": { @@ -137,39 +139,20 @@ "toLocal": 200000000, "toRemote": 734817000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "6be60e35b4774be2f51df14c08171d280a292ded4f2ff55dc0d8954721bb7dda:1", - "txOut": { - "amount": 949817, - "publicKeyScript": "0020844124d22cff4f4ad0b60954d33bccf2de3e94db2414365085d950bbf70881e9" - }, - "redeemScript": "5221022ffa44aa8560ac300a4a8a636483352fba7a84de091cad230523a52ab01f1f14210380092c5800eef8460d958b053d0a512b70a501c13e4747d01788f8fb1c58ee2d52ae" - }, - "tx": "02000000000101da7dbb214795d8c05df52f4fed2d290a281d17084cf11df5e24b77b4350ee66b0100000000d3b43480054a010000000000002200209873f14954f89df43de7a46e06c7deba49e37cc762f97222cb235909ac5459604a01000000000000220020e67947a2ee66d03883dfcdefa8e78db55331f35f8d508b25cb3600ce4bd61863983a000000000000220020efc826a60502482500685aeff625406bf8548f26eee43af45f22d0fdd4cfbd82400d0300000000002200200c7fc42fe1ced57f04af6dfdbdfc6182f2cb015f59e175a116488430a78547ff7d1a0b0000000000220020c3874cb93e76e55e23e349e8fe367a228c7d07c1638637cd08a09dfb64fba31a040047304402202c80481f8eb7d1d85c411879b7026dc605b3fb017d9c5b74a268db902914a7e202204927b3f197f1e8875414d38caefb1d7ea83295b81668c782b7f443f00639e7ef0147304402204c028ec20ee39c107bc73033becdbb1cf6588e309e6e25f5a036c8b6504e04ea02204a7d08b9739426c1bbbb4d15ff7984651506bbfb0a8af8b5acf6013d8eedcd6701475221022ffa44aa8560ac300a4a8a636483352fba7a84de091cad230523a52ab01f1f14210380092c5800eef8460d958b053d0a512b70a501c13e4747d01788f8fb1c58ee2d52ae49246120" - }, - "htlcTxsAndSigs": [ - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "fd49c1c1b8a84d2b00a0a1210c822cbbac8fc8fbf1afbbd69908c9b7e5df7cc4:2", - "txOut": { - "amount": 15000, - "publicKeyScript": "0020efc826a60502482500685aeff625406bf8548f26eee43af45f22d0fdd4cfbd82" - }, - "redeemScript": "76a91421cc15bef6a6a963e5f6821d382171a46d68c11b8763ac672103d944ba1ebdc6389bf8b09ae34a168096cb19e45362b255713b4391a4dce8f9b27c8201208763a914ec015adc9d9f0a185a8f4d73ea6f7d845117248288527c2103ab8d2d62fdfb672ceea64a9208d18a590b0c50bb2a0fe47c53a1b4bca8244a6e52ae677503101b06b175ac6851b27568" - }, - "tx": "0200000001c47cdfe5b7c90899d6bbaff1fbc88facbb2c820c21a1a0002b4da8b8c1c149fd02000000000100000001ce2c0000000000002200200c7fc42fe1ced57f04af6dfdbdfc6182f2cb015f59e175a116488430a78547ff00000000", - "paymentHash": "af9fe6d0f684cf5df6ce4f41596b276df64cc9bec18e7ea01d521c58454e4ade", - "htlcId": 0 - }, - "localSig": "4ea190f22441f8ea2c6ecdb9655d1f745a91ad1ef933e2404f35e72712eb060c6ac4a7daf1c68bd263978b0366b40f82a0ab4eb3cba32991c51e5e37ddc24e1c", - "remoteSig": "6cdad11404a129da5b7b41869b8d3e319e27ceffb590214c730c8b2afa8f18ed0495867aa19597357ae3bd0c8c9e5addcc850008a8f66395b8bee5c78af6f031" - } - ] - } + "txId": "fd49c1c1b8a84d2b00a0a1210c822cbbac8fc8fbf1afbbd69908c9b7e5df7cc4", + "remoteSig": { + "sig": "4c028ec20ee39c107bc73033becdbb1cf6588e309e6e25f5a036c8b6504e04ea4a7d08b9739426c1bbbb4d15ff7984651506bbfb0a8af8b5acf6013d8eedcd67" + }, + "htlcRemoteSigs": [ + "6cdad11404a129da5b7b41869b8d3e319e27ceffb590214c730c8b2afa8f18ed0495867aa19597357ae3bd0c8c9e5addcc850008a8f66395b8bee5c78af6f031" + ] + }, + "remoteCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 0, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 0, @@ -184,46 +167,31 @@ "remotePerCommitmentPoint": "02fcadc47b4049b300b5beba06a7e89cefd4d9045cc2525ae60b083c40c448f4d7" }, "nextRemoteCommit": { - "sig": { - "channelId": "e1545b51a8a33aea2d23dd4a6dbd4201d802b0b655ee597bf53b4b14f6e81232", - "signature": "8bbf6c180328b0da3f2fd12458131c8949a38a78a765f17815f93c149ef51f947d6d211c3f033376ca95562b3f5ad2d30fbcd64946b4e6302a03753c88711946", - "htlcSignatures": [ - "d3d1f7723a4fce734d7568fec0052f4d8650903616638f2eb37317fc0f8f3a68014e2c1e1c341aee05270a021d062042a4bd4ba266fdc41f90c231894dbc025a" + "index": 1, + "spec": { + "htlcsIn": [], + "htlcsOut": [ + { + "channelId": "e1545b51a8a33aea2d23dd4a6dbd4201d802b0b655ee597bf53b4b14f6e81232", + "id": 0, + "amountMsat": 15000000, + "paymentHash": "af9fe6d0f684cf5df6ce4f41596b276df64cc9bec18e7ea01d521c58454e4ade", + "cltvExpiry": 400144, + "onionRoutingPacket": "" + } ], - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.CommitSigTlv.Batch", - "size": 2 - } - ] - } + "feerate": 5000, + "toLocal": 734817000, + "toRemote": 200000000 }, - "commit": { - "index": 1, - "spec": { - "htlcsIn": [], - "htlcsOut": [ - { - "channelId": "e1545b51a8a33aea2d23dd4a6dbd4201d802b0b655ee597bf53b4b14f6e81232", - "id": 0, - "amountMsat": 15000000, - "paymentHash": "af9fe6d0f684cf5df6ce4f41596b276df64cc9bec18e7ea01d521c58454e4ade", - "cltvExpiry": 400144, - "onionRoutingPacket": "" - } - ], - "feerate": 5000, - "toLocal": 734817000, - "toRemote": 200000000 - }, - "txid": "6b04b88ed91481e9b74670f7962f5dd3a4797959e6d90fec1b0a561e3e2b7c6e", - "remotePerCommitmentPoint": "03074876bf6f74b5f7d6854c44d82cd93ce79e0dfe2e6c887e5fe78c240d86ebb1" - } + "txid": "6b04b88ed91481e9b74670f7962f5dd3a4797959e6d90fec1b0a561e3e2b7c6e", + "remotePerCommitmentPoint": "03074876bf6f74b5f7d6854c44d82cd93ce79e0dfe2e6c887e5fe78c240d86ebb1" } }, { "fundingTxIndex": 0, + "fundingInput": "6a7a6b72222e24230220295827b649c1b521f4c8f419af48f5ebbe28eda6f8e3:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "02464c82c5d5b7aac81955e9c80a470357252544aa3fe9cafc56539d787936f4a9", "localFundingStatus": { "status": "unconfirmed", @@ -232,6 +200,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 1, "spec": { @@ -250,39 +228,20 @@ "toLocal": 200000000, "toRemote": 785000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "6a7a6b72222e24230220295827b649c1b521f4c8f419af48f5ebbe28eda6f8e3:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00201cee81badac370245404251e447ea6907ce23417d7c1b5a7fe1dadb213bb82f6" - }, - "redeemScript": "522102464c82c5d5b7aac81955e9c80a470357252544aa3fe9cafc56539d787936f4a921028a57a142efd10167553269b59302dca54550a81ab64ade7a443b7419ac32974252ae" - }, - "tx": "02000000000101e3f8a6ed28beebf548af19f4c8f421b5c149b6275829200223242e22726b7a6a0000000000d3b43480054a0100000000000022002030c986b0c520a94b0590937d0f33aa78fb924ad4346bdda207e0330d186e44c64a01000000000000220020e8ebe833a6a9f66d3534b9d63fdefcc3c0ff70b6ec2eb6d3a08a163d6db417b8983a000000000000220020efc826a60502482500685aeff625406bf8548f26eee43af45f22d0fdd4cfbd82400d0300000000002200200c7fc42fe1ced57f04af6dfdbdfc6182f2cb015f59e175a116488430a78547ff84de0b0000000000220020c3874cb93e76e55e23e349e8fe367a228c7d07c1638637cd08a09dfb64fba31a04004830450221009dc287c5a78ad3480738aab05a14e8b36bdf1d3e97fa11af7e75edefcd6e5cbf0220404e33cc770464604896c65acbc31d7c403b90cf94c2c8a0e4ccd56af7ef68a501473044022008385f924918c5c7108e33893aa34b481cc79a11bf3e8199405139cbadca35f902207dd2962a7cb8995ff9040a3351538222069486853bfdc10b9302acc74e719ec90147522102464c82c5d5b7aac81955e9c80a470357252544aa3fe9cafc56539d787936f4a921028a57a142efd10167553269b59302dca54550a81ab64ade7a443b7419ac32974252ae49246120" - }, - "htlcTxsAndSigs": [ - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "c9dfa8a557f54092d5f72d6ec6236e6ee71246a14f0bd79c6c4804443c483f4c:2", - "txOut": { - "amount": 15000, - "publicKeyScript": "0020efc826a60502482500685aeff625406bf8548f26eee43af45f22d0fdd4cfbd82" - }, - "redeemScript": "76a91421cc15bef6a6a963e5f6821d382171a46d68c11b8763ac672103d944ba1ebdc6389bf8b09ae34a168096cb19e45362b255713b4391a4dce8f9b27c8201208763a914ec015adc9d9f0a185a8f4d73ea6f7d845117248288527c2103ab8d2d62fdfb672ceea64a9208d18a590b0c50bb2a0fe47c53a1b4bca8244a6e52ae677503101b06b175ac6851b27568" - }, - "tx": "02000000014c3f483c4404486c9cd70b4fa14612e76e6e23c66e2df7d59240f557a5a8dfc902000000000100000001ce2c0000000000002200200c7fc42fe1ced57f04af6dfdbdfc6182f2cb015f59e175a116488430a78547ff00000000", - "paymentHash": "af9fe6d0f684cf5df6ce4f41596b276df64cc9bec18e7ea01d521c58454e4ade", - "htlcId": 0 - }, - "localSig": "e738a32d1f85d1221cc5333d0869a384136696a712f84f7b7fa465e8b3ebc5070010edb38413d130b9e1854437e0179d3054fc0decc4287db686bd81a329bd50", - "remoteSig": "df602798b869e7f8176bea759bf5da249d4c00dfb47ebe14177e5c8634c9ca987c7a6895e46ed12aa0f3e6016dbccdff92c3d16c001b1a00958ab0987d1ae4ff" - } - ] - } + "txId": "c9dfa8a557f54092d5f72d6ec6236e6ee71246a14f0bd79c6c4804443c483f4c", + "remoteSig": { + "sig": "9dc287c5a78ad3480738aab05a14e8b36bdf1d3e97fa11af7e75edefcd6e5cbf404e33cc770464604896c65acbc31d7c403b90cf94c2c8a0e4ccd56af7ef68a5" + }, + "htlcRemoteSigs": [ + "df602798b869e7f8176bea759bf5da249d4c00dfb47ebe14177e5c8634c9ca987c7a6895e46ed12aa0f3e6016dbccdff92c3d16c001b1a00958ab0987d1ae4ff" + ] + }, + "remoteCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 0, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 0, @@ -297,42 +256,25 @@ "remotePerCommitmentPoint": "02fcadc47b4049b300b5beba06a7e89cefd4d9045cc2525ae60b083c40c448f4d7" }, "nextRemoteCommit": { - "sig": { - "channelId": "e1545b51a8a33aea2d23dd4a6dbd4201d802b0b655ee597bf53b4b14f6e81232", - "signature": "402909c7b961491bb4824bfcbf7eb0e662fd7ebcaceb6f91c763c9ae9c85f0db731c0fdb18041641e0cbf1fa90a184a9e3ab6fbaff2b1a02035cc74022e62ec4", - "htlcSignatures": [ - "ba990a568b995688f774ba8b8d00a9652025455d01c2e0a7561d422acf3a7eca4e92b0e8906c0152f9c172c257a2ff0962abf8875b5819a88ebc45c0dfae6678" + "index": 1, + "spec": { + "htlcsIn": [], + "htlcsOut": [ + { + "channelId": "e1545b51a8a33aea2d23dd4a6dbd4201d802b0b655ee597bf53b4b14f6e81232", + "id": 0, + "amountMsat": 15000000, + "paymentHash": "af9fe6d0f684cf5df6ce4f41596b276df64cc9bec18e7ea01d521c58454e4ade", + "cltvExpiry": 400144, + "onionRoutingPacket": "" + } ], - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.CommitSigTlv.Batch", - "size": 2 - } - ] - } + "feerate": 5000, + "toLocal": 785000000, + "toRemote": 200000000 }, - "commit": { - "index": 1, - "spec": { - "htlcsIn": [], - "htlcsOut": [ - { - "channelId": "e1545b51a8a33aea2d23dd4a6dbd4201d802b0b655ee597bf53b4b14f6e81232", - "id": 0, - "amountMsat": 15000000, - "paymentHash": "af9fe6d0f684cf5df6ce4f41596b276df64cc9bec18e7ea01d521c58454e4ade", - "cltvExpiry": 400144, - "onionRoutingPacket": "" - } - ], - "feerate": 5000, - "toLocal": 785000000, - "toRemote": 200000000 - }, - "txid": "8d0d30a4feae988b52f5baaae2219fe431566a213fe426bb8f902158015e44cc", - "remotePerCommitmentPoint": "03074876bf6f74b5f7d6854c44d82cd93ce79e0dfe2e6c887e5fe78c240d86ebb1" - } + "txid": "8d0d30a4feae988b52f5baaae2219fe431566a213fe426bb8f902158015e44cc", + "remotePerCommitmentPoint": "03074876bf6f74b5f7d6854c44d82cd93ce79e0dfe2e6c887e5fe78c240d86ebb1" } } ], diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Normal_ff34df87/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Normal_ff34df87/data.json index 22ef7451e..e99c03a0c 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Normal_ff34df87/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Normal_ff34df87/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.Normal", "commitments": { - "params": { + "channelParams": { "channelId": "ff34df87686094c967bdbd77601a201306d08c857600d75681c919dc74f4cee8", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/2093147472'/300721091/1090541724'/704900034'/1996474647/661362293'/847933381'/1399885939'/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 100, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -48,11 +43,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "0361dd1f9ce279e691e99a38d3c2f714b5158354ea19e60872c97f131c3ac976a1", "paymentBasepoint": "025976dee047bad7f29115a6d636b85ca391862086f95f3dfef9c6a406c091de1d", "delayedPaymentBasepoint": "023d9385b6e0106f3d211a4d285c7774c0593f39a1eac610d9e1ed5b47ef931214", @@ -104,6 +94,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "ac7d55d35d92e43e5066ca7d404ba4e0236d85dabae406c0a5da7926600443ba:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "03ced98078c4a4cb054c071e90ad74dcfa4ec4235f7c43c5e3b50b84d75f58855e", "localFundingStatus": { "status": "confirmed", @@ -112,6 +104,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 100, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 1, "spec": { @@ -219,231 +221,31 @@ "toLocal": 814000000, "toRemote": 114000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "ac7d55d35d92e43e5066ca7d404ba4e0236d85dabae406c0a5da7926600443ba:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "0020a72586c1e575ba88adf6196a86e6f30f79a115421be2baab5d81dbefd3f55004" - }, - "redeemScript": "522102901c687a18ccf56f9ae06c586ccc0a151383a994f03cbaa3d57fc75e1bfe9eab2103ced98078c4a4cb054c071e90ad74dcfa4ec4235f7c43c5e3b50b84d75f58855e52ae" - }, - "tx": "02000000000101ba4304602679daa5c006e4bada856d23e0a44b407dca66503ee4925dd3557dac000000000058d4bd80104a0100000000000022002052380ff5c5221dc17d4be63ed27a3a9fc3a739e2a40d680256627250b470dbad4a0100000000000022002077be8a36528674b625ea0fb9fe1da3719bc00831b6aa60b0a4d565a9a20bf51f70170000000000002200200c5f841665d884f5f2f1d3c2ba3ceb5a732792189108eed8527bcf102138b65e70170000000000002200202f500548eb86486434f03e36aadc6a41d711fae6ae0cf2f4a773abb49f301164701700000000000022002034ef6a0dd495579cc24720aea885bbecffbbd21a39cc57022e6829f1de71865170170000000000002200203576ee1e46448c65dda7444c48a78c9c03d3cf9ad15ef4d5cf2d89c9d239e6a770170000000000002200207cc6cd56dc24dbe5fea2130c5905c5b269f3ebcb84c4efa0b2e2e179d8dc2c8370170000000000002200207d2e9c73039131ce97b78ef0a082ad227835d509ad5b0f54543c46af7648f2eb70170000000000002200208f8edfe21f88a607ab807e78d3c84ff83349afd453c905335f7ad8e8dbfe48a5701700000000000022002095b60e27d44105025a817fec991a339fd6b9d755f5f8e3bfed683afcad284882701700000000000022002096c6e023e7f1168fda2f6676aaacec6dfa9bc0c8c0620e671d2997bde4f109027017000000000000220020b2a2bf7ed1c9fc5964b587687ff1f493bf49259ce7c383726d5fba7f03bfb4347017000000000000220020d20542298a8f9187870fc61ed9dd7b23ef57492a52e4aba4ab891cadf2c9ae597017000000000000220020dcd4dc76b4ac00cb294aeeab0acfcc479f02006e165dca1b7ebafc2464e6721b50bd010000000000220020a27d5c91d81040e20e16c094fdf7329a71ff026e2a34c7aace733bcd811897b2d82a0c00000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc0400483045022100b6d7808fc04df7fa019f0863ebff2e6dfc6c87b53ce40b32909a52dc71c280930220579556df2c0dc6b36efed3d33e2fb1ee704e154c05f27441b934a4bde756f3ea01483045022100bd71081f466344fb018eceabab814bdaf9d34df35327a43560677a13b96bbc3f02201669df9f5075854a415a6ebd32bcf1e6fe482510930da7abfc15f319f29e7f000147522102901c687a18ccf56f9ae06c586ccc0a151383a994f03cbaa3d57fc75e1bfe9eab2103ced98078c4a4cb054c071e90ad74dcfa4ec4235f7c43c5e3b50b84d75f58855e52ae33153920" - }, - "htlcTxsAndSigs": [ - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:2", - "txOut": { - "amount": 6000, - "publicKeyScript": "00200c5f841665d884f5f2f1d3c2ba3ceb5a732792189108eed8527bcf102138b65e" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c8201208763a91499a29a1e7bb539e2acddc5cbdabccad88e6a94d388527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae677503101b06b175ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c02000000000100000001a6090000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc00000000", - "paymentHash": "f1118c1cb106071d63ec19a3e25d533f04cac3753e9b815e499378480a534dc7", - "htlcId": 3 - }, - "localSig": "b4fab29b944ee8a6e883777ccf58f49a9faa954f17cfd75b570b0e6d06cb1625214b1fc933c1be976e512fe51fe1daab0b5e8727ec9518ef1911a6b174847515", - "remoteSig": "47f187dab4904e505d7eecb6fa0ab9d1dd3ae4194a215d73c0ed3310bba02e3455dc93c903bee48ed0613431dd771030e0ba43007300570463934cdda090b7c4" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:3", - "txOut": { - "amount": 6000, - "publicKeyScript": "00202f500548eb86486434f03e36aadc6a41d711fae6ae0cf2f4a773abb49f301164" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c820120876475527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae67a914e4a928c458dc80ce765fe5905a9b898ccfcfbec988ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c030000000001000000016e0a0000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc101b0600", - "htlcId": 5 - }, - "localSig": "c395fbaac396f7987f9f2cc2300ea8e57fadeec1982eea1405262a6b2983ff9e463a25021cdf946a5416758b733796a0d95ec8142c822649c5bfa0f9b0825511", - "remoteSig": "67afdc3b9bfc9ed2d0c2ff2c29d74e155f404a636d6257de8d576d011bb241a71e31f4b18a5370d5056595f71a40e07a27d68bd09b9724faf61e4fa0baebcc43" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:4", - "txOut": { - "amount": 6000, - "publicKeyScript": "002034ef6a0dd495579cc24720aea885bbecffbbd21a39cc57022e6829f1de718651" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c8201208763a91475639d4fd9bd686262a9a158318dbbb0022e829a88527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae677503101b06b175ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c04000000000100000001a6090000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc00000000", - "paymentHash": "2b08af5116e486d1b3008ca77d7fc7f2ad18ab50ddc2fbbd0175dbb25fbef4a0", - "htlcId": 4 - }, - "localSig": "cb196430a2288f081f6a3afaf37f84f4837cca07bd79fd6fbe954acf169939d9237acd6df5f04bbf160151878d503138b85be8ecf517d219362eb43a9c2ae012", - "remoteSig": "c0c6a5c5c5df836e3f10e7719a01641fcb017c704a28f5c7951da681f32f4827182f0ccdfd3722d0031449803b1f3268290b0854cdaf616ab4ee696f13eadfd3" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:5", - "txOut": { - "amount": 6000, - "publicKeyScript": "00203576ee1e46448c65dda7444c48a78c9c03d3cf9ad15ef4d5cf2d89c9d239e6a7" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c820120876475527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae67a91488ff0368ff40e390edb6cff35e168cabfb8feb8b88ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c050000000001000000016e0a0000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc101b0600", - "htlcId": 1 - }, - "localSig": "18685619c522ccc38cb0b00e41b2ea448b1ff0a1fb65426d6089567b2674d364332d62fc64746ae47acccae9d837a15072d4bf52549785506b8f3e7ee9219954", - "remoteSig": "1caddc268adfcad2dcdb40a8b9f87a1a46b9c53117b014a9438173e99026f31234ba7f594bfe64b3c6ba1faed65331bb5329e9598177d0b306ea781f785825ae" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:6", - "txOut": { - "amount": 6000, - "publicKeyScript": "00207cc6cd56dc24dbe5fea2130c5905c5b269f3ebcb84c4efa0b2e2e179d8dc2c83" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c820120876475527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae67a914ded18dc87347a66381a781d583561bca2b7e76df88ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c060000000001000000016e0a0000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc101b0600", - "htlcId": 2 - }, - "localSig": "74465f19df81e684d45d3268cdbde09b89610c4cd98fbb93072446f2f666e9ce00ab7960a61269e5b85a9f47f7cbd5f8b73497845239ce8492d004b674fed9b1", - "remoteSig": "939c90c63a3a53c1b0b0cefce54be2473cd0f31d540583407cc9966e4856d6236d880f515d2a93f934a494bbf6b4e9e7bd0de9b0b21e4eaee42bd2db8c91b2a4" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:7", - "txOut": { - "amount": 6000, - "publicKeyScript": "00207d2e9c73039131ce97b78ef0a082ad227835d509ad5b0f54543c46af7648f2eb" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c8201208763a914fa14704f4019b213a450db83f8ab9f59497ae81488527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae677503101b06b175ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c07000000000100000001a6090000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc00000000", - "paymentHash": "a42decc81faaf2ecd7d6910f75f2861e6c07ab0b6b9288f8e74f05ddf829908a", - "htlcId": 5 - }, - "localSig": "f0747a7e561c9a749ea6cfb3aa21824fa1580920e2ebd92009b37c7acc475cb2328714b8837e56ccdc669b22750922dc352ecb6b5cb3e2394050d24cae946e5d", - "remoteSig": "f6ab52d3a157f2bee88e39966684ba0d41eb89965fc0bb07d559518da66a758f17c36e1a157b71fc692867dfbe263a4862716d527b1ff8110a2f349bcd07dc6b" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:8", - "txOut": { - "amount": 6000, - "publicKeyScript": "00208f8edfe21f88a607ab807e78d3c84ff83349afd453c905335f7ad8e8dbfe48a5" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c820120876475527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae67a91415464c32a720a8f1e6c68a003372d67de8c06ec688ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c080000000001000000016e0a0000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc101b0600", - "htlcId": 3 - }, - "localSig": "9c89320a0eda02d3d0eece392116d846e509fd44ea77bb8e6952e2497f35378127457587ffc0d3e843adc803c367e05bd8e61b38dadbbc91511b43d35d75e420", - "remoteSig": "b6051fe473ff1598eff19b273acfd621ae4d64df8c1aabd4b4732cdb18a1d7e363dc5b0e2173b03594cba4b60d44cdc2234a793bc038084aba78de787571967a" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:9", - "txOut": { - "amount": 6000, - "publicKeyScript": "002095b60e27d44105025a817fec991a339fd6b9d755f5f8e3bfed683afcad284882" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c8201208763a9148b5b5be43ccce97466ccd2dcb1e647669f48431e88527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae677503101b06b175ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c09000000000100000001a6090000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc00000000", - "paymentHash": "0315d713b5ed37e3056f97414470693a6af57dd928583abb8fe2a1318b1f9c8f", - "htlcId": 1 - }, - "localSig": "4a18e872b0e4d5a63805d0ecf35a0b8f3ed39b132f3078d24a1b3381d077f2c143a3ea122748d919a2cb5eb5971d20db448a0231e47c16a828ef39317988e32e", - "remoteSig": "e27bdc941ca50886eaaa977deff3ffee17d17536daa1fcefafb4051ea5faa0035483a11f357afae6f5822812ef33630f2ef9ebf7c5a3656dabf5b406cd641a27" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:10", - "txOut": { - "amount": 6000, - "publicKeyScript": "002096c6e023e7f1168fda2f6676aaacec6dfa9bc0c8c0620e671d2997bde4f10902" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c820120876475527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae67a91448c122aa920940aa4204e515c684d8397746762288ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c0a0000000001000000016e0a0000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc101b0600", - "htlcId": 0 - }, - "localSig": "165a9263133c0e6d1f4e48398136179dae5f54fb585beaef45ee262b774aa6bf42f751f7139f3f24410d11e28782d0819829d30b75f30cdd56ce3ce6399df710", - "remoteSig": "05ba8b686a11ef82d0957f50564c0fe98eb3cc1f0e6a2cd763ab1c3a6d82aa8350e566b7d3db07ef8ecb72c134782ff836309d6d78581b323b42e42c73d6f80c" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:11", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020b2a2bf7ed1c9fc5964b587687ff1f493bf49259ce7c383726d5fba7f03bfb434" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c820120876475527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae67a914f58e98ea3d9888ff1a4e9fe9eb65126b457238dc88ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c0b0000000001000000016e0a0000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc101b0600", - "htlcId": 4 - }, - "localSig": "df86e3c111d58576a4aaeef4a131bce787cf089f4b1e38a68d78ec43c91bf437420325045cc3626c2798a9f4682fc94e43c6f4d16ad6eb0c73ae7c4ec3cc4644", - "remoteSig": "b1896d2e72deda37e57b78bc867958cd02a7e39469409ab6f473b44e3d3fb1796d0d8ffd6911c60a71dcc849e78145a8481ef736a49d9bbace328577e0e42015" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:12", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020d20542298a8f9187870fc61ed9dd7b23ef57492a52e4aba4ab891cadf2c9ae59" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c8201208763a914e94f37230abe5cdc9f5e100d5a4307cd389714d688527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae677503101b06b175ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c0c000000000100000001a6090000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc00000000", - "paymentHash": "e72f94f6c8f4e1031dd3058135c2f41e876dbc369bba1f7a4be1af65fd2d8457", - "htlcId": 0 - }, - "localSig": "7df693da0cf97e8fb47e022fa2682bcf25ef3a98373f2b47e40993cd66e867e77c9c8327b999a31682a042fd9d2752e790331b9f42ae54b8391ae88be53dc836", - "remoteSig": "690c9dae2624e928a3846a42ca586bd0fca88be694be75e6b42a595c4b7607220a83dbb738561b1450fc6e73571b974bbbcbbeab3af4248c4dbcb3fff4aca021" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", - "input": { - "outPoint": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044:13", - "txOut": { - "amount": 6000, - "publicKeyScript": "0020dcd4dc76b4ac00cb294aeeab0acfcc479f02006e165dca1b7ebafc2464e6721b" - }, - "redeemScript": "76a91414de26620cb67f6aa47a10ca324e8dc04e9d88058763ac67210268f6511e5065ac52e0f705af89a7042e72ff0ff6881be21f2be4c13ef2d12f937c8201208763a9142e99e1026831620457c0c725a857e4b6dee677f788527c2103849807323a1b164b540476a7b3e92dff406f91ac735122aeb8d2b9fd9bdee83452ae677503101b06b175ac6851b27568" - }, - "tx": "02000000014430a775f8f05be483eb86cf98326e8f3fe30edfee2c9515789a74e0e6f8740c0d000000000100000001a6090000000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc00000000", - "paymentHash": "fe6faf9ea5b5cdab26159d2a6b1444553d8a2d57e76ed8dbbcba3b14009ee77f", - "htlcId": 2 - }, - "localSig": "92bb30e3c748235cbee14707c727ec8950c71bad2726e899ec72cde33901adbb04e1195c2bc37eed6893fee528224337d76de26c49afbaf96d19f6676d7af77e", - "remoteSig": "6faddd6f6ff4772c55c3bfc6ea1a6449ce1843f9001e7d6d41064c5c663b7fad0b26acff54805aa9cc44ac1bfe6ef223ff15c3698fe552505e28e49bcacde64e" - } - ] - } + "txId": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044", + "remoteSig": { + "sig": "bd71081f466344fb018eceabab814bdaf9d34df35327a43560677a13b96bbc3f1669df9f5075854a415a6ebd32bcf1e6fe482510930da7abfc15f319f29e7f00" + }, + "htlcRemoteSigs": [ + "47f187dab4904e505d7eecb6fa0ab9d1dd3ae4194a215d73c0ed3310bba02e3455dc93c903bee48ed0613431dd771030e0ba43007300570463934cdda090b7c4", + "67afdc3b9bfc9ed2d0c2ff2c29d74e155f404a636d6257de8d576d011bb241a71e31f4b18a5370d5056595f71a40e07a27d68bd09b9724faf61e4fa0baebcc43", + "c0c6a5c5c5df836e3f10e7719a01641fcb017c704a28f5c7951da681f32f4827182f0ccdfd3722d0031449803b1f3268290b0854cdaf616ab4ee696f13eadfd3", + "1caddc268adfcad2dcdb40a8b9f87a1a46b9c53117b014a9438173e99026f31234ba7f594bfe64b3c6ba1faed65331bb5329e9598177d0b306ea781f785825ae", + "939c90c63a3a53c1b0b0cefce54be2473cd0f31d540583407cc9966e4856d6236d880f515d2a93f934a494bbf6b4e9e7bd0de9b0b21e4eaee42bd2db8c91b2a4", + "f6ab52d3a157f2bee88e39966684ba0d41eb89965fc0bb07d559518da66a758f17c36e1a157b71fc692867dfbe263a4862716d527b1ff8110a2f349bcd07dc6b", + "b6051fe473ff1598eff19b273acfd621ae4d64df8c1aabd4b4732cdb18a1d7e363dc5b0e2173b03594cba4b60d44cdc2234a793bc038084aba78de787571967a", + "e27bdc941ca50886eaaa977deff3ffee17d17536daa1fcefafb4051ea5faa0035483a11f357afae6f5822812ef33630f2ef9ebf7c5a3656dabf5b406cd641a27", + "05ba8b686a11ef82d0957f50564c0fe98eb3cc1f0e6a2cd763ab1c3a6d82aa8350e566b7d3db07ef8ecb72c134782ff836309d6d78581b323b42e42c73d6f80c", + "b1896d2e72deda37e57b78bc867958cd02a7e39469409ab6f473b44e3d3fb1796d0d8ffd6911c60a71dcc849e78145a8481ef736a49d9bbace328577e0e42015", + "690c9dae2624e928a3846a42ca586bd0fca88be694be75e6b42a595c4b7607220a83dbb738561b1450fc6e73571b974bbbcbbeab3af4248c4dbcb3fff4aca021", + "6faddd6f6ff4772c55c3bfc6ea1a6449ce1843f9001e7d6d41064c5c663b7fad0b26acff54805aa9cc44ac1bfe6ef223ff15c3698fe552505e28e49bcacde64e" + ] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 2, @@ -608,6 +410,9 @@ "publicKeyScript": "00148948b4526e08b8166d8f2a14e0ceab30d5a70160" } ], + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, "lockTime": 400000, "dustLimit": 1100, "targetFeerate": 253 @@ -721,18 +526,7 @@ "toLocal": 813718000, "toRemote": 114000000 }, - "commitTx": { - "input": { - "outPoint": "b278d58353fe5456eee48beb4a922e867d791d822e16e2c0a3ca2d5945eeee4a:0", - "txOut": { - "amount": 999718, - "publicKeyScript": "00205e1246b2a85801263f907f95cb472eb2605214ad738a9dc505eb8adc423d312f" - }, - "redeemScript": "52210390e60eaef418d6df57d50c7597eec0b23902f51e76a2fca4d7a7788980ecddce2103adb09c35563b342c924c2eab5a86789f9321c8dbbd351982cab0ec4c724de5d952ae" - }, - "tx": "02000000014aeeee45592dcaa3c0e2162e821d797d862e924aeb8be4ee5654fe5383d578b2000000000058d4bd80104a0100000000000022002018df4fed01bdab8155a6c24f76c2463550c0fe76e6de725cc39519f1354735ef4a01000000000000220020b4e9558549013f901a0cb091110e4a1c81068eb989d5793478aab773058968ec70170000000000002200200c5f841665d884f5f2f1d3c2ba3ceb5a732792189108eed8527bcf102138b65e70170000000000002200202f500548eb86486434f03e36aadc6a41d711fae6ae0cf2f4a773abb49f301164701700000000000022002034ef6a0dd495579cc24720aea885bbecffbbd21a39cc57022e6829f1de71865170170000000000002200203576ee1e46448c65dda7444c48a78c9c03d3cf9ad15ef4d5cf2d89c9d239e6a770170000000000002200207cc6cd56dc24dbe5fea2130c5905c5b269f3ebcb84c4efa0b2e2e179d8dc2c8370170000000000002200207d2e9c73039131ce97b78ef0a082ad227835d509ad5b0f54543c46af7648f2eb70170000000000002200208f8edfe21f88a607ab807e78d3c84ff83349afd453c905335f7ad8e8dbfe48a5701700000000000022002095b60e27d44105025a817fec991a339fd6b9d755f5f8e3bfed683afcad284882701700000000000022002096c6e023e7f1168fda2f6676aaacec6dfa9bc0c8c0620e671d2997bde4f109027017000000000000220020b2a2bf7ed1c9fc5964b587687ff1f493bf49259ce7c383726d5fba7f03bfb4347017000000000000220020d20542298a8f9187870fc61ed9dd7b23ef57492a52e4aba4ab891cadf2c9ae597017000000000000220020dcd4dc76b4ac00cb294aeeab0acfcc479f02006e165dca1b7ebafc2464e6721b50bd010000000000220020a27d5c91d81040e20e16c094fdf7329a71ff026e2a34c7aace733bcd811897b2be290c00000000002200209814f1f8a2346c0dc3372c5113547b6abc6eb1387b1812ede517100d9cb05bcc33153920" - }, - "htlcTxs": [] + "txId": "bbaf3ce132cf2dd1526e1312a134c3dc34bcac2f7d5176adb6fd457c9cdf3278" }, "right": null }, diff --git a/modules/core/src/commonTest/resources/nonreg/v4/ShuttingDown_fac54067/data.json b/modules/core/src/commonTest/resources/nonreg/v4/ShuttingDown_fac54067/data.json index 359de13cc..5545e005b 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/ShuttingDown_fac54067/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/ShuttingDown_fac54067/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.ShuttingDown", "commitments": { - "params": { + "channelParams": { "channelId": "85ad5df602e4b1517db06754a5c1f3aa68d59973bd19e29798a825f3fb22babf", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/1518760262'/928356704/894765365'/1448501779'/1699908170/1517889105'/1532125607/1017574687/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 100, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -48,11 +43,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "036a53172552a7d39cf17412cb5f33ee6af4de87718fe76c4f4009b5cd17e97c97", "paymentBasepoint": "03ee1b87dd67000d912feea1290f84b910b9fdf2866998c542bab58deaa7098ccd", "delayedPaymentBasepoint": "039ec449ed8149605dc80f585a761518e111c68fa69db9a5f051d32163a17c6051", @@ -105,6 +95,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "1bd438254211ea24b6355f64419ba21c89489aed2149bfe0227135b4a4692cd3:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "0304cc4d4dab3996ebed55ed57470f530f8a7bf8e01884576b5253852ad9a8d3f1", "localFundingStatus": { "status": "confirmed", @@ -113,6 +105,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 100, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 1, "spec": { @@ -139,55 +141,21 @@ "toLocal": 350000000, "toRemote": 150000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "1bd438254211ea24b6355f64419ba21c89489aed2149bfe0227135b4a4692cd3:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "0020f6d36666a728d1f6112bec77bd20f8e2fa93de3c72b6f5bd643cb0eb97f58bd5" - }, - "redeemScript": "52210200985f1cdf5f76b6d394037b886a5a792ae25349046a2228b561ded98dee8933210304cc4d4dab3996ebed55ed57470f530f8a7bf8e01884576b5253852ad9a8d3f152ae" - }, - "tx": "02000000000101d32c69a4b4357122e0bf4921ed9a48891ca29b41645f35b624ea11422538d41b000000000064abeb80064a0100000000000022002015b117f5aeb53a99c44e814b475b9ae52d55064007db3c75c8b52a8208f726b84a010000000000002200204d8dbd3b3e24cfdb232d579b121ef18f59fd558af7a9de72645e209992c567cdf0490200000000002200207e3c66fb6f3722e424b070476680022db4ff74129817013bc0f9c5c1cc7dfb6a400d030000000000220020fb800b19bc9562aca63bed8fa66c13723e769e50e0d910799515e836b1fb3142e09304000000000022002070cb969825434d5bdabbfbd192e51c66dc7a441323b32e2dfadaeec4ddb0a63ef037050000000000220020319fa8cf2e44793126a0044a07b9a11de7263e4ae6328fa8559dd0af00b6d09a0400473044022042f6abfe99cd43c8944b604c4436ea4c9aba6ea441419f768c05cab4150d1d7b02207e7e54fc6614febf52f66671e4da547c40ad0e6d716e19f12139053b0e9a50bd0147304402204fbc9db98fa48f937fb60e364bbb8e32e8f7c6419745cf0f16b04b75d46e6cbe0220587cffe2a58b5ad40659ec171991b6a80fb63fbcf41b3757f511615dc3ba4703014752210200985f1cdf5f76b6d394037b886a5a792ae25349046a2228b561ded98dee8933210304cc4d4dab3996ebed55ed57470f530f8a7bf8e01884576b5253852ad9a8d3f152ae03d59420" - }, - "htlcTxsAndSigs": [ - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "46e675be5b612cef94dbf2fe5d82dbfaa9f2f2f1542c941a711324f3f479ec66:3", - "txOut": { - "amount": 200000, - "publicKeyScript": "0020fb800b19bc9562aca63bed8fa66c13723e769e50e0d910799515e836b1fb3142" - }, - "redeemScript": "76a914d3fc9e9ca8b1533221c10fb76304af838cc47ea18763ac67210203d105df4c9e8d18598718e26fb5a452c6892baf9a0d20fda83739c418b22a227c820120876475527c2103e2b8be99981500c52f7b20dec96a3c24c179881fb3b861af72612fc74fdf397852ae67a914616d7b427efa6dc3e4f169d220396be2e41b89f088ac6851b27568" - }, - "tx": "020000000166ec79f4f32413711a942c54f1f2f2a9fadb825dfef2db94ef2c615bbe75e646030000000001000000013e00030000000000220020319fa8cf2e44793126a0044a07b9a11de7263e4ae6328fa8559dd0af00b6d09a101b0600", - "htlcId": 1 - }, - "localSig": "cb24434c96d8471c6c8e06106b5df3e46cb5b08f59070443472598768121d75f1fea9f0586a3afdf0c54a381f97c8f01cde951400edbbac0e0bc1c9aff7e3c22", - "remoteSig": "b93c1ee4c31a629d64e8ec83f229db9cd9ea67dccf2fffc1f08c5443cdc548bf0ac41485cda6c3c40b019db559094ee0e7e2a7deb47f73d6d50ec0a7cd60685d" - }, - { - "txinfo": { - "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", - "input": { - "outPoint": "46e675be5b612cef94dbf2fe5d82dbfaa9f2f2f1542c941a711324f3f479ec66:4", - "txOut": { - "amount": 300000, - "publicKeyScript": "002070cb969825434d5bdabbfbd192e51c66dc7a441323b32e2dfadaeec4ddb0a63e" - }, - "redeemScript": "76a914d3fc9e9ca8b1533221c10fb76304af838cc47ea18763ac67210203d105df4c9e8d18598718e26fb5a452c6892baf9a0d20fda83739c418b22a227c820120876475527c2103e2b8be99981500c52f7b20dec96a3c24c179881fb3b861af72612fc74fdf397852ae67a914ab83f84093ea8285fe192c59c2913bf753b2162088ac6851b27568" - }, - "tx": "020000000166ec79f4f32413711a942c54f1f2f2a9fadb825dfef2db94ef2c615bbe75e64604000000000100000001de86040000000000220020319fa8cf2e44793126a0044a07b9a11de7263e4ae6328fa8559dd0af00b6d09a101b0600", - "htlcId": 0 - }, - "localSig": "53fc636a3c35268d017944b3258e9fb3a6f6b05e287567caf853c24116fe308643771c028ab432522ee09d6712eabda36ea65ac9465d640c8e6a938f60091852", - "remoteSig": "ac68dd27ed4519112f275c3602807e65f3d337b04a34067d3f19024d6ae1187d3f15e783f6d21f9665ee9c1c65fbcefa2e9bd149e3935d35a8630827380d1f14" - } - ] - } + "txId": "46e675be5b612cef94dbf2fe5d82dbfaa9f2f2f1542c941a711324f3f479ec66", + "remoteSig": { + "sig": "4fbc9db98fa48f937fb60e364bbb8e32e8f7c6419745cf0f16b04b75d46e6cbe587cffe2a58b5ad40659ec171991b6a80fb63fbcf41b3757f511615dc3ba4703" + }, + "htlcRemoteSigs": [ + "b93c1ee4c31a629d64e8ec83f229db9cd9ea67dccf2fffc1f08c5443cdc548bf0ac41485cda6c3c40b019db559094ee0e7e2a7deb47f73d6d50ec0a7cd60685d", + "ac68dd27ed4519112f275c3602807e65f3d337b04a34067d3f19024d6ae1187d3f15e783f6d21f9665ee9c1c65fbcefa2e9bd149e3935d35a8630827380d1f14" + ] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 1, diff --git a/modules/core/src/commonTest/resources/nonreg/v4/WaitForChannelReady_fac54067/data.json b/modules/core/src/commonTest/resources/nonreg/v4/WaitForChannelReady_fac54067/data.json index a456f3842..b3ff939b5 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/WaitForChannelReady_fac54067/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/WaitForChannelReady_fac54067/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.WaitForChannelReady", "commitments": { - "params": { + "channelParams": { "channelId": "1c9c6492fc038dd610071d689aff8a57f88e1163ad006643b9491bd0e6fcf8b1", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/1652959757'/1970622116'/1290530919/55438481/1700694130/1017951873'/1967259419'/205356681'/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 100, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -48,11 +43,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "02c3d93ba81a89083fe6e9dc45a383b3c2bda0fd6f1af7cabf0eb432ab21e16f50", "paymentBasepoint": "035b552ede5d5ae25c718ae457059277e82d02963ab031b48d425d0d691e15dbf8", "delayedPaymentBasepoint": "03c9fcbef6b94e7994d2c998c71d5cd89f2237d8d79c81abeb427ec9e21fa43682", @@ -105,6 +95,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "af08fe6683da0905dd7cb1d7ad9a3b9ca3e9cc8a7eb39a7447df8c7747feb6d2:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "02358563da6d0e053c10fafed2ef5f0bdab8f938b55e6925520021f9185475af16", "localFundingStatus": { "status": "confirmed", @@ -113,6 +105,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 100, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 0, "spec": { @@ -122,20 +124,18 @@ "toLocal": 850000000, "toRemote": 150000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "af08fe6683da0905dd7cb1d7ad9a3b9ca3e9cc8a7eb39a7447df8c7747feb6d2:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "0020bd326696fdb651e82e8f68cc8b3c30fa373807f36f9e84b7a830170fec62c088" - }, - "redeemScript": "522102358563da6d0e053c10fafed2ef5f0bdab8f938b55e6925520021f9185475af162102e4cb3b07e3f734da72ab884164f460e8b753d2a6e2231fb46e6f54d7013ca27b52ae" - }, - "tx": "02000000000101d2b6fe47778cdf47749ab37e8acce9a39c3b9aadd7b17cdd0509da8366fe08af0000000000cec7d980044a010000000000002200207b8115266ca5386853fff93cfbe6b5339bb5ea35f27eda15fc619758c2f754c74a01000000000000220020a7ef145a9e6b18ca23fdc70033ae99a1e931cdaffcd521a77f79276105d279b2f049020000000000220020093666f54c77bfbb89e6aeb868c49c2e799a19cb044307067a1e6d924437e9f5c8df0c0000000000220020fc68f460b52c6646af92eadd15e1b31913a16fc0f4eb51605e506e54c06ee50404004730440220434a16a3d3092ce8dae91aca3b863501499c6b6a862d7e18fd580623294f657102200334414fbbd1f54c87c6a52294feb09d9a76cdb10a74e890ec0b6cfb70f0e06701483045022100eaba72a07e7c3a127dc6baa731ae13974be15568a9ef966d7c3e36a01456926d02203152133f522d3c6bbc43a3e25dcd171f8705f0b9d305ef54ce3477bddef2bca60147522102358563da6d0e053c10fafed2ef5f0bdab8f938b55e6925520021f9185475af162102e4cb3b07e3f734da72ab884164f460e8b753d2a6e2231fb46e6f54d7013ca27b52ae82e9cc20" - }, - "htlcTxsAndSigs": [] - } + "txId": "e2ad5c195fd0ecb839650322bf4d14543a2f60d1c3f04cca670c3187a7ae2894", + "remoteSig": { + "sig": "434a16a3d3092ce8dae91aca3b863501499c6b6a862d7e18fd580623294f65710334414fbbd1f54c87c6a52294feb09d9a76cdb10a74e890ec0b6cfb70f0e067" + }, + "htlcRemoteSigs": [] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 0, diff --git a/modules/core/src/commonTest/resources/nonreg/v4/WaitForFundingConfirmed_fac54067/data.json b/modules/core/src/commonTest/resources/nonreg/v4/WaitForFundingConfirmed_fac54067/data.json index 1437d43c0..662f442fe 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/WaitForFundingConfirmed_fac54067/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/WaitForFundingConfirmed_fac54067/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.WaitForFundingConfirmed", "commitments": { - "params": { + "channelParams": { "channelId": "2a6bf35f6378987185051faa8cc4ca745d380181bad61ba770056e4911aab062", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/1420872353/582035983/1224888769/201998979'/219022565'/1268209049'/1620759017'/420544194/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 100, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": true, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -48,11 +43,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "02e7abcc8dca9d319b135d5db704baa077ab4c1e72f937e1871a9c4539c95ef107", "paymentBasepoint": "03ed419a8300a3667c8e2b6a910774893e5ead821205b4d6b95284c205e6c481f8", "delayedPaymentBasepoint": "035e545d8500929d31729634c160b91379e3d745b99f4b04f9a380710451d029d5", @@ -104,6 +94,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "d61cfd4785c9a5a3d5a863f2b9378a050c792fd9773502b3c88dc4efe59d5d1b:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "02a23c28358ab531b3cc8782b7c43b38fe611e2b9b33d7cfa4d603e84ab2e470f2", "localFundingStatus": { "status": "unconfirmed", @@ -112,6 +104,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 100, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 0, "spec": { @@ -121,20 +123,18 @@ "toLocal": 850000000, "toRemote": 150000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "d61cfd4785c9a5a3d5a863f2b9378a050c792fd9773502b3c88dc4efe59d5d1b:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ef1ad1e81b50fe0246e72589e627b4d97cea9600c0101726004f78f03fc3d2a" - }, - "redeemScript": "522102a23c28358ab531b3cc8782b7c43b38fe611e2b9b33d7cfa4d603e84ab2e470f22102f75bf08cd2289a1b87580f3e649c2c12894a82d96ab3a23ba418a1c671032f1052ae" - }, - "tx": "020000000001011b5d9de5efc48dc8b3023577d92f790c058a37b9f263a8d5a3a5c98547fd1cd60000000000d53e5c80044a01000000000000220020ae649030a1cd5121a3c136cbc957821c9b33079ea164138a80af02cae0bee5544a01000000000000220020e52f404b2c08c7ab85f46ce3c14021ee3c83cfb0ec7667fb76202b5bfcf7a096f049020000000000220020f69abdf2dc02c4dee05465d1fdd1a2e85317576fca694c280b367688a1fa12c5c8df0c00000000002200204d65591b58deec2071632e7ec5ff644bfdd048281d9118411974c3d70bacdb7f0400483045022100b717883c313d0e73ed638ac1e8bdd4b205138e260a321fe92e7c4ac80073b09502205ecbb02b165d9c8201f4da4b92a298fd0dc8a35f878651319f8eacaef468640a01483045022100f6027a2fa57333b8a6e7ca518b035cb13f80c3a2180201e114b8d2817e74af6c0220199d4fe0ccbf5697f88267c6c9f4c85b8a14b52e1073f8792dad54dc5a0072790147522102a23c28358ab531b3cc8782b7c43b38fe611e2b9b33d7cfa4d603e84ab2e470f22102f75bf08cd2289a1b87580f3e649c2c12894a82d96ab3a23ba418a1c671032f1052aef0088220" - }, - "htlcTxsAndSigs": [] - } + "txId": "75adc69132706111bfaa36188f158d5c967a831be2d154cda1e901666bf05f63", + "remoteSig": { + "sig": "b717883c313d0e73ed638ac1e8bdd4b205138e260a321fe92e7c4ac80073b0955ecbb02b165d9c8201f4da4b92a298fd0dc8a35f878651319f8eacaef468640a" + }, + "htlcRemoteSigs": [] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 0, diff --git a/modules/core/src/commonTest/resources/nonreg/v4/WaitForRemotePublishFutureCommitment_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/WaitForRemotePublishFutureCommitment_ebb9087c/data.json index 86db62ee7..ee1332f74 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/WaitForRemotePublishFutureCommitment_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/WaitForRemotePublishFutureCommitment_ebb9087c/data.json @@ -1,7 +1,7 @@ { "type": "fr.acinq.lightning.channel.states.WaitForRemotePublishFutureCommitment", "commitments": { - "params": { + "channelParams": { "channelId": "0b5ac4a5254d1c7b3b1dafcd883f7db1f6e527ef86b05a9425d3e08a66ba33f4", "channelConfig": [ "funding_pubkey_based_channel_keypath" @@ -14,11 +14,6 @@ "localParams": { "nodeId": "037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c", "fundingKeyPath": "m/492583088'/1957315318/1182763714/2144039014'/576494277/259332362'/1989202166'/2017865733/1'", - "dustLimit": 1100, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 0, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "isChannelOpener": true, "paysCommitTxFees": false, "defaultFinalScriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", @@ -45,11 +40,6 @@ }, "remoteParams": { "nodeId": "0362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06", - "dustLimit": 1000, - "maxHtlcValueInFlightMsat": 1500000000, - "htlcMinimum": 1000, - "toSelfDelay": 144, - "maxAcceptedHtlcs": 100, "revocationBasepoint": "028c9dd03e2247e6909125b0cfcb2b7398c8212823de960d4c591333f17e4bd710", "paymentBasepoint": "028d4d4923ed6a606e494af3fd8502accabb59c1157a8af53d1bfc33eab0299d00", "delayedPaymentBasepoint": "02396233f2005ad262c70391036482281645886735e17d8d95e31a6cadddaeefbb", @@ -100,6 +90,8 @@ "active": [ { "fundingTxIndex": 0, + "fundingInput": "f2425505356da206e9d68591c35fe34a3776a400063d3b44de71bb23b32a121e:0", + "fundingAmount": 1000000, "remoteFundingPubkey": "027d2e4ca089015b111c4f6a2370d179f04ae26455a21d394a41374d6d79189758", "localFundingStatus": { "status": "confirmed", @@ -108,6 +100,16 @@ "remoteFundingStatus": { "status": "not-locked" }, + "commitmentFormat": { + "rfcName": "anchor_outputs" + }, + "localCommitParams": { + "dustLimit": 1100, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 0, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 + }, "localCommit": { "index": 0, "spec": { @@ -117,20 +119,18 @@ "toLocal": 800000000, "toRemote": 200000000 }, - "publishableTxs": { - "commitTx": { - "input": { - "outPoint": "f2425505356da206e9d68591c35fe34a3776a400063d3b44de71bb23b32a121e:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "0020afa0e32aadb38f5e0bc47da245a8f3f7244c1969a771b4850d3b7a83d5b5e052" - }, - "redeemScript": "5221027d2e4ca089015b111c4f6a2370d179f04ae26455a21d394a41374d6d791897582102fa66e4c9725b540da5b328ffb78a03cea168936ebace360d8a48bacb49e1c13052ae" - }, - "tx": "020000000001011e122ab323bb71de443b3d0600a476374ae35fc39185d6e906a26d35055542f20000000000a3e9bc80044a010000000000002200204624c53b736d3970f3b3e2c0a12885e02db545b60dc28ba80a39ccbcc8c295034a01000000000000220020f7ea41d701a92c76bc15a93cdf122f9ddbeca0689eecc70f42c5dea428a30bb7400d03000000000022002011aec6408a6cc9f0559eefb0cc5b6978725236df3fd5ed5f522343275090d98a781c0c00000000002200204a68f58d61d6c16f8bae470dd95e87ca8b6d7ba6efa733c479ebf20f91f494fe040047304402201d87fbb35b15229da1da1a4d10018e7b9b510b57ea41ee3e32d1b0fa72562bea02204521bd19dac13089cacde07debb5950560b75a2a74ac9e45017babe136bdd719014830450221008f5a6f23d56f0d1ec22a879d97a06c2080033a28c36bb459bbe4563896fb6a82022015d295b881141f6c512e178610a2c351250d2c5f27f42e332e7447ccea4595e001475221027d2e4ca089015b111c4f6a2370d179f04ae26455a21d394a41374d6d791897582102fa66e4c9725b540da5b328ffb78a03cea168936ebace360d8a48bacb49e1c13052ae9175a320" - }, - "htlcTxsAndSigs": [] - } + "txId": "adf80ca3cdb8625fc144f8087766475abbf8331955825e597b2bca77169056c7", + "remoteSig": { + "sig": "1d87fbb35b15229da1da1a4d10018e7b9b510b57ea41ee3e32d1b0fa72562bea4521bd19dac13089cacde07debb5950560b75a2a74ac9e45017babe136bdd719" + }, + "htlcRemoteSigs": [] + }, + "remoteCommitParams": { + "dustLimit": 1000, + "maxHtlcValueInFlightMsat": 1500000000, + "htlcMinimum": 1000, + "toSelfDelay": 144, + "maxAcceptedHtlcs": 100 }, "remoteCommit": { "index": 0,