From 3179d2f8b40b834e56210457736762e9c08e40b3 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 18 Nov 2024 14:24:51 +0100 Subject: [PATCH] Add specific splicing nonces to channel_reestablish --- .../fr/acinq/lightning/channel/Commitments.kt | 5 +- .../acinq/lightning/channel/states/Channel.kt | 27 ++- .../acinq/lightning/channel/states/Normal.kt | 55 +++++- .../acinq/lightning/channel/states/Syncing.kt | 39 ++++- .../lightning/transactions/Transactions.kt | 18 +- .../fr/acinq/lightning/wire/ChannelTlv.kt | 17 ++ .../acinq/lightning/wire/LightningMessages.kt | 4 + .../channel/states/SpliceTestsCommon.kt | 156 +++++++++++++++++- 8 files changed, 302 insertions(+), 19 deletions(-) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt index 30b7bb0db..df47dcf39 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt @@ -133,16 +133,13 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishabl val signed = Transactions.partialSign(localCommitTx, localFundingKey, localFundingKey.publicKey(), remoteFundingPubKey, localNonce!!, remoteSig.nonce) .flatMap { localSig -> Transactions.aggregatePartialSignatures(localCommitTx, localSig, remoteSig.partialSig, localFundingKey.publicKey(), remoteFundingPubKey, localNonce.second, remoteSig.nonce) } .map { aggSig -> Transactions.addAggregatedSignature(localCommitTx, aggSig) } - if (signed.isLeft) { - return Either.Left(InvalidCommitmentSignature(params.channelId, localCommitTx.tx.txid)) - } signed.right!! } } // no need to compute htlc sigs if commit sig doesn't check out when (val check = Transactions.checkSpendable(signedCommitTx)) { is Try.Failure -> { - log.error(check.error) { "remote signature $commit is invalid" } + log.error(check.error) { "remote signature $commit is invalid, localNonce = $localNonce" } return Either.Left(InvalidCommitmentSignature(params.channelId, signedCommitTx.tx.txid)) } else -> {} 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 bb245d331..fd612da4f 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 @@ -306,7 +306,6 @@ sealed class ChannelState { /** A channel state that is persisted to the DB. */ sealed class PersistedChannelState : ChannelState() { abstract val channelId: ByteVector32 - internal fun ChannelContext.createChannelReestablish(): HasEncryptedChannelData = when (val state = this@PersistedChannelState) { is WaitForFundingSigned -> { val myFirstPerCommitmentPoint = keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).commitmentPoint(0) @@ -332,6 +331,29 @@ sealed class PersistedChannelState : ChannelState() { true -> state.commitments.active.map { channelKeys.verificationNonce(it.fundingTxIndex, state.commitments.localCommitIndex + 1).second } else -> null } + val spliceNonces = when { + !state.commitments.isTaprootChannel -> null + state is Normal && state.spliceStatus is SpliceStatus.WaitingForSigs -> { + logger.info { "splice in progress, re-sending splice nonces" } + val localCommitIndex = when (state.spliceStatus.session.localCommit) { + is Either.Left -> state.spliceStatus.session.localCommit.value.index + is Either.Right -> state.spliceStatus.session.localCommit.value.index + } + listOf( + channelKeys.verificationNonce(state.spliceStatus.session.fundingTxIndex, localCommitIndex).second, + channelKeys.verificationNonce(state.spliceStatus.session.fundingTxIndex, localCommitIndex + 1).second + ) + } + + state.commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx -> { + logger.info { "splice may not have confirmed yet, re-sending splice nonces" } + listOf( + channelKeys.verificationNonce(state.commitments.latest.fundingTxIndex, state.commitments.localCommitIndex).second, + channelKeys.verificationNonce(state.commitments.latest.fundingTxIndex, state.commitments.localCommitIndex + 1).second + ) + } + else -> null + } val unsignedFundingTxId = when (state) { is WaitForFundingConfirmed -> state.getUnsignedFundingTxId() is Normal -> state.getUnsignedFundingTxId() // a splice was in progress, we tell our peer that we are remembering it and are expecting signatures @@ -339,7 +361,8 @@ sealed class PersistedChannelState : ChannelState() { } val tlvs: TlvStream = TlvStream(setOfNotNull( unsignedFundingTxId?.let { ChannelReestablishTlv.NextFunding(it) }, - myNextLocalNonces?.let { ChannelReestablishTlv.NextLocalNoncesTlv(it) } + myNextLocalNonces?.let { ChannelReestablishTlv.NextLocalNoncesTlv(it) }, + spliceNonces?.let { ChannelReestablishTlv.SpliceNoncesTlv(it) } )) ChannelReestablish( channelId = channelId, 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 e6637de91..4cc0d3346 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 @@ -10,6 +10,7 @@ import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchEventConfirmed import fr.acinq.lightning.blockchain.WatchEventSpent import fr.acinq.lightning.channel.* +import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.* @@ -185,7 +186,7 @@ data class Normal( is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityPurchase, cmd.message.channelData) } } - ignoreRetransmittedCommitSig(cmd.message) -> { + ignoreRetransmittedCommitSig(cmd.message, channelKeys()) -> { // We haven't received our peer's tx_signatures for the latest funding transaction and asked them to resend it on reconnection. // They also resend their corresponding commit_sig, but we have already received it so we should ignore it. // Note that the funding transaction may have confirmed while we were offline. @@ -978,13 +979,57 @@ data class Normal( return Pair(nextState, actions) } + /* + def ignoreRetransmittedCommitSig(commitSig: CommitSig, keyManager: ChannelKeyManager): Boolean = commitSig.sigOrPartialSig match { + case _ if !params.channelFeatures.hasFeature(Features.DualFunding) => false + case _ if commitSig.batchSize != 1 => false + case Left(_) => + commitSig.sigOrPartialSig == latest.localCommit.commitTxAndRemoteSig.remoteSig + case Right(psig) if active.size > 1 => + // we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed) + // => instead we simply check that the provided partial signature is valid for our latest commit tx + val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, latest.fundingTxIndex).publicKey + val Some(localNonce) = generateLocalNonce(keyManager, latest.fundingTxIndex, latest.localCommit.index) + val Right(oldPsig) = latest.localCommit.commitTxAndRemoteSig.remoteSig + val currentcheck = latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(psig, localFundingKey, localNonce, latest.remoteFundingPubKey) + val oldcheck = latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(oldPsig, localFundingKey, localNonce, latest.remoteFundingPubKey) + require(oldcheck) + currentcheck + case Right(psig) => + // we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed) + // => instead we simply check that the provided partial signature is valid for our latest commit tx + val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, latest.fundingTxIndex).publicKey + val Some(localNonce) = generateLocalNonce(keyManager, latest.fundingTxIndex, latest.localCommit.index) + val Right(oldPsig) = latest.localCommit.commitTxAndRemoteSig.remoteSig + val currentcheck = latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(psig, localFundingKey, localNonce, latest.remoteFundingPubKey) + val oldcheck = latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(oldPsig, localFundingKey, localNonce, latest.remoteFundingPubKey) + require(oldcheck) + currentcheck + } + */ /** This function should be used to ignore a commit_sig that we've already received. */ - private fun ignoreRetransmittedCommitSig(commit: CommitSig): Boolean { + private fun ignoreRetransmittedCommitSig(commit: CommitSig, channelKeys: KeyManager.ChannelKeys): Boolean { // If we already have a signed commitment transaction containing their signature, we must have previously received that commit_sig. val commitTx = commitments.latest.localCommit.publishableTxs.commitTx.tx - return commitments.params.channelFeatures.hasFeature(Feature.DualFunding) && - commit.batchSize == 1 && - commitTx.txIn.first().witness.stack.contains(Scripts.der(commit.signature, SigHash.SIGHASH_ALL)) + return when { + !commitments.params.channelFeatures.hasFeature(Feature.DualFunding) -> false + commit.batchSize != 1 -> false + commit.sigOrPartialSig.isLeft -> commitTx.txIn.first().witness.stack.contains(Scripts.der(commit.signature, SigHash.SIGHASH_ALL)) + this.commitments.active.size > 1 -> { + // we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed) + // => instead we simply check that the provided partial signature is valid for our latest commit tx + val localFundingKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex).publicKey() + val localNonce = channelKeys.verificationNonce(commitments.latest.fundingTxIndex, commitments.latest.localCommit.index).second + commitments.latest.localCommit.publishableTxs.commitTx.checkPartialSignature(commit.partialSig!!, localFundingKey, localNonce, commitments.latest.remoteFundingPubkey) + } + else -> { + // we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed) + // => instead we simply check that the provided partial signature is valid for our latest commit tx + val localFundingKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex).publicKey() + val localNonce = channelKeys.verificationNonce(commitments.latest.fundingTxIndex, commitments.latest.localCommit.index).second + commitments.latest.localCommit.publishableTxs.commitTx.checkPartialSignature(commit.partialSig!!, localFundingKey, localNonce, commitments.latest.remoteFundingPubkey) + } + } } /** If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. */ 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 4d8172b6d..d8cfd9e3e 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 @@ -1,6 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.PrivateKey +import fr.acinq.bitcoin.Script.tail import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.* @@ -123,6 +124,25 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: when (val syncResult = handleSync(state.commitments, cmd.message)) { is SyncResult.Failure -> handleSyncFailure(state.commitments, cmd.message, syncResult) is SyncResult.Success -> { + val (pendingRemoteNextLocalNonce, nextRemoteNonces) = when { + !state.commitments.isTaprootChannel -> Pair(null, listOf()) + state.spliceStatus is SpliceStatus.WaitingForSigs && cmd.message.nextLocalNonces.size == state.commitments.active.size -> { + Pair(cmd.message.secondSpliceNonce, cmd.message.nextLocalNonces) + } + + state.spliceStatus is SpliceStatus.WaitingForSigs && cmd.message.nextLocalNonces.size == state.commitments.active.size + 1 -> { + Pair(cmd.message.nextLocalNonces.firstOrNull(), cmd.message.nextLocalNonces.tail()) + } + + cmd.message.nextLocalNonces.size == state.commitments.active.size - 1 -> { + Pair(null, listOf(cmd.message.secondSpliceNonce!!) + cmd.message.nextLocalNonces) + } + + else -> { + Pair(null, cmd.message.nextLocalNonces) + } + } + // normal case, our data is up-to-date val actions = ArrayList() @@ -137,8 +157,16 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // resume splice signing session if any val spliceStatus1 = if (state.spliceStatus is SpliceStatus.WaitingForSigs && state.spliceStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) { // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. - logger.info { "re-sending commit_sig for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" } - val commitSig = state.spliceStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.spliceStatus.session, cmd.message.nextLocalNonces.firstOrNull()) + val spliceNonce = when { + state.spliceStatus.session.remoteCommit.index == cmd.message.nextLocalCommitmentNumber -> cmd.message.secondSpliceNonce + state.spliceStatus.session.remoteCommit.index == cmd.message.nextLocalCommitmentNumber - 1 -> cmd.message.firstSpliceNonce + else -> { + // we should never end up here, it would have been handled in handleSync() + error("invalid nextLocalCommitmentNumber in ChannelReestablish") + } + } + val commitSig = state.spliceStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.spliceStatus.session, spliceNonce) + logger.info { "re-sending commit_sig ${commitSig.partialSig} for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" } actions.add(ChannelAction.Message.Send(commitSig)) state.spliceStatus } else if (state.commitments.latest.fundingTxId == cmd.message.nextFundingTxId) { @@ -146,15 +174,16 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: is LocalFundingStatus.UnconfirmedFundingTx -> { if (localFundingStatus.sharedTx is PartiallySignedSharedTransaction) { // If we have not received their tx_signatures, we can't tell whether they had received our commit_sig, so we need to retransmit it - logger.info { "re-sending commit_sig for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" } + logger.info { "re-sending commit_sig and tx_signatures for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" } val commitSig = state.commitments.latest.remoteCommit.sign( channelKeys(), state.commitments.params, fundingTxIndex = state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput, - cmd.message.nextLocalNonces.firstOrNull() + cmd.message.firstSpliceNonce ) + logger.info { "computed $commitSig with remote nonce = ${nextRemoteNonces.firstOrNull()}" } actions.add(ChannelAction.Message.Send(commitSig)) } logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" } @@ -195,7 +224,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: actions.addAll(syncResult.retransmit.map { ChannelAction.Message.Send(it) }) // then we clean up unsigned updates - val commitments1 = discardUnsignedUpdates(state.commitments).copy(nextRemoteNonces = cmd.message.nextLocalNonces) + val commitments1 = discardUnsignedUpdates(state.commitments).copy(pendingRemoteNextLocalNonce = pendingRemoteNextLocalNonce, nextRemoteNonces = nextRemoteNonces) if (commitments1.changes.localHasChanges()) { actions.add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign)) 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 e07d45bd3..c3daa6828 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 @@ -23,11 +23,13 @@ import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.bitcoin.utils.Try +import fr.acinq.bitcoin.utils.getOrElse import fr.acinq.bitcoin.utils.runTrying import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.Commitments +import fr.acinq.lightning.channel.PartialSignatureWithNonce import fr.acinq.lightning.io.* import fr.acinq.lightning.transactions.CommitmentOutput.InHtlc import fr.acinq.lightning.transactions.CommitmentOutput.OutHtlc @@ -101,7 +103,21 @@ object Transactions { data class SpliceTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() @Serializable - data class CommitTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class CommitTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + fun checkPartialSignature(psig: PartialSignatureWithNonce, localPubKey: PublicKey, localNonce: IndividualNonce, remotePubKey: PublicKey): Boolean { + val inputIndex = this.tx.txIn.indexOfFirst { it.outPoint == input.outPoint } + val session = Musig2.taprootSession( + this.tx, + inputIndex, + listOf(this.input.txOut), + Scripts.sort(listOf(localPubKey, remotePubKey)), + listOf(localNonce, psig.nonce), + null + ) + val result = session.map { it.verify(psig.partialSig, psig.nonce, remotePubKey) }.getOrElse { false } + return result + } + } @Serializable sealed class HtlcTx : TransactionWithInputInfo() { 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 0a42e9f3d..5fbaea4af 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 @@ -285,6 +285,23 @@ sealed class ChannelReestablishTlv : Tlv { } } } + + data class SpliceNoncesTlv(val nonces: List) : ChannelReestablishTlv() { + override val tag: Long get() = SpliceNoncesTlv.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 6 + override fun read(input: Input): SpliceNoncesTlv { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return SpliceNoncesTlv(nonces) + } + } + } } sealed class ShutdownTlv : Tlv { 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 484fbd3a2..74d9e2ad4 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 @@ -1370,6 +1370,9 @@ data class ChannelReestablish( val nextFundingTxId: TxId? = tlvStream.get()?.txId val nextLocalNonces: List = tlvStream.get()?.nonces ?: listOf() + val spliceNonces: List = tlvStream.get()?.nonces ?: listOf() + val firstSpliceNonce = if (spliceNonces.isNotEmpty()) spliceNonces[0] else null + val secondSpliceNonce = if (spliceNonces.isNotEmpty()) spliceNonces[1] else null override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): ChannelReestablish = copy(tlvStream = tlvStream.addOrUpdate(ChannelReestablishTlv.ChannelData(ecd))) @@ -1391,6 +1394,7 @@ data class ChannelReestablish( ChannelReestablishTlv.ChannelData.tag to ChannelReestablishTlv.ChannelData.Companion as TlvValueReader, ChannelReestablishTlv.NextFunding.tag to ChannelReestablishTlv.NextFunding.Companion as TlvValueReader, ChannelReestablishTlv.NextLocalNoncesTlv.tag to ChannelReestablishTlv.NextLocalNoncesTlv.Companion as TlvValueReader, + ChannelReestablishTlv.SpliceNoncesTlv.tag to ChannelReestablishTlv.SpliceNoncesTlv.Companion as TlvValueReader, ) override fun read(input: Input): ChannelReestablish { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index 9cd3df80f..7e4a8fcb1 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -785,6 +785,33 @@ class SpliceTestsCommon : LightningTestSuite() { resolveHtlcs(alice4, bob4, htlcs, commitmentsCount = 2) } + @Test + fun `disconnect -- commit_sig not received -- simple taproot channels`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, _, bob1, _) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) + + val spliceStatus = alice1.state.spliceStatus + assertIs(spliceStatus) + + val (alice2, bob2, channelReestablishAlice) = disconnect(alice1, bob1) + assertEquals(channelReestablishAlice.nextFundingTxId, spliceStatus.session.fundingTx.txId) + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(channelReestablishAlice)) + assertIs>(bob3) + assertEquals(actionsBob3.size, 4) + val channelReestablishBob = actionsBob3.findOutgoingMessage() + val commitSigBob = actionsBob3.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob3.filterIsInstance().map { it.add }.toSet()) + assertEquals(channelReestablishBob.nextFundingTxId, spliceStatus.session.fundingTx.txId) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) + assertIs>(alice3) + assertEquals(actionsAlice3.size, 3) + val commitSigAlice = actionsAlice3.findOutgoingMessage() + val (alice4, bob4) = exchangeSpliceSigs(alice3, commitSigAlice, bob3, commitSigBob) + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice3.filterIsInstance().map { it.add }.toSet()) + resolveHtlcs(alice4, bob4, htlcs, commitmentsCount = 2) + } + @Test fun `disconnect -- commit_sig received by alice`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() @@ -814,10 +841,135 @@ class SpliceTestsCommon : LightningTestSuite() { resolveHtlcs(alice6, bob5, htlcs, commitmentsCount = 2) } + @Test + fun `disconnect -- commit_sig received by alice -- simple taproot channels`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) + val (alice2, _, bob2, commitSigBob1) = spliceInAndOutWithoutSigs(alice1, bob1, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(commitSigBob1)) + assertIs>(alice3) + assertTrue(actionsAlice3.isEmpty()) + val spliceStatus = alice3.state.spliceStatus + assertIs(spliceStatus) + + val (alice4, bob3, channelReestablishAlice) = disconnect(alice3, bob2) + assertEquals(channelReestablishAlice.nextFundingTxId, spliceStatus.session.fundingTx.txId) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) + assertIs>(bob4) + assertEquals(actionsBob4.size, 4) + val channelReestablishBob = actionsBob4.findOutgoingMessage() + val commitSigBob2 = actionsBob4.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) + assertEquals(channelReestablishBob.nextFundingTxId, spliceStatus.session.fundingTx.txId) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(channelReestablishBob)) + assertIs>(alice5) + assertEquals(actionsAlice5.size, 3) + val commitSigAlice = actionsAlice5.findOutgoingMessage() + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice5.filterIsInstance().map { it.add }.toSet()) + val (alice6, bob5) = exchangeSpliceSigs(alice5, commitSigAlice, bob4, commitSigBob2) + resolveHtlcs(alice6, bob5, htlcs, commitmentsCount = 2) + } + @Test fun `disconnect -- tx_signatures sent by bob`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, commitSigAlice1, bob1, commitSigBob1) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(80_000.sat), outAmount = 50_000.sat) + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSigAlice1)) + assertIs>(bob2) + val spliceTxId = actionsBob2.hasOutgoingMessage().txId + assertEquals(bob2.state.spliceStatus, SpliceStatus.None) + val (alice11, _) = alice1.process(ChannelCommand.MessageReceived(commitSigBob1)) + assertIs>(alice11) + + val (alice2, bob3, channelReestablishAlice) = disconnect(alice11, bob2) + assertEquals(channelReestablishAlice.nextFundingTxId, spliceTxId) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) + assertEquals(actionsBob4.size, 5) + val channelReestablishBob = actionsBob4.findOutgoingMessage() + val commitSigBob2 = actionsBob4.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) + val txSigsBob = actionsBob4.findOutgoingMessage() + assertEquals(channelReestablishBob.nextFundingTxId, spliceTxId) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) + assertEquals(actionsAlice3.size, 3) + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice3.filterIsInstance().map { it.add }.toSet()) + val commitSigAlice2 = actionsAlice3.findOutgoingMessage() + + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(commitSigBob2)) + assertTrue(actionsAlice4.isEmpty()) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(txSigsBob)) + assertIs>(alice5) + assertEquals(alice5.state.commitments.active.size, 2) + assertEquals(actionsAlice5.size, 8) + assertEquals(actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, spliceTxId) + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice5.filterIsInstance().map { it.add }.toSet()) + actionsAlice5.hasWatchConfirmed(spliceTxId) + actionsAlice5.has() + actionsAlice5.has() + val txSigsAlice = actionsAlice5.findOutgoingMessage() + + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(commitSigAlice2)) + assertTrue(actionsBob5.isEmpty()) + val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(txSigsAlice)) + assertIs>(bob6) + assertEquals(bob6.state.commitments.active.size, 2) + assertEquals(actionsBob6.size, 2) + assertEquals(actionsBob6.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, spliceTxId) + actionsBob6.has() + } + + @Test + fun `disconnect -- tx_signatures sent by bob, commit_sig not received by Alice -- simple taproot channels`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, commitSigAlice1, bob1, _) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(80_000.sat), outAmount = 50_000.sat) + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSigAlice1)) + assertIs>(bob2) + val spliceTxId = actionsBob2.hasOutgoingMessage().txId + assertEquals(bob2.state.spliceStatus, SpliceStatus.None) + + val (alice2, bob3, channelReestablishAlice) = disconnect(alice1, bob2) + assertEquals(channelReestablishAlice.nextFundingTxId, spliceTxId) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) + assertEquals(actionsBob4.size, 5) + val channelReestablishBob = actionsBob4.findOutgoingMessage() + val commitSigBob2 = actionsBob4.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) + val txSigsBob = actionsBob4.findOutgoingMessage() + assertEquals(channelReestablishBob.nextFundingTxId, spliceTxId) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) + assertEquals(actionsAlice3.size, 3) + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice3.filterIsInstance().map { it.add }.toSet()) + val commitSigAlice2 = actionsAlice3.findOutgoingMessage() + + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(commitSigBob2)) + assertTrue(actionsAlice4.isEmpty()) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(txSigsBob)) + assertIs>(alice5) + assertEquals(alice5.state.commitments.active.size, 2) + assertEquals(actionsAlice5.size, 8) + assertEquals(actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, spliceTxId) + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice5.filterIsInstance().map { it.add }.toSet()) + actionsAlice5.hasWatchConfirmed(spliceTxId) + actionsAlice5.has() + actionsAlice5.has() + val txSigsAlice = actionsAlice5.findOutgoingMessage() + + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(commitSigAlice2)) + assertTrue(actionsBob5.isEmpty()) + val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(txSigsAlice)) + assertIs>(bob6) + assertEquals(bob6.state.commitments.active.size, 2) + assertEquals(actionsBob6.size, 2) + assertEquals(actionsBob6.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, spliceTxId) + actionsBob6.has() + } + + @Test + fun `disconnect -- tx_signatures sent by bob -- simple taproot channels`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) val (alice1, commitSigAlice1, bob1, _) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(80_000.sat), outAmount = 50_000.sat) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSigAlice1)) assertIs>(bob2) @@ -1448,8 +1600,8 @@ class SpliceTestsCommon : LightningTestSuite() { companion object { private val spliceFeerate = FeeratePerKw(253.sat) - private fun reachNormalWithConfirmedFundingTx(zeroConf: Boolean = false): Pair, LNChannel> { - val (alice, bob) = reachNormal(zeroConf = zeroConf) + private fun reachNormalWithConfirmedFundingTx(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, zeroConf: Boolean = false): Pair, LNChannel> { + val (alice, bob) = reachNormal(channelType = channelType, zeroConf = zeroConf) val fundingTx = alice.commitments.latest.localFundingStatus.signedTx!! val (alice1, _) = alice.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_FUNDING_DEPTHOK, 42, 3, fundingTx))) val (bob1, _) = bob.process(ChannelCommand.WatchReceived(WatchEventConfirmed(bob.channelId, BITCOIN_FUNDING_DEPTHOK, 42, 3, fundingTx)))