diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt index 3f834bc8f..40fa91cfc 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt @@ -147,8 +147,6 @@ sealed class Feature { override val scopes: Set get() = setOf(FeatureScope.Invoice) } - // The following features have not been standardised, hence the high feature bits to avoid conflicts. - // We historically used the following feature bit in our invoices. // However, the spec assigned the same feature bit to `option_scid_alias` (https://github.com/lightning/bolts/pull/910). // We're moving this feature bit to 148, but we have to keep supporting it until enough wallet users have migrated, then we can remove it. @@ -160,6 +158,15 @@ sealed class Feature { override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice) } + @Serializable + object SimpleClose : Feature() { + override val rfcName get() = "option_simple_close" + override val mandatory get() = 60 + override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) + } + + // The following features have not been standardised, hence the high feature bits to avoid conflicts. + /** This feature bit should be activated when a node accepts having their channel reserve set to 0. */ @Serializable object ZeroReserveChannels : Feature() { @@ -340,6 +347,7 @@ data class Features(val activated: Map, val unknown: Se Feature.ChannelType, Feature.PaymentMetadata, Feature.TrampolinePayment, + Feature.SimpleClose, Feature.ExperimentalTrampolinePayment, Feature.ZeroReserveChannels, Feature.ZeroConfChannels, @@ -384,6 +392,7 @@ data class Features(val activated: Map, val unknown: Se Feature.PaymentSecret to listOf(Feature.VariableLengthOnion), Feature.BasicMultiPartPayment to listOf(Feature.PaymentSecret), Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey), + Feature.SimpleClose to listOf(Feature.ShutdownAnySegwit), Feature.TrampolinePayment to listOf(Feature.PaymentSecret), Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret), Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice), diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt index a088bee87..45a867410 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -173,6 +173,8 @@ data class NodeParams( require(features.hasFeature(Feature.ChannelType, FeatureSupport.Mandatory)) { "${Feature.ChannelType.rfcName} should be mandatory" } require(features.hasFeature(Feature.DualFunding, FeatureSupport.Mandatory)) { "${Feature.DualFunding.rfcName} should be mandatory" } require(features.hasFeature(Feature.RouteBlinding)) { "${Feature.RouteBlinding.rfcName} should be supported" } + require(features.hasFeature(Feature.ShutdownAnySegwit, FeatureSupport.Mandatory)) { "${Feature.ShutdownAnySegwit.rfcName} should be mandatory" } + require(features.hasFeature(Feature.SimpleClose, FeatureSupport.Mandatory)) { "${Feature.SimpleClose.rfcName} should be mandatory" } require(!features.hasFeature(Feature.ZeroConfChannels)) { "${Feature.ZeroConfChannels.rfcName} has been deprecated: use the zeroConfPeers whitelist instead" } require(!features.hasFeature(Feature.TrustedSwapInClient)) { "${Feature.TrustedSwapInClient.rfcName} has been deprecated" } require(!features.hasFeature(Feature.TrustedSwapInProvider)) { "${Feature.TrustedSwapInProvider.rfcName} has been deprecated" } @@ -204,6 +206,7 @@ data class NodeParams( Feature.Quiescence to FeatureSupport.Mandatory, Feature.ChannelType to FeatureSupport.Mandatory, Feature.PaymentMetadata to FeatureSupport.Optional, + Feature.SimpleClose to FeatureSupport.Mandatory, Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional, Feature.ZeroReserveChannels to FeatureSupport.Optional, Feature.WakeUpNotificationClient to FeatureSupport.Optional, 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 951ceb106..99c339973 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 @@ -6,7 +6,6 @@ import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.WatchEvent import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.states.ClosingFeerates import fr.acinq.lightning.channel.states.PersistedChannelState import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.utils.UUID @@ -105,7 +104,7 @@ sealed class ChannelCommand { } sealed class Close : ChannelCommand() { - data class MutualClose(val scriptPubKey: ByteVector?, val feerates: ClosingFeerates?) : Close(), ForbiddenDuringSplice, ForbiddenDuringQuiescence + data class MutualClose(val scriptPubKey: ByteVector?, val feerate: FeeratePerKw) : Close(), ForbiddenDuringSplice, ForbiddenDuringQuiescence data object ForceClose : Close() } 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 62f3eabd9..a298118b4 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 @@ -13,7 +13,6 @@ import fr.acinq.lightning.logging.LoggingContext import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* import fr.acinq.lightning.utils.toMilliSatoshi -import fr.acinq.lightning.wire.ClosingSigned /** * Details about a force-close where we published our commitment. @@ -370,10 +369,6 @@ data class LocalParams( features = nodeParams.features.initFeatures() ) - // The node responsible for the commit tx fees is also the node paying the mutual close fees. - // The other node's balance may be empty, which wouldn't allow them to pay the closing fees. - val paysClosingFees: Boolean = paysCommitTxFees - fun channelKeys(keyManager: KeyManager) = keyManager.channelKeys(fundingKeyPath) } @@ -397,8 +392,6 @@ data class RemoteParams( */ data class ChannelFlags(val announceChannel: Boolean, val nonInitiatorPaysCommitFees: Boolean) -data class ClosingTxProposed(val unsignedTx: ClosingTx, val localClosingSigned: ClosingSigned) - /** * @param miningFee fee paid to miners for the underlying on-chain transaction. * @param serviceFee fee paid to our peer for any service provided with the on-chain transaction. diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt index a005ac003..92d12a87f 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt @@ -1,9 +1,6 @@ package fr.acinq.lightning.channel -import fr.acinq.bitcoin.BlockHash -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Satoshi -import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.* import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi @@ -62,8 +59,10 @@ data class FeerateTooSmall (override val channelId: Byte data class FeerateTooDifferent (override val channelId: ByteVector32, val localFeeratePerKw: FeeratePerKw, val remoteFeeratePerKw: FeeratePerKw) : ChannelException(channelId, "local/remote feerates are too different: remoteFeeratePerKw=${remoteFeeratePerKw.toLong()} localFeeratePerKw=${localFeeratePerKw.toLong()}") data class InvalidCommitmentSignature (override val channelId: ByteVector32, val txId: TxId) : ChannelException(channelId, "invalid commitment signature: txId=$txId") data class InvalidHtlcSignature (override val channelId: ByteVector32, val txId: TxId) : ChannelException(channelId, "invalid htlc signature: txId=$txId") +data class CannotGenerateClosingTx (override val channelId: ByteVector32) : ChannelException(channelId, "failed to generate closing transaction: all outputs are trimmed") +data class MissingCloseSignature (override val channelId: ByteVector32) : ChannelException(channelId, "closing_complete is missing a signature for a closing transaction including our output") data class InvalidCloseSignature (override val channelId: ByteVector32, val txId: TxId) : ChannelException(channelId, "invalid close signature: txId=$txId") -data class InvalidCloseAmountBelowDust (override val channelId: ByteVector32, val txId: TxId) : ChannelException(channelId, "invalid closing tx: some outputs are below dust: txId=$txId") +data class InvalidCloseeScript (override val channelId: ByteVector32, val received: ByteVector, val expected: ByteVector) : ChannelException(channelId, "invalid closee script used in closing_complete: our latest script is $expected, you're using $received") data class CommitSigCountMismatch (override val channelId: ByteVector32, val expected: Int, val actual: Int) : ChannelException(channelId, "commit sig count mismatch: expected=$expected actual=$actual") data class HtlcSigCountMismatch (override val channelId: ByteVector32, val expected: Int, val actual: Int) : ChannelException(channelId, "htlc sig count mismatch: expected=$expected actual: $actual") data class ForcedLocalCommit (override val channelId: ByteVector32) : ChannelException(channelId, "forced local commit") 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 4c55be458..bd2040d00 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 @@ -8,8 +8,6 @@ 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.Feature -import fr.acinq.lightning.Features import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.BITCOIN_OUTPUT_SPENT @@ -21,13 +19,11 @@ import fr.acinq.lightning.blockchain.fee.FeerateTolerance import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.channel.Helpers.Closing.inputsAlreadySpent import fr.acinq.lightning.channel.states.Channel -import fr.acinq.lightning.channel.states.ClosingFeerates -import fr.acinq.lightning.channel.states.ClosingFees import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForCommitment import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForRevocation import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.crypto.ShaChain -import fr.acinq.lightning.logging.* +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 @@ -37,7 +33,10 @@ import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.Htl 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.* +import fr.acinq.lightning.utils.getValue +import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.utils.sum +import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* import kotlin.math.max @@ -221,7 +220,14 @@ object Helpers { ) } - data class PairOfCommitTxs(val localSpec: CommitmentSpec, val localCommitTx: Transactions.TransactionWithInputInfo.CommitTx, val localHtlcTxs: List, val remoteSpec: CommitmentSpec, val remoteCommitTx: Transactions.TransactionWithInputInfo.CommitTx, val remoteHtlcTxs: List) + data class PairOfCommitTxs( + val localSpec: CommitmentSpec, + val localCommitTx: Transactions.TransactionWithInputInfo.CommitTx, + val localHtlcTxs: List, + val remoteSpec: CommitmentSpec, + val remoteCommitTx: Transactions.TransactionWithInputInfo.CommitTx, + val remoteHtlcTxs: List + ) /** * Creates both sides' first commitment transaction. @@ -247,7 +253,7 @@ object Helpers { remotePerCommitmentPoint: PublicKey ): Either { val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote) - val remoteSpec = CommitmentSpec(localHtlcs.map{ it.opposite() }.toSet(), commitTxFeerate, toLocal = toRemote, toRemote = toLocal) + val remoteSpec = CommitmentSpec(localHtlcs.map { it.opposite() }.toSet(), commitTxFeerate, toLocal = toRemote, toRemote = toLocal) if (!localParams.paysCommitTxFees) { // They are responsible for paying the commitment transaction fee: we need to make sure they can afford it! @@ -296,7 +302,7 @@ object Helpers { // used only to compute tx weights and estimate fees private val dummyPublicKey by lazy { PrivateKey(ByteArray(32) { 1.toByte() }).publicKey() } - private fun isValidFinalScriptPubkey(scriptPubKey: ByteArray, allowAnySegwit: Boolean): Boolean { + private fun isValidFinalScriptPubkey(scriptPubKey: ByteArray, allowAnySegwit: Boolean, allowOpReturn: Boolean): Boolean { return runTrying { val script = Script.parse(scriptPubKey) when { @@ -306,88 +312,129 @@ object Helpers { Script.isPay2wsh(script) -> true // option_shutdown_anysegwit doesn't cover segwit v0 Script.isNativeWitnessScript(script) && script[0] != OP_0 -> allowAnySegwit + script[0] == OP_RETURN -> allowOpReturn else -> false } }.getOrElse { false } } - fun isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = isValidFinalScriptPubkey(scriptPubKey.toByteArray(), allowAnySegwit) + fun isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean, allowOpReturn: Boolean): Boolean = isValidFinalScriptPubkey(scriptPubKey.toByteArray(), allowAnySegwit, allowOpReturn) - private fun firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, requestedFeerate: ClosingFeerates): ClosingFees { - // this is just to estimate the weight which depends on the size of the pubkey scripts - val dummyClosingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.params.localParams.paysClosingFees, 0.sat, 0.sat, commitment.localCommit.spec) - val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, commitment.remoteFundingPubkey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) - return requestedFeerate.computeFees(closingWeight) - } - - fun firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, requestedFeerate: ClosingFeerates): ClosingFees = - firstClosingFee(commitment, localScriptPubkey.toByteArray(), remoteScriptPubkey.toByteArray(), requestedFeerate) - - fun nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 - - fun makeFirstClosingTx( + /** We are the closer: we sign closing transactions for which we pay the fees. */ + fun makeClosingTxs( channelKeys: KeyManager.ChannelKeys, commitment: FullCommitment, - localScriptPubkey: ByteArray, - remoteScriptPubkey: ByteArray, - requestedFeerate: ClosingFeerates - ): Pair { - val closingFees = firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, requestedFeerate) - return makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFees) + localScriptPubkey: ByteVector, + remoteScriptPubkey: ByteVector, + feerate: FeeratePerKw, + lockTime: Long, + ): Either> { + // 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) + 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())) + } + } + } + // 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) + // The actual fee we're paying will be bigger than the one we previously computed if we omit our output. + val actualFee = closingTxs.preferred?.fee ?: 0.sat + if (actualFee == 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)) }, + ) + ) + val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, actualFee, lockTime, tlvs) + return Either.Right(Pair(closingTxs, closingComplete)) } - fun makeClosingTx( - channelKeys: KeyManager.ChannelKeys, - commitment: FullCommitment, - localScriptPubkey: ByteArray, - remoteScriptPubkey: ByteArray, - closingFees: ClosingFees - ): Pair { - val allowAnySegwit = Features.canUseFeature(commitment.params.localParams.features, commitment.params.remoteParams.features, Feature.ShutdownAnySegwit) - require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit)) { "invalid localScriptPubkey" } - require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit)) { "invalid remoteScriptPubkey" } - val dustLimit = commitment.params.localParams.dustLimit.max(commitment.params.remoteParams.dustLimit) - val closingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.params.localParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) - val localClosingSig = Transactions.sign(closingTx, channelKeys.fundingKey(commitment.fundingTxIndex)) - val closingSigned = ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) - return Pair(closingTx, closingSigned) - } - - fun checkClosingSignature( + /** + * We are the closee: we choose one of the closer's transactions and sign it back. + * + * Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that they + * are not using our latest script (race condition between our closing_complete and theirs). + */ + fun signClosingTx( channelKeys: KeyManager.ChannelKeys, commitment: FullCommitment, - localScriptPubkey: ByteArray, - remoteScriptPubkey: ByteArray, - remoteClosingFee: Satoshi, - remoteClosingSig: ByteVector64 - ): Either> { - val (closingTx, closingSigned) = makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee)) - return if (checkClosingDustAmounts(closingTx)) { - val signedClosingTx = Transactions.addSigs(closingTx, channelKeys.fundingPubKey(commitment.fundingTxIndex), commitment.remoteFundingPubkey, closingSigned.signature, remoteClosingSig) - when (Transactions.checkSpendable(signedClosingTx)) { - is Try.Success -> Either.Right(Pair(signedClosingTx, closingSigned)) - is Try.Failure -> Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + localScriptPubkey: ByteVector, + remoteScriptPubkey: ByteVector, + closingComplete: ClosingComplete + ): Either> { + val closingFee = Transactions.ClosingTxFee.PaidByThem(closingComplete.fees) + val closingTxs = Transactions.makeClosingTxs(commitment.commitInput, 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) { + return Either.Left(MissingCloseSignature(commitment.channelId)) + } + if (closingTxs.localAndRemote != null && closingTxs.localOnly == null && closingComplete.closerAndCloseeOutputsSig == null) { + return Either.Left(MissingCloseSignature(commitment.channelId)) + } + if (closingTxs.localAndRemote == null && closingTxs.localOnly != null && closingComplete.closeeOutputOnlySig == null) { + return Either.Left(MissingCloseSignature(commitment.channelId)) + } + // We choose the closing signature that matches our preferred closing transaction. + 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) } } }, + ) + return when (val preferred = closingTxsWithSigs.firstOrNull()) { + null -> Either.Left(MissingCloseSignature(commitment.channelId)) + 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))))) + } } - } else { - Either.Left(InvalidCloseAmountBelowDust(commitment.channelId, closingTx.tx.txid)) } } /** - * Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk - * that the closing transaction will not be relayed to miners' mempool and will not confirm. - * The various dust limits are detailed in https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits + * We are the closer: they sent us their signature so we should now have a fully signed closing transaction. + * + * Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that we + * sent another closing_complete before receiving their closing_sig, which is now obsolete: we ignore it and wait + * for their next closing_sig that will match our latest closing_complete. */ - fun checkClosingDustAmounts(closingTx: ClosingTx): Boolean { - return closingTx.tx.txOut.all { txOut -> - val publicKeyScript = Script.parse(txOut.publicKeyScript) - when { - Script.isPay2pkh(publicKeyScript) -> txOut.amount >= 546.sat - Script.isPay2sh(publicKeyScript) -> txOut.amount >= 540.sat - Script.isPay2wpkh(publicKeyScript) -> txOut.amount >= 294.sat - Script.isPay2wsh(publicKeyScript) -> txOut.amount >= 330.sat - Script.isNativeWitnessScript(publicKeyScript) -> txOut.amount >= 354.sat - else -> txOut.amount >= 546.sat + fun receiveClosingSig( + channelKeys: KeyManager.ChannelKeys, + commitment: FullCommitment, + closingTxs: Transactions.ClosingTxs, + closingSig: ClosingSig + ): 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) } }, + closingSig.closeeOutputOnlySig?.let { sig -> closingTxs.remoteOnly?.let { tx -> Pair(tx, sig) } }, + ) + return when (val preferred = closingTxsWithSig.firstOrNull()) { + null -> Either.Left(MissingCloseSignature(commitment.channelId)) + 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) + } } } } 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 705edeea1..3d042c8cf 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 @@ -16,10 +16,12 @@ import fr.acinq.lightning.channel.Helpers.Closing.claimRevokedRemoteCommitTxOutp import fr.acinq.lightning.channel.Helpers.Closing.getRemotePerCommitmentSecret import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.db.ChannelClosingType -import fr.acinq.lightning.logging.* +import fr.acinq.lightning.logging.LoggingContext +import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.serialization.channel.Encryption.from import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx -import fr.acinq.lightning.utils.* +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.* import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull @@ -77,7 +79,8 @@ sealed class ChannelState { it is ChannelAction.Message.Send && it.message is CommitSig -> it.copy(message = it.message.withChannelData(EncryptedChannelData.from(privateKey, this@ChannelState), logger)) it is ChannelAction.Message.Send && it.message is RevokeAndAck -> it.copy(message = it.message.withChannelData(EncryptedChannelData.from(privateKey, this@ChannelState), logger)) it is ChannelAction.Message.Send && it.message is Shutdown -> it.copy(message = it.message.withChannelData(EncryptedChannelData.from(privateKey, this@ChannelState), logger)) - it is ChannelAction.Message.Send && it.message is ClosingSigned -> it.copy(message = it.message.withChannelData(EncryptedChannelData.from(privateKey, this@ChannelState), logger)) + it is ChannelAction.Message.Send && it.message is ClosingComplete -> it.copy(message = it.message.withChannelData(EncryptedChannelData.from(privateKey, this@ChannelState), logger)) + it is ChannelAction.Message.Send && it.message is ClosingSig -> it.copy(message = it.message.withChannelData(EncryptedChannelData.from(privateKey, this@ChannelState), logger)) else -> it } } @@ -114,7 +117,7 @@ sealed class ChannelState { } } - private fun ChannelContext.emitClosingEvents(oldState: ChannelStateWithCommitments, newState: Closing): List { + internal fun ChannelContext.emitClosingEvents(oldState: ChannelStateWithCommitments, newState: Closing): List { val channelBalance = oldState.commitments.latest.localCommit.spec.toLocal return if (channelBalance > 0.msat) { when { @@ -223,20 +226,15 @@ sealed class ChannelState { is Normal -> forceClose(state) is ShuttingDown -> forceClose(state) is Negotiating -> when { - state.bestUnpublishedClosingTx != null -> { - // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that + state.publishedClosingTxs.isNotEmpty() -> { + // If we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that. val nextState = Closing( state.commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = state.closingTxProposed.flatten().map { it.unsignedTx } + listOf(state.bestUnpublishedClosingTx), - mutualClosePublished = listOf(state.bestUnpublishedClosingTx) - ) - val actions = listOf( - ChannelAction.Storage.StoreState(nextState), - ChannelAction.Blockchain.PublishTx(state.bestUnpublishedClosingTx), - ChannelAction.Blockchain.SendWatch(WatchConfirmed(state.channelId, state.bestUnpublishedClosingTx.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_TX_CONFIRMED(state.bestUnpublishedClosingTx.tx))) + waitingSinceBlock = state.waitingSinceBlock, + mutualCloseProposed = state.proposedClosingTxs.flatMap { it.all }, + mutualClosePublished = state.publishedClosingTxs ) - Pair(nextState, actions) + Pair(nextState, listOf(ChannelAction.Storage.StoreState(nextState))) } else -> forceClose(state) } @@ -266,20 +264,18 @@ sealed class ChannelState { logger.error { "peer sent error: ascii='${e.toAscii()}' bin=${e.data.toHex()}" } return when (this@ChannelState) { is Closing -> Pair(this@ChannelState, listOf()) // nothing to do, there is already a spending tx published - is Negotiating -> when (this@ChannelState.bestUnpublishedClosingTx) { - null -> this.spendLocalCurrent() - else -> { + is Negotiating -> when { + publishedClosingTxs.isNotEmpty() -> { + // If we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that. val nexState = Closing( commitments = commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, - mutualClosePublished = listOfNotNull(bestUnpublishedClosingTx) + waitingSinceBlock = waitingSinceBlock, + mutualCloseProposed = proposedClosingTxs.flatMap { it.all }, + mutualClosePublished = publishedClosingTxs ) - Pair(nexState, buildList { - add(ChannelAction.Storage.StoreState(nexState)) - addAll(doPublish(bestUnpublishedClosingTx, nexState.channelId)) - }) + Pair(nexState, listOf(ChannelAction.Storage.StoreState(nexState))) } + else -> this.spendLocalCurrent() } is WaitForFundingSigned -> Pair(Aborted, listOf(ChannelAction.Storage.RemoveChannel(this@ChannelState))) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) @@ -343,7 +339,6 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { override val channelId: ByteVector32 get() = commitments.channelId val isChannelOpener: Boolean get() = commitments.params.localParams.isChannelOpener val paysCommitTxFees: Boolean get() = commitments.params.localParams.paysCommitTxFees - val paysClosingFees: Boolean get() = commitments.params.localParams.paysClosingFees val remoteNodeId: PublicKey get() = commitments.remoteNodeId fun ChannelContext.channelKeys(): KeyManager.ChannelKeys = commitments.params.localParams.channelKeys(keyManager) @@ -434,8 +429,9 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { is Closing -> this@ChannelStateWithCommitments.copy(remoteCommitPublished = remoteCommitPublished) is Negotiating -> Closing( commitments = commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, + waitingSinceBlock = waitingSinceBlock, + mutualCloseProposed = proposedClosingTxs.flatMap { it.all }, + mutualClosePublished = publishedClosingTxs, remoteCommitPublished = remoteCommitPublished ) else -> Closing( @@ -463,8 +459,9 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { is Closing -> copy(nextRemoteCommitPublished = remoteCommitPublished) is Negotiating -> Closing( commitments = commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, + waitingSinceBlock = waitingSinceBlock, + mutualCloseProposed = proposedClosingTxs.flatMap { it.all }, + mutualClosePublished = publishedClosingTxs, nextRemoteCommitPublished = remoteCommitPublished ) else -> Closing( @@ -495,8 +492,9 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { } is Negotiating -> Closing( commitments = commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, + waitingSinceBlock = waitingSinceBlock, + mutualCloseProposed = proposedClosingTxs.flatMap { it.all }, + mutualClosePublished = publishedClosingTxs, revokedCommitPublished = listOf(revokedCommitPublished) ) else -> Closing( @@ -548,7 +546,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { val remoteCommitPublished = claimRemoteCommitMainOutput(channelKeys(), commitments.params, tx, currentOnChainFeerates().claimMainFeerate) val nextState = when (this@ChannelStateWithCommitments) { is Closing -> this@ChannelStateWithCommitments.copy(remoteCommitPublished = remoteCommitPublished) - is Negotiating -> Closing(commitments, waitingSinceBlock = currentBlockHeight.toLong(), mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, remoteCommitPublished = remoteCommitPublished) + is Negotiating -> Closing(commitments, waitingSinceBlock, proposedClosingTxs.flatMap { it.all }, publishedClosingTxs, remoteCommitPublished = remoteCommitPublished) else -> Closing(commitments, waitingSinceBlock = currentBlockHeight.toLong(), remoteCommitPublished = remoteCommitPublished) } return Pair(nextState, buildList { @@ -584,8 +582,9 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { is Closing -> copy(localCommitPublished = localCommitPublished) is Negotiating -> Closing( commitments = commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, + waitingSinceBlock = waitingSinceBlock, + mutualCloseProposed = proposedClosingTxs.flatMap { it.all }, + mutualClosePublished = publishedClosingTxs, localCommitPublished = localCommitPublished ) else -> Closing( @@ -621,18 +620,6 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { logger.error { channelEx.message } when { this@ChannelStateWithCommitments is Closing -> Pair(this@ChannelStateWithCommitments, listOf()) // nothing to do, there is already a spending tx published - this@ChannelStateWithCommitments is Negotiating && this@ChannelStateWithCommitments.bestUnpublishedClosingTx != null -> { - val nexState = Closing( - commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, - mutualClosePublished = listOfNotNull(bestUnpublishedClosingTx) - ) - Pair(nexState, buildList { - add(ChannelAction.Storage.StoreState(nexState)) - addAll(doPublish(bestUnpublishedClosingTx, nexState.channelId)) - }) - } else -> { val error = Error(channelId, channelEx.message) spendLocalCurrent().run { copy(second = second + ChannelAction.Message.Send(error)) } @@ -670,9 +657,6 @@ object Channel { // A dust limit of 354 sat ensures all segwit outputs will relay with default relay policies. val MIN_DUST_LIMIT = 354.sat - // we won't exchange more than this many signatures when negotiating the closing fee - const val MAX_NEGOTIATION_ITERATIONS = 20 - // this is defined in BOLT 11 val MIN_CLTV_EXPIRY_DELTA = CltvExpiryDelta(18) val MAX_CLTV_EXPIRY_DELTA = CltvExpiryDelta(2 * 7 * 144) // two weeks 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 be378bca1..636d35f3d 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 @@ -1,11 +1,9 @@ package fr.acinq.lightning.channel.states -import fr.acinq.bitcoin.Satoshi import fr.acinq.bitcoin.Transaction import fr.acinq.bitcoin.updated import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.blockchain.* -import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.Helpers.Closing.claimCurrentLocalCommitTxOutputs import fr.acinq.lightning.channel.Helpers.Closing.claimRemoteCommitTxOutputs @@ -15,24 +13,11 @@ 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.timedOutHtlcs -import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx import fr.acinq.lightning.utils.getValue import fr.acinq.lightning.wire.ChannelReestablish import fr.acinq.lightning.wire.Error -data class ClosingFees(val preferred: Satoshi, val min: Satoshi, val max: Satoshi) { - constructor(preferred: Satoshi) : this(preferred, preferred, preferred) -} - -data class ClosingFeerates(val preferred: FeeratePerKw, val min: FeeratePerKw, val max: FeeratePerKw) { - fun computeFees(closingTxWeight: Int): ClosingFees = ClosingFees(Transactions.weight2fee(preferred, closingTxWeight), Transactions.weight2fee(min, closingTxWeight), Transactions.weight2fee(max, closingTxWeight)) - - companion object { - operator fun invoke(preferred: FeeratePerKw): ClosingFeerates = ClosingFeerates(preferred, preferred / 2, preferred * 2) - } -} - sealed class ClosingType data class MutualClose(val tx: ClosingTx) : ClosingType() data class LocalClose(val localCommit: LocalCommit, val localCommitPublished: LocalCommitPublished) : ClosingType() 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 321f2062a..20a425245 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 @@ -1,144 +1,88 @@ package fr.acinq.lightning.channel.states +import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.Transaction -import fr.acinq.bitcoin.updated import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.blockchain.* +import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* -import fr.acinq.lightning.channel.states.Channel.MAX_NEGOTIATION_ITERATIONS +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx -import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.wire.ClosingSigned -import fr.acinq.lightning.wire.ClosingSignedTlv -import fr.acinq.lightning.wire.Error -import fr.acinq.lightning.wire.Shutdown +import fr.acinq.lightning.wire.* data class Negotiating( override val commitments: Commitments, - val localShutdown: Shutdown, - val remoteShutdown: Shutdown, - val closingTxProposed: List>, // one list for every negotiation (there can be several in case of disconnection) - val bestUnpublishedClosingTx: ClosingTx?, - val closingFeerates: ClosingFeerates? + val lastClosingFeerate: FeeratePerKw?, + val localScript: ByteVector, + val remoteScript: ByteVector, + // Closing transactions we created, where we pay the fees (unsigned). + 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 waitingSinceBlock: Long, // how many blocks since we initiated the closing ) : ChannelStateWithCommitments() { - init { - require(closingTxProposed.isNotEmpty()) { "there must always be a list for the current negotiation" } - require(!paysClosingFees || !closingTxProposed.any { it.isEmpty() }) { "the node paying the closing fees must have at least one closing signature for every negotiation attempt because it initiates the closing" } - } - override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> { return when (cmd) { is ChannelCommand.MessageReceived -> when (cmd.message) { - is ClosingSigned -> { - val remoteClosingFee = cmd.message.feeSatoshis - logger.info { "received closing fee=$remoteClosingFee" } - when (val result = - Helpers.Closing.checkClosingSignature(channelKeys(), commitments.latest, localShutdown.scriptPubKey.toByteArray(), remoteShutdown.scriptPubKey.toByteArray(), cmd.message.feeSatoshis, cmd.message.signature)) { - is Either.Left -> handleLocalError(cmd, result.value) - is Either.Right -> { - val (signedClosingTx, closingSignedRemoteFees) = result.value - val lastLocalClosingSigned = closingTxProposed.last().lastOrNull()?.localClosingSigned - when { - lastLocalClosingSigned?.feeSatoshis == remoteClosingFee -> { - logger.info { "they accepted our fee, publishing closing tx: closingTxId=${signedClosingTx.tx.txid}" } - completeMutualClose(signedClosingTx, null) - } - closingTxProposed.flatten().size >= MAX_NEGOTIATION_ITERATIONS -> { - logger.warning { "could not agree on closing fees after $MAX_NEGOTIATION_ITERATIONS iterations, publishing closing tx: closingTxId=${signedClosingTx.tx.txid}" } - completeMutualClose(signedClosingTx, closingSignedRemoteFees) - } - lastLocalClosingSigned?.tlvStream?.get()?.let { it.min <= remoteClosingFee && remoteClosingFee <= it.max } == true -> { - val localFeeRange = lastLocalClosingSigned.tlvStream.get()!! - logger.info { "they chose closing fee=$remoteClosingFee within our fee range (min=${localFeeRange.max} max=${localFeeRange.max}), publishing closing tx: closingTxId=${signedClosingTx.tx.txid}" } - completeMutualClose(signedClosingTx, closingSignedRemoteFees) - } - commitments.latest.localCommit.spec.toLocal == 0.msat -> { - logger.info { "we have nothing at stake, publishing closing tx: closingTxId=${signedClosingTx.tx.txid}" } - completeMutualClose(signedClosingTx, closingSignedRemoteFees) - } - else -> { - val theirFeeRange = cmd.message.tlvStream.get() - val ourFeeRange = closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate) - when { - theirFeeRange != null && !paysClosingFees -> { - // if we are not paying the on-chain fees and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation - // we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation - val closingFees = Helpers.Closing.firstClosingFee(commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, ourFeeRange) - val closingFee = when { - closingFees.preferred > theirFeeRange.max -> theirFeeRange.max - // if we underestimate the fee, then we're happy with whatever they propose (it will confirm more quickly and we're not paying it) - closingFees.preferred < remoteClosingFee -> remoteClosingFee - else -> closingFees.preferred - } - if (closingFee == remoteClosingFee) { - logger.info { "accepting their closing fee=$remoteClosingFee, publishing closing tx: closingTxId=${signedClosingTx.tx.txid}" } - completeMutualClose(signedClosingTx, closingSignedRemoteFees) - } else { - val (closingTx, closingSigned) = Helpers.Closing.makeClosingTx( - channelKeys(), - commitments.latest, - localShutdown.scriptPubKey.toByteArray(), - remoteShutdown.scriptPubKey.toByteArray(), - ClosingFees(closingFee, theirFeeRange.min, theirFeeRange.max) - ) - logger.info { "proposing closing fee=${closingSigned.feeSatoshis}" } - val closingProposed1 = closingTxProposed.updated( - closingTxProposed.lastIndex, - closingTxProposed.last() + listOf(ClosingTxProposed(closingTx, closingSigned)) - ) - val nextState = this@Negotiating.copy( - commitments = commitments.copy(remoteChannelData = cmd.message.channelData), - closingTxProposed = closingProposed1, - bestUnpublishedClosingTx = signedClosingTx - ) - val actions = listOf(ChannelAction.Storage.StoreState(nextState), ChannelAction.Message.Send(closingSigned)) - Pair(nextState, actions) - } - } - else -> { - val (closingTx, closingSigned) = run { - // if we are not the initiator and we were waiting for them to send their first closing_signed, we compute our firstClosingFee, otherwise we use the last one we sent - val localClosingFees = Helpers.Closing.firstClosingFee(commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, ourFeeRange) - val nextPreferredFee = Helpers.Closing.nextClosingFee(lastLocalClosingSigned?.feeSatoshis ?: localClosingFees.preferred, remoteClosingFee) - Helpers.Closing.makeClosingTx( - channelKeys(), - commitments.latest, - localShutdown.scriptPubKey.toByteArray(), - remoteShutdown.scriptPubKey.toByteArray(), - localClosingFees.copy(preferred = nextPreferredFee) - ) - } - when { - lastLocalClosingSigned?.feeSatoshis == closingSigned.feeSatoshis -> { - // next computed fee is the same than the one we previously sent (probably because of rounding) - logger.info { "accepting their closing fee=$remoteClosingFee, publishing closing tx: closingTxId=${signedClosingTx.tx.txid}" } - completeMutualClose(signedClosingTx, null) - } - closingSigned.feeSatoshis == remoteClosingFee -> { - logger.info { "we have converged, publishing closing tx: closingTxId=${signedClosingTx.tx.txid}" } - completeMutualClose(signedClosingTx, closingSigned) - } - else -> { - logger.info { "proposing closing fee=${closingSigned.feeSatoshis}" } - val closingProposed1 = closingTxProposed.updated( - closingTxProposed.lastIndex, - closingTxProposed.last() + listOf(ClosingTxProposed(closingTx, closingSigned)) - ) - val nextState = this@Negotiating.copy( - commitments = commitments.copy(remoteChannelData = cmd.message.channelData), - closingTxProposed = closingProposed1, - bestUnpublishedClosingTx = signedClosingTx - ) - val actions = listOf(ChannelAction.Storage.StoreState(nextState), ChannelAction.Message.Send(closingSigned)) - Pair(nextState, actions) - } - } - } - } - } + is Shutdown -> { + if (cmd.message.scriptPubKey != remoteScript) { + // This may lead to a signature mismatch: peers must use closing_complete to update their closing script. + logger.warning { "received shutdown changing remote script, this may lead to a signature mismatch (previous=$remoteScript, current=${cmd.message.scriptPubKey})" } + val nextState = this@Negotiating.copy(remoteScript = cmd.message.scriptPubKey) + Pair(nextState, listOf(ChannelAction.Storage.StoreState(nextState))) + } else { + // This is a retransmission of their previous shutdown, we can ignore it. + Pair(this@Negotiating, listOf()) + } + } + is ClosingComplete -> { + // Note that if there is a failure here and we don't send our closing_sig, they may eventually disconnect. + // On reconnection, we will retransmit shutdown with our latest scripts, so future signing attempts should work. + if (cmd.message.closeeScriptPubKey != localScript) { + logger.warning { "their closing_complete is not using our latest script: this may happen if we changed our script while they were sending closing_complete" } + // No need to persist their latest script, they will re-send it on reconnection. + val nextState = this@Negotiating.copy(remoteScript = cmd.message.closerScriptPubKey) + Pair(nextState, listOf(ChannelAction.Message.Send(Warning(channelId, InvalidCloseeScript(channelId, cmd.message.closeeScriptPubKey, localScript).message)))) + } else { + when (val result = Helpers.Closing.signClosingTx(channelKeys(), commitments.latest, cmd.message.closeeScriptPubKey, cmd.message.closerScriptPubKey, cmd.message)) { + is Either.Left -> { + logger.warning { "invalid closing_complete: ${result.value.message}" } + Pair(this@Negotiating, listOf(ChannelAction.Message.Send(Warning(channelId, result.value.message)))) } + is Either.Right -> { + val (signedClosingTx, closingSig) = result.value + logger.debug { "signing remote mutual close transaction: ${signedClosingTx.tx}" } + val nextState = this@Negotiating.copy(remoteScript = cmd.message.closerScriptPubKey, publishedClosingTxs = publishedClosingTxs + signedClosingTx) + val actions = listOf( + ChannelAction.Storage.StoreState(nextState), + ChannelAction.Blockchain.PublishTx(signedClosingTx), + ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, signedClosingTx.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_TX_CONFIRMED(signedClosingTx.tx))), + ChannelAction.Message.Send(closingSig) + ) + Pair(nextState, actions) + } + } + } + } + is ClosingSig -> { + when (val result = Helpers.Closing.receiveClosingSig(channelKeys(), commitments.latest, proposedClosingTxs.last(), cmd.message)) { + is Either.Left -> { + logger.warning { "invalid closing_sig: ${result.value.message}" } + Pair(this@Negotiating, listOf(ChannelAction.Message.Send(Warning(channelId, result.value.message)))) + } + is Either.Right -> { + val signedClosingTx = result.value + logger.debug { "received signatures for local mutual close transaction: ${signedClosingTx.tx}" } + val nextState = this@Negotiating.copy(publishedClosingTxs = publishedClosingTxs + signedClosingTx) + val actions = listOf( + ChannelAction.Storage.StoreState(nextState), + ChannelAction.Blockchain.PublishTx(signedClosingTx), + ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, signedClosingTx.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_TX_CONFIRMED(signedClosingTx.tx))), + ) + Pair(nextState, actions) } } } @@ -146,18 +90,23 @@ data class Negotiating( else -> unhandled(cmd) } is ChannelCommand.WatchReceived -> when (val watch = cmd.watch) { - is WatchEventConfirmed -> updateFundingTxStatus(watch) + is WatchEventConfirmed -> when { + // One of our published transactions confirmed, the channel is now closed. + publishedClosingTxs.any { it.tx.txid == watch.tx.txid } -> completeMutualClose(publishedClosingTxs.first { it.tx.txid == watch.tx.txid }) + // A transaction that we proposed for which they didn't send us their signature was confirmed, the channel is now closed. + proposedClosingTxs.flatMap { it.all }.any { it.tx.txid == watch.tx.txid } -> completeMutualClose(getMutualClosePublished(watch.tx)) + // Otherwise, this must be a funding transaction that just got confirmed. + else -> updateFundingTxStatus(watch) + } is WatchEventSpent -> when { - watch.event is BITCOIN_FUNDING_SPENT && closingTxProposed.flatten().any { it.unsignedTx.tx.txid == watch.tx.txid } -> { - // they can publish a closing tx with any sig we sent them, even if we are not done negotiating - logger.info { "closing tx published: closingTxId=${watch.tx.txid}" } + watch.event is BITCOIN_FUNDING_SPENT && publishedClosingTxs.any { it.tx.txid == watch.tx.txid } -> { + // This is one of the transactions we already published, we have nothing to do. + Pair(this@Negotiating, listOf()) + } + watch.event is BITCOIN_FUNDING_SPENT && proposedClosingTxs.flatMap { it.all }.any { it.tx.txid == watch.tx.txid } -> { + // They published one of our closing transactions without sending us their signature. val closingTx = getMutualClosePublished(watch.tx) - val nextState = Closing( - commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, - mutualClosePublished = listOf(closingTx) - ) + val nextState = this@Negotiating.copy(publishedClosingTxs = publishedClosingTxs + closingTx) val actions = listOf( ChannelAction.Storage.StoreState(nextState), ChannelAction.Blockchain.PublishTx(closingTx), @@ -173,7 +122,25 @@ data class Negotiating( is ChannelCommand.Htlc.Add -> handleCommandError(cmd, ChannelUnavailable(channelId)) is ChannelCommand.Htlc -> unhandled(cmd) is ChannelCommand.Close.ForceClose -> handleLocalError(cmd, ForcedLocalCommit(channelId)) - is ChannelCommand.Close.MutualClose -> handleCommandError(cmd, ClosingAlreadyInProgress(channelId)) + is ChannelCommand.Close.MutualClose -> { + if (lastClosingFeerate?.let { cmd.feerate < it } == true) { + handleCommandError(cmd, InvalidRbfFeerate(channelId, cmd.feerate, lastClosingFeerate * 1.2)) + } else { + when (val result = Helpers.Closing.makeClosingTxs(channelKeys(), commitments.latest, cmd.scriptPubKey ?: localScript, remoteScript, cmd.feerate, currentBlockHeight.toLong())) { + is Either.Left -> handleCommandError(cmd, result.value) + is Either.Right -> { + val (closingTxs, closingComplete) = result.value + logger.debug { "signing local mutual close transactions: $closingTxs" } + val nextState = this@Negotiating.copy(localScript = closingComplete.closerScriptPubKey, lastClosingFeerate = cmd.feerate, proposedClosingTxs = proposedClosingTxs + closingTxs) + val actions = buildList { + add(ChannelAction.Storage.StoreState(nextState)) + add(ChannelAction.Message.Send(closingComplete)) + } + Pair(nextState, actions) + } + } + } + } is ChannelCommand.Init -> unhandled(cmd) is ChannelCommand.Funding -> unhandled(cmd) is ChannelCommand.Closing -> unhandled(cmd) @@ -182,25 +149,27 @@ data class Negotiating( } } - /** Return full information about a known closing tx. */ + /** Return full information about a closing tx that we proposed and they then published. */ internal fun getMutualClosePublished(tx: Transaction): 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 closingTxProposed.flatten().first { it.unsignedTx.tx.txid == tx.txid }.unsignedTx.copy(tx = tx) + // 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) } - private fun ChannelContext.completeMutualClose(signedClosingTx: ClosingTx, closingSigned: ClosingSigned?): Pair> { - val nextState = Closing( - commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, - mutualClosePublished = listOf(signedClosingTx) + internal fun ChannelContext.completeMutualClose(signedClosingTx: ClosingTx): Pair> { + logger.info { "channel was closed with txId=${signedClosingTx.tx.txid}" } + val nextState = Closed( + Closing( + commitments, + waitingSinceBlock = waitingSinceBlock, + mutualCloseProposed = proposedClosingTxs.flatMap { it.all }, + mutualClosePublished = listOf(signedClosingTx) + ) ) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) - closingSigned?.let { add(ChannelAction.Message.Send(it)) } - add(ChannelAction.Blockchain.PublishTx(signedClosingTx)) - add(ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, signedClosingTx.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_TX_CONFIRMED(signedClosingTx.tx)))) + addAll(emitClosingEvents(this@Negotiating, nextState.state)) + add(ChannelAction.Storage.SetLocked(signedClosingTx.tx.txid)) } return Pair(nextState, actions) } 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 aa9074126..86d6e64a0 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 @@ -9,6 +9,7 @@ import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchEventConfirmed import fr.acinq.lightning.blockchain.WatchEventSpent +import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions @@ -22,7 +23,7 @@ data class Normal( val remoteChannelUpdate: ChannelUpdate?, val localShutdown: Shutdown?, val remoteShutdown: Shutdown?, - val closingFeerates: ClosingFeerates?, + val closingFeerate: FeeratePerKw?, val spliceStatus: SpliceStatus, ) : ChannelStateWithCommitments() { @@ -89,15 +90,16 @@ 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 when { localShutdown != null -> handleCommandError(cmd, ClosingAlreadyInProgress(channelId), channelUpdate) commitments.changes.localHasUnsignedOutgoingHtlcs() -> handleCommandError(cmd, CannotCloseWithUnsignedOutgoingHtlcs(channelId), channelUpdate) commitments.changes.localHasUnsignedOutgoingUpdateFee() -> handleCommandError(cmd, CannotCloseWithUnsignedOutgoingUpdateFee(channelId), channelUpdate) - !Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit) -> handleCommandError(cmd, InvalidFinalScript(channelId), channelUpdate) + !Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit, allowOpReturn) -> handleCommandError(cmd, InvalidFinalScript(channelId), channelUpdate) else -> { val shutdown = Shutdown(channelId, localScriptPubkey) - val newState = this@Normal.copy(localShutdown = shutdown, closingFeerates = cmd.feerates) + val newState = this@Normal.copy(localShutdown = shutdown, closingFeerate = cmd.feerate) val actions = listOf(ChannelAction.Storage.StoreState(newState), ChannelAction.Message.Send(shutdown)) Pair(newState, actions) } @@ -236,22 +238,10 @@ data class Normal( actions.add(ChannelAction.Message.Send(localShutdown)) if (commitments1.latest.remoteCommit.spec.htlcs.isNotEmpty()) { // we just signed htlcs that need to be resolved now - ShuttingDown(commitments1, localShutdown, remoteShutdown, closingFeerates) + ShuttingDown(commitments1, localShutdown, remoteShutdown, closingFeerate) } else { - logger.warning { "we have no htlcs but have not replied with our Shutdown yet, this should never happen" } - val closingTxProposed = if (paysClosingFees) { - val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( - channelKeys(), - commitments1.latest, - localShutdown.scriptPubKey.toByteArray(), - remoteShutdown.scriptPubKey.toByteArray(), - closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate), - ) - listOf(listOf(ClosingTxProposed(closingTx, closingSigned))) - } else { - listOf(listOf()) - } - Negotiating(commitments1, localShutdown, remoteShutdown, closingTxProposed, bestUnpublishedClosingTx = null, closingFeerates) + logger.warning { "we have no htlcs but have not replied with our shutdown yet, this should never happen" } + Negotiating(commitments1, closingFeerate, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, listOf(), listOf(), currentBlockHeight.toLong()) } } else { this@Normal.copy(commitments = commitments1) @@ -270,6 +260,7 @@ 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) // they have pending unsigned htlcs => they violated the spec, close the channel // they don't have pending unsigned htlcs // we have pending unsigned htlcs @@ -285,7 +276,7 @@ data class Normal( // there are pending signed changes => go to SHUTDOWN // there are no changes => go to NEGOTIATING when { - !Helpers.Closing.isValidFinalScriptPubkey(cmd.message.scriptPubKey, allowAnySegwit) -> handleLocalError(cmd, InvalidFinalScript(channelId)) + !Helpers.Closing.isValidFinalScriptPubkey(cmd.message.scriptPubKey, allowAnySegwit, allowOpReturn) -> handleLocalError(cmd, InvalidFinalScript(channelId)) commitments.changes.remoteHasUnsignedOutgoingHtlcs() -> handleLocalError(cmd, CannotCloseWithUnsignedOutgoingHtlcs(channelId)) commitments.changes.remoteHasUnsignedOutgoingUpdateFee() -> handleLocalError(cmd, CannotCloseWithUnsignedOutgoingUpdateFee(channelId)) commitments.changes.localHasUnsignedOutgoingHtlcs() -> { @@ -310,33 +301,10 @@ data class Normal( if (this@Normal.localShutdown == null) actions.add(ChannelAction.Message.Send(localShutdown)) val commitments1 = commitments.copy(remoteChannelData = cmd.message.channelData) when { - commitments1.hasNoPendingHtlcsOrFeeUpdate() && paysClosingFees -> { - val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( - channelKeys(), - commitments1.latest, - localShutdown.scriptPubKey.toByteArray(), - cmd.message.scriptPubKey.toByteArray(), - closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate), - ) - val nextState = Negotiating( - commitments1, - localShutdown, - cmd.message, - listOf(listOf(ClosingTxProposed(closingTx, closingSigned))), - bestUnpublishedClosingTx = null, - closingFeerates - ) - actions.addAll(listOf(ChannelAction.Storage.StoreState(nextState), ChannelAction.Message.Send(closingSigned))) - Pair(nextState, actions) - } - commitments1.hasNoPendingHtlcsOrFeeUpdate() -> { - val nextState = Negotiating(commitments1, localShutdown, cmd.message, listOf(listOf()), null, closingFeerates) - actions.add(ChannelAction.Storage.StoreState(nextState)) - Pair(nextState, actions) - } + commitments1.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation(commitments1, localShutdown, cmd.message, actions) else -> { // there are some pending changes, we need to wait for them to be settled (fail/fulfill htlcs and sign fee updates) - val nextState = ShuttingDown(commitments1, localShutdown, cmd.message, closingFeerates) + val nextState = ShuttingDown(commitments1, localShutdown, cmd.message, closingFeerate) actions.add(ChannelAction.Storage.StoreState(nextState)) Pair(nextState, actions) } @@ -407,7 +375,7 @@ data class Normal( spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.InsufficientFunds(balanceAfterFees, liquidityFees, spliceStatus.command.currentFeeCredit)) val action = listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidSpliceRequest(channelId).message))) Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), action) - } else if (spliceStatus.command.spliceOut?.scriptPubKey?.let { Helpers.Closing.isValidFinalScriptPubkey(it, allowAnySegwit = true) } == false) { + } else if (spliceStatus.command.spliceOut?.scriptPubKey?.let { Helpers.Closing.isValidFinalScriptPubkey(it, allowAnySegwit = true, allowOpReturn = true) } == false) { logger.warning { "cannot do splice: invalid splice-out script" } spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.InvalidSpliceOutPubKeyScript) val action = listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidSpliceRequest(channelId).message))) @@ -930,6 +898,39 @@ data class Normal( else -> null } + private fun ChannelContext.startClosingNegotiation(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, actions: List): Pair> { + val localScript = localShutdown.scriptPubKey + val remoteScript = remoteShutdown.scriptPubKey + val currentHeight = currentBlockHeight.toLong() + return when (closingFeerate) { + null -> { + logger.info { "mutual close was initiated by our peer, waiting for remote closing_complete" } + val nextState = Negotiating(commitments, closingFeerate, localScript, remoteScript, listOf(), listOf(), currentHeight) + val actions1 = listOf(ChannelAction.Storage.StoreState(nextState)) + Pair(nextState, actions + actions1) + } + else -> { + when (val closingResult = Helpers.Closing.makeClosingTxs(channelKeys(), commitments.latest, localScript, remoteScript, closingFeerate, currentHeight)) { + is Either.Left -> { + logger.warning { "cannot create local closing txs, waiting for remote closing_complete: ${closingResult.value.message}" } + val nextState = Negotiating(commitments, closingFeerate, localScript, remoteScript, listOf(), listOf(), currentHeight) + val actions1 = listOf(ChannelAction.Storage.StoreState(nextState)) + Pair(nextState, actions + actions1) + } + is Either.Right -> { + val (closingTxs, closingComplete) = closingResult.value + val nextState = Negotiating(commitments, closingFeerate, localScript, remoteScript, listOf(closingTxs), listOf(), currentHeight) + val actions1 = listOf( + ChannelAction.Storage.StoreState(nextState), + ChannelAction.Message.Send(closingComplete), + ) + Pair(nextState, actions + actions1) + } + } + } + } + } + private fun ChannelContext.handleCommandResult(command: ChannelCommand, result: Either>, commit: Boolean): Pair> { return when (result) { is Either.Left -> handleCommandError(command, result.value, channelUpdate) 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 10264fb9e..54e0d2413 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 @@ -56,8 +56,8 @@ data class Offline(val state: PersistedChannelState) : ChannelState() { is ChannelCommand.MessageReceived -> unhandled(cmd) is ChannelCommand.WatchReceived -> when (state) { is ChannelStateWithCommitments -> when (val watch = cmd.watch) { - is WatchEventConfirmed -> { - if (watch.event is BITCOIN_FUNDING_DEPTHOK) { + is WatchEventConfirmed -> when { + watch.event is BITCOIN_FUNDING_DEPTHOK -> { when (val res = state.run { acceptFundingTxConfirmed(watch) }) { is Either.Left -> Pair(this@Offline, listOf()) is Either.Right -> { @@ -75,26 +75,32 @@ data class Offline(val state: PersistedChannelState) : ChannelState() { Pair(this@Offline.copy(state = nextState), actions + listOf(ChannelAction.Storage.StoreState(nextState))) } } - } else { - Pair(this@Offline, listOf()) } + state is Negotiating && state.publishedClosingTxs.any { it.tx.txid == watch.tx.txid } -> { + // One of our published transactions confirmed, the channel is now closed. + state.run { completeMutualClose(publishedClosingTxs.first { it.tx.txid == watch.tx.txid }) } + } + state is Negotiating && state.proposedClosingTxs.flatMap { it.all }.any { it.tx.txid == watch.tx.txid } -> { + // A transaction that we proposed for which they didn't send us their signature was confirmed, the channel is now closed. + state.run { completeMutualClose(getMutualClosePublished(watch.tx)) } + } + else -> Pair(this@Offline, listOf()) } is WatchEventSpent -> when { - state is Negotiating && state.closingTxProposed.flatten().any { it.unsignedTx.tx.txid == watch.tx.txid } -> { - logger.info { "closing tx published: closingTxId=${watch.tx.txid}" } - val closingTx = state.getMutualClosePublished(watch.tx) - val nextState = Closing( - state.commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = state.closingTxProposed.flatten().map { it.unsignedTx }, - mutualClosePublished = listOf(closingTx) - ) + state is Negotiating && state.publishedClosingTxs.any { it.tx.txid == watch.tx.txid } -> { + // This is one of the transactions we already published, we have nothing to do. + Pair(this@Offline, listOf()) + } + state is Negotiating && state.proposedClosingTxs.flatMap { it.all }.any { it.tx.txid == watch.tx.txid } -> { + // They published one of our closing transactions without sending us their signature. + val closingTx = state.run { getMutualClosePublished(watch.tx) } + val nextState = state.copy(publishedClosingTxs = state.publishedClosingTxs + closingTx) val actions = listOf( ChannelAction.Storage.StoreState(nextState), ChannelAction.Blockchain.PublishTx(closingTx), ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, watch.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_TX_CONFIRMED(watch.tx))) ) - Pair(nextState, actions) + Pair(Offline(nextState), actions) } else -> { val (nextState, actions) = state.run { handlePotentialForceClose(watch) } 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 44a0d0d39..86b3c2bac 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 @@ -3,6 +3,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.blockchain.WatchEventConfirmed import fr.acinq.lightning.blockchain.WatchEventSpent +import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.wire.* @@ -11,7 +12,7 @@ data class ShuttingDown( override val commitments: Commitments, val localShutdown: Shutdown, val remoteShutdown: Shutdown, - val closingFeerates: ClosingFeerates? + val closingFeerate: FeeratePerKw? ) : ChannelStateWithCommitments() { override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) @@ -45,40 +46,16 @@ data class ShuttingDown( is Either.Right -> { val (commitments1, revocation) = result.value when { - commitments1.hasNoPendingHtlcsOrFeeUpdate() && paysClosingFees -> { - val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( - channelKeys(), - commitments1.latest, - localShutdown.scriptPubKey.toByteArray(), - remoteShutdown.scriptPubKey.toByteArray(), - closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate) - ) - val nextState = Negotiating( - commitments1, - localShutdown, - remoteShutdown, - listOf(listOf(ClosingTxProposed(closingTx, closingSigned))), - bestUnpublishedClosingTx = null, - closingFeerates - ) - val actions = listOf( - ChannelAction.Storage.StoreState(nextState), - ChannelAction.Message.Send(revocation), - ChannelAction.Message.Send(closingSigned) - ) - Pair(nextState, actions) - } - commitments1.hasNoPendingHtlcsOrFeeUpdate() -> { - val nextState = Negotiating(commitments1, localShutdown, remoteShutdown, listOf(listOf()), null, closingFeerates) - val actions = listOf(ChannelAction.Storage.StoreState(nextState), ChannelAction.Message.Send(revocation)) - Pair(nextState, actions) - } + commitments1.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation(commitments1, listOf(ChannelAction.Message.Send(revocation))) else -> { val nextState = this@ShuttingDown.copy(commitments = commitments1) - val actions = mutableListOf(ChannelAction.Storage.StoreState(nextState), ChannelAction.Message.Send(revocation)) - if (commitments1.changes.localHasChanges()) { - // if we have newly acknowledged changes let's sign them - actions.add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign)) + val actions = buildList { + add(ChannelAction.Storage.StoreState(nextState)) + add(ChannelAction.Message.Send(revocation)) + if (commitments1.changes.localHasChanges()) { + // if we have newly acknowledged changes let's sign them + add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign)) + } } Pair(nextState, actions) } @@ -91,43 +68,32 @@ data class ShuttingDown( is Either.Left -> handleLocalError(cmd, result.value) is Either.Right -> { val (commitments1, actions) = result.value - val actions1 = actions.toMutableList() when { - commitments1.hasNoPendingHtlcsOrFeeUpdate() && paysClosingFees -> { - val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( - channelKeys(), - commitments1.latest, - localShutdown.scriptPubKey.toByteArray(), - remoteShutdown.scriptPubKey.toByteArray(), - closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate) - ) - val nextState = Negotiating( - commitments1, - localShutdown, - remoteShutdown, - listOf(listOf(ClosingTxProposed(closingTx, closingSigned))), - bestUnpublishedClosingTx = null, - closingFeerates - ) - actions1.addAll(listOf(ChannelAction.Storage.StoreState(nextState), ChannelAction.Message.Send(closingSigned))) - Pair(nextState, actions1) - } - commitments1.hasNoPendingHtlcsOrFeeUpdate() -> { - val nextState = Negotiating(commitments1, localShutdown, remoteShutdown, listOf(listOf()), null, closingFeerates) - actions1.add(ChannelAction.Storage.StoreState(nextState)) - Pair(nextState, actions1) - } + commitments1.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation(commitments1, actions) else -> { val nextState = this@ShuttingDown.copy(commitments = commitments1) - actions1.add(ChannelAction.Storage.StoreState(nextState)) - if (commitments1.changes.localHasChanges() && commitments1.remoteNextCommitInfo.isLeft) { - actions1.add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign)) + val actions1 = buildList { + addAll(actions) + add(ChannelAction.Storage.StoreState(nextState)) + if (commitments1.changes.localHasChanges() && commitments1.remoteNextCommitInfo.isLeft) { + add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign)) + } } Pair(nextState, actions1) } } } } + is Shutdown -> { + if (cmd.message.scriptPubKey != remoteShutdown.scriptPubKey) { + logger.debug { "our peer updated their shutdown script (previous=${remoteShutdown.scriptPubKey}, current=${cmd.message.scriptPubKey})" } + val nextState = this@ShuttingDown.copy(remoteShutdown = cmd.message) + Pair(nextState, listOf(ChannelAction.Storage.StoreState(nextState))) + } else { + // This is a retransmission of their previous shutdown, we can ignore it. + Pair(this@ShuttingDown, listOf()) + } + } is Error -> { handleRemoteError(cmd.message) } @@ -174,7 +140,24 @@ data class ShuttingDown( is ChannelCommand.Htlc.Settlement.Fail -> handleCommandResult(cmd, commitments.sendFail(cmd, staticParams.nodeParams.nodePrivateKey), cmd.commit) is ChannelCommand.Htlc.Settlement.FailMalformed -> handleCommandResult(cmd, commitments.sendFailMalformed(cmd), cmd.commit) is ChannelCommand.Commitment.UpdateFee -> handleCommandResult(cmd, commitments.sendFee(cmd), cmd.commit) - is ChannelCommand.Close.MutualClose -> handleCommandError(cmd, ClosingAlreadyInProgress(channelId)) + is ChannelCommand.Close.MutualClose -> { + // We may be updating our closing script or feerate. + val localShutdown1 = cmd.scriptPubKey?.let { if (it != localShutdown.scriptPubKey) localShutdown.copy(scriptPubKey = it) else null } + when (localShutdown1) { + null -> { + val nextState = this@ShuttingDown.copy(closingFeerate = cmd.feerate) + Pair(nextState, listOf(ChannelAction.Storage.StoreState(nextState))) + } + else -> { + val nextState = this@ShuttingDown.copy(closingFeerate = cmd.feerate, localShutdown = localShutdown1) + val actions = listOf( + ChannelAction.Storage.StoreState(nextState), + ChannelAction.Message.Send(localShutdown1), + ) + Pair(nextState, actions) + } + } + } is ChannelCommand.Close.ForceClose -> handleLocalError(cmd, ForcedLocalCommit(channelId)) is ChannelCommand.WatchReceived -> when (val watch = cmd.watch) { is WatchEventConfirmed -> updateFundingTxStatus(watch) @@ -190,6 +173,39 @@ data class ShuttingDown( } } + private fun ChannelContext.startClosingNegotiation(commitments: Commitments, actions: List): Pair> { + val localScript = localShutdown.scriptPubKey + val remoteScript = remoteShutdown.scriptPubKey + val currentHeight = currentBlockHeight.toLong() + return when (closingFeerate) { + null -> { + logger.info { "mutual close was initiated by our peer, waiting for remote closing_complete" } + val nextState = Negotiating(commitments, closingFeerate, localScript, remoteScript, listOf(), listOf(), currentHeight) + val actions1 = listOf(ChannelAction.Storage.StoreState(nextState)) + Pair(nextState, actions + actions1) + } + else -> { + when (val closingResult = Helpers.Closing.makeClosingTxs(channelKeys(), commitments.latest, localScript, remoteScript, closingFeerate, currentHeight)) { + is Either.Left -> { + logger.warning { "cannot create local closing txs, waiting for remote closing_complete: ${closingResult.value.message}" } + val nextState = Negotiating(commitments, closingFeerate, localScript, remoteScript, listOf(), listOf(), currentHeight) + val actions1 = listOf(ChannelAction.Storage.StoreState(nextState)) + Pair(nextState, actions + actions1) + } + is Either.Right -> { + val (closingTxs, closingComplete) = closingResult.value + val nextState = Negotiating(commitments, closingFeerate, localScript, remoteScript, listOf(closingTxs), listOf(), currentHeight) + val actions1 = listOf( + ChannelAction.Storage.StoreState(nextState), + ChannelAction.Message.Send(closingComplete), + ) + Pair(nextState, actions + actions1) + } + } + } + } + } + private fun ChannelContext.handleCommandResult(command: ChannelCommand, result: Either>, commit: Boolean): Pair> { return when (result) { is Either.Left -> handleCommandError(command, result.value) 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 a14f9e471..524fcce3e 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 @@ -225,36 +225,11 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: } } } - // negotiation restarts from the beginning, and is initialized by the initiator - // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them - is Negotiating -> - if (state.paysClosingFees) { - // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch - val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( - channelKeys(), - state.commitments.latest, - state.localShutdown.scriptPubKey.toByteArray(), - state.remoteShutdown.scriptPubKey.toByteArray(), - state.closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate) - ) - val closingTxProposed1 = state.closingTxProposed + listOf(listOf(ClosingTxProposed(closingTx, closingSigned))) - val nextState = state.copy(closingTxProposed = closingTxProposed1) - val actions = listOf( - ChannelAction.Storage.StoreState(nextState), - ChannelAction.Message.Send(state.localShutdown), - ChannelAction.Message.Send(closingSigned) - ) - Pair(nextState, actions) - } else { - // we start a new round of negotiation - val closingTxProposed1 = if (state.closingTxProposed.last().isEmpty()) state.closingTxProposed else state.closingTxProposed + listOf(listOf()) - val nextState = state.copy(closingTxProposed = closingTxProposed1) - val actions = listOf( - ChannelAction.Storage.StoreState(nextState), - ChannelAction.Message.Send(state.localShutdown) - ) - Pair(nextState, actions) - } + is Negotiating -> { + // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. + val shutdown = Shutdown(channelId, state.localScript) + Pair(state, listOf(ChannelAction.Message.Send(shutdown))) + } is Closing, is Closed, is WaitForRemotePublishFutureCommitment -> unhandled(cmd) } Pair(nextState, buildList { @@ -271,21 +246,20 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: is ChannelCommand.WatchReceived -> when (state) { is ChannelStateWithCommitments -> when (val watch = cmd.watch) { is WatchEventSpent -> when { - state is Negotiating && state.closingTxProposed.flatten().any { it.unsignedTx.tx.txid == watch.tx.txid } -> { - logger.info { "closing tx published: closingTxId=${watch.tx.txid}" } - val closingTx = state.getMutualClosePublished(watch.tx) - val nextState = Closing( - state.commitments, - waitingSinceBlock = currentBlockHeight.toLong(), - mutualCloseProposed = state.closingTxProposed.flatten().map { it.unsignedTx }, - mutualClosePublished = listOf(closingTx) - ) + state is Negotiating && state.publishedClosingTxs.any { it.tx.txid == watch.tx.txid } -> { + // This is one of the transactions we already published, we have nothing to do. + Pair(this@Syncing, listOf()) + } + state is Negotiating && state.proposedClosingTxs.flatMap { it.all }.any { it.tx.txid == watch.tx.txid } -> { + // They published one of our closing transactions without sending us their signature. + val closingTx = state.run { getMutualClosePublished(watch.tx) } + val nextState = state.copy(publishedClosingTxs = state.publishedClosingTxs + closingTx) val actions = listOf( ChannelAction.Storage.StoreState(nextState), ChannelAction.Blockchain.PublishTx(closingTx), ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, watch.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_TX_CONFIRMED(watch.tx))) ) - Pair(nextState, actions) + Pair(this@Syncing.copy(state = nextState), actions) } else -> { val (nextState, actions) = state.run { handlePotentialForceClose(watch) } @@ -296,8 +270,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: } } } - is WatchEventConfirmed -> { - if (watch.event is BITCOIN_FUNDING_DEPTHOK) { + is WatchEventConfirmed -> when { + watch.event is BITCOIN_FUNDING_DEPTHOK -> { when (val res = state.run { acceptFundingTxConfirmed(watch) }) { is Either.Left -> Pair(this@Syncing, listOf()) is Either.Right -> { @@ -315,9 +289,16 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: Pair(this@Syncing.copy(state = nextState), actions + listOf(ChannelAction.Storage.StoreState(nextState))) } } - } else { - Pair(this@Syncing, listOf()) } + state is Negotiating && state.publishedClosingTxs.any { it.tx.txid == watch.tx.txid } -> { + // One of our published transactions confirmed, the channel is now closed. + state.run { completeMutualClose(publishedClosingTxs.first { it.tx.txid == watch.tx.txid }) } + } + state is Negotiating && state.proposedClosingTxs.flatMap { it.all }.any { it.tx.txid == watch.tx.txid } -> { + // A transaction that we proposed for which they didn't send us their signature was confirmed, the channel is now closed. + state.run { completeMutualClose(getMutualClosePublished(watch.tx)) } + } + else -> Pair(this@Syncing, listOf()) } } is WaitForFundingSigned -> Pair(this@Syncing, listOf()) @@ -406,7 +387,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // we resend the same updates and the same sig, and preserve the same ordering val signedUpdates = commitments.changes.localChanges.signed val commitSigs = commitments.active.map { it.nextRemoteCommit }.filterIsInstance().map { it.sig } - SyncResult.Success(retransmit = when (retransmitRevocation) { + val retransmit = when (retransmitRevocation) { null -> buildList { addAll(signedUpdates) addAll(commitSigs) @@ -424,7 +405,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: addAll(commitSigs) } } - }) + } + SyncResult.Success(retransmit) } remoteChannelReestablish.nextLocalCommitmentNumber == (commitments.nextRemoteCommitIndex + 1) -> { // we just sent a new commit_sig, they have received it but we haven't received their revocation 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 00f3ac999..f689ec50e 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,7 +7,6 @@ // serialization code in this file. JsonSerializers.CommitmentSerializer::class, JsonSerializers.CommitmentsSerializer::class, - JsonSerializers.ClosingFeeratesSerializer::class, JsonSerializers.LocalParamsSerializer::class, JsonSerializers.RemoteParamsSerializer::class, JsonSerializers.LocalCommitSerializer::class, @@ -41,7 +40,6 @@ JsonSerializers.TransactionSerializer::class, JsonSerializers.OutPointSerializer::class, JsonSerializers.TxOutSerializer::class, - JsonSerializers.ClosingTxProposedSerializer::class, JsonSerializers.LocalCommitPublishedSerializer::class, JsonSerializers.RemoteCommitPublishedSerializer::class, JsonSerializers.RevokedCommitPublishedSerializer::class, @@ -73,19 +71,20 @@ JsonSerializers.LocalFundingStatusSerializer::class, JsonSerializers.RemoteFundingStatusSerializer::class, JsonSerializers.ShutdownSerializer::class, - JsonSerializers.ClosingSignedSerializer::class, + JsonSerializers.ClosingCompleteSerializer::class, + JsonSerializers.ClosingSigSerializer::class, JsonSerializers.CommitSigSerializer::class, JsonSerializers.EncryptedChannelDataSerializer::class, JsonSerializers.ChannelReestablishDataSerializer::class, JsonSerializers.FundingCreatedSerializer::class, JsonSerializers.ChannelReadySerializer::class, JsonSerializers.ChannelReadyTlvShortChannelIdTlvSerializer::class, - JsonSerializers.ClosingSignedTlvFeeRangeSerializer::class, JsonSerializers.ShutdownTlvChannelDataSerializer::class, JsonSerializers.GenericTlvSerializer::class, JsonSerializers.TlvStreamSerializer::class, JsonSerializers.ShutdownTlvSerializer::class, - JsonSerializers.ClosingSignedTlvSerializer::class, + JsonSerializers.ClosingCompleteTlvSerializer::class, + JsonSerializers.ClosingSigTlvSerializer::class, JsonSerializers.ChannelReestablishTlvSerializer::class, JsonSerializers.ChannelReadyTlvSerializer::class, JsonSerializers.CommitSigTlvAlternativeFeerateSigSerializer::class, @@ -208,7 +207,6 @@ object JsonSerializers { subclass(CommitSigTlv.AlternativeFeerateSigs::class, CommitSigTlvAlternativeFeerateSigsSerializer) subclass(CommitSigTlv.Batch::class, CommitSigTlvBatchSerializer) subclass(ShutdownTlv.ChannelData::class, ShutdownTlvChannelDataSerializer) - subclass(ClosingSignedTlv.FeeRange::class, ClosingSignedTlvFeeRangeSerializer) subclass(UpdateAddHtlcTlv.PathKey::class, UpdateAddHtlcTlvPathKeySerializer) } // TODO The following declarations are required because serializers for [TransactionWithInputInfo] @@ -352,9 +350,6 @@ object JsonSerializers { @Serializer(forClass = Commitments::class) object CommitmentsSerializer - @Serializer(forClass = ClosingFeerates::class) - object ClosingFeeratesSerializer - @Serializer(forClass = LocalParams::class) object LocalParamsSerializer @@ -458,9 +453,6 @@ object JsonSerializers { delegateSerializer = KeyPathSerializer ) - @Serializer(forClass = ClosingTxProposed::class) - object ClosingTxProposedSerializer - @Serializer(forClass = LocalCommitPublished::class) object LocalCommitPublishedSerializer @@ -506,8 +498,11 @@ object JsonSerializers { @Serializer(forClass = Shutdown::class) object ShutdownSerializer - @Serializer(forClass = ClosingSigned::class) - object ClosingSignedSerializer + @Serializer(forClass = ClosingComplete::class) + object ClosingCompleteSerializer + + @Serializer(forClass = ClosingSig::class) + object ClosingSigSerializer @Serializer(forClass = CommitSig::class) object CommitSigSerializer @@ -524,9 +519,6 @@ object JsonSerializers { @Serializer(forClass = ChannelReadyTlv.ShortChannelIdTlv::class) object ChannelReadyTlvShortChannelIdTlvSerializer - @Serializer(forClass = ClosingSignedTlv.FeeRange::class) - object ClosingSignedTlvFeeRangeSerializer - @Serializer(forClass = ShutdownTlv.ChannelData::class) object ShutdownTlvChannelDataSerializer @@ -545,8 +537,11 @@ object JsonSerializers { @Serializer(forClass = CommitSigTlv::class) object CommitSigTlvSerializer - @Serializer(forClass = ClosingSignedTlv::class) - object ClosingSignedTlvSerializer + @Serializer(forClass = ClosingCompleteTlv::class) + object ClosingCompleteTlvSerializer + + @Serializer(forClass = ClosingSigTlv::class) + object ClosingSigTlvSerializer @Serializer(forClass = ChannelReadyTlv::class) object ChannelReadyTlvSerializer diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/ChannelState.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/ChannelState.kt index 1ea499568..7a106a467 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/ChannelState.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/ChannelState.kt @@ -15,6 +15,8 @@ PrivateKeyKSerializer::class, ShutdownSerializer::class, ClosingSignedSerializer::class, + ClosingCompleteSerializer::class, + ClosingSigSerializer::class, SatoshiKSerializer::class, UpdateAddHtlcSerializer::class, CommitSigSerializer::class, @@ -24,6 +26,8 @@ CommitSigTlvSerializer::class, ShutdownTlvSerializer::class, ClosingSignedTlvSerializer::class, + ClosingCompleteTlvSerializer::class, + ClosingSigTlvSerializer::class, ChannelReadyTlvSerializer::class, ChannelReestablishTlvSerializer::class, TlvStreamSerializer::class, @@ -71,6 +75,12 @@ object ShutdownSerializer @Serializer(forClass = ClosingSigned::class) object ClosingSignedSerializer +@Serializer(forClass = ClosingComplete::class) +object ClosingCompleteSerializer + +@Serializer(forClass = ClosingSig::class) +object ClosingSigSerializer + @Serializer(forClass = ChannelUpdate::class) object ChannelUpdateSerializer @@ -150,6 +160,12 @@ object CommitSigTlvSerializer @Serializer(forClass = ClosingSignedTlv::class) object ClosingSignedTlvSerializer +@Serializer(forClass = ClosingCompleteTlv::class) +object ClosingCompleteTlvSerializer + +@Serializer(forClass = ClosingSigTlv::class) +object ClosingSigTlvSerializer + @Serializer(forClass = ChannelReadyTlv::class) object ChannelReadyTlvSerializer @@ -382,9 +398,7 @@ internal data class ChannelVersion(val bits: ByteVector) { } @Serializable -internal data class ClosingTxProposed(val unsignedTx: Transactions.TransactionWithInputInfo.ClosingTx, val localClosingSigned: ClosingSigned) { - fun export() = fr.acinq.lightning.channel.ClosingTxProposed(unsignedTx, localClosingSigned) -} +internal data class ClosingTxProposed(val unsignedTx: Transactions.TransactionWithInputInfo.ClosingTx, val localClosingSigned: ClosingSigned) @Serializable internal data class Commitments( @@ -574,11 +588,12 @@ internal data class Negotiating( override fun export() = Negotiating( commitments.export(), - localShutdown, - remoteShutdown, - closingTxProposed.map { x -> x.map { it.export() } }, - bestUnpublishedClosingTx, - null + null, + localShutdown.scriptPubKey, + remoteShutdown.scriptPubKey, + listOf(), + listOf(), + 0 ) } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/ChannelState.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/ChannelState.kt index 1dc862ed1..5ad5e6788 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/ChannelState.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/ChannelState.kt @@ -15,6 +15,8 @@ PrivateKeyKSerializer::class, ShutdownSerializer::class, ClosingSignedSerializer::class, + ClosingCompleteSerializer::class, + ClosingSigSerializer::class, SatoshiKSerializer::class, UpdateAddHtlcSerializer::class, CommitSigSerializer::class, @@ -24,6 +26,8 @@ CommitSigTlvSerializer::class, ShutdownTlvSerializer::class, ClosingSignedTlvSerializer::class, + ClosingCompleteTlvSerializer::class, + ClosingSigTlvSerializer::class, ChannelReadyTlvSerializer::class, ChannelReestablishTlvSerializer::class, TlvStreamSerializer::class, @@ -71,6 +75,12 @@ object ShutdownSerializer @Serializer(forClass = ClosingSigned::class) object ClosingSignedSerializer +@Serializer(forClass = ClosingComplete::class) +object ClosingCompleteSerializer + +@Serializer(forClass = ClosingSig::class) +object ClosingSigSerializer + @Serializer(forClass = ChannelUpdate::class) object ChannelUpdateSerializer @@ -150,6 +160,12 @@ object CommitSigTlvSerializer @Serializer(forClass = ClosingSignedTlv::class) object ClosingSignedTlvSerializer +@Serializer(forClass = ClosingCompleteTlv::class) +object ClosingCompleteTlvSerializer + +@Serializer(forClass = ClosingSigTlv::class) +object ClosingSigTlvSerializer + @Serializer(forClass = ChannelReadyTlv::class) object ChannelReadyTlvSerializer @@ -369,14 +385,10 @@ internal data class ChannelFeatures(val bin: ByteVector) { } @Serializable -internal data class ClosingFeerates(val preferred: FeeratePerKw, val min: FeeratePerKw, val max: FeeratePerKw) { - fun export() = fr.acinq.lightning.channel.states.ClosingFeerates(preferred, min, max) -} +internal data class ClosingFeerates(val preferred: FeeratePerKw, val min: FeeratePerKw, val max: FeeratePerKw) @Serializable -internal data class ClosingTxProposed(val unsignedTx: Transactions.TransactionWithInputInfo.ClosingTx, val localClosingSigned: ClosingSigned) { - fun export() = fr.acinq.lightning.channel.ClosingTxProposed(unsignedTx, localClosingSigned) -} +internal data class ClosingTxProposed(val unsignedTx: Transactions.TransactionWithInputInfo.ClosingTx, val localClosingSigned: ClosingSigned) @Serializable internal data class Commitments( @@ -536,7 +548,7 @@ internal data class Normal( remoteChannelUpdate, localShutdown, remoteShutdown, - closingFeerates?.export(), + closingFeerates?.preferred, SpliceStatus.None, ) } @@ -555,7 +567,7 @@ internal data class ShuttingDown( commitments.export(), localShutdown, remoteShutdown, - closingFeerates?.export() + closingFeerates?.preferred ) } @@ -578,11 +590,12 @@ internal data class Negotiating( override fun export() = Negotiating( commitments.export(), - localShutdown, - remoteShutdown, - closingTxProposed.map { x -> x.map { it.export() } }, - bestUnpublishedClosingTx, - closingFeerates?.export() + closingFeerates?.preferred, + localShutdown.scriptPubKey, + remoteShutdown.scriptPubKey, + listOf(), + listOf(), + 0 ) } 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 b3cae75e6..5afad60d1 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 @@ -48,16 +48,19 @@ object Deserialization { 0x00 -> readWaitForFundingConfirmedWithPushAmount() 0x01 -> readWaitForChannelReady() 0x02 -> readNormalLegacy() - 0x03 -> readShuttingDown() - 0x04 -> readNegotiating() + 0x03 -> readShuttingDownBeforeSimpleClose() + 0x04 -> readNegotiatingBeforeSimpleClose() 0x05 -> readClosing() 0x06 -> readWaitForRemotePublishFutureCommitment() 0x07 -> readClosed() 0x0a -> readWaitForFundingSignedLegacy() - 0x0b -> readNormal() + 0x0b -> readNormalBeforeSimpleClose() 0x0c -> readWaitForFundingSignedWithPushAmount() 0x0d -> readWaitForFundingSigned() 0x0e -> readWaitForFundingConfirmed() + 0x0f -> readNormal() + 0x10 -> readShuttingDown() + 0x11 -> readNegotiating() else -> error("unknown discriminator $discriminator for class ${PersistedChannelState::class}") } @@ -148,7 +151,28 @@ object Deserialization { remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate }, localShutdown = readNullable { readLightningMessage() as Shutdown }, remoteShutdown = readNullable { readLightningMessage() as Shutdown }, - closingFeerates = readNullable { readClosingFeerates() }, + closingFeerate = readNullable { FeeratePerKw(readNumber().sat) }, + spliceStatus = when (val discriminator = read()) { + 0x00 -> SpliceStatus.None + 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(), readNullable { readLiquidityPurchase() }, readCollection { readChannelOrigin() }.toList()) + else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") + }, + ) + + private fun Input.readNormalBeforeSimpleClose(): Normal = Normal( + commitments = readCommitments(), + shortChannelId = ShortChannelId(readNumber()), + channelUpdate = readLightningMessage() as ChannelUpdate, + remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate }, + localShutdown = readNullable { readLightningMessage() as Shutdown }, + remoteShutdown = readNullable { readLightningMessage() as Shutdown }, + closingFeerate = readNullable { + // We used to store three closing feerates for fee range negotiation. + val preferred = FeeratePerKw(readNumber().sat) + readNumber() + readNumber() + preferred + }, spliceStatus = when (val discriminator = read()) { 0x00 -> SpliceStatus.None 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(), readNullable { readLiquidityPurchase() }, readCollection { readChannelOrigin() }.toList()) @@ -163,7 +187,13 @@ object Deserialization { remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate }, localShutdown = readNullable { readLightningMessage() as Shutdown }, remoteShutdown = readNullable { readLightningMessage() as Shutdown }, - closingFeerates = readNullable { readClosingFeerates() }, + closingFeerate = readNullable { + // We used to store three closing feerates for fee range negotiation. + val preferred = FeeratePerKw(readNumber().sat) + readNumber() + readNumber() + preferred + }, spliceStatus = when (val discriminator = read()) { 0x00 -> SpliceStatus.None 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(), null, readCollection { readChannelOrigin() }.toList()) @@ -171,27 +201,63 @@ object Deserialization { }, ) - private fun Input.readShuttingDown(): ShuttingDown = ShuttingDown( + private fun Input.readShuttingDownBeforeSimpleClose(): ShuttingDown = ShuttingDown( commitments = readCommitments(), localShutdown = readLightningMessage() as Shutdown, remoteShutdown = readLightningMessage() as Shutdown, - closingFeerates = readNullable { readClosingFeerates() } + closingFeerate = readNullable { + // We used to store three closing feerates for fee range negotiation. + val preferred = FeeratePerKw(readNumber().sat) + readNumber() + readNumber() + preferred + } ) - private fun Input.readNegotiating(): Negotiating = Negotiating( + private fun Input.readShuttingDown(): ShuttingDown = ShuttingDown( commitments = readCommitments(), localShutdown = readLightningMessage() as Shutdown, remoteShutdown = readLightningMessage() as Shutdown, - closingTxProposed = readCollection { + closingFeerate = readNullable { FeeratePerKw(readNumber().sat) } + ) + + private fun Input.readNegotiatingBeforeSimpleClose(): Negotiating { + val commitments = readCommitments() + val localShutdown = readLightningMessage() as Shutdown + val remoteShutdown = readLightningMessage() as Shutdown + // We cannot convert the closing transactions created with the old closing protocol to the new one. + // We simply ignore them, which will lead to a force-close if one of the proposed transactions is published. + readCollection { readCollection { - ClosingTxProposed( - unsignedTx = readTransactionWithInputInfo() as ClosingTx, - localClosingSigned = readLightningMessage() as ClosingSigned - ) + readTransactionWithInputInfo() // unsigned closing tx + readDelimitedByteArray() // closing_signed message }.toList() + }.toList() + val bestUnpublishedClosingTx = readNullable { readTransactionWithInputInfo() as ClosingTx } + val closingFeerate = readNullable { + // We used to store three closing feerates for fee range negotiation. + val preferred = FeeratePerKw(readNumber().sat) + readNumber() + readNumber() + preferred + } + return Negotiating(commitments, closingFeerate, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, listOf(), listOfNotNull(bestUnpublishedClosingTx), waitingSinceBlock = 0) + } + + private fun Input.readNegotiating(): Negotiating = Negotiating( + commitments = readCommitments(), + lastClosingFeerate = readNullable { FeeratePerKw(readNumber().sat) }, + localScript = readDelimitedByteArray().byteVector(), + remoteScript = readDelimitedByteArray().byteVector(), + proposedClosingTxs = readCollection { + Transactions.ClosingTxs( + readNullable { readTransactionWithInputInfo() as ClosingTx }, + readNullable { readTransactionWithInputInfo() as ClosingTx }, + readNullable { readTransactionWithInputInfo() as ClosingTx }, + ) }.toList(), - bestUnpublishedClosingTx = readNullable { readTransactionWithInputInfo() as ClosingTx }, - closingFeerates = readNullable { readClosingFeerates() } + publishedClosingTxs = readCollection { readTransactionWithInputInfo() as ClosingTx }.toList(), + waitingSinceBlock = readNumber() ) private fun Input.readClosing(): Closing = Closing( @@ -720,10 +786,4 @@ object Deserialization { 0x0e -> SpliceTx(input = readInputInfo(), tx = readTransaction()) else -> error("unknown discriminator $discriminator for class ${Transactions.TransactionWithInputInfo::class}") } - - private fun Input.readClosingFeerates(): ClosingFeerates = ClosingFeerates( - preferred = FeeratePerKw(readNumber().sat), - min = FeeratePerKw(readNumber().sat), - max = 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/v4/Serialization.kt index b00c0587e..0e03e0952 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/v4/Serialization.kt @@ -27,7 +27,6 @@ import fr.acinq.lightning.serialization.common.liquidityads.Serialization.writeL import fr.acinq.lightning.transactions.* import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* import fr.acinq.lightning.wire.LightningCodecs -import fr.acinq.lightning.wire.LiquidityAds /** * Serialization for [ChannelStateWithCommitments]. @@ -68,13 +67,13 @@ object Serialization { write(0x01); writeWaitForChannelReady(o) } is Normal -> { - write(0x0b); writeNormal(o) + write(0x0f); writeNormal(o) } is ShuttingDown -> { - write(0x03); writeShuttingDown(o) + write(0x10); writeShuttingDown(o) } is Negotiating -> { - write(0x04); writeNegotiating(o) + write(0x11); writeNegotiating(o) } is Closing -> { write(0x05); writeClosing(o) @@ -98,7 +97,8 @@ object Serialization { writeNullable(fundingTx) { writeBtcObject(it) } writeNumber(waitingSinceBlock) writeNullable(deferred) { writeLightningMessage(it) } - writeEither(lastSent, + writeEither( + lastSent, writeLeft = { writeLightningMessage(it) }, writeRight = { writeLightningMessage(it) } ) @@ -146,7 +146,7 @@ object Serialization { writeNullable(remoteChannelUpdate) { writeLightningMessage(it) } writeNullable(localShutdown) { writeLightningMessage(it) } writeNullable(remoteShutdown) { writeLightningMessage(it) } - writeNullable(closingFeerates) { writeClosingFeerates(it) } + writeNullable(closingFeerate) { writeNumber(it.toLong()) } when (spliceStatus) { is SpliceStatus.WaitingForSigs -> { write(0x01) @@ -164,23 +164,21 @@ object Serialization { writeCommitments(commitments) writeLightningMessage(localShutdown) writeLightningMessage(remoteShutdown) - writeNullable(closingFeerates) { writeClosingFeerates(it) } + writeNullable(closingFeerate) { writeNumber(it.toLong()) } } private fun Output.writeNegotiating(o: Negotiating) = o.run { writeCommitments(commitments) - writeLightningMessage(localShutdown) - writeLightningMessage(remoteShutdown) - writeCollection(closingTxProposed) { - writeCollection(it) { closingTxProposed -> - closingTxProposed.run { - writeTransactionWithInputInfo(unsignedTx) - writeLightningMessage(localClosingSigned) - } - } - } - writeNullable(bestUnpublishedClosingTx) { writeTransactionWithInputInfo(it) } - writeNullable(closingFeerates) { writeClosingFeerates(it) } + writeNullable(lastClosingFeerate) { writeNumber(it.toLong()) } + 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) } + } + writeCollection(publishedClosingTxs) { writeTransactionWithInputInfo(it) } + writeNumber(waitingSinceBlock) } private fun Output.writeClosing(o: Closing) = o.run { @@ -573,7 +571,8 @@ object Serialization { writeNumber(entry.key) writeString(entry.value.toString()) } - writeEither(remoteNextCommitInfo, + writeEither( + remoteNextCommitInfo, writeLeft = { writeNumber(it.sentAfterLocalCommitIndex) }, writeRight = { writePublicKey(it) } ) @@ -668,10 +667,4 @@ object Serialization { } } } - - private fun Output.writeClosingFeerates(o: ClosingFeerates): Unit = o.run { - writeNumber(preferred.toLong()) - writeNumber(min.toLong()) - writeNumber(max.toLong()) - } } \ No newline at end of file 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 fe755c57b..ef8bb43e1 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 @@ -139,6 +139,19 @@ object Transactions { } } + sealed class ClosingTxFee { + data class PaidByUs(val fee: Satoshi) : ClosingTxFee() + data class PaidByThem(val fee: Satoshi) : ClosingTxFee() + } + + @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) + + override fun toString(): String = "localAndRemote=${localAndRemote?.tx?.toString()}, localOnly=${localOnly?.tx?.toString()}, remoteOnly=${remoteOnly?.tx?.toString()}" + } + /** * When *local* *current* [[CommitTx]] is published: * - [[ClaimDelayedOutputTx]] spends to-local output of [[CommitTx]] after a delay @@ -196,11 +209,20 @@ object Transactions { Script.isPay2wpkh(script) -> 294.sat Script.isPay2wsh(script) -> 330.sat Script.isNativeWitnessScript(script) -> 354.sat + script[0] == OP_RETURN -> 0.sat // OP_RETURN is never dust else -> 546.sat } }.getOrElse { 546.sat } } + /** When an output is using OP_RETURN, we usually want to make sure its amount is 0, otherwise bitcoin nodes won't accept it. */ + fun isOpReturn(scriptPubKey: ByteVector): Boolean { + return runTrying { + val script = Script.parse(scriptPubKey) + script[0] == OP_RETURN + }.getOrElse { false } + } + /** Offered HTLCs below this amount will be trimmed. */ fun offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = dustLimit + weight2fee(spec.feerate, Commitments.HTLC_TIMEOUT_WEIGHT) @@ -735,39 +757,52 @@ object Transactions { } } - fun makeClosingTx( - commitTxInput: InputInfo, - localScriptPubKey: ByteArray, - remoteScriptPubKey: ByteArray, - localPaysClosingFees: Boolean, - dustLimit: Satoshi, - closingFee: Satoshi, - spec: CommitmentSpec - ): TransactionWithInputInfo.ClosingTx { + fun makeClosingTxs(input: InputInfo, spec: CommitmentSpec, fee: ClosingTxFee, lockTime: Long, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector): ClosingTxs { require(spec.htlcs.isEmpty()) { "there shouldn't be any pending htlcs" } - val (toLocalAmount, toRemoteAmount) = if (localPaysClosingFees) { - Pair(spec.toLocal.truncateToSatoshi() - closingFee, spec.toRemote.truncateToSatoshi()) - } else { - Pair(spec.toLocal.truncateToSatoshi(), spec.toRemote.truncateToSatoshi() - closingFee) - } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway - - val toLocalOutputOpt = toLocalAmount.takeIf { it >= dustLimit }?.let { TxOut(it, localScriptPubKey) } - val toRemoteOutputOpt = toRemoteAmount.takeIf { it >= dustLimit }?.let { TxOut(it, remoteScriptPubKey) } + val txNoOutput = Transaction(2, listOf(TxIn(input.outPoint, listOf(), sequence = 0xFFFFFFFDL)), listOf(), lockTime) - val tx = LexicographicalOrdering.sort( - Transaction( - version = 2, - txIn = listOf(TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = 0xffffffffL)), - txOut = listOfNotNull(toLocalOutputOpt, toRemoteOutputOpt), - lockTime = 0 + // We compute the remaining balance for each side after paying the closing fees. + // This lets us decide whether outputs can be included in the closing transaction or not. + val (toLocalAmount, toRemoteAmount) = when (fee) { + is ClosingTxFee.PaidByUs -> Pair(spec.toLocal.truncateToSatoshi() - fee.fee, spec.toRemote.truncateToSatoshi()) + is ClosingTxFee.PaidByThem -> Pair(spec.toLocal.truncateToSatoshi(), spec.toRemote.truncateToSatoshi() - fee.fee) + } + val toLocalOutput = when { + toLocalAmount >= dustLimit(localScriptPubKey) -> TxOut(if (isOpReturn(localScriptPubKey)) 0.sat else toLocalAmount, localScriptPubKey) + else -> null + } + val toRemoteOutput = when { + toRemoteAmount >= dustLimit(remoteScriptPubKey) -> TxOut(if (isOpReturn(remoteScriptPubKey)) 0.sat else toRemoteAmount, remoteScriptPubKey) + else -> null + } + // We may create multiple closing transactions based on which outputs may be included. + 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 + } + ClosingTxs( + localAndRemote = TransactionWithInputInfo.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), + remoteOnly = null + ) + } + toLocalOutput != null -> ClosingTxs( + localAndRemote = null, + localOnly = TransactionWithInputInfo.ClosingTx(input, txNoOutput.copy(txOut = listOf(toLocalOutput)), 0), + remoteOnly = null, ) - ) - val toLocalOutput = when (val toLocalIndex = findPubKeyScriptIndex(tx, localScriptPubKey)) { - is TxResult.Skipped -> null - is TxResult.Success -> toLocalIndex.result + toRemoteOutput != null -> ClosingTxs( + localAndRemote = null, + localOnly = null, + remoteOnly = TransactionWithInputInfo.ClosingTx(input, txNoOutput.copy(txOut = listOf(toRemoteOutput)), null) + ) + else -> ClosingTxs(localAndRemote = null, localOnly = null, remoteOnly = null) } - return TransactionWithInputInfo.ClosingTx(commitTxInput, tx, toLocalOutput) } private fun findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteArray): TxResult { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt index 227c7c5e5..f15fee5dc 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt @@ -238,3 +238,93 @@ sealed class ClosingSignedTlv : Tlv { } } } + +sealed class ClosingCompleteTlv : Tlv { + /** Signature for a closing transaction containing only the closer's output. */ + data class CloserOutputOnly(val sig: ByteVector64) : ClosingCompleteTlv() { + override val tag: Long get() = CloserOutputOnly.tag + override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out) + + companion object : TlvValueReader { + const val tag: Long = 1 + override fun read(input: Input): CloserOutputOnly = CloserOutputOnly(LightningCodecs.bytes(input, 64).toByteVector64()) + } + } + + /** Signature for a closing transaction containing only the closee's output. */ + data class CloseeOutputOnly(val sig: ByteVector64) : ClosingCompleteTlv() { + override val tag: Long get() = CloseeOutputOnly.tag + override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out) + + companion object : TlvValueReader { + const val tag: Long = 2 + override fun read(input: Input): CloseeOutputOnly = CloseeOutputOnly(LightningCodecs.bytes(input, 64).toByteVector64()) + } + } + + /** Signature for a closing transaction containing the closer and closee's outputs. */ + data class CloserAndCloseeOutputs(val sig: ByteVector64) : ClosingCompleteTlv() { + override val tag: Long get() = CloserAndCloseeOutputs.tag + override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out) + + companion object : TlvValueReader { + const val tag: Long = 3 + override fun read(input: Input): CloserAndCloseeOutputs = CloserAndCloseeOutputs(LightningCodecs.bytes(input, 64).toByteVector64()) + } + } + + data class ChannelData(val ecb: EncryptedChannelData) : ClosingCompleteTlv() { + override val tag: Long get() = ChannelData.tag + override fun write(out: Output) = LightningCodecs.writeBytes(ecb.data, out) + + companion object : TlvValueReader { + const val tag: Long = 0x47010000 + override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) + } + } +} + +sealed class ClosingSigTlv : Tlv { + /** Signature for a closing transaction containing only the closer's output. */ + data class CloserOutputOnly(val sig: ByteVector64) : ClosingSigTlv() { + override val tag: Long get() = CloserOutputOnly.tag + override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out) + + companion object : TlvValueReader { + const val tag: Long = 1 + override fun read(input: Input): CloserOutputOnly = CloserOutputOnly(LightningCodecs.bytes(input, 64).toByteVector64()) + } + } + + /** Signature for a closing transaction containing only the closee's output. */ + data class CloseeOutputOnly(val sig: ByteVector64) : ClosingSigTlv() { + override val tag: Long get() = CloseeOutputOnly.tag + override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out) + + companion object : TlvValueReader { + const val tag: Long = 2 + override fun read(input: Input): CloseeOutputOnly = CloseeOutputOnly(LightningCodecs.bytes(input, 64).toByteVector64()) + } + } + + /** Signature for a closing transaction containing the closer and closee's outputs. */ + data class CloserAndCloseeOutputs(val sig: ByteVector64) : ClosingSigTlv() { + override val tag: Long get() = CloserAndCloseeOutputs.tag + override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out) + + companion object : TlvValueReader { + const val tag: Long = 3 + override fun read(input: Input): CloserAndCloseeOutputs = CloserAndCloseeOutputs(LightningCodecs.bytes(input, 64).toByteVector64()) + } + } + + data class ChannelData(val ecb: EncryptedChannelData) : ClosingSigTlv() { + override val tag: Long get() = ChannelData.tag + override fun write(out: Output) = LightningCodecs.writeBytes(ecb.data, out) + + companion object : TlvValueReader { + const val tag: Long = 0x47010000 + override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) + } + } +} 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 b022baa9b..1560155e5 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 @@ -78,6 +78,8 @@ interface LightningMessage { ChannelUpdate.type -> ChannelUpdate.read(stream) Shutdown.type -> Shutdown.read(stream) ClosingSigned.type -> ClosingSigned.read(stream) + ClosingComplete.type -> ClosingComplete.read(stream) + ClosingSig.type -> ClosingSig.read(stream) OnionMessage.type -> OnionMessage.read(stream) WillAddHtlc.type -> WillAddHtlc.read(stream) WillFailHtlc.type -> WillFailHtlc.read(stream) @@ -1612,6 +1614,110 @@ data class ClosingSigned( } } +data class ClosingComplete( + override val channelId: ByteVector32, + val closerScriptPubKey: ByteVector, + val closeeScriptPubKey: ByteVector, + val fees: Satoshi, + val lockTime: Long, + val tlvStream: TlvStream = TlvStream.empty() +) : ChannelMessage, HasChannelId, HasEncryptedChannelData { + override val type: Long get() = ClosingComplete.type + + val closerOutputOnlySig: ByteVector64? = tlvStream.get()?.sig + val closeeOutputOnlySig: ByteVector64? = tlvStream.get()?.sig + val closerAndCloseeOutputsSig: ByteVector64? = tlvStream.get()?.sig + + override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty + override fun withNonEmptyChannelData(ecd: EncryptedChannelData): ClosingComplete = copy(tlvStream = tlvStream.addOrUpdate(ClosingCompleteTlv.ChannelData(ecd))) + + override fun write(out: Output) { + LightningCodecs.writeBytes(channelId, out) + LightningCodecs.writeU16(closerScriptPubKey.size(), out) + LightningCodecs.writeBytes(closerScriptPubKey, out) + LightningCodecs.writeU16(closeeScriptPubKey.size(), out) + LightningCodecs.writeBytes(closeeScriptPubKey, out) + LightningCodecs.writeU64(fees.toLong(), out) + LightningCodecs.writeU32(lockTime.toInt(), out) + TlvStreamSerializer(false, readers).write(tlvStream, out) + } + + companion object : LightningMessageReader { + const val type: Long = 40 + + @Suppress("UNCHECKED_CAST") + val readers = mapOf( + ClosingCompleteTlv.CloserOutputOnly.tag to ClosingCompleteTlv.CloserOutputOnly.Companion as TlvValueReader, + ClosingCompleteTlv.CloseeOutputOnly.tag to ClosingCompleteTlv.CloseeOutputOnly.Companion as TlvValueReader, + ClosingCompleteTlv.CloserAndCloseeOutputs.tag to ClosingCompleteTlv.CloserAndCloseeOutputs.Companion as TlvValueReader, + ClosingCompleteTlv.ChannelData.tag to ClosingCompleteTlv.ChannelData.Companion as TlvValueReader + ) + + override fun read(input: Input): ClosingComplete { + return ClosingComplete( + LightningCodecs.bytes(input, 32).byteVector32(), + LightningCodecs.bytes(input, LightningCodecs.u16(input)).byteVector(), + LightningCodecs.bytes(input, LightningCodecs.u16(input)).byteVector(), + LightningCodecs.u64(input).sat, + LightningCodecs.u32(input).toLong(), + TlvStreamSerializer(false, readers).read(input) + ) + } + } +} + +data class ClosingSig( + override val channelId: ByteVector32, + val closerScriptPubKey: ByteVector, + val closeeScriptPubKey: ByteVector, + val fees: Satoshi, + val lockTime: Long, + val tlvStream: TlvStream = TlvStream.empty() +) : ChannelMessage, HasChannelId, HasEncryptedChannelData { + override val type: Long get() = ClosingSig.type + + val closerOutputOnlySig: ByteVector64? = tlvStream.get()?.sig + val closeeOutputOnlySig: ByteVector64? = tlvStream.get()?.sig + val closerAndCloseeOutputsSig: ByteVector64? = tlvStream.get()?.sig + + override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty + override fun withNonEmptyChannelData(ecd: EncryptedChannelData): ClosingSig = copy(tlvStream = tlvStream.addOrUpdate(ClosingSigTlv.ChannelData(ecd))) + + override fun write(out: Output) { + LightningCodecs.writeBytes(channelId, out) + LightningCodecs.writeU16(closerScriptPubKey.size(), out) + LightningCodecs.writeBytes(closerScriptPubKey, out) + LightningCodecs.writeU16(closeeScriptPubKey.size(), out) + LightningCodecs.writeBytes(closeeScriptPubKey, out) + LightningCodecs.writeU64(fees.toLong(), out) + LightningCodecs.writeU32(lockTime.toInt(), out) + TlvStreamSerializer(false, readers).write(tlvStream, out) + } + + companion object : LightningMessageReader { + const val type: Long = 41 + + @Suppress("UNCHECKED_CAST") + val readers = mapOf( + ClosingSigTlv.CloserOutputOnly.tag to ClosingSigTlv.CloserOutputOnly.Companion as TlvValueReader, + ClosingSigTlv.CloseeOutputOnly.tag to ClosingSigTlv.CloseeOutputOnly.Companion as TlvValueReader, + ClosingSigTlv.CloserAndCloseeOutputs.tag to ClosingSigTlv.CloserAndCloseeOutputs.Companion as TlvValueReader, + ClosingSigTlv.ChannelData.tag to ClosingSigTlv.ChannelData.Companion as TlvValueReader + ) + + override fun read(input: Input): ClosingSig { + return ClosingSig( + LightningCodecs.bytes(input, 32).byteVector32(), + LightningCodecs.bytes(input, LightningCodecs.u16(input)).byteVector(), + LightningCodecs.bytes(input, LightningCodecs.u16(input)).byteVector(), + LightningCodecs.u64(input).sat, + LightningCodecs.u32(input).toLong(), + TlvStreamSerializer(false, readers).read(input) + ) + } + } +} + data class OnionMessage( val pathKey: PublicKey, val onionRoutingPacket: OnionRoutingPacket diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt index dae11c3c3..d292482ee 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt @@ -5,14 +5,12 @@ import fr.acinq.bitcoin.Bitcoin.computeP2PkhAddress import fr.acinq.bitcoin.Bitcoin.computeP2ShOfP2WpkhAddress import fr.acinq.bitcoin.Bitcoin.computeP2WpkhAddress import fr.acinq.lightning.Lightning.randomKey -import fr.acinq.lightning.channel.Helpers.Closing.checkClosingDustAmounts import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.sat import fr.acinq.secp256k1.Hex import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertTrue class HelpersTestsCommon : LightningTestSuite() { @@ -52,30 +50,24 @@ class HelpersTestsCommon : LightningTestSuite() { @Test fun `check closing tx amounts above dust`() { - val p2pkhBelowDust = listOf(TxOut(545.sat, Script.pay2pkh(randomKey().publicKey()))) - val p2shBelowDust = listOf(TxOut(539.sat, Script.pay2sh(Hex.decode("0000000000000000000000000000000000000000")))) - val p2wpkhBelowDust = listOf(TxOut(293.sat, Script.pay2wpkh(randomKey().publicKey()))) - val p2wshBelowDust = listOf(TxOut(329.sat, Script.pay2wsh(Hex.decode("0000000000000000000000000000000000000000")))) - val p2trBelowDust = listOf(TxOut(353.sat, Script.pay2tr(randomKey().publicKey().xOnly()))) - val allOutputsAboveDust = listOf( + val outputsBelowDust = listOf( + TxOut(545.sat, Script.pay2pkh(randomKey().publicKey())), + TxOut(539.sat, Script.pay2sh(Hex.decode("0000000000000000000000000000000000000000"))), + TxOut(293.sat, Script.pay2wpkh(randomKey().publicKey())), + TxOut(329.sat, Script.pay2wsh(Hex.decode("0000000000000000000000000000000000000000"))), + TxOut(353.sat, Script.pay2tr(randomKey().publicKey().xOnly())), + ) + outputsBelowDust.forEach { assertTrue(it.amount < Transactions.dustLimit(it.publicKeyScript)) } + + val outputsAboveDust = listOf( TxOut(546.sat, Script.pay2pkh(randomKey().publicKey())), TxOut(540.sat, Script.pay2sh(Hex.decode("0000000000000000000000000000000000000000"))), TxOut(294.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(330.sat, Script.pay2wsh(Hex.decode("0000000000000000000000000000000000000000"))), - TxOut(354.sat, Script.pay2tr(randomKey().publicKey().xOnly())) + TxOut(354.sat, Script.pay2tr(randomKey().publicKey().xOnly())), + TxOut(0.sat, listOf(OP_RETURN, OP_PUSHDATA(Hex.decode("deadbeef")))), ) - - fun toClosingTx(txOut: List): Transactions.TransactionWithInputInfo.ClosingTx { - val input = Transactions.InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000.sat, listOf()), listOf()) - return Transactions.TransactionWithInputInfo.ClosingTx(input, Transaction(2, listOf(), txOut, 0), null) - } - - assertTrue(checkClosingDustAmounts(toClosingTx(allOutputsAboveDust))) - assertFalse(checkClosingDustAmounts(toClosingTx(p2pkhBelowDust))) - assertFalse(checkClosingDustAmounts(toClosingTx(p2shBelowDust))) - assertFalse(checkClosingDustAmounts(toClosingTx(p2wpkhBelowDust))) - assertFalse(checkClosingDustAmounts(toClosingTx(p2wshBelowDust))) - assertFalse(checkClosingDustAmounts(toClosingTx(p2trBelowDust))) + outputsAboveDust.forEach { assertTrue { it.amount >= Transactions.dustLimit(it.publicKeyScript) } } } } \ No newline at end of file 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 649e0baf0..9489416f7 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 @@ -259,38 +259,60 @@ object TestsHelper { return Triple(alice1, bob1, fundingTx) } - fun mutualCloseAlice(alice: LNChannel, bob: LNChannel, scriptPubKey: ByteVector? = null, feerates: ClosingFeerates? = null): Triple, LNChannel, ClosingSigned> { - val (alice1, actionsAlice1) = alice.process(ChannelCommand.Close.MutualClose(scriptPubKey, feerates)) + fun mutualCloseAlice(alice: LNChannel, bob: LNChannel, closingFeerate: FeeratePerKw, scriptPubKey: ByteVector? = null): Triple, LNChannel, Transaction> { + val (alice1, actionsAlice1) = alice.process(ChannelCommand.Close.MutualClose(scriptPubKey, closingFeerate)) assertIs>(alice1) val shutdownAlice = actionsAlice1.findOutgoingMessage() - assertNull(actionsAlice1.findOutgoingMessageOpt()) + assertNull(actionsAlice1.findOutgoingMessageOpt()) val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(shutdownAlice)) assertIs>(bob1) val shutdownBob = actionsBob1.findOutgoingMessage() - assertNull(actionsBob1.findOutgoingMessageOpt()) + assertNull(actionsBob1.findOutgoingMessageOpt()) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) assertIs>(alice2) - val closingSignedAlice = actionsAlice2.findOutgoingMessage() - return Triple(alice2, bob1, closingSignedAlice) + val closingComplete = actionsAlice2.findOutgoingMessage() + + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(closingComplete)) + assertIs>(bob2) + val closingSig = actionsBob2.findOutgoingMessage() + + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(closingSig)) + 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) + return Triple(alice3, bob2, closingTx) } - fun mutualCloseBob(alice: LNChannel, bob: LNChannel, scriptPubKey: ByteVector? = null, feerates: ClosingFeerates? = null): Triple, LNChannel, ClosingSigned> { - val (bob1, actionsBob1) = bob.process(ChannelCommand.Close.MutualClose(scriptPubKey, feerates)) + fun mutualCloseBob(alice: LNChannel, bob: LNChannel, closingFeerate: FeeratePerKw, scriptPubKey: ByteVector? = null): Triple, LNChannel, Transaction> { + val (bob1, actionsBob1) = bob.process(ChannelCommand.Close.MutualClose(scriptPubKey, closingFeerate)) assertIs>(bob1) val shutdownBob = actionsBob1.findOutgoingMessage() - assertNull(actionsBob1.findOutgoingMessageOpt()) + assertNull(actionsBob1.findOutgoingMessageOpt()) val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(shutdownBob)) assertIs>(alice1) val shutdownAlice = actionsAlice1.findOutgoingMessage() - val closingSignedAlice = actionsAlice1.findOutgoingMessage() + assertNull(actionsAlice1.findOutgoingMessageOpt()) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(shutdownAlice)) assertIs>(bob2) - assertNull(actionsBob2.findOutgoingMessageOpt()) - return Triple(alice1, bob2, closingSignedAlice) + val closingComplete = actionsBob2.findOutgoingMessage() + + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(closingComplete)) + assertIs>(alice2) + val closingSig = actionsAlice2.findOutgoingMessage() + + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(closingSig)) + 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) + return Triple(alice2, bob3, closingTx) } fun localClose(s: LNChannel): Pair, LocalCommitPublished> { 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 08c3e527d..c812737be 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 @@ -1,12 +1,10 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* -import fr.acinq.bitcoin.Bitcoin.computeP2PkhAddress import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning import fr.acinq.lightning.blockchain.* -import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.TestsHelper.addHtlc import fr.acinq.lightning.channel.TestsHelper.claimHtlcSuccessTxs @@ -18,8 +16,6 @@ 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.mutualCloseAlice -import fr.acinq.lightning.channel.TestsHelper.mutualCloseBob import fr.acinq.lightning.channel.TestsHelper.reachNormal import fr.acinq.lightning.channel.TestsHelper.remoteClose import fr.acinq.lightning.channel.TestsHelper.useAlternativeCommitSig @@ -28,7 +24,10 @@ import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.utils.* +import fr.acinq.lightning.utils.UUID +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.utils.toMilliSatoshi import fr.acinq.lightning.wire.* import kotlin.test.* @@ -36,8 +35,9 @@ class ClosingTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Htlc_Add`() { - val (alice, _, _) = initMutualClose() - val (_, actions) = alice.process( + val (alice, _) = reachNormal() + val (alice1, _) = localClose(alice) + val (_, actions) = alice1.process( ChannelCommand.Htlc.Add( 1000000.msat, ByteVector32.Zeroes, @@ -47,91 +47,7 @@ class ClosingTestsCommon : LightningTestSuite() { ) ) assertEquals(1, actions.size) - assertTrue { (actions.first() as ChannelAction.ProcessCmdRes.AddFailed).error == ChannelUnavailable(alice.state.channelId) } - } - - @Test - fun `recv ChannelCommand_Htlc_Settlement_Fulfill -- nonexistent htlc`() { - val (alice, _, _) = initMutualClose() - val (_, actions) = alice.process(ChannelCommand.Htlc.Settlement.Fulfill(1, ByteVector32.Zeroes)) - assertTrue { actions.size == 1 && (actions.first() as ChannelAction.ProcessCmdRes.NotExecuted).t is UnknownHtlcId } - } - - @Test - fun `recv BITCOIN_FUNDING_SPENT -- mutual close before converging`() { - val (alice0, bob0) = reachNormal() - // alice initiates a closing with a low fee - val (alice1, aliceActions1) = alice0.process(ChannelCommand.Close.MutualClose(null, ClosingFeerates(FeeratePerKw(500.sat), FeeratePerKw(250.sat), FeeratePerKw(1000.sat)))) - val shutdown0 = aliceActions1.findOutgoingMessage() - val (bob1, bobActions1) = bob0.process(ChannelCommand.MessageReceived(shutdown0)) - assertIs(bob1.state) - val shutdown1 = bobActions1.findOutgoingMessage() - val (alice2, aliceActions2) = alice1.process(ChannelCommand.MessageReceived(shutdown1)) - assertIs(alice2.state) - val closingSigned0 = aliceActions2.findOutgoingMessage() - - // they don't converge yet, but bob has a publishable commit tx now - val (bob2, bobActions2) = bob1.process(ChannelCommand.MessageReceived(closingSigned0)) - assertIs(bob2.state) - val mutualCloseTx = bob2.state.bestUnpublishedClosingTx - assertNotNull(mutualCloseTx) - val closingSigned1 = bobActions2.findOutgoingMessage() - assertNotEquals(closingSigned0.feeSatoshis, closingSigned1.feeSatoshis) - - // let's make bob publish this closing tx - val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(Error(ByteVector32.Zeroes, ""))) - assertIs(bob3.state) - assertEquals(ChannelAction.Blockchain.PublishTx(mutualCloseTx), bobActions3.filterIsInstance().first()) - assertEquals(mutualCloseTx, bob3.state.mutualClosePublished.last()) - bobActions3.find().also { - assertEquals(mutualCloseTx.tx.txid, it.txId) - assertEquals(ChannelClosingType.Mutual, it.closingType) - assertTrue(it.isSentToDefaultAddress) - } - - // actual test starts here - val (bob4, _) = bob3.process(ChannelCommand.WatchReceived(WatchEventSpent(ByteVector32.Zeroes, BITCOIN_FUNDING_SPENT, mutualCloseTx.tx))) - val (bob5, bobActions5) = bob4.process(ChannelCommand.WatchReceived(WatchEventConfirmed(ByteVector32.Zeroes, BITCOIN_TX_CONFIRMED(mutualCloseTx.tx), 0, 0, mutualCloseTx.tx))) - assertIs(bob5.state) - assertContains(bobActions5, ChannelAction.Storage.SetLocked(mutualCloseTx.tx.txid)) - } - - @Test - fun `recv BITCOIN_TX_CONFIRMED -- mutual close`() { - val (alice0, _, _) = initMutualClose() - val mutualCloseTx = alice0.state.mutualClosePublished.last() - - // actual test starts here - val (alice1, actions1) = alice0.process(ChannelCommand.WatchReceived(WatchEventConfirmed(ByteVector32.Zeroes, BITCOIN_TX_CONFIRMED(mutualCloseTx.tx), 0, 0, mutualCloseTx.tx))) - assertIs(alice1.state) - assertContains(actions1, ChannelAction.Storage.SetLocked(mutualCloseTx.tx.txid)) - } - - @Test - fun `recv BITCOIN_TX_CONFIRMED -- mutual close with external btc address`() { - val pubKey = Lightning.randomKey().publicKey() - val bobBtcAddr = computeP2PkhAddress(pubKey, TestConstants.Bob.nodeParams.chainHash) - val bobFinalScript = Script.write(Script.pay2pkh(pubKey)).toByteVector() - - val (alice1, bob1) = reachNormal() - val (_, bob2, aliceClosingSigned) = mutualCloseBob(alice1, bob1, scriptPubKey = bobFinalScript) - - val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(aliceClosingSigned)) - assertIs(bob3.state) - val bobClosingSigned = bobActions3.findOutgoingMessageOpt() - assertNotNull(bobClosingSigned) - val mutualCloseTx = bob3.state.mutualClosePublished.last() - bobActions3.find().also { - assertEquals(mutualCloseTx.tx.txid, it.txId) - assertEquals(ChannelClosingType.Mutual, it.closingType) - assertFalse(it.isSentToDefaultAddress) - assertEquals(bobBtcAddr, it.address) - } - - // actual test starts here - val (bob4, bobActions4) = bob3.process(ChannelCommand.WatchReceived(WatchEventConfirmed(ByteVector32.Zeroes, BITCOIN_TX_CONFIRMED(mutualCloseTx.tx), 0, 0, mutualCloseTx.tx))) - assertIs(bob4.state) - assertContains(bobActions4, ChannelAction.Storage.SetLocked(mutualCloseTx.tx.txid)) + assertEquals((actions.first() as ChannelAction.ProcessCmdRes.AddFailed).error, ChannelUnavailable(alice.state.channelId)) } @Test @@ -554,15 +470,13 @@ class ClosingTestsCommon : LightningTestSuite() { @Test fun `recv BITCOIN_FUNDING_SPENT -- remote commit`() { - val (alice, _, bobCommitTxs) = initMutualClose(withPayments = true) - // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxs.last().commitTx.tx + val (alice0, bob0) = reachNormal() + val bobCommitTx = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx assertEquals(4, bobCommitTx.txOut.size) // main outputs and anchors - val (aliceClosing, remoteCommitPublished) = remoteClose(bobCommitTx, alice) + val (_, remoteCommitPublished) = remoteClose(bobCommitTx, alice0) assertNotNull(remoteCommitPublished.claimMainOutputTx) assertTrue(remoteCommitPublished.claimHtlcSuccessTxs().isEmpty()) assertTrue(remoteCommitPublished.claimHtlcTimeoutTxs().isEmpty()) - assertEquals(alice.state, aliceClosing.state.copy(remoteCommitPublished = null)) } @Test @@ -1714,26 +1628,17 @@ class ClosingTestsCommon : LightningTestSuite() { @Test fun `recv ChannelReestablish`() { - val (alice0, bob0, _) = initMutualClose() - val bobCurrentPerCommitmentPoint = bob0.commitments.params.localParams.channelKeys(bob0.ctx.keyManager).commitmentPoint(bob0.commitments.localCommitIndex) - val channelReestablish = ChannelReestablish(bob0.channelId, 42, 42, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint) - val (alice1, actions1) = alice0.process(ChannelCommand.MessageReceived(channelReestablish)) - assertIs(alice1.state) - assertNull(alice1.state.localCommitPublished) - assertNull(alice1.state.remoteCommitPublished) - assertTrue(alice1.state.mutualClosePublished.isNotEmpty()) - val error = actions1.hasOutgoingMessage() - assertEquals(error.toAscii(), FundingTxSpent(alice0.channelId, alice0.state.mutualClosePublished.first().tx.txid).message) - } - - @Test - fun `recv ChannelCommand_Close_MutualClose`() { - val (alice0, _, _) = initMutualClose() - val cmdClose = ChannelCommand.Close.MutualClose(null, null) - val (_, actions) = alice0.process(cmdClose) - val commandError = actions.filterIsInstance().first() - assertEquals(cmdClose, commandError.cmd) - assertEquals(ClosingAlreadyInProgress(alice0.channelId), commandError.t) + val (alice, bob) = reachNormal() + val (alice1, lcp) = localClose(alice) + val bobCurrentPerCommitmentPoint = bob.commitments.params.localParams.channelKeys(bob.ctx.keyManager).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) + assertNotNull(alice2.state.localCommitPublished) + assertEquals(lcp, alice2.state.localCommitPublished) + assertNull(alice2.state.remoteCommitPublished) + val error = actions2.hasOutgoingMessage() + assertEquals(error.toAscii(), FundingTxSpent(alice.channelId, lcp.commitTx.txid).message) } @Test @@ -1757,52 +1662,13 @@ class ClosingTestsCommon : LightningTestSuite() { @Test fun `recv Disconnected`() { - val (alice0, _, _) = initMutualClose() - val (alice1, _) = alice0.process(ChannelCommand.Disconnected) - assertTrue { alice1.state is Closing } + val (alice, _) = reachNormal() + val (alice1, _) = localClose(alice) + val (alice2, _) = alice1.process(ChannelCommand.Disconnected) + assertIs(alice2.state) } companion object { - fun initMutualClose(withPayments: Boolean = false): Triple, LNChannel, List> { - val (aliceInit, bobInit) = reachNormal() - var mutableAlice: LNChannel = aliceInit - var mutableBob: LNChannel = bobInit - - val bobCommitTxs = if (!withPayments) { - listOf() - } else { - listOf(100_000_000.msat, 200_000_000.msat, 300_000_000.msat).map { amount -> - val (nodes, r, htlc) = addHtlc(amount, payer = mutableAlice, payee = mutableBob) - mutableAlice = nodes.first - mutableBob = nodes.second - - with(crossSign(mutableAlice, mutableBob)) { - mutableAlice = first - mutableBob = second - } - - val bobCommitTx1 = mutableBob.commitments.latest.localCommit.publishableTxs - - with(fulfillHtlc(htlc.id, r, payer = mutableAlice, payee = mutableBob)) { - mutableAlice = first - mutableBob = second - } - with(crossSign(mutableBob, mutableAlice)) { - mutableBob = first - mutableAlice = second - } - - val bobCommitTx2 = mutableBob.commitments.latest.localCommit.publishableTxs - listOf(bobCommitTx1, bobCommitTx2) - }.flatten() - } - - val (alice1, bob1, aliceCloseSig) = mutualCloseAlice(mutableAlice, mutableBob) - val (alice2, bob2) = NegotiatingTestsCommon.converge(alice1, bob1, aliceCloseSig) ?: error("converge should not return null") - - return Triple(alice2, bob2, bobCommitTxs) - } - data class RevokedCloseFixture(val alice: LNChannel, val bob: LNChannel, val bobRevokedTxs: List, val htlcsAlice: List, val htlcsBob: List) fun prepareRevokedClose(): RevokedCloseFixture { 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 c1226aaf8..46461af78 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 @@ -2,14 +2,17 @@ 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.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* +import fr.acinq.lightning.channel.TestsHelper.addHtlc +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.mutualCloseAlice -import fr.acinq.lightning.channel.TestsHelper.mutualCloseBob import fr.acinq.lightning.channel.TestsHelper.reachNormal +import fr.acinq.lightning.db.ChannelClosingType import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat @@ -30,492 +33,454 @@ class NegotiatingTestsCommon : LightningTestSuite() { actions1.hasCommandError() } - private fun testClosingSignedDifferentFees(alice: LNChannel, bob: LNChannel, bobInitiates: Boolean = false) { - // alice and bob see different on-chain feerates - val alice1 = alice.updateFeerate(FeeratePerKw(5_000.sat)) - val bob1 = bob.updateFeerate(FeeratePerKw(7_500.sat)) - val (alice2, bob2, aliceCloseSig1) = if (bobInitiates) mutualCloseBob(alice1, bob1) else mutualCloseAlice(alice1, bob1) - - // alice is initiator so she initiates the negotiation - assertEquals(aliceCloseSig1.feeSatoshis, 3370.sat) // matches a feerate of 5000 sat/kw - val aliceFeeRange = aliceCloseSig1.tlvStream.get() - assertNotNull(aliceFeeRange) - assertTrue(aliceFeeRange.min < aliceCloseSig1.feeSatoshis) - assertTrue(aliceCloseSig1.feeSatoshis < aliceFeeRange.max) - assertEquals(alice2.state.closingTxProposed.size, 1) - assertEquals(alice2.state.closingTxProposed.last().size, 1) - assertNull(alice2.state.bestUnpublishedClosingTx) - - // bob answers with a counter proposition in alice's fee range - val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(aliceCloseSig1)) - assertIs>(bob3) - val bobCloseSig1 = bobActions3.findOutgoingMessage() - assertTrue(aliceFeeRange.min < bobCloseSig1.feeSatoshis) - assertTrue(bobCloseSig1.feeSatoshis < aliceFeeRange.max) - assertNotNull(bobCloseSig1.tlvStream.get()) - assertTrue(aliceCloseSig1.feeSatoshis < bobCloseSig1.feeSatoshis) - assertNotNull(bob3.state.bestUnpublishedClosingTx) - - // alice accepts this proposition - val (alice3, aliceActions3) = alice2.process(ChannelCommand.MessageReceived(bobCloseSig1)) - assertIs>(alice3) - val mutualCloseTx = aliceActions3.findPublishTxs().first() - assertEquals(aliceActions3.findWatch().txId, mutualCloseTx.txid) - assertEquals(mutualCloseTx.txOut.size, 2) // NB: anchors are removed from the closing tx - val aliceCloseSig2 = aliceActions3.findOutgoingMessage() - assertEquals(aliceCloseSig2.feeSatoshis, bobCloseSig1.feeSatoshis) - val (bob4, bobActions4) = bob3.process(ChannelCommand.MessageReceived(aliceCloseSig2)) - assertIs>(bob4) - bobActions4.hasPublishTx(mutualCloseTx) - assertEquals(bobActions4.findWatch().txId, mutualCloseTx.txid) - assertEquals(alice3.state.mutualClosePublished.map { it.tx }, listOf(mutualCloseTx)) - assertEquals(bob4.state.mutualClosePublished.map { it.tx }, listOf(mutualCloseTx)) - } - @Test - fun `recv ClosingSigned -- theirCloseFee != ourCloseFee`() { - val (alice, bob) = reachNormal() - testClosingSignedDifferentFees(alice, bob) + fun `recv ClosingComplete -- both outputs`() { + val (alice, bob, closingCompleteAlice, closingCompleteBob) = init() + + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(closingCompleteBob)) + assertEquals(4, actionsAlice1.size) + actionsAlice1.has() + val closingTxBob = actionsAlice1.findPublishTxs().first() + actionsAlice1.hasWatchConfirmed(closingTxBob.txid) + val closingSigAlice = actionsAlice1.findOutgoingMessage() + + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingCompleteAlice)) + assertEquals(4, actionsBob1.size) + actionsBob1.has() + val closingTxAlice = actionsBob1.findPublishTxs().first() + actionsBob1.hasWatchConfirmed(closingTxAlice.txid) + val closingSigBob = actionsBob1.findOutgoingMessage() + + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(closingSigBob)) + assertIs(alice2.state) + assertEquals(3, actionsAlice2.size) + actionsAlice2.has() + assertTrue(actionsAlice2.findPublishTxs().contains(closingTxAlice)) + actionsAlice2.hasWatchConfirmed(closingTxAlice.txid) + + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(closingSigAlice)) + assertIs(bob2.state) + assertEquals(3, actionsBob2.size) + actionsBob2.has() + assertTrue(actionsBob2.findPublishTxs().contains(closingTxBob)) + actionsBob2.hasWatchConfirmed(closingTxBob.txid) } @Test - fun `recv ClosingSigned -- theirCloseFee != ourCloseFee + bob starts closing`() { - val (alice, bob) = reachNormal() - testClosingSignedDifferentFees(alice, bob, bobInitiates = true) + fun `recv ClosingComplete -- both outputs -- external address`() { + val closingScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).toByteVector() + val (alice, bob, closingComplete, _) = init(aliceClosingScript = closingScript) + + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete)) + assertIs(bob1.state) + val closingTx = actionsBob1.findPublishTxs().first() + assertTrue(closingTx.txOut.any { it.publicKeyScript == closingScript }) + val closingSig = actionsBob1.findOutgoingMessage() + + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(closingSig)) + assertIs(alice1.state) + assertTrue(actionsAlice1.findPublishTxs().contains(closingTx)) } @Test - fun `recv ClosingSigned -- theirMinCloseFee greater than ourCloseFee`() { - val (alice, bob) = reachNormal() - val alice1 = alice.updateFeerate(FeeratePerKw(10_000.sat)) - val bob1 = bob.updateFeerate(FeeratePerKw(2_500.sat)) - - val (_, bob2, aliceCloseSig) = mutualCloseAlice(alice1, bob1) - val (bob3, actions) = bob2.process(ChannelCommand.MessageReceived(aliceCloseSig)) - assertIs>(bob3) - val bobCloseSig = actions.findOutgoingMessage() - assertEquals(bobCloseSig.feeSatoshis, aliceCloseSig.feeSatoshis) + fun `recv ClosingComplete -- single output`() { + val (alice, bob) = reachNormal(bobFundingAmount = 0.sat) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) + val shutdownAlice = actionsAlice1.findOutgoingMessage() + val (bob1, actionsBob1) = bob.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) + val shutdownBob = actionsBob1.findOutgoingMessage() + + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(shutdownAlice)) + assertNull(actionsBob2.findOutgoingMessageOpt()) // Bob cannot pay mutual close fees. + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) + val closingCompleteAlice = actionsAlice2.findOutgoingMessage() + assertNull(closingCompleteAlice.closerAndCloseeOutputsSig) + assertNotNull(closingCompleteAlice.closerOutputOnlySig) + + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(closingCompleteAlice)) + assertIs(bob3.state) + val closingTxAlice = actionsBob3.findPublishTxs().first() + assertEquals(1, closingTxAlice.txOut.size) + val closingSigBob = actionsBob3.findOutgoingMessage() + assertNotNull(closingSigBob.closerOutputOnlySig) + + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(closingSigBob)) + assertIs(alice3.state) + assertTrue(actionsAlice3.findPublishTxs().contains(closingTxAlice)) + actionsAlice3.hasWatchConfirmed(closingTxAlice.txid) } @Test - fun `recv ClosingSigned -- theirMaxCloseFee smaller than ourCloseFee`() { - val (alice, bob) = reachNormal() - val alice1 = alice.updateFeerate(FeeratePerKw(5_000.sat)) - val bob1 = bob.updateFeerate(FeeratePerKw(20_000.sat)) - - val (_, bob2, aliceCloseSig) = mutualCloseAlice(alice1, bob1) - val (_, actions) = bob2.process(ChannelCommand.MessageReceived(aliceCloseSig)) - val bobCloseSig = actions.findOutgoingMessage() - assertEquals(bobCloseSig.feeSatoshis, aliceCloseSig.tlvStream.get()!!.max) - } - - private fun testClosingSignedSameFees(alice: LNChannel, bob: LNChannel, bobInitiates: Boolean = false) { - val alice1 = alice.updateFeerate(FeeratePerKw(5_000.sat)) - val bob1 = bob.updateFeerate(FeeratePerKw(5_000.sat)) - val (alice2, bob2, aliceCloseSig1) = if (bobInitiates) mutualCloseBob(alice1, bob1) else mutualCloseAlice(alice1, bob1) - - // alice is initiator so she initiates the negotiation - assertEquals(aliceCloseSig1.feeSatoshis, 3370.sat) // matches a feerate of 5000 sat/kw - val aliceFeeRange = aliceCloseSig1.tlvStream.get() - assertNotNull(aliceFeeRange) - - // bob agrees with that proposal - val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(aliceCloseSig1)) - assertIs>(bob3) - val bobCloseSig1 = bobActions3.findOutgoingMessage() - assertNotNull(bobCloseSig1.tlvStream.get()) - assertEquals(aliceCloseSig1.feeSatoshis, bobCloseSig1.feeSatoshis) - val mutualCloseTx = bobActions3.findPublishTxs().first() - assertEquals(mutualCloseTx.txOut.size, 2) // NB: anchors are removed from the closing tx - - val (alice3, aliceActions3) = alice2.process(ChannelCommand.MessageReceived(bobCloseSig1)) - assertIs>(alice3) - aliceActions3.hasPublishTx(mutualCloseTx) + fun `recv ClosingComplete -- with concurrent script updates`() { + val (alice, bob, closingCompleteAlice1, closingCompleteBob1) = init() + + // Alice sends closing_sig in response to Bob's first closing_complete. + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(closingCompleteBob1)) + assertEquals(4, actionsAlice1.size) + actionsAlice1.has() + val closingTx1 = actionsAlice1.findPublishTxs().first() + actionsAlice1.hasWatchConfirmed(closingTx1.txid) + val closingSigAlice1 = actionsAlice1.hasOutgoingMessage() + + // Bob updates his closing script before receiving Alice's closing_complete and closing_sig. + val closingScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() + val (bob1, actionsBob1) = bob.process(ChannelCommand.Close.MutualClose(closingScript, TestConstants.feeratePerKw * 1.5)) + assertEquals(2, actionsBob1.size) + actionsBob1.has() + val closingCompleteBob2 = actionsBob1.findOutgoingMessage() + assertEquals(closingScript, closingCompleteBob2.closerScriptPubKey) + assertEquals(closingCompleteAlice1.closerScriptPubKey, closingCompleteBob2.closeeScriptPubKey) + + // Bob ignores Alice's obsolete closing_complete. + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(closingCompleteAlice1)) + actionsBob2.hasOutgoingMessage() + // Bob ignores Alice's obsolete closing_sig. + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(closingSigAlice1)) + actionsBob3.hasOutgoingMessage() + + // Alice handles Bob's updated closing_complete. + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(closingCompleteBob2)) + assertIs(alice2.state) + assertEquals(4, actionsAlice2.size) + actionsAlice2.has() + val closingTx2 = actionsAlice2.findPublishTxs().first() + assertTrue(closingTx2.txOut.any { it.publicKeyScript == closingScript }) + actionsAlice2.hasWatchConfirmed(closingTx2.txid) + val closingSigAlice2 = actionsAlice2.findOutgoingMessage() + + // Bob receives Alice's closing_sig for his updated closing_complete. + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(closingSigAlice2)) + assertIs(bob4.state) + assertEquals(3, actionsBob4.size) + actionsBob4.has() + assertEquals(closingTx2, actionsBob4.findPublishTxs().first()) + actionsBob4.hasWatchConfirmed(closingTx2.txid) } @Test - fun `recv ClosingSigned -- theirCloseFee == ourCloseFee`() { - val (alice, bob) = reachNormal() - testClosingSignedSameFees(alice, bob) + fun `recv ClosingComplete -- missing closee output`() { + val (_, bob, closingComplete, _) = init() + + // Bob expects to receive a signature for a closing transaction containing his output, so he ignores Alice's + // closing_complete instead of sending back his closing_sig. + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete.copy(tlvStream = TlvStream(ClosingCompleteTlv.CloserOutputOnly(closingComplete.closerOutputOnlySig!!))))) + assertIs(bob1.state) + assertEquals(1, actionsBob1.size) + actionsBob1.hasOutgoingMessage() + + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(closingComplete)) + assertIs(bob2.state) + val closingTxAlice = actionsBob2.findPublishTxs().first() + assertTrue(closingTxAlice.txOut.size == 2) + actionsBob2.findOutgoingMessage() } @Test - fun `recv ClosingSigned -- theirCloseFee == ourCloseFee + bob starts closing`() { - val (alice, bob) = reachNormal() - testClosingSignedSameFees(alice, bob, bobInitiates = true) - } + fun `recv ClosingSig -- missing signature`() { + val (alice, bob, closingComplete, _) = init() - @Test - fun `recv ClosingSigned -- theirCloseFee == ourCloseFee -- non-initiator pays commit fees`() { - val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, aliceFundingAmount = 200_000.sat, requestRemoteFunding = TestConstants.bobFundingAmount) - assertFalse(alice.commitments.params.localParams.paysCommitTxFees) - assertTrue(bob.commitments.params.localParams.paysCommitTxFees) - // Alice sends all of her balance to Bob. - val (nodes1, r, htlc) = TestsHelper.addHtlc(alice.commitments.availableBalanceForSend(), alice, bob) - val (alice1, bob1) = TestsHelper.crossSign(nodes1.first, nodes1.second) - val (alice2, bob2) = TestsHelper.fulfillHtlc(htlc.id, r, alice1, bob1) - val (bob3, alice3) = TestsHelper.crossSign(bob2, alice2) - assertEquals(0.msat, alice3.commitments.latest.localCommit.spec.toLocal) - // Alice and Bob agree on the current feerate. - val alice4 = alice3.updateFeerate(FeeratePerKw(3_000.sat)) - val bob4 = bob3.updateFeerate(FeeratePerKw(3_000.sat)) - // Bob initiates the mutual close. - val (bob5, actionsBob5) = bob4.process(ChannelCommand.Close.MutualClose(null, null)) - assertIs>(bob5) - val shutdownBob = actionsBob5.findOutgoingMessage() - assertNull(actionsBob5.findOutgoingMessageOpt()) - val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(shutdownBob)) - assertIs>(alice5) - val shutdownAlice = actionsAlice5.findOutgoingMessage() - assertNull(actionsAlice5.findOutgoingMessageOpt()) - val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(shutdownAlice)) - assertIs>(bob6) - val closingSignedBob = actionsBob6.findOutgoingMessage() - val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(closingSignedBob)) - assertIs(alice6.state) - val closingSignedAlice = actionsAlice6.findOutgoingMessage() - val mutualCloseTx = actionsAlice6.findPublishTxs().first() - assertEquals(1, mutualCloseTx.txOut.size) - val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(closingSignedAlice)) - assertIs(bob7.state) - actionsBob7.hasPublishTx(mutualCloseTx) + val (_, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete)) + val closingSig = actionsBob1.findOutgoingMessage() + + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(closingSig.copy(tlvStream = TlvStream.empty()))) + assertIs(alice1.state) + assertEquals(1, actionsAlice1.size) + actionsAlice1.hasOutgoingMessage() } @Test - fun `override on-chain fee estimator -- initiator`() { - val (alice, bob) = reachNormal() - val alice1 = alice.updateFeerate(FeeratePerKw(10_000.sat)) - val bob1 = bob.updateFeerate(FeeratePerKw(10_000.sat)) - - // alice initiates the negotiation with a very low feerate - val (alice2, bob2, aliceCloseSig) = mutualCloseAlice(alice1, bob1, feerates = ClosingFeerates(FeeratePerKw(2_500.sat), FeeratePerKw(2_000.sat), FeeratePerKw(3_000.sat))) - assertEquals(aliceCloseSig.feeSatoshis, 1685.sat) - assertEquals(aliceCloseSig.tlvStream.get(), ClosingSignedTlv.FeeRange(1348.sat, 2022.sat)) - - // bob chooses alice's highest fee - val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(aliceCloseSig)) - val bobCloseSig = bobActions3.findOutgoingMessage() - assertEquals(bobCloseSig.feeSatoshis, 2022.sat) - - // alice accepts this proposition - val (alice3, aliceActions3) = alice2.process(ChannelCommand.MessageReceived(bobCloseSig)) - assertIs>(alice3) - val mutualCloseTx = aliceActions3.findPublishTxs().first() - val aliceCloseSig2 = aliceActions3.findOutgoingMessage() - assertEquals(aliceCloseSig2.feeSatoshis, 2022.sat) - - val (bob4, bobActions4) = bob3.process(ChannelCommand.MessageReceived(aliceCloseSig2)) - assertIs>(bob4) - bobActions4.hasPublishTx(mutualCloseTx) + fun `recv ClosingSig -- invalid signature`() { + val (alice, bob, closingComplete, _) = init() + + val (_, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete)) + val closingSig = actionsBob1.findOutgoingMessage() + + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(closingSig.copy(tlvStream = TlvStream(ClosingSigTlv.CloserAndCloseeOutputs(randomBytes64()))))) + assertIs(alice1.state) + actionsAlice1.hasOutgoingMessage() } @Test - fun `override on-chain fee estimator -- non-initiator`() { - val (alice, bob) = reachNormal() - val alice1 = alice.updateFeerate(FeeratePerKw(10_000.sat)) - val bob1 = bob.updateFeerate(FeeratePerKw(10_000.sat)) - - // alice is initiator, so bob's override will simply be ignored - val (alice2, bob2, aliceCloseSig) = mutualCloseBob(alice1, bob1, feerates = ClosingFeerates(FeeratePerKw(2_500.sat), FeeratePerKw(2_000.sat), FeeratePerKw(3_000.sat))) - assertEquals(aliceCloseSig.feeSatoshis, 6740.sat) // matches a feerate of 10 000 sat/kw - - // bob directly agrees because their fee estimator matches - val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(aliceCloseSig)) - assertIs>(bob3) - val mutualCloseTx = bobActions3.findPublishTxs().first() - val bobCloseSig = bobActions3.findOutgoingMessage() - assertEquals(bobCloseSig.feeSatoshis, aliceCloseSig.feeSatoshis) - - // alice accepts this proposition - val (alice3, aliceActions3) = alice2.process(ChannelCommand.MessageReceived(bobCloseSig)) - assertIs>(alice3) - aliceActions3.hasPublishTx(mutualCloseTx) + fun `recv ClosingComplete and ClosingSig with encrypted channel data`() { + val (alice, bob, closingCompleteAlice, closingCompleteBob) = init() + assertTrue(alice.commitments.params.localParams.features.hasFeature(Feature.ChannelBackupProvider)) + assertTrue(bob.commitments.params.localParams.features.hasFeature(Feature.ChannelBackupClient)) + assertTrue(closingCompleteAlice.channelData.isEmpty()) + assertFalse(closingCompleteBob.channelData.isEmpty()) + val (_, actions1) = bob.process(ChannelCommand.MessageReceived(closingCompleteAlice)) + val closingSigBob = actions1.hasOutgoingMessage() + assertFalse(closingSigBob.channelData.isEmpty()) } @Test - fun `recv ClosingSigned -- nothing at stake`() { - val (alice, bob) = reachNormal(bobFundingAmount = 0.sat) - val alice1 = alice.updateFeerate(FeeratePerKw(5_000.sat)) - val bob1 = bob.updateFeerate(FeeratePerKw(10_000.sat)) - - // Bob has nothing at stake - val (_, bob2, aliceCloseSig) = mutualCloseBob(alice1, bob1) - val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(aliceCloseSig)) - assertIs>(bob3) - val mutualCloseTx = bobActions3.findPublishTxs().first() - assertEquals(bob3.state.mutualClosePublished.map { it.tx }, listOf(mutualCloseTx)) - assertEquals(bobActions3.findWatches().map { it.event }, listOf(BITCOIN_TX_CONFIRMED(mutualCloseTx))) + fun `recv BITCOIN_TX_CONFIRMED -- signed mutual close`() { + val (alice, bob, closingComplete, _) = init() + + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete)) + val closingTx = actionsBob1.findPublishTxs().first() + val closingSig = actionsBob1.findOutgoingMessage() + + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(closingSig)) + assertTrue(actionsAlice1.findPublishTxs().contains(closingTx)) + actionsAlice1.hasWatchConfirmed(closingTx.txid) + + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_TX_CONFIRMED(closingTx), 0, 0, closingTx))) + assertIs(alice2.state) + assertEquals(3, actionsAlice2.size) + actionsAlice2.has() + actionsAlice2.find().also { + assertEquals(closingTx.txid, it.txId) + assertEquals(ChannelClosingType.Mutual, it.closingType) + assertTrue(it.miningFees > 0.sat) + assertEquals(850_000.sat, it.amount + it.miningFees) + } + actionsAlice2.find().also { assertEquals(closingTx.txid, it.txId) } + + val (bob2, actionsBob2) = bob1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(bob.channelId, BITCOIN_TX_CONFIRMED(closingTx), 0, 0, closingTx))) + assertIs(bob2.state) + assertEquals(3, actionsBob2.size) + actionsBob2.has() + actionsBob2.find().also { + assertEquals(closingTx.txid, it.txId) + assertEquals(ChannelClosingType.Mutual, it.closingType) + assertEquals(150_000.sat, it.amount) + } + actionsBob2.find().also { assertEquals(closingTx.txid, it.txId) } } @Test - fun `recv ClosingSigned -- other side ignores our fee range + initiator`() { - val (alice, bob) = reachNormal() - val alice1 = alice.updateFeerate(FeeratePerKw(1_000.sat)) - val (alice2, bob2, aliceCloseSig1) = mutualCloseAlice(alice1, bob) - val aliceFeeRange = aliceCloseSig1.tlvStream.get() - assertNotNull(aliceFeeRange) - assertEquals(aliceCloseSig1.feeSatoshis, 674.sat) - assertEquals(aliceFeeRange.max, 1348.sat) - assertEquals(alice2.state.closingTxProposed.last().size, 1) - assertNull(alice2.state.bestUnpublishedClosingTx) - - // bob makes a proposal outside our fee range - val (_, bobCloseSig1) = makeLegacyClosingSigned(alice2, bob2, 2_500.sat) - val (alice3, actions3) = alice2.process(ChannelCommand.MessageReceived(bobCloseSig1)) - assertIs>(alice3) - val aliceCloseSig2 = actions3.findOutgoingMessage() - assertTrue(aliceCloseSig1.feeSatoshis < aliceCloseSig2.feeSatoshis) - assertTrue(aliceCloseSig2.feeSatoshis < 1600.sat) - assertEquals(alice3.state.closingTxProposed.last().size, 2) - assertNotNull(alice3.state.bestUnpublishedClosingTx) - - val (_, bobCloseSig2) = makeLegacyClosingSigned(alice2, bob2, 2_000.sat) - val (alice4, actions4) = alice3.process(ChannelCommand.MessageReceived(bobCloseSig2)) - assertIs>(alice4) - val aliceCloseSig3 = actions4.findOutgoingMessage() - assertTrue(aliceCloseSig2.feeSatoshis < aliceCloseSig3.feeSatoshis) - assertTrue(aliceCloseSig3.feeSatoshis < 1800.sat) - assertEquals(alice4.state.closingTxProposed.last().size, 3) - assertNotNull(alice4.state.bestUnpublishedClosingTx) - - val (_, bobCloseSig3) = makeLegacyClosingSigned(alice2, bob2, 1_800.sat) - val (alice5, actions5) = alice4.process(ChannelCommand.MessageReceived(bobCloseSig3)) - assertIs>(alice5) - val aliceCloseSig4 = actions5.findOutgoingMessage() - assertTrue(aliceCloseSig3.feeSatoshis < aliceCloseSig4.feeSatoshis) - assertTrue(aliceCloseSig4.feeSatoshis < 1800.sat) - assertEquals(alice5.state.closingTxProposed.last().size, 4) - assertNotNull(alice5.state.bestUnpublishedClosingTx) - - val (_, bobCloseSig4) = makeLegacyClosingSigned(alice2, bob2, aliceCloseSig4.feeSatoshis) - val (alice6, actions6) = alice5.process(ChannelCommand.MessageReceived(bobCloseSig4)) - assertIs>(alice6) - val mutualCloseTx = actions6.findPublishTxs().first() - assertEquals(alice6.state.mutualClosePublished.size, 1) - assertEquals(mutualCloseTx, alice6.state.mutualClosePublished.first().tx) + fun `recv BITCOIN_TX_CONFIRMED -- proposed mutual close`() { + val (alice, bob, closingComplete, _) = init() + + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete)) + val closingTx = actionsBob1.findPublishTxs().first() + actionsBob1.findOutgoingMessage() + + val (alice1, actionsAlice1) = alice.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_TX_CONFIRMED(closingTx), 0, 0, closingTx))) + assertIs(alice1.state) + assertEquals(3, actionsAlice1.size) + actionsAlice1.has() + actionsAlice1.find().also { + assertEquals(closingTx.txid, it.txId) + assertEquals(ChannelClosingType.Mutual, it.closingType) + assertTrue(it.miningFees > 0.sat) + assertEquals(850_000.sat, it.amount + it.miningFees) + } + actionsAlice1.find().also { assertEquals(closingTx.txid, it.txId) } + + val (bob2, actionsBob2) = bob1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(bob.channelId, BITCOIN_TX_CONFIRMED(closingTx), 0, 0, closingTx))) + assertIs(bob2.state) + assertEquals(3, actionsBob2.size) + actionsBob2.has() + actionsBob2.find().also { + assertEquals(closingTx.txid, it.txId) + assertEquals(ChannelClosingType.Mutual, it.closingType) + assertEquals(150_000.sat, it.amount) + } + actionsBob2.find().also { assertEquals(closingTx.txid, it.txId) } } @Test - fun `recv ClosingSigned -- other side ignores our fee range + non-initiator`() { - val (alice, bob) = reachNormal() - val bob1 = bob.updateFeerate(FeeratePerKw(10_000.sat)) - val (alice2, bob2, _) = mutualCloseBob(alice, bob1) - - // alice starts with a very low proposal - val (aliceCloseSig1, _) = makeLegacyClosingSigned(alice2, bob2, 500.sat) - val (bob3, actions3) = bob2.process(ChannelCommand.MessageReceived(aliceCloseSig1)) - assertIs>(bob3) - val bobCloseSig1 = actions3.findOutgoingMessage() - assertTrue(3000.sat < bobCloseSig1.feeSatoshis) - assertEquals(bob3.state.closingTxProposed.last().size, 1) - assertNotNull(bob3.state.bestUnpublishedClosingTx) - - val (aliceCloseSig2, _) = makeLegacyClosingSigned(alice2, bob2, 750.sat) - val (bob4, actions4) = bob3.process(ChannelCommand.MessageReceived(aliceCloseSig2)) - assertIs>(bob4) - val bobCloseSig2 = actions4.findOutgoingMessage() - assertTrue(2000.sat < bobCloseSig2.feeSatoshis) - assertEquals(bob4.state.closingTxProposed.last().size, 2) - assertNotNull(bob4.state.bestUnpublishedClosingTx) - - val (aliceCloseSig3, _) = makeLegacyClosingSigned(alice2, bob2, 1000.sat) - val (bob5, actions5) = bob4.process(ChannelCommand.MessageReceived(aliceCloseSig3)) - assertIs>(bob5) - val bobCloseSig3 = actions5.findOutgoingMessage() - assertTrue(1500.sat < bobCloseSig3.feeSatoshis) - assertEquals(bob5.state.closingTxProposed.last().size, 3) - assertNotNull(bob5.state.bestUnpublishedClosingTx) - - val (aliceCloseSig4, _) = makeLegacyClosingSigned(alice2, bob2, 1300.sat) - val (bob6, actions6) = bob5.process(ChannelCommand.MessageReceived(aliceCloseSig4)) - assertIs>(bob6) - val bobCloseSig4 = actions6.findOutgoingMessage() - assertTrue(1300.sat < bobCloseSig4.feeSatoshis) - assertEquals(bob6.state.closingTxProposed.last().size, 4) - assertNotNull(bob6.state.bestUnpublishedClosingTx) - - val (aliceCloseSig5, _) = makeLegacyClosingSigned(alice2, bob2, bobCloseSig4.feeSatoshis) - val (bob7, actions7) = bob6.process(ChannelCommand.MessageReceived(aliceCloseSig5)) - assertIs>(bob7) - val mutualCloseTx = actions7.findPublishTxs().first() - assertEquals(bob7.state.mutualClosePublished.size, 1) - assertEquals(mutualCloseTx, bob7.state.mutualClosePublished.first().tx) + fun `recv BITCOIN_FUNDING_SPENT -- proposed mutual close`() { + val (alice, bob, closingComplete, _) = init() + val (_, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete)) + val closingTx = actionsBob1.findPublishTxs().first() + actionsBob1.findOutgoingMessage() + + val (alice1, actionsAlice1) = alice.process(ChannelCommand.WatchReceived(WatchEventSpent(alice.channelId, BITCOIN_FUNDING_SPENT, closingTx))) + assertIs(alice1.state) + assertEquals(3, actionsAlice1.size) + actionsAlice1.has() + actionsAlice1.hasPublishTx(closingTx) + actionsAlice1.hasWatchConfirmed(closingTx.txid) + + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_TX_CONFIRMED(closingTx), 0, 0, closingTx))) + assertIs(alice2.state) + assertEquals(3, actionsAlice2.size) + actionsAlice2.has() + actionsAlice2.has() + actionsAlice2.find().also { assertEquals(closingTx.txid, it.txId) } } @Test - fun `recv ClosingSigned -- other side ignores our fee range + max iterations reached`() { - val (alice, bob) = reachNormal() - val alice1 = alice.updateFeerate(FeeratePerKw(1_000.sat)) - val (alice2, bob2, aliceCloseSig1) = mutualCloseAlice(alice1, bob) - assertIs>(alice2) - var mutableAlice: LNChannel = alice2 - var aliceCloseSig = aliceCloseSig1 - - for (i in 1..Channel.MAX_NEGOTIATION_ITERATIONS) { - val feeRange = aliceCloseSig.tlvStream.get() - assertNotNull(feeRange) - val bobNextFee = (aliceCloseSig.feeSatoshis + 500.sat).max(feeRange.max + 1.sat) - val (_, bobClosing) = makeLegacyClosingSigned(alice2, bob2, bobNextFee) - val (aliceNew, actions) = mutableAlice.process(ChannelCommand.MessageReceived(bobClosing)) - aliceCloseSig = actions.findOutgoingMessage() - assertIs>(aliceNew) - mutableAlice = aliceNew - } - - assertIs>(mutableAlice) - assertEquals(mutableAlice.state.mutualClosePublished.size, 1) + fun `recv BITCOIN_FUNDING_SPENT -- proposed mutual close -- offline`() { + val (alice, bob, closingComplete, _) = init() + val (_, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete)) + val closingTx = actionsBob1.findPublishTxs().first() + actionsBob1.findOutgoingMessage() + + val (aliceOffline, actionsAliceOffline) = alice.process(ChannelCommand.Disconnected) + assertIs(aliceOffline.state) + assertTrue(actionsAliceOffline.isEmpty()) + + val (alice1, actionsAlice1) = aliceOffline.process(ChannelCommand.WatchReceived(WatchEventSpent(alice.channelId, BITCOIN_FUNDING_SPENT, closingTx))) + assertIs(alice1.state) + assertEquals(3, actionsAlice1.size) + actionsAlice1.has() + actionsAlice1.hasPublishTx(closingTx) + actionsAlice1.hasWatchConfirmed(closingTx.txid) + + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_TX_CONFIRMED(closingTx), 0, 0, closingTx))) + assertIs(alice2.state) + assertEquals(3, actionsAlice2.size) + actionsAlice2.has() + actionsAlice2.has() + actionsAlice2.find().also { assertEquals(closingTx.txid, it.txId) } } @Test - fun `recv ClosingSigned -- invalid signature`() { - val (_, bob, aliceCloseSig) = init() - val (bob1, actions) = bob.process(ChannelCommand.MessageReceived(aliceCloseSig.copy(feeSatoshis = 99_000.sat))) - assertIs>(bob1) - actions.hasOutgoingMessage() - actions.hasWatch() - actions.findPublishTxs().contains(bob.commitments.latest.localCommit.publishableTxs.commitTx.tx) + fun `recv BITCOIN_FUNDING_SPENT -- proposed mutual close -- syncing`() { + val (alice, bob, closingComplete, _) = init() + val (_, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete)) + val closingTx = actionsBob1.findPublishTxs().first() + actionsBob1.findOutgoingMessage() + + val (aliceOffline, actionsAliceOffline) = alice.process(ChannelCommand.Disconnected) + assertIs(aliceOffline.state) + assertTrue(actionsAliceOffline.isEmpty()) + + val (aliceSyncing, actionsAliceSyncing) = aliceOffline.process(ChannelCommand.Connected(Init(alice.staticParams.nodeParams.features), Init(bob.staticParams.nodeParams.features))) + assertIs(aliceSyncing.state) + actionsAliceSyncing.hasOutgoingMessage() + + val (alice1, actionsAlice1) = aliceSyncing.process(ChannelCommand.WatchReceived(WatchEventSpent(alice.channelId, BITCOIN_FUNDING_SPENT, closingTx))) + assertIs(alice1.state) + assertEquals(3, actionsAlice1.size) + actionsAlice1.has() + actionsAlice1.hasPublishTx(closingTx) + actionsAlice1.hasWatchConfirmed(closingTx.txid) + + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_TX_CONFIRMED(closingTx), 0, 0, closingTx))) + assertIs(alice2.state) + assertEquals(3, actionsAlice2.size) + actionsAlice2.has() + actionsAlice2.has() + actionsAlice2.find().also { assertEquals(closingTx.txid, it.txId) } } @Test - fun `recv ClosingSigned with encrypted channel data`() { - val (alice, bob, aliceCloseSig) = init() - assertTrue(alice.commitments.params.localParams.features.hasFeature(Feature.ChannelBackupProvider)) - assertTrue(bob.commitments.params.localParams.features.hasFeature(Feature.ChannelBackupClient)) - assertTrue(aliceCloseSig.channelData.isEmpty()) - val (_, actions1) = bob.process(ChannelCommand.MessageReceived(aliceCloseSig)) - val bobCloseSig = actions1.hasOutgoingMessage() - assertFalse(bobCloseSig.channelData.isEmpty()) + fun `recv BITCOIN_FUNDING_SPENT -- their commit`() { + val (alice, bob, closingComplete, _) = init() + val (alice1, bob1) = signClosingTxAlice(alice, bob, closingComplete) + + val bobCommitTx = bob1.commitments.latest.localCommit.publishableTxs.commitTx.tx + val (alice2, actions2) = alice1.process(ChannelCommand.WatchReceived(WatchEventSpent(alice.channelId, BITCOIN_FUNDING_SPENT, bobCommitTx))) + assertIs>(alice2) + assertEquals(5, actions2.size) + assertNotNull(alice2.state.remoteCommitPublished) + assertTrue(alice2.state.mutualClosePublished.isNotEmpty()) + actions2.has() + val claimMain = actions2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + actions2.hasWatchConfirmed(bobCommitTx.txid) + actions2.hasWatchConfirmed(claimMain.txid) + actions2.has() } @Test - fun `recv BITCOIN_FUNDING_SPENT -- counterparty's mutual close`() { - // NB: we're not the initiator here - val (bob, alice, fundingTx) = reachNormal() - val priv = randomKey() - - // Alice initiates a mutual close with a custom final script - val finalScript = Script.write(Script.pay2pkh(priv.publicKey())).toByteVector() - val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(finalScript, null)) - val shutdownA = actions1.findOutgoingMessage() - - // Bob replies with Shutdown + ClosingSigned - val (bob1, actions2) = bob.process(ChannelCommand.MessageReceived(shutdownA)) - val shutdownB = actions2.findOutgoingMessage() - val closingSignedB = actions2.findOutgoingMessage() - - // Alice agrees with Bob's closing fee, publishes her closing tx and replies with her own ClosingSigned - val (alice2, _) = alice1.process(ChannelCommand.MessageReceived(shutdownB)) - val (alice3, actions4) = alice2.process(ChannelCommand.MessageReceived(closingSignedB)) - assertIs>(alice3) - val closingTxA = actions4.filterIsInstance().first().tx - val closingSignedA = actions4.findOutgoingMessage() - val watch = actions4.findWatch() - assertEquals(watch.txId, closingTxA.txid) - - assertEquals(fundingTx.txid, closingTxA.txIn[0].outPoint.txid) - // check that our closing tx is correctly signed - Transaction.correctlySpends(closingTxA, fundingTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - - // Bob published his closing tx (which should be the same as Alice's) - val (bob2, actions5) = bob1.process(ChannelCommand.MessageReceived(closingSignedA)) - assertIs>(bob2) - val closingTxB = actions5.filterIsInstance().first().tx - assertEquals(closingTxA, closingTxB) - - // Alice sees Bob's closing tx (which should be the same as the one she published) - val (alice4, _) = alice3.process(ChannelCommand.WatchReceived(WatchEventSpent(alice3.channelId, BITCOIN_FUNDING_SPENT, closingTxB))) - assertIs>(alice4) + fun `recv BITCOIN_FUNDING_SPENT -- revoked tx`() { + val (alice, bob, revokedCommit) = run { + val (alice0, bob0) = reachNormal() + val revokedCommit = bob0.commitments.latest.localCommit.publishableTxs.commitTx.tx + 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) + val (bob4, alice4) = crossSign(bob3, alice3) + Triple(alice4, bob4, revokedCommit) + } - val (alice5, _) = alice4.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice3.channelId, BITCOIN_TX_CONFIRMED(closingTxA), 144, 0, closingTxA))) - assertIs>(alice5) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) + val shutdownAlice = actionsAlice1.findOutgoingMessage() + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(shutdownAlice)) + assertIs>(bob1) + val shutdownBob = actionsBob1.findOutgoingMessage() + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) + assertIs>(alice2) + val closingComplete = actionsAlice2.findOutgoingMessage() + val (alice3, _) = signClosingTxAlice(alice2, bob1, closingComplete) + + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.WatchReceived(WatchEventSpent(alice.channelId, BITCOIN_FUNDING_SPENT, revokedCommit))) + assertIs>(alice4) + assertTrue(alice4.state.revokedCommitPublished.isNotEmpty()) + assertTrue(alice4.state.mutualClosePublished.isNotEmpty()) + actionsAlice4.has() + 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.has() } @Test - fun `recv BITCOIN_FUNDING_SPENT -- an older mutual close`() { - val (alice, bob) = reachNormal() - val alice1 = alice.updateFeerate(FeeratePerKw(1_000.sat)) - val bob1 = bob.updateFeerate(FeeratePerKw(10_000.sat)) - val (alice2, bob2, aliceCloseSig1) = mutualCloseAlice(alice1, bob1) - - val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(aliceCloseSig1)) - assertIs>(bob3) - bobActions3.findOutgoingMessage() - val firstMutualCloseTx = bob3.state.bestUnpublishedClosingTx - assertNotNull(firstMutualCloseTx) - - val (_, bobCloseSig1) = makeLegacyClosingSigned(alice2, bob2, 3_000.sat) - assertNotEquals(bobCloseSig1.feeSatoshis, aliceCloseSig1.feeSatoshis) - val (alice3, aliceActions3) = alice2.process(ChannelCommand.MessageReceived(bobCloseSig1)) - assertIs>(alice3) - val aliceCloseSig2 = aliceActions3.findOutgoingMessage() - assertNotEquals(aliceCloseSig2.feeSatoshis, bobCloseSig1.feeSatoshis) - val latestMutualCloseTx = alice3.state.bestUnpublishedClosingTx - assertNotNull(latestMutualCloseTx) - assertNotEquals(firstMutualCloseTx.tx.txid, latestMutualCloseTx.tx.txid) - - // at this point bob will receive a new signature, but he decides instead to publish the first mutual close - val (alice4, aliceActions4) = alice3.process(ChannelCommand.WatchReceived(WatchEventSpent(alice3.channelId, BITCOIN_FUNDING_SPENT, firstMutualCloseTx.tx))) - assertIs>(alice4) - aliceActions4.has() - aliceActions4.hasPublishTx(firstMutualCloseTx.tx) - assertEquals(aliceActions4.hasWatch().txId, firstMutualCloseTx.tx.txid) + fun `recv ChannelCommand_Close_MutualClose to bump feerate`() { + val (alice, _, closingComplete, _) = init() + val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw * 1.25)) + assertIs(alice1.state) + val closingComplete1 = actions1.hasOutgoingMessage() + assertTrue(closingComplete.fees < closingComplete1.fees) + assertEquals(closingComplete.closerScriptPubKey, closingComplete1.closerScriptPubKey) + assertEquals(closingComplete.closeeScriptPubKey, closingComplete1.closeeScriptPubKey) } @Test - fun `recv ChannelCommand_Close_MutualClose`() { - val (alice, _, _) = init() - val (alice1, actions) = alice.process(ChannelCommand.Close.MutualClose(null, null)) + fun `recv ChannelCommand_Close_MutualClose with feerate too low`() { + val (alice, _) = init() + val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(null, FeeratePerKw(2500.sat))) assertEquals(alice1, alice) - assertEquals(actions, listOf(ChannelAction.ProcessCmdRes.NotExecuted(ChannelCommand.Close.MutualClose(null, null), ClosingAlreadyInProgress(alice.channelId)))) + assertEquals(actions1, listOf(ChannelAction.ProcessCmdRes.NotExecuted(ChannelCommand.Close.MutualClose(null, FeeratePerKw(2500.sat)), InvalidRbfFeerate(alice.channelId, FeeratePerKw(2500.sat), FeeratePerKw(6000.sat))))) } @Test fun `recv Error`() { - val (alice, _, _) = init() - val (alice1, actions) = alice.process(ChannelCommand.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) + val (alice, _) = init() + val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) assertIs>(alice1) - actions.hasPublishTx(alice.commitments.latest.localCommit.publishableTxs.commitTx.tx) - assertTrue(actions.findWatches().map { it.event }.contains(BITCOIN_TX_CONFIRMED(alice.commitments.latest.localCommit.publishableTxs.commitTx.tx))) + actions1.hasPublishTx(alice.commitments.latest.localCommit.publishableTxs.commitTx.tx) + assertTrue(actions1.findWatches().map { it.event }.contains(BITCOIN_TX_CONFIRMED(alice.commitments.latest.localCommit.publishableTxs.commitTx.tx))) } companion object { - fun init(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs): Triple, LNChannel, ClosingSigned> { - val (alice, bob) = reachNormal(channelType = channelType) - return mutualCloseAlice(alice, bob) - } - - private fun makeLegacyClosingSigned(alice: LNChannel, bob: LNChannel, closingFee: Satoshi): Pair { - val aliceScript = alice.state.localShutdown.scriptPubKey.toByteArray() - val bobScript = bob.state.localShutdown.scriptPubKey.toByteArray() - val aliceKeys = alice.ctx.keyManager.channelKeys(alice.commitments.params.localParams.fundingKeyPath) - val bobKeys = bob.ctx.keyManager.channelKeys(bob.commitments.params.localParams.fundingKeyPath) - val (_, aliceClosingSigned) = Helpers.Closing.makeClosingTx(aliceKeys, alice.commitments.latest, aliceScript, bobScript, ClosingFees(closingFee, closingFee, closingFee)) - val (_, bobClosingSigned) = Helpers.Closing.makeClosingTx(bobKeys, bob.commitments.latest, bobScript, aliceScript, ClosingFees(closingFee, closingFee, closingFee)) - return Pair(aliceClosingSigned.copy(tlvStream = TlvStream.empty()), bobClosingSigned.copy(tlvStream = TlvStream.empty())) + data class Fixture(val alice: LNChannel, val bob: LNChannel, val closingCompleteAlice: ClosingComplete, val closingCompleteBob: ClosingComplete) + + fun init( + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + aliceFundingAmount: Satoshi = TestConstants.aliceFundingAmount, + bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, + aliceClosingFeerate: FeeratePerKw = TestConstants.feeratePerKw, + bobClosingFeerate: FeeratePerKw = TestConstants.feeratePerKw, + aliceClosingScript: ByteVector? = null, + bobClosingScript: ByteVector? = null, + ): Fixture { + val (alice, bob) = reachNormal(channelType = channelType, aliceFundingAmount = aliceFundingAmount, bobFundingAmount = bobFundingAmount) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.Close.MutualClose(aliceClosingScript, aliceClosingFeerate)) + val shutdownAlice = actionsAlice1.findOutgoingMessage() + assertNull(actionsAlice1.findOutgoingMessageOpt()) + + val (bob1, actionsBob1) = bob.process(ChannelCommand.Close.MutualClose(bobClosingScript, bobClosingFeerate)) + val shutdownBob = actionsBob1.findOutgoingMessage() + assertNull(actionsBob1.findOutgoingMessageOpt()) + + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(shutdownAlice)) + assertIs>(bob2) + val closingCompleteBob = actionsBob2.findOutgoingMessage() + + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) + assertIs>(alice2) + val closingCompleteAlice = actionsAlice2.findOutgoingMessage() + assertEquals(closingCompleteAlice.closerScriptPubKey, closingCompleteBob.closeeScriptPubKey) + assertEquals(closingCompleteAlice.closeeScriptPubKey, closingCompleteBob.closerScriptPubKey) + + return Fixture(alice2, bob2, closingCompleteAlice, closingCompleteBob) } - tailrec fun converge(a: LNChannel, b: LNChannel, aliceCloseSig: ClosingSigned?): Pair, LNChannel>? { - return when { - a.state !is ChannelStateWithCommitments || b.state !is ChannelStateWithCommitments -> null - a.state is Closing && b.state is Closing -> Pair(LNChannel(a.ctx, a.state), LNChannel(b.ctx, b.state)) - aliceCloseSig != null -> { - val (b1, actions) = b.process(ChannelCommand.MessageReceived(aliceCloseSig)) - val bobCloseSig = actions.findOutgoingMessageOpt() - if (bobCloseSig != null) { - val (a1, actions2) = a.process(ChannelCommand.MessageReceived(bobCloseSig)) - return converge(a1, b1, actions2.findOutgoingMessageOpt()) - } - val bobClosingTx = actions.filterIsInstance().map { it.tx }.firstOrNull() - if (bobClosingTx != null && bobClosingTx.txIn[0].outPoint == a.commitments.latest.localCommit.publishableTxs.commitTx.input.outPoint && a.state !is Closing) { - // Bob just spent the funding tx - val (a1, actions2) = a.process(ChannelCommand.WatchReceived(WatchEventSpent(a.channelId, BITCOIN_FUNDING_SPENT, bobClosingTx))) - actions2.find().also { assertEquals(bobClosingTx.txid, it.txId) } - return converge(a1, b1, actions2.findOutgoingMessageOpt()) - } - converge(a, b1, null) - } - else -> null - } + fun signClosingTxAlice(alice: LNChannel, bob: LNChannel, closingComplete: ClosingComplete): Pair, LNChannel> { + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete)) + assertIs>(bob1) + val closingTx = actionsBob1.findPublishTxs().first() + val closingSig = actionsBob1.findOutgoingMessage() + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(closingSig)) + assertIs>(alice1) + assertTrue(actionsAlice1.findPublishTxs().contains(closingTx)) + return Pair(alice1, bob1) } } 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 f9c426e59..96c8064c7 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 @@ -368,7 +368,7 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Htlc_Add -- after having sent Shutdown`() { val (alice0, _) = reachNormal() - val (alice1, actionsAlice1) = alice0.process(ChannelCommand.Close.MutualClose(null, null)) + val (alice1, actionsAlice1) = alice0.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) actionsAlice1.findOutgoingMessage() assertIs>(alice1) assertTrue(alice1.state.localShutdown != null && alice1.state.remoteShutdown == null) @@ -388,7 +388,7 @@ class NormalTestsCommon : LightningTestSuite() { actionsAlice1.findOutgoingMessage() // at the same time bob initiates a closing - val (_, actionsBob1) = bob0.process(ChannelCommand.Close.MutualClose(null, null)) + val (_, actionsBob1) = bob0.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) val shutdown = actionsBob1.findOutgoingMessage() val (alice2, _) = alice1.process(ChannelCommand.MessageReceived(shutdown)) @@ -1514,7 +1514,7 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Close_MutualClose -- no pending htlcs`() { val (alice, _) = reachNormal() assertNull(alice.state.localShutdown) - val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(null, null)) + val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertIs>(alice1) actions1.hasOutgoingMessage() assertNotNull(alice1.state.localShutdown) @@ -1525,7 +1525,7 @@ class NormalTestsCommon : LightningTestSuite() { val (alice, bob) = reachNormal() val (nodes, _, _) = addHtlc(1000.msat, payer = alice, payee = bob) val (alice1, _) = nodes - val (alice2, actions1) = alice1.process(ChannelCommand.Close.MutualClose(null, null)) + val (alice2, actions1) = alice1.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertIs>(alice2) actions1.hasCommandError() } @@ -1535,7 +1535,7 @@ class NormalTestsCommon : LightningTestSuite() { val (alice, bob) = reachNormal() val (nodes, _, _) = addHtlc(1000.msat, payer = alice, payee = bob) val (_, bob1) = nodes - val (bob2, actions1) = bob1.process(ChannelCommand.Close.MutualClose(null, null)) + val (bob2, actions1) = bob1.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertIs>(bob2) actions1.hasOutgoingMessage() assertNotNull(bob2.state.localShutdown) @@ -1545,16 +1545,7 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Close_MutualClose -- with invalid final script`() { val (alice, _) = reachNormal() assertNull(alice.state.localShutdown) - val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(ByteVector("00112233445566778899"), null)) - assertIs>(alice1) - actions1.hasCommandError() - } - - @Test - fun `recv ChannelCommand_Close_MutualClose -- with unsupported native segwit script`() { - val (alice, _) = reachNormal(aliceFeatures = TestConstants.Alice.nodeParams.features.remove(Feature.ShutdownAnySegwit)) - assertNull(alice.state.localShutdown) - val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(ByteVector("51050102030405"), null)) + val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(ByteVector("00112233445566778899"), TestConstants.feeratePerKw)) assertIs>(alice1) actions1.hasCommandError() } @@ -1563,7 +1554,7 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Close_MutualClose -- with native segwit script`() { val (alice, _) = reachNormal() assertNull(alice.state.localShutdown) - val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(ByteVector("51050102030405"), null)) + val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(ByteVector("51050102030405"), TestConstants.feeratePerKw)) actions1.hasOutgoingMessage() assertIs>(alice1) assertNotNull(alice1.state.localShutdown) @@ -1574,7 +1565,7 @@ class NormalTestsCommon : LightningTestSuite() { val (alice, bob) = reachNormal() val (nodes, _, _) = addHtlc(1000.msat, payer = alice, payee = bob) val (alice1, _) = crossSign(nodes.first, nodes.second) - val (alice2, actions1) = alice1.process(ChannelCommand.Close.MutualClose(null, null)) + val (alice2, actions1) = alice1.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) actions1.hasOutgoingMessage() assertIs>(alice2) assertNotNull(alice2.state.localShutdown) @@ -1584,11 +1575,11 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Close_MutualClose -- two in a row`() { val (alice, _) = reachNormal() assertNull(alice.state.localShutdown) - val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(null, null)) + val (alice1, actions1) = alice.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertIs>(alice1) actions1.hasOutgoingMessage() assertNotNull(alice1.state.localShutdown) - val (alice2, actions2) = alice1.process(ChannelCommand.Close.MutualClose(null, null)) + val (alice2, actions2) = alice1.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertIs>(alice2) actions2.hasCommandError() } @@ -1600,7 +1591,7 @@ class NormalTestsCommon : LightningTestSuite() { val (alice1, actions1) = nodes.first.process(ChannelCommand.Commitment.Sign) assertIs>(alice1) actions1.hasOutgoingMessage() - val (alice2, actions2) = alice1.process(ChannelCommand.Close.MutualClose(null, null)) + val (alice2, actions2) = alice1.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertIs>(alice2) actions2.hasOutgoingMessage() } @@ -1610,10 +1601,10 @@ class NormalTestsCommon : LightningTestSuite() { val (alice, _) = reachNormal() val (alice1, actions1) = alice.process(ChannelCommand.Commitment.UpdateFee(FeeratePerKw(20_000.sat), false)) actions1.hasOutgoingMessage() - val (alice2, actions2) = alice1.process(ChannelCommand.Close.MutualClose(null, null)) + val (alice2, actions2) = alice1.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) actions2.hasCommandError() val (alice3, _) = alice2.process(ChannelCommand.Commitment.Sign) - val (alice4, actions4) = alice3.process(ChannelCommand.Close.MutualClose(null, null)) + val (alice4, actions4) = alice3.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertIs>(alice4) actions4.hasOutgoingMessage() } @@ -1624,14 +1615,13 @@ class NormalTestsCommon : LightningTestSuite() { val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(Shutdown(alice.channelId, bob.commitments.params.localParams.defaultFinalScriptPubKey))) assertIs>(alice1) actions1.hasOutgoingMessage() - actions1.hasOutgoingMessage() } @Test fun `recv Shutdown -- with unacked sent htlcs`() { val (alice, bob) = reachNormal() val (nodes, _, _) = addHtlc(50000000.msat, payer = alice, payee = bob) - val (bob1, actions1) = nodes.second.process(ChannelCommand.Close.MutualClose(null, null)) + val (bob1, actions1) = nodes.second.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) val shutdown = actions1.findOutgoingMessage() val (alice1, actions2) = nodes.first.process(ChannelCommand.MessageReceived(shutdown)) @@ -1671,7 +1661,7 @@ class NormalTestsCommon : LightningTestSuite() { // Bob initiates a close before receiving the signature. val (bob1, _) = bob.process(ChannelCommand.MessageReceived(updateFee)) - val (bob2, bobActions2) = bob1.process(ChannelCommand.Close.MutualClose(null, null)) + val (bob2, bobActions2) = bob1.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) val shutdownBob = bobActions2.hasOutgoingMessage() val (bob3, bobActions3) = bob2.process(ChannelCommand.MessageReceived(sigAlice)) @@ -1686,15 +1676,15 @@ class NormalTestsCommon : LightningTestSuite() { val (alice5, aliceActions5) = alice4.process(ChannelCommand.MessageReceived(sigBob)) assertIs>(alice5) val revAlice = aliceActions5.hasOutgoingMessage() - val closingAlice = aliceActions5.hasOutgoingMessage() val (bob5, _) = bob4.process(ChannelCommand.MessageReceived(shutdownAlice)) - val (bob6, _) = bob5.process(ChannelCommand.MessageReceived(revAlice)) - val (bob7, bobActions7) = bob6.process(ChannelCommand.MessageReceived(closingAlice)) - assertIs>(bob7) - val closingBob = bobActions7.hasOutgoingMessage() - val (alice6, _) = alice5.process(ChannelCommand.MessageReceived(closingBob)) - assertIs>(alice6) + val (bob6, bobActions6) = bob5.process(ChannelCommand.MessageReceived(revAlice)) + val closingCompleteBob = bobActions6.hasOutgoingMessage() + val (alice6, aliceActions6) = alice5.process(ChannelCommand.MessageReceived(closingCompleteBob)) + assertIs(alice6.state) + val closingAlice = aliceActions6.hasOutgoingMessage() + val (bob7, _) = bob6.process(ChannelCommand.MessageReceived(closingAlice)) + assertIs(bob7.state) } @Test @@ -1707,16 +1697,6 @@ class NormalTestsCommon : LightningTestSuite() { actions1.hasWatch() } - @Test - fun `recv Shutdown -- with unsupported native segwit script`() { - val (_, bob) = reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ShutdownAnySegwit)) - val (bob1, actions1) = bob.process(ChannelCommand.MessageReceived(Shutdown(bob.channelId, ByteVector("51050102030405")))) - assertIs>(bob1) - actions1.hasOutgoingMessage() - assertEquals(2, actions1.filterIsInstance().count()) - actions1.hasWatch() - } - @Test fun `recv Shutdown -- with native segwit script`() { val (_, bob) = reachNormal() @@ -1730,7 +1710,7 @@ class NormalTestsCommon : LightningTestSuite() { val (alice, bob) = reachNormal() val (nodes, _, _) = addHtlc(50000000.msat, payer = alice, payee = bob) val (_, bob1) = crossSign(nodes.first, nodes.second) - val (bob2, actions1) = bob1.process(ChannelCommand.Close.MutualClose(null, null)) + val (bob2, actions1) = bob1.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) actions1.hasOutgoingMessage() // actual test begins @@ -1759,7 +1739,7 @@ class NormalTestsCommon : LightningTestSuite() { val (nodes, _, _) = addHtlc(50000000.msat, payer = alice, payee = bob) val (alice1, actions1) = nodes.first.process(ChannelCommand.Commitment.Sign) actions1.hasOutgoingMessage() - val (_, actions2) = bob.process(ChannelCommand.Close.MutualClose(null, null)) + val (_, actions2) = bob.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) val shutdown = actions2.findOutgoingMessage() // actual test begins @@ -1772,7 +1752,7 @@ class NormalTestsCommon : LightningTestSuite() { fun `recv Shutdown -- while waiting for a RevokeAndAck with pending outgoing htlc`() { val (alice, bob) = reachNormal() // let's make bob send a Shutdown message - val (bob1, actions1) = bob.process(ChannelCommand.Close.MutualClose(null, null)) + val (bob1, actions1) = bob.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) val shutdown = actions1.findOutgoingMessage() // this is just so we have something to sign 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 8b4979192..290656d0b 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 @@ -77,7 +77,7 @@ class QuiescenceTestsCommon : LightningTestSuite() { val cmds = listOf( ChannelCommand.Htlc.Add(1_000_000.msat, Lightning.randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(alice.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket, UUID.randomUUID()), ChannelCommand.Commitment.UpdateFee(FeeratePerKw(100.sat)), - ChannelCommand.Close.MutualClose(null, null), + ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw), ) cmds.forEach { alice1.process(it).second.findCommandError() @@ -92,7 +92,7 @@ class QuiescenceTestsCommon : LightningTestSuite() { val cmds = listOf( ChannelCommand.Htlc.Add(1_000_000.msat, Lightning.randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(alice.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket, UUID.randomUUID()), ChannelCommand.Commitment.UpdateFee(FeeratePerKw(100.sat)), - ChannelCommand.Close.MutualClose(null, null) + ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw) ) cmds.forEach { alice1.process(it).second.findCommandError() @@ -314,16 +314,16 @@ class QuiescenceTestsCommon : LightningTestSuite() { val (alice1, actionsAlice1) = alice.process(createSpliceCommand(alice)) val stfuAlice = actionsAlice1.findOutgoingMessage() // But Bob is concurrently initiating a mutual close, which should "win". - val (bob1, actionsBob1) = bob.process(ChannelCommand.Close.MutualClose(null, null)) + val (bob1, actionsBob1) = bob.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) val shutdownBob = actionsBob1.hasOutgoingMessage() val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(stfuAlice)) assertNull(actionsBob2.findOutgoingMessageOpt()) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) assertIs(alice2.state) val shutdownAlice = actionsAlice2.findOutgoingMessage() - actionsAlice2.findOutgoingMessage() val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(shutdownAlice)) assertIs(bob3.state) + actionsBob3.findOutgoingMessage() actionsBob3.has() } 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 421c12fef..a04dcbba6 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 @@ -360,32 +360,13 @@ class ShutdownTestsCommon : LightningTestSuite() { val (_, bob0) = reachNormal() assertTrue(bob0.commitments.params.localParams.features.hasFeature(Feature.ChannelBackupClient)) assertFalse(bob0.commitments.params.channelFeatures.hasFeature(Feature.ChannelBackupClient)) // this isn't a permanent channel feature - val (bob1, actions1) = bob0.process(ChannelCommand.Close.MutualClose(null, null)) + val (bob1, actions1) = bob0.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertIs>(bob1) val blob = EncryptedChannelData.from(bob1.staticParams.nodeParams.nodePrivateKey, bob1.state) val shutdown = actions1.findOutgoingMessage() assertEquals(blob, shutdown.channelData) } - @Test - fun `recv Shutdown with non-initiator paying commit fees`() { - val (alice, bob) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) - assertFalse(alice.commitments.params.localParams.paysCommitTxFees) - assertTrue(bob.commitments.params.localParams.paysCommitTxFees) - // Alice can initiate a mutual close, even though she's not paying the commitment fees. - // Bob will send closing_signed first since he's paying the commitment fees. - val (alice1, actionsAlice1) = alice.process(ChannelCommand.Close.MutualClose(null, null)) - assertIs>(alice1) - val shutdownAlice = actionsAlice1.findOutgoingMessage() - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(shutdownAlice)) - assertIs>(bob1) - val shutdownBob = actionsBob1.findOutgoingMessage() - actionsBob1.findOutgoingMessage() - val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) - assertIs>(alice2) - assertNull(actionsAlice2.findOutgoingMessageOpt()) - } - @Test fun `recv CheckHtlcTimeout -- no htlc timed out`() { val (alice, _) = init() @@ -490,10 +471,36 @@ class ShutdownTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Close_MutualClose`() { - val (alice, _) = init() - val (alice1, actions) = alice.process(ChannelCommand.Close.MutualClose(null, null)) - assertEquals(alice1, alice) - assertEquals(actions, listOf(ChannelAction.ProcessCmdRes.NotExecuted(ChannelCommand.Close.MutualClose(null, null), ClosingAlreadyInProgress(alice.channelId)))) + val (alice, bob) = init() + + // Alice updates our closing feerate. + val (alice1, actionsAlice1) = alice.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw * 1.5)) + assertIs(alice1.state) + assertEquals(TestConstants.feeratePerKw * 1.5, alice1.state.closingFeerate) + assertEquals(1, actionsAlice1.size) + actionsAlice1.has() + + // Alice updates her closing script. + val aliceScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.Close.MutualClose(aliceScript, TestConstants.feeratePerKw * 2)) + assertIs(alice2.state) + assertEquals(TestConstants.feeratePerKw * 2, alice2.state.closingFeerate) + assertEquals(2, actionsAlice2.size) + actionsAlice2.has() + val shutdownAlice = actionsAlice2.findOutgoingMessage() + assertEquals(shutdownAlice, alice2.state.localShutdown) + assertEquals(aliceScript, shutdownAlice.scriptPubKey) + + // Bob updates his closing feerate and script. + val bobScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() + val (bob1, actionsBob1) = bob.process(ChannelCommand.Close.MutualClose(bobScript, TestConstants.feeratePerKw * 1.5)) + assertIs(bob1.state) + assertEquals(TestConstants.feeratePerKw * 1.5, bob1.state.closingFeerate) + assertEquals(2, actionsBob1.size) + actionsBob1.has() + val shutdownBob = actionsBob1.findOutgoingMessage() + assertEquals(shutdownBob.scriptPubKey, bob1.state.localShutdown.scriptPubKey) + assertEquals(bobScript, shutdownBob.scriptPubKey) } private fun testLocalForceClose(alice: LNChannel, actions: List) { @@ -591,17 +598,17 @@ class ShutdownTestsCommon : LightningTestSuite() { fun shutdown(alice: LNChannel, bob: LNChannel): Pair, LNChannel> { // Alice initiates a closing - val (alice1, actionsAlice) = alice.process(ChannelCommand.Close.MutualClose(null, null)) - val shutdown = actionsAlice.findOutgoingMessage() - val (bob1, actionsBob) = bob.process(ChannelCommand.MessageReceived(shutdown)) - val shutdown1 = actionsBob.findOutgoingMessage() - val (alice2, _) = alice1.process(ChannelCommand.MessageReceived(shutdown1)) + val (alice1, actionsAlice) = alice.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) + val shutdownAlice = actionsAlice.findOutgoingMessage() + val (bob1, actionsBob) = bob.process(ChannelCommand.MessageReceived(shutdownAlice)) + val shutdownBob = actionsBob.findOutgoingMessage() + val (alice2, _) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) assertIs>(alice2) assertIs>(bob1) assertIs(alice2.state) assertIs(bob1.state) - if (alice2.state.commitments.params.channelFeatures.hasFeature(Feature.ChannelBackupClient)) assertFalse(shutdown.channelData.isEmpty()) - if (bob1.state.commitments.params.channelFeatures.hasFeature(Feature.ChannelBackupClient)) assertFalse(shutdown1.channelData.isEmpty()) + if (alice2.state.commitments.params.channelFeatures.hasFeature(Feature.ChannelBackupClient)) assertFalse(shutdownAlice.channelData.isEmpty()) + if (bob1.state.commitments.params.channelFeatures.hasFeature(Feature.ChannelBackupClient)) assertFalse(shutdownBob.channelData.isEmpty()) return Pair(alice2, bob1) } } 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 965cc58b5..3aa428744 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 @@ -125,7 +125,7 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Close_MutualClose`() { val (alice, _, bob, _) = init() listOf(alice, bob).forEach { state -> - val (state1, actions1) = state.process(ChannelCommand.Close.MutualClose(null, null)) + val (state1, actions1) = state.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertEquals(state, state1) assertEquals(1, actions1.size) actions1.hasCommandError() 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 51c66cc51..6d89aaa41 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 @@ -331,7 +331,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { fun `recv ChannelCommand_Close_MutualClose`() { val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs) listOf(alice, bob).forEach { state -> - val (state1, actions1) = state.process(ChannelCommand.Close.MutualClose(null, null)) + val (state1, actions1) = state.process(ChannelCommand.Close.MutualClose(null, TestConstants.feeratePerKw)) assertEquals(state, state1) actions1.hasCommandError() } 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 8a9247cb6..22562a3b8 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 @@ -54,6 +54,8 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { Feature.BasicMultiPartPayment to FeatureSupport.Optional, Feature.DualFunding to FeatureSupport.Mandatory, Feature.RouteBlinding to FeatureSupport.Optional, + Feature.ShutdownAnySegwit to FeatureSupport.Mandatory, + Feature.SimpleClose to FeatureSupport.Mandatory, ) // The following invoice requires payment_metadata. val invoice1 = run { 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 52aef868e..48862512e 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 @@ -78,6 +78,7 @@ object TestConstants { Feature.Quiescence to FeatureSupport.Mandatory, Feature.ChannelType to FeatureSupport.Mandatory, Feature.PaymentMetadata to FeatureSupport.Optional, + Feature.SimpleClose to FeatureSupport.Mandatory, Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional, Feature.WakeUpNotificationProvider to FeatureSupport.Optional, Feature.ChannelBackupProvider to FeatureSupport.Optional, 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 6163254b7..bb11df266 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 @@ -43,7 +43,7 @@ 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.makeClosingTx +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 @@ -650,54 +650,63 @@ class TransactionsTestsCommon : LightningTestSuite() { @Test fun `find our output in closing tx`() { - val localPubKeyScript = write(pay2wpkh(PrivateKey(randomBytes32()).publicKey())) - val remotePubKeyScript = write(pay2wpkh(PrivateKey(randomBytes32()).publicKey())) + val localPubKeyScript = write(pay2wpkh(PrivateKey(randomBytes32()).publicKey())).byteVector() + val remotePubKeyScript = write(pay2wpkh(PrivateKey(randomBytes32()).publicKey())).byteVector() run { - // Different amounts, both outputs untrimmed, local is the initiator: + // Different amounts, both outputs untrimmed, local is closer: val spec = CommitmentSpec(setOf(), feerate, 150_000_000.msat, 250_000_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000.sat, spec) - assertEquals(2, closingTx.tx.txOut.size) - assertNotNull(closingTx.toLocalIndex) - assertEquals(localPubKeyScript.toByteVector(), closingTx.toLocalOutput!!.publicKeyScript) - assertEquals(149_000.sat, closingTx.toLocalOutput!!.amount) // initiator pays the fee - val toRemoteIndex = (closingTx.toLocalIndex!! + 1) % 2 - assertEquals(250_000.sat, closingTx.tx.txOut[toRemoteIndex].amount) + val closingTxs = makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByUs(5_000.sat), 0, localPubKeyScript, remotePubKeyScript) + assertNotNull(closingTxs.localAndRemote) + assertNotNull(closingTxs.localOnly) + assertNull(closingTxs.remoteOnly) + val localAndRemote = closingTxs.localAndRemote?.toLocalOutput!! + assertEquals(localPubKeyScript, localAndRemote.publicKeyScript) + assertEquals(145_000.sat, localAndRemote.amount) + val localOnly = closingTxs.localOnly?.toLocalOutput!! + assertEquals(localPubKeyScript, localOnly.publicKeyScript) + assertEquals(145_000.sat, localOnly.amount) } run { - // Same amounts, both outputs untrimmed, local is not the initiator: - val spec = CommitmentSpec(setOf(), feerate, 150_000_000.msat, 150_000_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = false, localDustLimit, 1000.sat, spec) - assertEquals(2, closingTx.tx.txOut.size) - assertNotNull(closingTx.toLocalIndex) - assertEquals(localPubKeyScript.toByteVector(), closingTx.toLocalOutput!!.publicKeyScript) - assertEquals(150_000.sat, closingTx.toLocalOutput!!.amount) - val toRemoteIndex = (closingTx.toLocalIndex!! + 1) % 2 - assertEquals(149_000.sat, closingTx.tx.txOut[toRemoteIndex].amount) + // 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) + assertNotNull(closingTxs.localAndRemote) + assertNotNull(closingTxs.localOnly) + assertNull(closingTxs.remoteOnly) + val localAndRemote = closingTxs.localAndRemote?.toLocalOutput!! + assertEquals(localPubKeyScript, localAndRemote.publicKeyScript) + assertEquals(150_000.sat, localAndRemote.amount) + val localOnly = closingTxs.localOnly?.toLocalOutput!! + assertEquals(localPubKeyScript, localOnly.publicKeyScript) + assertEquals(150_000.sat, localOnly.amount) } run { // Their output is trimmed: - val spec = CommitmentSpec(setOf(), feerate, 150_000_000.msat, 1_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = false, localDustLimit, 1000.sat, spec) - assertEquals(1, closingTx.tx.txOut.size) - assertNotNull(closingTx.toLocalOutput) - assertEquals(localPubKeyScript.toByteVector(), closingTx.toLocalOutput!!.publicKeyScript) - assertEquals(150_000.sat, closingTx.toLocalOutput!!.amount) - assertEquals(0, closingTx.toLocalIndex!!) + 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) + assertEquals(1, closingTxs.all.size) + assertNotNull(closingTxs.localOnly) + assertEquals(1, closingTxs.localOnly!!.tx.txOut.size) + val toLocal = closingTxs.localOnly?.toLocalOutput!! + assertEquals(localPubKeyScript, toLocal.publicKeyScript) + assertEquals(150_000.sat, toLocal.amount) } run { // Our output is trimmed: - val spec = CommitmentSpec(setOf(), feerate, 50_000.msat, 150_000_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000.sat, spec) - assertEquals(1, closingTx.tx.txOut.size) - assertNull(closingTx.toLocalOutput) + 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) + assertEquals(1, closingTxs.all.size) + assertNotNull(closingTxs.remoteOnly) + assertNull(closingTxs.remoteOnly?.toLocalOutput) } run { // Both outputs are trimmed: val spec = CommitmentSpec(setOf(), feerate, 50_000.msat, 10_000.msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localPaysClosingFees = true, localDustLimit, 1000.sat, spec) - assertTrue(closingTx.tx.txOut.isEmpty()) - assertNull(closingTx.toLocalOutput) + val closingTxs = 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 516a649e1..29abf632d 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 @@ -380,7 +380,8 @@ class LightningCodecsTestsCommon : LightningTestSuite() { 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 willFund = + LiquidityAds.WillFundRates(listOf(fundingLease), 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") @@ -683,50 +684,32 @@ class LightningCodecsTestsCommon : LightningTestSuite() { } @Test - fun `encode - decode closing_signed`() { - val defaultSig = ByteVector64("01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") - val testCases = listOf( - Hex.decode("0027 0100000000000000000000000000000000000000000000000000000000000000 0000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") to ClosingSigned( - ByteVector32.One, - 0.sat, - ByteVector64.Zeroes - ), - Hex.decode("0027 0100000000000000000000000000000000000000000000000000000000000000 00000000000003e8 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") to ClosingSigned( - ByteVector32.One, - 1000.sat, - ByteVector64.Zeroes - ), - Hex.decode("0027 0100000000000000000000000000000000000000000000000000000000000000 00000000000005dc 01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingSigned( - ByteVector32.One, - 1500.sat, - defaultSig - ), - Hex.decode("0027 0100000000000000000000000000000000000000000000000000000000000000 00000000000005dc 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0110000000000000006400000000000007d0") to ClosingSigned( - ByteVector32.One, - 1500.sat, - ByteVector64.Zeroes, - TlvStream(ClosingSignedTlv.FeeRange(100.sat, 2000.sat)) - ), - Hex.decode("0027 0100000000000000000000000000000000000000000000000000000000000000 00000000000003e8 01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 0110000000000000006400000000000007d0") to ClosingSigned( - ByteVector32.One, - 1000.sat, - defaultSig, - TlvStream(ClosingSignedTlv.FeeRange(100.sat, 2000.sat)) - ), - Hex.decode("0027 0100000000000000000000000000000000000000000000000000000000000000 0000000000000064 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0110000000000000006400000000000003e8 030401020304") to ClosingSigned( - ByteVector32.One, - 100.sat, - ByteVector64.Zeroes, - TlvStream(setOf(ClosingSignedTlv.FeeRange(100.sat, 1000.sat)), setOf(GenericTlv(3, ByteVector("01020304")))) - ), + fun `encode - decode closing messages`() { + val channelId = ByteVector32("58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86") + val sig1 = ByteVector64("01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") + val sig2 = ByteVector64("02020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") + val sig3 = ByteVector64("03030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") + val closerScript = Hex.decode("deadbeef").byteVector() + val closeeScript = Hex.decode("d43db3ef1234").byteVector() + val testCases = mapOf( + // @formatter:off + Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 0), + Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 000c96a8 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 825_000, TlvStream(ClosingCompleteTlv.CloseeOutputOnly(sig1))), + Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserAndCloseeOutputs(sig1))), + Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserOutputOnly(sig1), ClosingCompleteTlv.CloserAndCloseeOutputs(sig2))), + Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserOutputOnly(sig1), ClosingCompleteTlv.CloseeOutputOnly(sig2), ClosingCompleteTlv.CloserAndCloseeOutputs(sig3))), + Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0), + Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloseeOutputOnly(sig1))), + Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloserAndCloseeOutputs(sig1))), + Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloserOutputOnly(sig1), ClosingSigTlv.CloserAndCloseeOutputs(sig2))), + Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloserOutputOnly(sig1), ClosingSigTlv.CloseeOutputOnly(sig2), ClosingSigTlv.CloserAndCloseeOutputs(sig3))), + // @formatter:on ) - testCases.forEach { - val decoded = LightningMessage.decode(it.first) - assertNotNull(decoded) - assertEquals(decoded, it.second) - val reEncoded = LightningMessage.encode(decoded) - assertContentEquals(reEncoded, it.first) + val decoded = LightningMessage.decode(it.key) + assertEquals(it.value, decoded) + val encoded = LightningMessage.encode(it.value) + assertContentEquals(it.key, encoded) } } @@ -774,13 +757,12 @@ class LightningCodecsTestsCommon : LightningTestSuite() { Hex.decode("0026") + channelId.toByteArray() + Hex.decode("002a") + randomData + Hex.decode("01 02 0102") + Hex.decode("fe47010000 00") to Shutdown(channelId, randomData.toByteVector(), TlvStream(setOf(ShutdownTlv.ChannelData(EncryptedChannelData.empty)), setOf(GenericTlv(1, ByteVector("0102"))))), Hex.decode("0026") + channelId.toByteArray() + Hex.decode("002a") + randomData + Hex.decode("fe47010000 07 cccccccccccccc") to Shutdown(channelId, randomData.toByteVector()).withChannelData(ByteVector("cccccccccccccc")), Hex.decode("0026") + channelId.toByteArray() + Hex.decode("002a") + randomData + Hex.decode("01 02 0102") + Hex.decode("fe47010000 07 cccccccccccccc") to Shutdown(channelId, randomData.toByteVector(), TlvStream(setOf(ShutdownTlv.ChannelData(EncryptedChannelData(ByteVector("cccccccccccccc")))), setOf(GenericTlv(1, ByteVector("0102"))))), - // closing_signed - Hex.decode("0027") + channelId.toByteArray() + Hex.decode("00000000075bcd15") + signature.toByteArray() to ClosingSigned(channelId, 123456789.sat, signature), - Hex.decode("0027") + channelId.toByteArray() + Hex.decode("00000000075bcd15") + signature.toByteArray() + Hex.decode("03 02 0102") to ClosingSigned(channelId, 123456789.sat, signature, TlvStream(setOf(), setOf(GenericTlv(3, ByteVector("0102"))))), - Hex.decode("0027") + channelId.toByteArray() + Hex.decode("00000000075bcd15") + signature.toByteArray() + Hex.decode("fe47010000 00") to ClosingSigned(channelId, 123456789.sat, signature, TlvStream(ClosingSignedTlv.ChannelData(EncryptedChannelData.empty))), - Hex.decode("0027") + channelId.toByteArray() + Hex.decode("00000000075bcd15") + signature.toByteArray() + Hex.decode("03 02 0102") + Hex.decode("fe47010000 00") to ClosingSigned(channelId, 123456789.sat, signature, TlvStream(setOf(ClosingSignedTlv.ChannelData(EncryptedChannelData.empty)), setOf(GenericTlv(3, ByteVector("0102"))))), - Hex.decode("0027") + channelId.toByteArray() + Hex.decode("00000000075bcd15") + signature.toByteArray() + Hex.decode("fe47010000 07 cccccccccccccc") to ClosingSigned(channelId, 123456789.sat, signature).withChannelData(ByteVector("cccccccccccccc")), - Hex.decode("0027") + channelId.toByteArray() + Hex.decode("00000000075bcd15") + signature.toByteArray() + Hex.decode("03 02 0102") + Hex.decode("fe47010000 07 cccccccccccccc") to ClosingSigned(channelId, 123456789.sat, signature, TlvStream(setOf(ClosingSignedTlv.ChannelData(EncryptedChannelData(ByteVector("cccccccccccccc")))), setOf(GenericTlv(3, ByteVector("0102"))))) + // closing_complete + Hex.decode("0028") + channelId.toByteArray() + Hex.decode("0004deadbeef 0004deadbeef 0000000000000451 00000000") + Hex.decode("fe47010000 00") to ClosingComplete(channelId, Hex.decode("deadbeef").byteVector(), Hex.decode("deadbeef").byteVector(), 1105.sat, 0, TlvStream(ClosingCompleteTlv.ChannelData(EncryptedChannelData.empty))), + Hex.decode("0028") + channelId.toByteArray() + Hex.decode("0004deadbeef 0004deadbeef 0000000000000451 00000000") + Hex.decode("fe47010000 07 cccccccccccccc") to ClosingComplete(channelId, Hex.decode("deadbeef").byteVector(), Hex.decode("deadbeef").byteVector(), 1105.sat, 0).withChannelData(ByteVector("cccccccccccccc")), + // closing_sig + Hex.decode("0029") + channelId.toByteArray() + Hex.decode("0004deadbeef 0004deadbeef 0000000000000451 00000000") + Hex.decode("fe47010000 00") to ClosingSig(channelId, Hex.decode("deadbeef").byteVector(), Hex.decode("deadbeef").byteVector(), 1105.sat, 0, TlvStream(ClosingSigTlv.ChannelData(EncryptedChannelData.empty))), + Hex.decode("0029") + channelId.toByteArray() + Hex.decode("0004deadbeef 0004deadbeef 0000000000000451 00000000") + Hex.decode("fe47010000 07 cccccccccccccc") to ClosingSig(channelId, Hex.decode("deadbeef").byteVector(), Hex.decode("deadbeef").byteVector(), 1105.sat, 0).withChannelData(ByteVector("cccccccccccccc")), ) // @formatter:on @@ -803,7 +785,8 @@ class LightningCodecsTestsCommon : LightningTestSuite() { CommitSig(randomBytes32(), randomBytes64(), listOf()), RevokeAndAck(randomBytes32(), randomKey(), randomKey().publicKey()), Shutdown(randomBytes32(), ByteVector("deadbeef")), - ClosingSigned(randomBytes32(), 0.sat, randomBytes64()), + ClosingComplete(randomBytes32(), ByteVector.empty, ByteVector.empty, 250.sat, 0), + ClosingSig(randomBytes32(), ByteVector.empty, ByteVector.empty, 250.sat, 0), ) messages.forEach { assertEquals(it.withChannelData(belowLimit).channelData, belowLimit) diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json index adf1a8f6f..9889b01cb 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json @@ -173,487 +173,12 @@ }, "remotePerCommitmentSecrets": "" }, - "localShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c" - }, - "remoteShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ShutdownTlv.ChannelData", - "ecb": { - "data": "56ac7d2006dabb7b7e26a032ac384ce15362f230e007a713e45ee0a514aef433db46a55d4650585517be36dc68babddf369cbfa743e9ad976640545119c378548729fd5750f0772ae26e1ac80a288f3baa7ae2416e9ea796403fe0b2111a919370021aba83cc9c3abd6671ddb7e2c54eaa64701b86d5b70ac2a44f723e399c449ff1b9ccb8e20481ea1ab6b9a752b38601fb202f4cf960cfdf3c77c91829747fbff494ab3701148abfb09bdee3bffe1bc46d157ae16e9d701764532db299530a3679b6ecce62fab88a75a8524b1b7484faf08b9347e1e3e92623286b98a27f8236f9d94831c489c9b66846d2e65b4c75a2850bf30b3268a3122589a58223265e5d62df74cf77ab3e781c3c1612142810b30c5e45775c70e6f17b3829688d69b5ed8769bd70018bc19de67fce65240ed94f1179087e74ca49212edd8d735f015a077e5cf6328d426d69e31898c0aafafe6d21b5784bf337508ba33a13c88a34a2d04e9f48e0f3ce7ad704524b9360adbf70d91b7900ccd2a53ce4da6f7b66226c860936f5549c7065f572e9e6debcec7560d5f2ec49bd257cd23a0da2dab7b72cc3118ee72826c15cc033bced6d35187d955019d1988549be9712ce4561ca1247497a6e60b506cb7f21dea80205fc66d2e13382998538a5c0dae806d18b8fce515a7538a881c5a12f67bc11e838b1154901f4fc84eb3165477f4bdf02dcf3d8ab42ba2ade55ee22cdf07bacf2b561cf05ebd3f8eca283c451ec09f0a8cd7b3ecbb83d17157e9830bb3d85d7143ebab10306544820b3811e89b7ce013d96db368bd9eff243e575dc10b2e7c576b9df66ad6c7379e6dda3423bde90a564d9995ade0a72842816996bbdbfaae0946ed2747c3247904fafae4bbe792bbba66ca29b1f9b0179a4093bb363dd3abbc6478441528b28daf1797d2d8fd3f200f0a6fdb8bfd4dfb02506bb16d4db103531d6f13d9c46613d4f03b84be135e19c4ede30717613efc50d6f28b6a36b5e61839ccd7a36a0d8ad6e81691f0772cff88f0e800bb9539e71ea3f1a52f281a9ba7c469fa917dbc75d8b7dc9e702c6a70d5d46b453804771adb6bc8cf85476ac31657776e674b632358f1e49ac1a9a4934736830224a136d76be62cf925af87746be07ef128b11a06d798ac7780843d46a2a5d3dd12eac27ef1af0a12581ec423df8be05aa0473b7cc30e55cf77d7255d1b2f9d0223e3dea9912552aee6fc1c9d3266d69266d40c8b85730e1a4327c956a51e06705e8b3f47463ab4bac168253ae55583523ed4220d97908932efc57616b4977e2418f26eb1077a4320386abf788ff8456bf044f5b95821520f8985f734a97dbcd5ad21d66d1b870a73f5bba1c4f382d369a3c8ebff86ce0b7fa29cd589053f1fbdc1fc392ccac7d548f11326ccc22cf641b7f10afa5d79c1b2015b768bda455fe573bd15001259a9ef3a0c47e3e4937065a4712d2d8d49c989bd59e3d8c130913fef431d954fd34b09947d40dce38a6301602ef65bcd1d874ae23bde4436cd0c0c2d8c671f0f73b42ee20fe083de4ce2380b4a33568eee72b82e6bf157dcee288091f9ffcc0837ae75ab3aba9eb7e081a5e491239a19e778dca132b958ee845a8de97876721766507191e74dd6b5ed4c08ba9b9547525fc0b809c46a4a7dc529df6f9b33f27905f355043c31f4a32cf2d1f7086a6af0455fc424da920ec2906a7464823b9f8bab0d693c8ea8a55317df43e6c6bda79015814558da1fc2c92ed6c80863544788dac519f418349a7707d59a1dcd160ea4867a562dd04c780c954d3e43bff6c8186eb4b80359ed267e216c96b0ef3d57fd3d39a99cc7e034afd2d3f6c62cfcdea85eec1f2ef1f2fc5f272a6117034b6c8b5085a1d02892d942b952a17fef041d4082296a0fc800497b86760d89b7eaf03a90c5482fe5304fe81fd4bda06a8a333a03a819b3341ab9335f12dcdb8fbdb27c235d80ee0d41844c564f9c84de48c198064a9111d236e6c3e60f36e3c656216c191097f17b285fba088391f5b65a430e92eeaae792b243fe05c88887bc9d52480bc63628e5a1e4821a977e1411dd22d343aca4aebe1b9b6efd23197bb3701fcbbbab1d5f2f426b85601c5ff37d79377f41dd8ebd16af52ae50ed0b45d4db319b1d6789cd2b6c3fa2e1ba2abb2a00d1267e0fe7467aa2be5a76af028c15f9ce9f5edb31c1aea177795ee1ccee5b667bac787f6115659a264fd5be27cd77e4eab64c4ac30a57eec78bcf3b13a0119ea60a0e8953d2f955c10295a5b0ffe4e653042deed40473a143e6201a1f64052f0d6edaa5796a4956e477745b0e00805f4cf4e0e3300ce338d8f27e8bd6c3fb626222f3e09ce38ac076bb49726e31aa4e48372f3638e7ddb66451496387e4a8c9d0ed3268f368455c3a055e70edb94bcf7961a51c740c9e1de1cbbc70c189dd48c1208a86b0fc85264659597bcd3c0980c87146446d6d2edee022b91573f8f734d2d8e21b6b1b78873c39ec34dad9b90d9b6214c33dec5dc087c108c9c8a70de64b6e5cb794cc35f581af2416831968d0021ce6e7d56e4880c34481e99cc6ac41a36619fc0de2628196f4d6f4b10ac82ff810302bab14cd58b6c10143a758f93a4bc99893d6d0a6391c5a4cd9c90ca2ce6e8310d4293305548a618cd0a5d4477dbb7911ce5bf92fc8810d549cb440ff9d5607811ecc5a0e878b780c51814fda3826dc4f108f63ac9e2a74cc0a375a5be4c9eed9dc836dfd3761f966f58a6eb60ca6d8102d34d5640cab6c253b7fbb6a17e0eece493f2541dbb9c43b6ea618deecb816cc5af8ad9b1914d445d622656cbfe9e5e22aa341412fdfb0b59b6baee4c56a9fe1ae146e47e7d3aa3f9b2eec7492e6ec1d01eb9ecdf1c9488c3c8a02b5582e145256cc6df17bd3947e4528941e358a83bc32802a047d7c3564e8c8f4f3869a5d66a34421efdc67dde9cafc5fbe685be01d0c420f764d3a7f027225d9b3ac980a7b3af4536b7bf89abbce5d9a4474f461240097ec5d81e21d969a41065b3818b7936231d28f208b066c272641d011c2412e0ecff7115feeca6bf137068a0da8d88360e9741fd1c9147208355b0eb466dc00b48e1845868834dcdc21d2719eea5e0f40df5e04dbb45a87ef473212f0ffd2ea97f82487938a25893eb834ca6b9c27b1910c9e3a4fa57d7c453e25bb9a193d3b71df314f93f10b7ee2dbc0498e16dff70aa79cb61c2b22cf471014f4046f71358ed17082dd0a797c8e4a624bf79d774ef1892232c7f2fb8cb9958b3912d120e84adab1f4f9c943311212f9f006870741dd47d109fde5de7f3973aad517bafb3b83c9249c7283e5e8b977488068e6f7a90418b87824b55abb62e045d3605cfe8271339a0e6d5090629d33882c9339533ec4ae2436d6e0fd48e853c56d68744cec5b5acb0c3246c301d5dbc96db9f8bc38adb66fb9e5d8712f3f6761abaaea03a57a57a9dd8c4fdab4969770fd3f3afe8545e7f12152d01eada01133a0578391a2ff9a57d828dbfeb558cfba31dafdba1b629dd497352b8f92d630aa20ce9e95d416f5d65ce95efec6bc62fa3822127b19285e9a3420a48227bac6dcda6d3531ce57f401250cf581fa8bba6f1c23fc3269d6b01a347369225619a784f08f9a84b03a2b2d44874fe943712811555898f4812f4c9cb622dc2a9cd49996d086d6a7df33eb0436746ff3180d408e19aea960f4beb0ca12170549e874c23fdf27654fd870708bde5ff98f26d26defdfdf5d2dab2e29db10d53579a6913a1ea31c49d0bc6aaca7ec0babff6e5c251d72198adbd5b1192c33d756ecd4fe4b1e35a2d43dcd4a3ed292cb4a26b236d986d83cca36b4e31ed3d1cb1409b3dff0f437b2b3897300cb8de1672e6b878a7bfff9dd75e01e16b6bff56d06b392c0349cd85b99de6cfffdf9e93c3ecb2f688d3b2b7442dd257b0893572cfbf348b2f28989eba7b8579384884b0352a77207af3d5c18d9acb09f944e6881d9deb9a1a9248c4b9f74f341982122777e95da9667cd0f1f09dcad04de4899aa0cc6bd2ee428ef3a4224c37e911c202328262192ec8180d0cd8329e569caf9bfb515819872e65066433b92ec6ee289f278e8fde4eba02a6c09e695643ff3f439687dbb3a20e7cc4d99ccc54727bc19f831ff28efb29717036dbf78465ca8b63e2d1154d6629b4f4c576bd967f9a5cc554862728fd2a7bd7ac9c1f9b628b3542acf023013baf6a8002feb051e2d2ac8ca66c97d428026cea6e428aa72d96b7ef53bb185360a3ac0f079e9e9d11f51829262eb18b1682d48bfc5024e65fd7af0f94f5b940c5e7b2760ff841def1506855cf85e679cb904dcf829687aede7fb70840e9ad6d9b7ad7b27ffb6def8a840f40d11f794bde4bbced2b5b63fc2c263461ade7f02fee63a4e0907a7f1b44a0e3656c8e4ccc6f00731972e9b1337dbe72d70fe715b93ee4e106ca3c330356a5983e74dec3a5e75d20b5e5fad7e51f28ce38f966bf83a44bb83a6a9ec694a2d61d71f536f6bb3da3c787f2c763cc2045067d56d2600aa0f4f24bfaf43b37fb8fece78d91ec511fcce7680bbda8684cdb0e9a619cd9888321bbd3d60ebec5ce4787b9ce687ffcd136db1f2a2877143c7dcb17bcbbdc7c8b5e3850097e7a12691211b4e607361a4834bf6f8f73575c3009a0dc6f2fc257c23404fc293933e3b1c31d0975968da2d4e54ac082289ab76c725f38913d0480cb61c6bff80b3f30b25662a39459619417d820cd25b34265bea62809d9e52b57a7e1c604ee4390e39d6185f6391dbd46f514dd6af3978de827adf594da0f0d430a5f9e216a003f128533bf40377b5c0c5bcd391f77c9e3e9aed8994a514c21ab5e38ca0176c8427d742e66db449d5657a05266d96f4fa3f2adefcafed21bc955c9b26cffe1f98e3a17b002387c14eddd1cc3ddda603e1076669bd0d6eda1079c9fc976437cd999c37b655ae612587c74b7cd00ae658e54572818be38c62f9c2ac0c97cae81c4eddfe4940ba79a545e54b0cfb439b81336e7d8f191b5885d19138c3d57badcaec8a1320715304bccd54c7f04a0577a80f0c600a92719982844f27130cee8932577e5b237c8432fcbf80e0c1dc97e5ae31a9eb4027964465cbf531cc9ebe8fd1750d681bfb1590265801bf7c8bbb876b17daa008876961fc7db1a7e4d896075248d845035817e1aafa68ce8f4a6cb1f344d11056ecbe423f3df6b2c9d22db72f9cb35860c096d3c6a960012420b4133c6b567" - } - } - ] - } - }, - "closingTxProposed": [ - [ - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d15e320c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 674, - "signature": "9fbce74a05823e5ff30227d6d96abf1adea0e2c3e225f19011d2c26fbc955b8758877c062bfe97f7a311988946421561dec3009292133c7b9c7153044c793949", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d10e310c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1010, - "signature": "959c687c87ee014e988275260a28f5522b9aa7df58201780e8da0714c80466db75747a0cbe30ad56656ed9f7d05599fc959758ed23566b815565dc20177d366c", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d114300c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1260, - "signature": "7ba4aab1322712d9ac5b12b0ead3c2386eab48e380fb0e93218de67de109f1d5552694f878c64a8b536e7be333bbec781fe2838845852db3905de3fcb46aa536", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d11a2f0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1510, - "signature": "12804a6c0c36c336baffe28a211042a1fc1cdd3c0626e06643a29a30d282b2b076487caa9a6e619ab141ffd6775bd4e1da32d55f87a548ea27421720a3e8ad08", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1202e0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1760, - "signature": "4214aa761eaec84f0bc571ad5bd2a427ba9ce6fc363adcb3c4b81aa5ced969d92361859b2eb9557afab41ebe54dcd18d645781085e3aa534819e57d8bea9d2e2", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1262d0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 2010, - "signature": "a66e4ad93233f6add8965f12ecb66f7c06a3e7a8ee42f692bb50426a7a32d470323c560e5069cc1801f85ad13a3bf149601a1a4b86d5d82cfb934b75f4dd5757", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d12c2c0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 2260, - "signature": "86c58c46963050df8fb7babb0dbbada4f4275c07089c9c7ac56a1b032b00bfd82244dc56d99ecff8f117e0b11bcf589a33f40c2248c7dd37eb84caa97e6000f5", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1322b0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 2510, - "signature": "6395a7e0db7cffd3d87b3edff6cee0d9fc0cd75a7d968b4ec4ff013bb27d368460e4e35277d0019d55dda79d7fab336ee1332452a406b273808c36ed3a7ea379", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1382a0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 2760, - "signature": "cc97e4cbd3048272c8da6e3abb8bfb0c00e0febbd91b7d73ca9be4769ac69a115b481bf55291214e5e479f5749eb183562dda84c57d292066522671462ac4f80", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d13e290c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 3010, - "signature": "7da842faca84a073d474dfac71b0a15052216c8b398b4609cac8d7fd336787c046507e39739109f4c69d40ba6dbe79f17d250d0a95defb1004f3c3c790491cd8", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d144280c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 3260, - "signature": "e0cab5a41fb2ac378593652d7c4b54b9662ca6bbe5b17ad1e87c7b206b4c1ee826200fae1897061ef33fe0f393e4b48fb3cf37b7922cf128fd69a277535f09d9", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d14a270c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 3510, - "signature": "c1a543b0bc4a8093d70d9e952b2e6a7d62ec8bb189297089109ab71f81d61f8024c79a176321c1402b41463c3eda28f2c544cbf6b8197e077eb4e20a5f9b7533", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d150260c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 3760, - "signature": "4279947219b647ca78f9f61028bec3c646e4045bbcd2f0a2aee2e14096a0843e7646e6a9f4be01aafe9093464d8db418ac9ec40f46951b9af8360137eda02dee", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d156250c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 4010, - "signature": "45e552dfea26356bfe073bda30d91bd009dd7bded18259bb484dab76736366656d7b5f387c4d7d6fa61390e14bedbe96f0f632b41160156932ed09647c29e3d9", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d15c240c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 4260, - "signature": "25662fe3ce073f2fec18372d52ef37c8bf42b462de18ca3589a7cd6044637f90174880f091d10052e8a8de8c64114f27403491c4484cebf7f2b9207417486d54", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d162230c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 4510, - "signature": "c6d02f7322fd5d333acbd3204b68dbaf8748e10c164b771b1c939dca5af948aa0767de9d5b1f528c388aa5ee21f9f2b0d9260a24604021356aabe272cd205053", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - } - ] + "lastClosingFeerate": null, + "localScript": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", + "remoteScript": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", + "proposedClosingTxs": [ ], - "bestUnpublishedClosingTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "020000000001012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d168220c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c0400483045022100ee9c22bd9db9fc8052eea4a0bfe2448eec7739ba8fa445292d81d181c87939240220409f1bfde401451519347d569ecfcfc9c70b07d4ec50bf229f6f74b17b3d6800014830450221008d6d347ec3a7dfe63c563146ba6acfd7c9392638eef97e864cc9f076ea98870202200337eea4d72e6dc38c3e482d86a5fe095037e8ce64c484668f64f17058987d180147522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae00000000", - "toLocalIndex": 1 - }, - "closingFeerates": null + "publishedClosingTxs": [ + ], + "waitingSinceBlock": 0 } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json index 1630cbe83..ee263a5a6 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json @@ -173,18 +173,12 @@ }, "remotePerCommitmentSecrets": "" }, - "localShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "76a9143a52c0fc59997d1e53009d607e3d93ca56b1014c88ac" - }, - "remoteShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c" - }, - "closingTxProposed": [ - [ - ] + "lastClosingFeerate": null, + "localScript": "76a9143a52c0fc59997d1e53009d607e3d93ca56b1014c88ac", + "remoteScript": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", + "proposedClosingTxs": [ + ], + "publishedClosingTxs": [ ], - "bestUnpublishedClosingTx": null, - "closingFeerates": null + "waitingSinceBlock": 0 } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json index 7b9031ac8..80108f0c7 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json @@ -173,57 +173,12 @@ }, "remotePerCommitmentSecrets": "" }, - "localShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1" - }, - "remoteShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c" - }, - "closingTxProposed": [ - [ - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d11a2d0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 0 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 2022, - "signature": "517e72591fed6c80b3bd8314cf4e44a3ca19a19882ab92d6238ab01e5fa76ab52435b680685bec0f3f04d2bd75bb4e137d1175c87987d99662450efc66b907c3", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 1348, - "max": 2022 - } - ] - } - } - } - ] + "lastClosingFeerate": null, + "localScript": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", + "remoteScript": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", + "proposedClosingTxs": [ ], - "bestUnpublishedClosingTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "020000000001012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d16b2e0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c0400483045022100b2cc62e81ae67b7f67421055afda676fdf049cd8b5810afe7d13c4c0f63f724002206acf483e46ec6517a208d00851e19e964aab2de5290a5c599366701f4102a50e0147304402206dd03bb394e2058717cd89586906385e457a4ed3634af2960b3928f113e1e7de02205ac9c9322430c11240769a51b43cbaf203c7c04f199c3d0d457b21f387c21cdb0147522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae00000000", - "toLocalIndex": 0 - }, - "closingFeerates": null + "publishedClosingTxs": [ + ], + "waitingSinceBlock": 0 } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json index 4e47c6c15..2bffd1c79 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json @@ -176,56 +176,12 @@ "data": "8c0ca0219d38704b18133d3e9c6f2fff5fcba2fa4d7ddca7cd38361c62d91123930e013d96e802c4ef5e6c9cb1f101aa956ff6fd3375a525dd9a1483c4e39135fc8d732b2109b7965a03f4acb1a4b0c3e9de40ec5d0b075f0b9a6d7ee71f6fba369ba60c5de117d04338216a29a4034ab7a3fe6b162d2fffd8d8ffb1a1e120aba163590a45add7b0cb61a56acd91fa206fadece0959b20d127e7b64ee0a2dfd945bcd9b31cade08fb9dc778e101f6de98b23d805c82415b48f72bad94058c651a9bf017a75c518d7cd8975e2ee114691aab5c05c5178273744408ecbdb20119eb638019cabb65baf2f379bdabeba39fab76c027b7ef802c5561cc9d80675621aef1cc1a60d27ede5b54c40a92e71ea7579376480583bd18be60875b4789f59197c4647eb0581a6b777d0e0cbef26166cd73a501a39133f9f7d529b8cd7a67356c9509f0d0254edb193434b72e34235adddec1bb43ad0605fe92651d3f68499a96e788aed0d641e15a166eb45c181d3a71f87bd92fbd3a309026534da87120a24c8b0a87ec13bd4eae6954085b8d1b3a0b1316f8602974a2949d0daf65b915ed77c78a67d78c49d1538a2365e50da0931237df3ea18613f3dedc1ed6d73686528b7fba8a84270a88d715879630e7bf2bcfcbc26e3d02ea691aa23f7cb9db872933aed347c86068f422111c22f2d91a509792304771274dd8c34dbd645bc319ef17ebacdc42dcba613105bab6e0f520869dcab50e7591fe803a4dc1301997af79983481b803b71800d3f552ea3401dfb93e9019c4e566d7112aab6169f05e0017f522776942a2b26114fa11969daf6a761933d1650a75f1d645943e85bb0be8338721bd43671c5e6d18052420dcbff9c28ed02888e8e5bb89bbd45f3d043887289ab36bc311621a62e5c874cc333e28cc275cc4242f4e524c0ddb9f5ea9199eedb0668f21048b11f486c5ba712d5edcbea80e85148e4a51a3a1c84842ed7db02b88c121d0426335183ee62e6a20ceb1040a658e769fc261e27eafed669d7c6bd8bb58111325a8e5c1faf44ed32ceb1791a8b1d2a482a6353beb4e7a4bded388408c1bbb72823e846a16a5e65612d412dbf9a0d6c21545fb965f3d43f4e16b9a1daa885433a4dbcb45cf25e5cf8e43bfadc4c6663061fee3894da75582ed83201a1e171271c638057a9c3d8faed7036c4f7e2a6bb45ab87f102803f4b9d0ebfe6342740c5082367dc5257652edb933f96a90113775e738bcd22e97730596f3d1631bf0ef05d96c8579f1f6a503631dc79c8238cdfd1e6ba4ebdbc1a6884f455f1048d66d275fa4deeff73bea7f6611e100f7e7ec94276fb85b8fb1f47cb08b2e09e97e744f64c0733e8f9e340f2e6e2d75adf7693692de5f4360aa5f666707db1488ad6be41312689ae6e8d6f7f1113c757f9aeb690a3545493c6a26b6f01926efd9da9747d444c11d60f3da935de3007c8bf7538e696663c8aa98b1b58881f666ba00d7c619747d385e59e3f5885d6a4d1ddc70df9ca2d6c871cb6143647a194bd4ada7baf8f3421faa74dd7e18ee66d57e9281f9235e749a09ccca9b15e0a5bcc2bc49ee8aa7d9b9321e173863d40e3625412f1d0bd115e9dc07fa3addc5e6df48e3464c09b06c2fc02929fafb6b848f17e0d0f234c27280412754f651763caaa55fbf90e911b2bb64a69b1d2c79b4430bbde2f07630e635f7937d827686bcb622f287049c607944af8a3aacceaf74a7d709fecd909d99722486654d097c63781ac283347ab4f37d8d9d15f5638ae3f2780930752a8d1c19f25b904e59837464680b57901d7d09dd029bc3d0201d0929616ba5e7c7d967618678885c05da4abb1df78cbf3def8a5c7c1497a78266a62670471da7eeb65c1127275b9f534e4162bd33f4d77036a0823b0f53fb33bb6ff41f419379e4b21723feaa0d627a693625af1b1030a5436bb487fc4b8f8dda4bf513a58d44637df3901751f006e560cd8e53bf46e1512b7c449a79638bb14624af9b1409bcf301d32d4dcbf9078f2efdf356af11cdcee29fff984134de2003898cb60117ada37c2c20c963bfd337cb2c9c70b78efa746b6137fe70140aae2c7cb30697e54a5baa2a5ab4188b7ac8f4910a53f195537965e5229a41511c0a02b046a2f7b92acb0dfea2c2ab34209c04f5b92296fc4214c2644536ccf9495a79da81a53688f86a2b070f727b39e0dfa1d379c4bbaebe0d9f0c907fa90cb43383dbb033bb859e4dcb43efbcc388f1a235eb40979ef295a3bc1099aa0cf9788f776fca622dd8589079d20fd2c265e3b4d6812588ad226dfb61efb00eac0536409cb9b51d46a838dc347b8c1b9c521204f3cf99157e892ccad0a046a6c8bffebb810ef374cd7bd31d390706d53f983ade51bc920c5fbe8625fed4b34304207964e8ad12bd78c4b4590ed4d224e7f8b366bf1af81e0000b9a2543ac84a57a64d04d8be82e7888974dc4bfdc64772402952f0e0b42d4857aa7081c54f93313c74b5a5abe10615086458815ed93d226a80528383d9336dc21f8d5d40ec40c1cd954be32d8a31c777a96eef4e36bd9acedaa1fe1d806a73623e31c19992c741018f876da5b88dc8c3fffa82363273cf10952444f53308d2e6f40f52cff30b68c7dc38b56763aef19c29bb742bdf5d7b60b59a7c50075a258c9a8ab3ebb04eb7080555b5a114fb9e7ce9760f819a72217fd52b27c7beabb14a2ab837e73dbe64e31fae6b1051bb50df8d7866d38de6b119eb37bc7036f8896c096efd3b453cb9658eb72431cecb54a9c487e52ee32416a08784fae535df25a2518244d19abb78e3cc8485b5c89123c0f1cb5eca8ad96c95c6539ea14440e5d9f299bd8bba6259481b7b0401b3b5b26d0e755c996da5f9668ddc76ab96c61f83233e24b3fa6b27114799f3ac0d17951883cb1cdad6ab72384a1f821d9ffffb728aa288d6aaacb93558bfba2ec59e67fc67ecf278eb11ac844894f67aa4bc4799da7bd39644096fe737b81d6adc69dbc06f4a153b5b6c3e57e544fec36deeffdc9e24a194a8e1abed5a361f5131c38d6e0cf05b86886a217283c865aca2919a6d46886ece53bd43cc5af069619fb12c812f92c007df3b504c66e7f10a593fdd8d3c4e6a8c743ced232732ad438a667df097bac7566de7fdc85225845545b189d85a83b18d4053f3c6fc2105fbba0e254c2a5c753b97d1791045630527672f82eadeaec0db03c3f880377de04e1b7cbf09fbf87c04def97cd8dee544581e877788f21b7567a955dfd7a06a816dc7ac11529400acb16abf4e7730f16c3312ac43a84b62e9aed266134ef2ee0db3bf2aedbc12f838eec6d0f1427defe77bdb650aa5398dd4989b94b9236a70e68d48c2e4f5bab36312fd1f1e4593cd9995878fd6a2b12515c0d41c9f0aeaac69fb77a013d8d4704ab4a173574d595edc90882c1bf947e9fdbe0ab1d680e3b4ec6e497cdd6a8e00e8dc9bca52066997d1c973ba27aadbe36876f1011dc11d6835acbbe5c7cfd6c3204b2a44dbf89b9715fd4797cf4200a4ca858705f624a1b48d75e166a1cb0c68e8dc0c994d69e199835e9f62f9b24167f28781f2f35ea1149f53bbda63bcb27b74f0e3e4704026e84e873306de2300488e9a182d2e85e46ac04b89c94277bc1f883a7047f62d117e45055bba398612091e9fcd4bbdd0d15e4b41c97898d9acdf904aa6cf144ef62ef7dd26f755c2fe353ed5eff1aef005ab99b7e355111a9ba5c5d03874914526e0356bc78f09543d6419e87be1df37d34393b12f87c676f49556a6ae5c91c194bbecf27936a91f3688a501c2ccb9a32fea57e195d3b0e0fd0b4640840400ec086678ee67ba0bd774de3c111979708409207e4d4bd1dce2321b77ccc71127be8318fe92e6ec730a47112bd069c9cad37361f693d07a90b0a37e5875938b06d6370d1f1c1c280bfd83fc5e2d0b803e467eacfd37c48ca37721193859cd499653f5dfd78c26e4b181e82b5005cac0e9bd55fb2850dd4c70cc1e3fe5c81167336f36839fce30f245061191282585ff911c9e4af395ad4cf77b6e4f17d389ae8fbd61a3a3a4e73ccae0716507afa7ac4d0f2349c3aa56f47b6aa78806d7c20b50e125ee329921ffe96f8123d3e4be8be89b2318b6e016c385728b7a02f7dbb14552e2863453c33e3854047e6d8f84fe9f56c9200962025182d418580a294a0d09e9740e187e3be88236f62542b56bf08f72a859ae8a809e78a90e86b9b2cde799f9cca6049d5f49e97365744bede14be8cf680175cca275444e37327ea04d624faed898e0fc7751f1ac7d6241d93472c2d6ee8925fedc9169539799701174a81d3d6e00a9ac9647286d08b6c982c6baf86810d1e9f91c581f0c755273009389019bcb68a7ffbb4f0bc7de1ec45004c93f1a04a8c4cb60490654f616250c14bb54e9fe1fa1ad866830451afe98559d3e0e85073c2950d181feeb935c6cb69962a644ba8736535c11e7a9efbc17e9669b1c232985e9c5eaad29618ad9f461f1438d8c35c691d3d3bf80642a11f57cbf19ec1a7e63b2289d7c0e178052b6b344267d99e018c3f724f44ac465097ddea64d35e36c78736cfe6488991c4469d25b132db3560744e418be6ef3452ab5a73f66205e1ba67944cf1a556e60b037fb9ec6579f8638be744e075e0fc99722bcfc40efaf603c00e16b714ada2b7390cf2e9867df6c5961d96cb63298037df5f20e69b3b4bd2d2a24f68c48e02538fc1b2723fff9b52f18012d2024c40bfefb202b6292065b2cafb67aa1539e9d8b96ca039cc08e232fbb44cb46d7ce5017b14cd7f4e4e1675c2bd0b8d74958ac3c1232134d91dd02d701eef4ab953237b548f289f7e9ff902e7b39c40226596037271dd3296865618fd62a0abe228adb4286217b6049d157bd3a91b8997730fb29f0b086c9a0cd56f7f0ad198eedf098c86371570db86a7e2e972d58c8baee2e9e7aa34534e3f11e210d21ab8bb9de9517910e67d24881c8d2a72baa26b440bcdba17aa1c0d3448e3b78c62ad09bdf8a686af48107386c8a2fe1e8b575f9be0780a642305e9e9147f93fbc71447780dda6bc865d3412e5aecf0e842603bf65f059c9a3c3515ed949152e68d386415aaad1e8048be59bef3e4bab26d9cdc15a81a84f51dad584db81558c2c12c0728b0a028ea4729b6f032585d45feae6bc12e237febab2bef1117c366380078d163de200e03fa31640e9e927737f2679c8104f74b7ac4d36e32bd4a7d84632a2cf95e161fb3b5fa81321d5afe02467f059d13f440a03592a3f2b2ee93f667e2ed66559ef53cd734bed10b16062d6581be18e4717cf8837443653a5ffe6da47a73883210b7129bd1543b745f5ada684a84003f8955ae9150126f22397d5e38c814899b67" } }, - "localShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c" - }, - "remoteShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "76a9143a52c0fc59997d1e53009d607e3d93ca56b1014c88ac", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ShutdownTlv.ChannelData", - "ecb": { - "data": "8c0ca0219d38704b18133d3e9c6f2fff5fcba2fa4d7ddca7cd38361c62d91123930e013d96e802c4ef5e6c9cb1f101aa956ff6fd3375a525dd9a1483c4e39135fc8d732b2109b7965a03f4acb1a4b0c3e9de40ec5d0b075f0b9a6d7ee71f6fba369ba60c5de117d04338216a29a4034ab7a3fe6b162d2fffd8d8ffb1a1e120aba163590a45add7b0cb61a56acd91fa206fadece0959b20d127e7b64ee0a2dfd945bcd9b31cade08fb9dc778e101f6de98b23d805c82415b48f72bad94058c651a9bf017a75c518d7cd8975e2ee114691aab5c05c5178273744408ecbdb20119eb638019cabb65baf2f379bdabeba39fab76c027b7ef802c5561cc9d80675621aef1cc1a60d27ede5b54c40a92e71ea7579376480583bd18be60875b4789f59197c4647eb0581a6b777d0e0cbef26166cd73a501a39133f9f7d529b8cd7a67356c9509f0d0254edb193434b72e34235adddec1bb43ad0605fe92651d3f68499a96e788aed0d641e15a166eb45c181d3a71f87bd92fbd3a309026534da87120a24c8b0a87ec13bd4eae6954085b8d1b3a0b1316f8602974a2949d0daf65b915ed77c78a67d78c49d1538a2365e50da0931237df3ea18613f3dedc1ed6d73686528b7fba8a84270a88d715879630e7bf2bcfcbc26e3d02ea691aa23f7cb9db872933aed347c86068f422111c22f2d91a509792304771274dd8c34dbd645bc319ef17ebacdc42dcba613105bab6e0f520869dcab50e7591fe803a4dc1301997af79983481b803b71800d3f552ea3401dfb93e9019c4e566d7112aab6169f05e0017f522776942a2b26114fa11969daf6a761933d1650a75f1d645943e85bb0be8338721bd43671c5e6d18052420dcbff9c28ed02888e8e5bb89bbd45f3d043887289ab36bc311621a62e5c874cc333e28cc275cc4242f4e524c0ddb9f5ea9199eedb0668f21048b11f486c5ba712d5edcbea80e85148e4a51a3a1c84842ed7db02b88c121d0426335183ee62e6a20ceb1040a658e769fc261e27eafed669d7c6bd8bb58111325a8e5c1faf44ed32ceb1791a8b1d2a482a6353beb4e7a4bded388408c1bbb72823e846a16a5e65612d412dbf9a0d6c21545fb965f3d43f4e16b9a1daa885433a4dbcb45cf25e5cf8e43bfadc4c6663061fee3894da75582ed83201a1e171271c638057a9c3d8faed7036c4f7e2a6bb45ab87f102803f4b9d0ebfe6342740c5082367dc5257652edb933f96a90113775e738bcd22e97730596f3d1631bf0ef05d96c8579f1f6a503631dc79c8238cdfd1e6ba4ebdbc1a6884f455f1048d66d275fa4deeff73bea7f6611e100f7e7ec94276fb85b8fb1f47cb08b2e09e97e744f64c0733e8f9e340f2e6e2d75adf7693692de5f4360aa5f666707db1488ad6be41312689ae6e8d6f7f1113c757f9aeb690a3545493c6a26b6f01926efd9da9747d444c11d60f3da935de3007c8bf7538e696663c8aa98b1b58881f666ba00d7c619747d385e59e3f5885d6a4d1ddc70df9ca2d6c871cb6143647a194bd4ada7baf8f3421faa74dd7e18ee66d57e9281f9235e749a09ccca9b15e0a5bcc2bc49ee8aa7d9b9321e173863d40e3625412f1d0bd115e9dc07fa3addc5e6df48e3464c09b06c2fc02929fafb6b848f17e0d0f234c27280412754f651763caaa55fbf90e911b2bb64a69b1d2c79b4430bbde2f07630e635f7937d827686bcb622f287049c607944af8a3aacceaf74a7d709fecd909d99722486654d097c63781ac283347ab4f37d8d9d15f5638ae3f2780930752a8d1c19f25b904e59837464680b57901d7d09dd029bc3d0201d0929616ba5e7c7d967618678885c05da4abb1df78cbf3def8a5c7c1497a78266a62670471da7eeb65c1127275b9f534e4162bd33f4d77036a0823b0f53fb33bb6ff41f419379e4b21723feaa0d627a693625af1b1030a5436bb487fc4b8f8dda4bf513a58d44637df3901751f006e560cd8e53bf46e1512b7c449a79638bb14624af9b1409bcf301d32d4dcbf9078f2efdf356af11cdcee29fff984134de2003898cb60117ada37c2c20c963bfd337cb2c9c70b78efa746b6137fe70140aae2c7cb30697e54a5baa2a5ab4188b7ac8f4910a53f195537965e5229a41511c0a02b046a2f7b92acb0dfea2c2ab34209c04f5b92296fc4214c2644536ccf9495a79da81a53688f86a2b070f727b39e0dfa1d379c4bbaebe0d9f0c907fa90cb43383dbb033bb859e4dcb43efbcc388f1a235eb40979ef295a3bc1099aa0cf9788f776fca622dd8589079d20fd2c265e3b4d6812588ad226dfb61efb00eac0536409cb9b51d46a838dc347b8c1b9c521204f3cf99157e892ccad0a046a6c8bffebb810ef374cd7bd31d390706d53f983ade51bc920c5fbe8625fed4b34304207964e8ad12bd78c4b4590ed4d224e7f8b366bf1af81e0000b9a2543ac84a57a64d04d8be82e7888974dc4bfdc64772402952f0e0b42d4857aa7081c54f93313c74b5a5abe10615086458815ed93d226a80528383d9336dc21f8d5d40ec40c1cd954be32d8a31c777a96eef4e36bd9acedaa1fe1d806a73623e31c19992c741018f876da5b88dc8c3fffa82363273cf10952444f53308d2e6f40f52cff30b68c7dc38b56763aef19c29bb742bdf5d7b60b59a7c50075a258c9a8ab3ebb04eb7080555b5a114fb9e7ce9760f819a72217fd52b27c7beabb14a2ab837e73dbe64e31fae6b1051bb50df8d7866d38de6b119eb37bc7036f8896c096efd3b453cb9658eb72431cecb54a9c487e52ee32416a08784fae535df25a2518244d19abb78e3cc8485b5c89123c0f1cb5eca8ad96c95c6539ea14440e5d9f299bd8bba6259481b7b0401b3b5b26d0e755c996da5f9668ddc76ab96c61f83233e24b3fa6b27114799f3ac0d17951883cb1cdad6ab72384a1f821d9ffffb728aa288d6aaacb93558bfba2ec59e67fc67ecf278eb11ac844894f67aa4bc4799da7bd39644096fe737b81d6adc69dbc06f4a153b5b6c3e57e544fec36deeffdc9e24a194a8e1abed5a361f5131c38d6e0cf05b86886a217283c865aca2919a6d46886ece53bd43cc5af069619fb12c812f92c007df3b504c66e7f10a593fdd8d3c4e6a8c743ced232732ad438a667df097bac7566de7fdc85225845545b189d85a83b18d4053f3c6fc2105fbba0e254c2a5c753b97d1791045630527672f82eadeaec0db03c3f880377de04e1b7cbf09fbf87c04def97cd8dee544581e877788f21b7567a955dfd7a06a816dc7ac11529400acb16abf4e7730f16c3312ac43a84b62e9aed266134ef2ee0db3bf2aedbc12f838eec6d0f1427defe77bdb650aa5398dd4989b94b9236a70e68d48c2e4f5bab36312fd1f1e4593cd9995878fd6a2b12515c0d41c9f0aeaac69fb77a013d8d4704ab4a173574d595edc90882c1bf947e9fdbe0ab1d680e3b4ec6e497cdd6a8e00e8dc9bca52066997d1c973ba27aadbe36876f1011dc11d6835acbbe5c7cfd6c3204b2a44dbf89b9715fd4797cf4200a4ca858705f624a1b48d75e166a1cb0c68e8dc0c994d69e199835e9f62f9b24167f28781f2f35ea1149f53bbda63bcb27b74f0e3e4704026e84e873306de2300488e9a182d2e85e46ac04b89c94277bc1f883a7047f62d117e45055bba398612091e9fcd4bbdd0d15e4b41c97898d9acdf904aa6cf144ef62ef7dd26f755c2fe353ed5eff1aef005ab99b7e355111a9ba5c5d03874914526e0356bc78f09543d6419e87be1df37d34393b12f87c676f49556a6ae5c91c194bbecf27936a91f3688a501c2ccb9a32fea57e195d3b0e0fd0b4640840400ec086678ee67ba0bd774de3c111979708409207e4d4bd1dce2321b77ccc71127be8318fe92e6ec730a47112bd069c9cad37361f693d07a90b0a37e5875938b06d6370d1f1c1c280bfd83fc5e2d0b803e467eacfd37c48ca37721193859cd499653f5dfd78c26e4b181e82b5005cac0e9bd55fb2850dd4c70cc1e3fe5c81167336f36839fce30f245061191282585ff911c9e4af395ad4cf77b6e4f17d389ae8fbd61a3a3a4e73ccae0716507afa7ac4d0f2349c3aa56f47b6aa78806d7c20b50e125ee329921ffe96f8123d3e4be8be89b2318b6e016c385728b7a02f7dbb14552e2863453c33e3854047e6d8f84fe9f56c9200962025182d418580a294a0d09e9740e187e3be88236f62542b56bf08f72a859ae8a809e78a90e86b9b2cde799f9cca6049d5f49e97365744bede14be8cf680175cca275444e37327ea04d624faed898e0fc7751f1ac7d6241d93472c2d6ee8925fedc9169539799701174a81d3d6e00a9ac9647286d08b6c982c6baf86810d1e9f91c581f0c755273009389019bcb68a7ffbb4f0bc7de1ec45004c93f1a04a8c4cb60490654f616250c14bb54e9fe1fa1ad866830451afe98559d3e0e85073c2950d181feeb935c6cb69962a644ba8736535c11e7a9efbc17e9669b1c232985e9c5eaad29618ad9f461f1438d8c35c691d3d3bf80642a11f57cbf19ec1a7e63b2289d7c0e178052b6b344267d99e018c3f724f44ac465097ddea64d35e36c78736cfe6488991c4469d25b132db3560744e418be6ef3452ab5a73f66205e1ba67944cf1a556e60b037fb9ec6579f8638be744e075e0fc99722bcfc40efaf603c00e16b714ada2b7390cf2e9867df6c5961d96cb63298037df5f20e69b3b4bd2d2a24f68c48e02538fc1b2723fff9b52f18012d2024c40bfefb202b6292065b2cafb67aa1539e9d8b96ca039cc08e232fbb44cb46d7ce5017b14cd7f4e4e1675c2bd0b8d74958ac3c1232134d91dd02d701eef4ab953237b548f289f7e9ff902e7b39c40226596037271dd3296865618fd62a0abe228adb4286217b6049d157bd3a91b8997730fb29f0b086c9a0cd56f7f0ad198eedf098c86371570db86a7e2e972d58c8baee2e9e7aa34534e3f11e210d21ab8bb9de9517910e67d24881c8d2a72baa26b440bcdba17aa1c0d3448e3b78c62ad09bdf8a686af48107386c8a2fe1e8b575f9be0780a642305e9e9147f93fbc71447780dda6bc865d3412e5aecf0e842603bf65f059c9a3c3515ed949152e68d386415aaad1e8048be59bef3e4bab26d9cdc15a81a84f51dad584db81558c2c12c0728b0a028ea4729b6f032585d45feae6bc12e237febab2bef1117c366380078d163de200e03fa31640e9e927737f2679c8104f74b7ac4d36e32bd4a7d84632a2cf95e161fb3b5fa81321d5afe02467f059d13f440a03592a3f2b2ee93f667e2ed66559ef53cd734bed10b16062d6581be18e4717cf8837443653a5ffe6da47a73883210b7129bd1543b745f5ada684a84003f8955ae9150126f22397d5e38c814899b67" - } - } - ] - } - }, - "closingTxProposed": [ - [ - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d0300000000001976a9143a52c0fc59997d1e53009d607e3d93ca56b1014c88ac341a0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 6860, - "signature": "b9978e8ceff7e273183cee9ffc17676e8410e3ccf6922e99a0739fe9d176a5da19dbff7a5e3aa7aa1327eff9ff2ffd01dd92fa465fe65c6ea7b88cbd179213ea", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 3430, - "max": 13720 - } - ] - } - } - } - ] + "lastClosingFeerate": null, + "localScript": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", + "remoteScript": "76a9143a52c0fc59997d1e53009d607e3d93ca56b1014c88ac", + "proposedClosingTxs": [ + ], + "publishedClosingTxs": [ ], - "bestUnpublishedClosingTx": null, - "closingFeerates": null + "waitingSinceBlock": 0 } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json index dc5f9ac36..142c0b247 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json @@ -189,6 +189,6 @@ "remoteChannelUpdate": null, "localShutdown": null, "remoteShutdown": null, - "closingFeerates": null, + "closingFeerate": null, "spliceStatus": "None" } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json index 19c16c145..a35e09ffc 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json @@ -364,6 +364,6 @@ "remoteChannelUpdate": null, "localShutdown": null, "remoteShutdown": null, - "closingFeerates": null, + "closingFeerate": null, "spliceStatus": "None" } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json index 6e05cb781..cb828a052 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json @@ -202,6 +202,6 @@ }, "localShutdown": null, "remoteShutdown": null, - "closingFeerates": null, + "closingFeerate": null, "spliceStatus": "None" } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json index 074594c09..62df91eaf 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json @@ -372,6 +372,6 @@ "remoteChannelUpdate": null, "localShutdown": null, "remoteShutdown": null, - "closingFeerates": null, + "closingFeerate": null, "spliceStatus": "None" } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json index 379308d25..a6c3e793b 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json @@ -221,6 +221,6 @@ "remoteChannelUpdate": null, "localShutdown": null, "remoteShutdown": null, - "closingFeerates": null, + "closingFeerate": null, "spliceStatus": "None" } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json b/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json index 51a957638..8d5f89863 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json @@ -262,5 +262,5 @@ ] } }, - "closingFeerates": null + "closingFeerate": null } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json b/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json index f259c7f52..f455bda2d 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json @@ -287,5 +287,5 @@ "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", "scriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c" }, - "closingFeerates": null + "closingFeerate": null } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json index 65dbbdfbd..c45d40625 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json @@ -171,123 +171,12 @@ }, "remotePerCommitmentSecrets": "" }, - "localShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c" - }, - "remoteShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ShutdownTlv.ChannelData", - "ecb": { - "data": "56ac7d2006dabb7b7e26a032ac384ce15362f230e007a713e45ee0a514aef433db46a55d4650585517be36dc68babddf369cbfa743e9ad976640545119c378548729fd5750f0772ae26e1ac80a288f3baa7ae2416e9ea796403fe0b2111a919370021aba83cc9c3abd6671ddb7e2c54eaa64701b86d5b70ac2a44f723e399c449ff1b9ccb8e20481ea1ab6b9a752b38601fb202f4cf960cfdf3c77c91829747fbff494ab3701148abfb09bdee3bffe1bc46d157ae16e9d701764532db299530a3679b6ecce62fab88a75a8524b1b7484faf08b9347e1e3e92623286b98a27f8236f9d94831c489c9b66846d2e65b4c75a2850bf30b3268a3122589a58223265e5d62df74cf77ab3e781c3c1612142810b30c5e45775c70e6f17b3829688d69b5ed8769bd70018bc19de67fce65240ed94f1179087e74ca49212edd8d735f015a077e5cf6328d426d69e31898c0aafafe6d21b5784bf337508ba33a13c88a34a2d04e9f48e0f3ce7ad704524b9360adbf70d91b7900ccd2a53ce4da6f7b66226c860936f5549c7065f572e9e6debcec7560d5f2ec49bd257cd23a0da2dab7b72cc3118ee72826c15cc033bced6d35187d955019d1988549be9712ce4561ca1247497a6e60b506cb7f21dea80205fc66d2e13382998538a5c0dae806d18b8fce515a7538a881c5a12f67bc11e838b1154901f4fc84eb3165477f4bdf02dcf3d8ab42ba2ade55ee22cdf07bacf2b561cf05ebd3f8eca283c451ec09f0a8cd7b3ecbb83d17157e9830bb3d85d7143ebab10306544820b3811e89b7ce013d96db368bd9eff243e575dc10b2e7c576b9df66ad6c7379e6dda3423bde90a564d9995ade0a72842816996bbdbfaae0946ed2747c3247904fafae4bbe792bbba66ca29b1f9b0179a4093bb363dd3abbc6478441528b28daf1797d2d8fd3f200f0a6fdb8bfd4dfb02506bb16d4db103531d6f13d9c46613d4f03b84be135e19c4ede30717613efc50d6f28b6a36b5e61839ccd7a36a0d8ad6e81691f0772cff88f0e800bb9539e71ea3f1a52f281a9ba7c469fa917dbc75d8b7dc9e702c6a70d5d46b453804771adb6bc8cf85476ac31657776e674b632358f1e49ac1a9a4934736830224a136d76be62cf925af87746be07ef128b11a06d798ac7780843d46a2a5d3dd12eac27ef1af0a12581ec423df8be05aa0473b7cc30e55cf77d7255d1b2f9d0223e3dea9912552aee6fc1c9d3266d69266d40c8b85730e1a4327c956a51e06705e8b3f47463ab4bac168253ae55583523ed4220d97908932efc57616b4977e2418f26eb1077a4320386abf788ff8456bf044f5b95821520f8985f734a97dbcd5ad21d66d1b870a73f5bba1c4f382d369a3c8ebff86ce0b7fa29cd589053f1fbdc1fc392ccac7d548f11326ccc22cf641b7f10afa5d79c1b2015b768bda455fe573bd15001259a9ef3a0c47e3e4937065a4712d2d8d49c989bd59e3d8c130913fef431d954fd34b09947d40dce38a6301602ef65bcd1d874ae23bde4436cd0c0c2d8c671f0f73b42ee20fe083de4ce2380b4a33568eee72b82e6bf157dcee288091f9ffcc0837ae75ab3aba9eb7e081a5e491239a19e778dca132b958ee845a8de97876721766507191e74dd6b5ed4c08ba9b9547525fc0b809c46a4a7dc529df6f9b33f27905f355043c31f4a32cf2d1f7086a6af0455fc424da920ec2906a7464823b9f8bab0d693c8ea8a55317df43e6c6bda79015814558da1fc2c92ed6c80863544788dac519f418349a7707d59a1dcd160ea4867a562dd04c780c954d3e43bff6c8186eb4b80359ed267e216c96b0ef3d57fd3d39a99cc7e034afd2d3f6c62cfcdea85eec1f2ef1f2fc5f272a6117034b6c8b5085a1d02892d942b952a17fef041d4082296a0fc800497b86760d89b7eaf03a90c5482fe5304fe81fd4bda06a8a333a03a819b3341ab9335f12dcdb8fbdb27c235d80ee0d41844c564f9c84de48c198064a9111d236e6c3e60f36e3c656216c191097f17b285fba088391f5b65a430e92eeaae792b243fe05c88887bc9d52480bc63628e5a1e4821a977e1411dd22d343aca4aebe1b9b6efd23197bb3701fcbbbab1d5f2f426b85601c5ff37d79377f41dd8ebd16af52ae50ed0b45d4db319b1d6789cd2b6c3fa2e1ba2abb2a00d1267e0fe7467aa2be5a76af028c15f9ce9f5edb31c1aea177795ee1ccee5b667bac787f6115659a264fd5be27cd77e4eab64c4ac30a57eec78bcf3b13a0119ea60a0e8953d2f955c10295a5b0ffe4e653042deed40473a143e6201a1f64052f0d6edaa5796a4956e477745b0e00805f4cf4e0e3300ce338d8f27e8bd6c3fb626222f3e09ce38ac076bb49726e31aa4e48372f3638e7ddb66451496387e4a8c9d0ed3268f368455c3a055e70edb94bcf7961a51c740c9e1de1cbbc70c189dd48c1208a86b0fc85264659597bcd3c0980c87146446d6d2edee022b91573f8f734d2d8e21b6b1b78873c39ec34dad9b90d9b6214c33dec5dc087c108c9c8a70de64b6e5cb794cc35f581af2416831968d0021ce6e7d56e4880c34481e99cc6ac41a36619fc0de2628196f4d6f4b10ac82ff810302bab14cd58b6c10143a758f93a4bc99893d6d0a6391c5a4cd9c90ca2ce6e8310d4293305548a618cd0a5d4477dbb7911ce5bf92fc8810d549cb440ff9d5607811ecc5a0e878b780c51814fda3826dc4f108f63ac9e2a74cc0a375a5be4c9eed9dc836dfd3761f966f58a6eb60ca6d8102d34d5640cab6c253b7fbb6a17e0eece493f2541dbb9c43b6ea618deecb816cc5af8ad9b1914d445d622656cbfe9e5e22aa341412fdfb0b59b6baee4c56a9fe1ae146e47e7d3aa3f9b2eec7492e6ec1d01eb9ecdf1c9488c3c8a02b5582e145256cc6df17bd3947e4528941e358a83bc32802a047d7c3564e8c8f4f3869a5d66a34421efdc67dde9cafc5fbe685be01d0c420f764d3a7f027225d9b3ac980a7b3af4536b7bf89abbce5d9a4474f461240097ec5d81e21d969a41065b3818b7936231d28f208b066c272641d011c2412e0ecff7115feeca6bf137068a0da8d88360e9741fd1c9147208355b0eb466dc00b48e1845868834dcdc21d2719eea5e0f40df5e04dbb45a87ef473212f0ffd2ea97f82487938a25893eb834ca6b9c27b1910c9e3a4fa57d7c453e25bb9a193d3b71df314f93f10b7ee2dbc0498e16dff70aa79cb61c2b22cf471014f4046f71358ed17082dd0a797c8e4a624bf79d774ef1892232c7f2fb8cb9958b3912d120e84adab1f4f9c943311212f9f006870741dd47d109fde5de7f3973aad517bafb3b83c9249c7283e5e8b977488068e6f7a90418b87824b55abb62e045d3605cfe8271339a0e6d5090629d33882c9339533ec4ae2436d6e0fd48e853c56d68744cec5b5acb0c3246c301d5dbc96db9f8bc38adb66fb9e5d8712f3f6761abaaea03a57a57a9dd8c4fdab4969770fd3f3afe8545e7f12152d01eada01133a0578391a2ff9a57d828dbfeb558cfba31dafdba1b629dd497352b8f92d630aa20ce9e95d416f5d65ce95efec6bc62fa3822127b19285e9a3420a48227bac6dcda6d3531ce57f401250cf581fa8bba6f1c23fc3269d6b01a347369225619a784f08f9a84b03a2b2d44874fe943712811555898f4812f4c9cb622dc2a9cd49996d086d6a7df33eb0436746ff3180d408e19aea960f4beb0ca12170549e874c23fdf27654fd870708bde5ff98f26d26defdfdf5d2dab2e29db10d53579a6913a1ea31c49d0bc6aaca7ec0babff6e5c251d72198adbd5b1192c33d756ecd4fe4b1e35a2d43dcd4a3ed292cb4a26b236d986d83cca36b4e31ed3d1cb1409b3dff0f437b2b3897300cb8de1672e6b878a7bfff9dd75e01e16b6bff56d06b392c0349cd85b99de6cfffdf9e93c3ecb2f688d3b2b7442dd257b0893572cfbf348b2f28989eba7b8579384884b0352a77207af3d5c18d9acb09f944e6881d9deb9a1a9248c4b9f74f341982122777e95da9667cd0f1f09dcad04de4899aa0cc6bd2ee428ef3a4224c37e911c202328262192ec8180d0cd8329e569caf9bfb515819872e65066433b92ec6ee289f278e8fde4eba02a6c09e695643ff3f439687dbb3a20e7cc4d99ccc54727bc19f831ff28efb29717036dbf78465ca8b63e2d1154d6629b4f4c576bd967f9a5cc554862728fd2a7bd7ac9c1f9b628b3542acf023013baf6a8002feb051e2d2ac8ca66c97d428026cea6e428aa72d96b7ef53bb185360a3ac0f079e9e9d11f51829262eb18b1682d48bfc5024e65fd7af0f94f5b940c5e7b2760ff841def1506855cf85e679cb904dcf829687aede7fb70840e9ad6d9b7ad7b27ffb6def8a840f40d11f794bde4bbced2b5b63fc2c263461ade7f02fee63a4e0907a7f1b44a0e3656c8e4ccc6f00731972e9b1337dbe72d70fe715b93ee4e106ca3c330356a5983e74dec3a5e75d20b5e5fad7e51f28ce38f966bf83a44bb83a6a9ec694a2d61d71f536f6bb3da3c787f2c763cc2045067d56d2600aa0f4f24bfaf43b37fb8fece78d91ec511fcce7680bbda8684cdb0e9a619cd9888321bbd3d60ebec5ce4787b9ce687ffcd136db1f2a2877143c7dcb17bcbbdc7c8b5e3850097e7a12691211b4e607361a4834bf6f8f73575c3009a0dc6f2fc257c23404fc293933e3b1c31d0975968da2d4e54ac082289ab76c725f38913d0480cb61c6bff80b3f30b25662a39459619417d820cd25b34265bea62809d9e52b57a7e1c604ee4390e39d6185f6391dbd46f514dd6af3978de827adf594da0f0d430a5f9e216a003f128533bf40377b5c0c5bcd391f77c9e3e9aed8994a514c21ab5e38ca0176c8427d742e66db449d5657a05266d96f4fa3f2adefcafed21bc955c9b26cffe1f98e3a17b002387c14eddd1cc3ddda603e1076669bd0d6eda1079c9fc976437cd999c37b655ae612587c74b7cd00ae658e54572818be38c62f9c2ac0c97cae81c4eddfe4940ba79a545e54b0cfb439b81336e7d8f191b5885d19138c3d57badcaec8a1320715304bccd54c7f04a0577a80f0c600a92719982844f27130cee8932577e5b237c8432fcbf80e0c1dc97e5ae31a9eb4027964465cbf531cc9ebe8fd1750d681bfb1590265801bf7c8bbb876b17daa008876961fc7db1a7e4d896075248d845035817e1aafa68ce8f4a6cb1f344d11056ecbe423f3df6b2c9d22db72f9cb35860c096d3c6a960012420b4133c6b567" - } - } - ] - } - }, - "closingTxProposed": [ - [ - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d15e320c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 674, - "signature": "9fbce74a05823e5ff30227d6d96abf1adea0e2c3e225f19011d2c26fbc955b8758877c062bfe97f7a311988946421561dec3009292133c7b9c7153044c793949", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1ce2e0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1586, - "signature": "006a1e2e00b0f2bbf61c20a9d1346e5df613344a59a26fcdaceb4d5a68a54e277ce25a9832c56e6a65372a1ee96cf559c2a6dd92d7cb7648dc12a81d8e177701", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1002e0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1792, - "signature": "690cdadcf5f89b49a2509571bb563934c054aaf7d5ba1a05db8293f98b20d13f4d5ee294430a44238dc55e2d535a537683fa26df4f4d314d3515ee223df6d512", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - } - ] + "lastClosingFeerate": null, + "localScript": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", + "remoteScript": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", + "proposedClosingTxs": [ ], - "bestUnpublishedClosingTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "020000000001012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1302d0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c04004730440220375de514cc4965caf75b10029df0efffd804aa7c912964a7e8f53544b4bf4182022068d1bc35cf91d9576ae9c658c47e33443665402dad89ef00b231032d58ebc01d0148304502210096286d2f0bf1327c074f643c483d7bb96bfaa8b37b1b6235f72cf21d9094efaf02204ef0813a42b6b76e221f0bac47d11806d3a61db74ed5136ba8144c25d1fcc85d0147522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae00000000", - "toLocalIndex": 1 - }, - "closingFeerates": null + "publishedClosingTxs": [ + ], + "waitingSinceBlock": 0 } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json index 7cadbc73e..c45d40625 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json @@ -171,235 +171,12 @@ }, "remotePerCommitmentSecrets": "" }, - "localShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c" - }, - "remoteShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ShutdownTlv.ChannelData", - "ecb": { - "data": "56ac7d2006dabb7b7e26a032ac384ce15362f230e007a713e45ee0a514aef433db46a55d4650585517be36dc68babddf369cbfa743e9ad976640545119c378548729fd5750f0772ae26e1ac80a288f3baa7ae2416e9ea796403fe0b2111a919370021aba83cc9c3abd6671ddb7e2c54eaa64701b86d5b70ac2a44f723e399c449ff1b9ccb8e20481ea1ab6b9a752b38601fb202f4cf960cfdf3c77c91829747fbff494ab3701148abfb09bdee3bffe1bc46d157ae16e9d701764532db299530a3679b6ecce62fab88a75a8524b1b7484faf08b9347e1e3e92623286b98a27f8236f9d94831c489c9b66846d2e65b4c75a2850bf30b3268a3122589a58223265e5d62df74cf77ab3e781c3c1612142810b30c5e45775c70e6f17b3829688d69b5ed8769bd70018bc19de67fce65240ed94f1179087e74ca49212edd8d735f015a077e5cf6328d426d69e31898c0aafafe6d21b5784bf337508ba33a13c88a34a2d04e9f48e0f3ce7ad704524b9360adbf70d91b7900ccd2a53ce4da6f7b66226c860936f5549c7065f572e9e6debcec7560d5f2ec49bd257cd23a0da2dab7b72cc3118ee72826c15cc033bced6d35187d955019d1988549be9712ce4561ca1247497a6e60b506cb7f21dea80205fc66d2e13382998538a5c0dae806d18b8fce515a7538a881c5a12f67bc11e838b1154901f4fc84eb3165477f4bdf02dcf3d8ab42ba2ade55ee22cdf07bacf2b561cf05ebd3f8eca283c451ec09f0a8cd7b3ecbb83d17157e9830bb3d85d7143ebab10306544820b3811e89b7ce013d96db368bd9eff243e575dc10b2e7c576b9df66ad6c7379e6dda3423bde90a564d9995ade0a72842816996bbdbfaae0946ed2747c3247904fafae4bbe792bbba66ca29b1f9b0179a4093bb363dd3abbc6478441528b28daf1797d2d8fd3f200f0a6fdb8bfd4dfb02506bb16d4db103531d6f13d9c46613d4f03b84be135e19c4ede30717613efc50d6f28b6a36b5e61839ccd7a36a0d8ad6e81691f0772cff88f0e800bb9539e71ea3f1a52f281a9ba7c469fa917dbc75d8b7dc9e702c6a70d5d46b453804771adb6bc8cf85476ac31657776e674b632358f1e49ac1a9a4934736830224a136d76be62cf925af87746be07ef128b11a06d798ac7780843d46a2a5d3dd12eac27ef1af0a12581ec423df8be05aa0473b7cc30e55cf77d7255d1b2f9d0223e3dea9912552aee6fc1c9d3266d69266d40c8b85730e1a4327c956a51e06705e8b3f47463ab4bac168253ae55583523ed4220d97908932efc57616b4977e2418f26eb1077a4320386abf788ff8456bf044f5b95821520f8985f734a97dbcd5ad21d66d1b870a73f5bba1c4f382d369a3c8ebff86ce0b7fa29cd589053f1fbdc1fc392ccac7d548f11326ccc22cf641b7f10afa5d79c1b2015b768bda455fe573bd15001259a9ef3a0c47e3e4937065a4712d2d8d49c989bd59e3d8c130913fef431d954fd34b09947d40dce38a6301602ef65bcd1d874ae23bde4436cd0c0c2d8c671f0f73b42ee20fe083de4ce2380b4a33568eee72b82e6bf157dcee288091f9ffcc0837ae75ab3aba9eb7e081a5e491239a19e778dca132b958ee845a8de97876721766507191e74dd6b5ed4c08ba9b9547525fc0b809c46a4a7dc529df6f9b33f27905f355043c31f4a32cf2d1f7086a6af0455fc424da920ec2906a7464823b9f8bab0d693c8ea8a55317df43e6c6bda79015814558da1fc2c92ed6c80863544788dac519f418349a7707d59a1dcd160ea4867a562dd04c780c954d3e43bff6c8186eb4b80359ed267e216c96b0ef3d57fd3d39a99cc7e034afd2d3f6c62cfcdea85eec1f2ef1f2fc5f272a6117034b6c8b5085a1d02892d942b952a17fef041d4082296a0fc800497b86760d89b7eaf03a90c5482fe5304fe81fd4bda06a8a333a03a819b3341ab9335f12dcdb8fbdb27c235d80ee0d41844c564f9c84de48c198064a9111d236e6c3e60f36e3c656216c191097f17b285fba088391f5b65a430e92eeaae792b243fe05c88887bc9d52480bc63628e5a1e4821a977e1411dd22d343aca4aebe1b9b6efd23197bb3701fcbbbab1d5f2f426b85601c5ff37d79377f41dd8ebd16af52ae50ed0b45d4db319b1d6789cd2b6c3fa2e1ba2abb2a00d1267e0fe7467aa2be5a76af028c15f9ce9f5edb31c1aea177795ee1ccee5b667bac787f6115659a264fd5be27cd77e4eab64c4ac30a57eec78bcf3b13a0119ea60a0e8953d2f955c10295a5b0ffe4e653042deed40473a143e6201a1f64052f0d6edaa5796a4956e477745b0e00805f4cf4e0e3300ce338d8f27e8bd6c3fb626222f3e09ce38ac076bb49726e31aa4e48372f3638e7ddb66451496387e4a8c9d0ed3268f368455c3a055e70edb94bcf7961a51c740c9e1de1cbbc70c189dd48c1208a86b0fc85264659597bcd3c0980c87146446d6d2edee022b91573f8f734d2d8e21b6b1b78873c39ec34dad9b90d9b6214c33dec5dc087c108c9c8a70de64b6e5cb794cc35f581af2416831968d0021ce6e7d56e4880c34481e99cc6ac41a36619fc0de2628196f4d6f4b10ac82ff810302bab14cd58b6c10143a758f93a4bc99893d6d0a6391c5a4cd9c90ca2ce6e8310d4293305548a618cd0a5d4477dbb7911ce5bf92fc8810d549cb440ff9d5607811ecc5a0e878b780c51814fda3826dc4f108f63ac9e2a74cc0a375a5be4c9eed9dc836dfd3761f966f58a6eb60ca6d8102d34d5640cab6c253b7fbb6a17e0eece493f2541dbb9c43b6ea618deecb816cc5af8ad9b1914d445d622656cbfe9e5e22aa341412fdfb0b59b6baee4c56a9fe1ae146e47e7d3aa3f9b2eec7492e6ec1d01eb9ecdf1c9488c3c8a02b5582e145256cc6df17bd3947e4528941e358a83bc32802a047d7c3564e8c8f4f3869a5d66a34421efdc67dde9cafc5fbe685be01d0c420f764d3a7f027225d9b3ac980a7b3af4536b7bf89abbce5d9a4474f461240097ec5d81e21d969a41065b3818b7936231d28f208b066c272641d011c2412e0ecff7115feeca6bf137068a0da8d88360e9741fd1c9147208355b0eb466dc00b48e1845868834dcdc21d2719eea5e0f40df5e04dbb45a87ef473212f0ffd2ea97f82487938a25893eb834ca6b9c27b1910c9e3a4fa57d7c453e25bb9a193d3b71df314f93f10b7ee2dbc0498e16dff70aa79cb61c2b22cf471014f4046f71358ed17082dd0a797c8e4a624bf79d774ef1892232c7f2fb8cb9958b3912d120e84adab1f4f9c943311212f9f006870741dd47d109fde5de7f3973aad517bafb3b83c9249c7283e5e8b977488068e6f7a90418b87824b55abb62e045d3605cfe8271339a0e6d5090629d33882c9339533ec4ae2436d6e0fd48e853c56d68744cec5b5acb0c3246c301d5dbc96db9f8bc38adb66fb9e5d8712f3f6761abaaea03a57a57a9dd8c4fdab4969770fd3f3afe8545e7f12152d01eada01133a0578391a2ff9a57d828dbfeb558cfba31dafdba1b629dd497352b8f92d630aa20ce9e95d416f5d65ce95efec6bc62fa3822127b19285e9a3420a48227bac6dcda6d3531ce57f401250cf581fa8bba6f1c23fc3269d6b01a347369225619a784f08f9a84b03a2b2d44874fe943712811555898f4812f4c9cb622dc2a9cd49996d086d6a7df33eb0436746ff3180d408e19aea960f4beb0ca12170549e874c23fdf27654fd870708bde5ff98f26d26defdfdf5d2dab2e29db10d53579a6913a1ea31c49d0bc6aaca7ec0babff6e5c251d72198adbd5b1192c33d756ecd4fe4b1e35a2d43dcd4a3ed292cb4a26b236d986d83cca36b4e31ed3d1cb1409b3dff0f437b2b3897300cb8de1672e6b878a7bfff9dd75e01e16b6bff56d06b392c0349cd85b99de6cfffdf9e93c3ecb2f688d3b2b7442dd257b0893572cfbf348b2f28989eba7b8579384884b0352a77207af3d5c18d9acb09f944e6881d9deb9a1a9248c4b9f74f341982122777e95da9667cd0f1f09dcad04de4899aa0cc6bd2ee428ef3a4224c37e911c202328262192ec8180d0cd8329e569caf9bfb515819872e65066433b92ec6ee289f278e8fde4eba02a6c09e695643ff3f439687dbb3a20e7cc4d99ccc54727bc19f831ff28efb29717036dbf78465ca8b63e2d1154d6629b4f4c576bd967f9a5cc554862728fd2a7bd7ac9c1f9b628b3542acf023013baf6a8002feb051e2d2ac8ca66c97d428026cea6e428aa72d96b7ef53bb185360a3ac0f079e9e9d11f51829262eb18b1682d48bfc5024e65fd7af0f94f5b940c5e7b2760ff841def1506855cf85e679cb904dcf829687aede7fb70840e9ad6d9b7ad7b27ffb6def8a840f40d11f794bde4bbced2b5b63fc2c263461ade7f02fee63a4e0907a7f1b44a0e3656c8e4ccc6f00731972e9b1337dbe72d70fe715b93ee4e106ca3c330356a5983e74dec3a5e75d20b5e5fad7e51f28ce38f966bf83a44bb83a6a9ec694a2d61d71f536f6bb3da3c787f2c763cc2045067d56d2600aa0f4f24bfaf43b37fb8fece78d91ec511fcce7680bbda8684cdb0e9a619cd9888321bbd3d60ebec5ce4787b9ce687ffcd136db1f2a2877143c7dcb17bcbbdc7c8b5e3850097e7a12691211b4e607361a4834bf6f8f73575c3009a0dc6f2fc257c23404fc293933e3b1c31d0975968da2d4e54ac082289ab76c725f38913d0480cb61c6bff80b3f30b25662a39459619417d820cd25b34265bea62809d9e52b57a7e1c604ee4390e39d6185f6391dbd46f514dd6af3978de827adf594da0f0d430a5f9e216a003f128533bf40377b5c0c5bcd391f77c9e3e9aed8994a514c21ab5e38ca0176c8427d742e66db449d5657a05266d96f4fa3f2adefcafed21bc955c9b26cffe1f98e3a17b002387c14eddd1cc3ddda603e1076669bd0d6eda1079c9fc976437cd999c37b655ae612587c74b7cd00ae658e54572818be38c62f9c2ac0c97cae81c4eddfe4940ba79a545e54b0cfb439b81336e7d8f191b5885d19138c3d57badcaec8a1320715304bccd54c7f04a0577a80f0c600a92719982844f27130cee8932577e5b237c8432fcbf80e0c1dc97e5ae31a9eb4027964465cbf531cc9ebe8fd1750d681bfb1590265801bf7c8bbb876b17daa008876961fc7db1a7e4d896075248d845035817e1aafa68ce8f4a6cb1f344d11056ecbe423f3df6b2c9d22db72f9cb35860c096d3c6a960012420b4133c6b567" - } - } - ] - } - }, - "closingTxProposed": [ - [ - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d15e320c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 674, - "signature": "9fbce74a05823e5ff30227d6d96abf1adea0e2c3e225f19011d2c26fbc955b8758877c062bfe97f7a311988946421561dec3009292133c7b9c7153044c793949", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d10e310c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1010, - "signature": "959c687c87ee014e988275260a28f5522b9aa7df58201780e8da0714c80466db75747a0cbe30ad56656ed9f7d05599fc959758ed23566b815565dc20177d366c", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d114300c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1260, - "signature": "7ba4aab1322712d9ac5b12b0ead3c2386eab48e380fb0e93218de67de109f1d5552694f878c64a8b536e7be333bbec781fe2838845852db3905de3fcb46aa536", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d11a2f0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1510, - "signature": "12804a6c0c36c336baffe28a211042a1fc1cdd3c0626e06643a29a30d282b2b076487caa9a6e619ab141ffd6775bd4e1da32d55f87a548ea27421720a3e8ad08", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1202e0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 1760, - "signature": "4214aa761eaec84f0bc571ad5bd2a427ba9ce6fc363adcb3c4b81aa5ced969d92361859b2eb9557afab41ebe54dcd18d645781085e3aa534819e57d8bea9d2e2", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1262d0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 2010, - "signature": "a66e4ad93233f6add8965f12ecb66f7c06a3e7a8ee42f692bb50426a7a32d470323c560e5069cc1801f85ad13a3bf149601a1a4b86d5d82cfb934b75f4dd5757", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - }, - { - "unsignedTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "02000000012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d12c2c0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c00000000", - "toLocalIndex": 1 - }, - "localClosingSigned": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "feeSatoshis": 2260, - "signature": "86c58c46963050df8fb7babb0dbbada4f4275c07089c9c7ac56a1b032b00bfd82244dc56d99ecff8f117e0b11bcf589a33f40c2248c7dd37eb84caa97e6000f5", - "tlvStream": { - "records": [ - { - "type": "fr.acinq.lightning.wire.ClosingSignedTlv.FeeRange", - "min": 337, - "max": 1348 - } - ] - } - } - } - ] + "lastClosingFeerate": null, + "localScript": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", + "remoteScript": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", + "proposedClosingTxs": [ ], - "bestUnpublishedClosingTx": { - "input": { - "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", - "txOut": { - "amount": 1000000, - "publicKeyScript": "00204ba42b50e089764e60e9e0c76c57e41d252d6beb976332f14a7b255bbb5322b8" - }, - "redeemScript": "522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae" - }, - "tx": "020000000001012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd0000000000ffffffff02400d03000000000016001434947cfb2e8f6054ddf12daed4308cbe342580d1322b0c000000000016001405e0104aa726e34ff5cd3a6320d05c0862b5b01c040047304402206395a7e0db7cffd3d87b3edff6cee0d9fc0cd75a7d968b4ec4ff013bb27d3684022060e4e35277d0019d55dda79d7fab336ee1332452a406b273808c36ed3a7ea37901483045022100d1a0d9307bd4fbb4561de2392e27d1c42dcd1da524cdea0144444e338f9e0f440220546af31b1a48bfcaf8b0f8a98f8ab7618c4c850c5226b976288bfed1e15516030147522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452ae00000000", - "toLocalIndex": 1 - }, - "closingFeerates": null + "publishedClosingTxs": [ + ], + "waitingSinceBlock": 0 } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json index 8269f04df..8fd2e2879 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json @@ -173,18 +173,12 @@ }, "remotePerCommitmentSecrets": "" }, - "localShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "001434947cfb2e8f6054ddf12daed4308cbe342580d1" - }, - "remoteShutdown": { - "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", - "scriptPubKey": "51050102030405" - }, - "closingTxProposed": [ - [ - ] + "lastClosingFeerate": null, + "localScript": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", + "remoteScript": "51050102030405", + "proposedClosingTxs": [ + ], + "publishedClosingTxs": [ ], - "bestUnpublishedClosingTx": null, - "closingFeerates": null + "waitingSinceBlock": 0 } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json index bc1a7bcdf..d314af94c 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json @@ -262,6 +262,6 @@ "scriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c" }, "remoteShutdown": null, - "closingFeerates": null, + "closingFeerate": null, "spliceStatus": "None" } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json index 01a0bcfe3..0eafd8510 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json @@ -233,6 +233,6 @@ "remoteChannelUpdate": null, "localShutdown": null, "remoteShutdown": null, - "closingFeerates": null, + "closingFeerate": null, "spliceStatus": "None" } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json index dec32f7ff..a7dfc44d1 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json @@ -200,6 +200,6 @@ }, "localShutdown": null, "remoteShutdown": null, - "closingFeerates": null, + "closingFeerate": null, "spliceStatus": "None" } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json index 5fe2fa192..de93fad72 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json @@ -370,6 +370,6 @@ "remoteChannelUpdate": null, "localShutdown": null, "remoteShutdown": null, - "closingFeerates": null, + "closingFeerate": null, "spliceStatus": "None" } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json b/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json index 397d90e94..4e55bb041 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json @@ -285,5 +285,5 @@ "channelId": "2f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd", "scriptPubKey": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c" }, - "closingFeerates": null + "closingFeerate": null } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json b/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json index e63f60088..8e194ef13 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json @@ -277,5 +277,5 @@ ] } }, - "closingFeerates": null + "closingFeerate": null } \ No newline at end of file