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..146a4b336 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 @@ -130,19 +130,26 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishabl is Either.Right -> { val remoteSig = commit.sigOrPartialSig.right - val signed = Transactions.partialSign(localCommitTx, localFundingKey, localFundingKey.publicKey(), remoteFundingPubKey, localNonce!!, remoteSig.nonce) + val partialCheck = localCommitTx.checkPartialSignature(remoteSig, localFundingKey.publicKey(), localNonce!!.second, remoteFundingPubKey) + println("partialCheck = $partialCheck") + 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) { + require(!partialCheck){"partialCheck should be false"} return Either.Left(InvalidCommitmentSignature(params.channelId, localCommitTx.tx.txid)) } + if (!partialCheck) { + val partialCheck1 = localCommitTx.checkPartialSignature(remoteSig, localFundingKey.publicKey(), localNonce.second, remoteFundingPubKey) + println(partialCheck1) + } 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 79aa93453..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 @@ -332,7 +332,8 @@ sealed class PersistedChannelState : ChannelState() { else -> null } val spliceNonces = when { - state.commitments.isTaprootChannel && state is Normal && state.spliceStatus is SpliceStatus.WaitingForSigs -> { + !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 @@ -344,11 +345,11 @@ sealed class PersistedChannelState : ChannelState() { ) } - state.commitments.isTaprootChannel && state.commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx -> { + 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.latest.localCommit.index).second, - channelKeys.verificationNonce(state.commitments.latest.fundingTxIndex, state.commitments.latest.localCommit.index + 1).second + channelKeys.verificationNonce(state.commitments.latest.fundingTxIndex, state.commitments.localCommitIndex).second, + channelKeys.verificationNonce(state.commitments.latest.fundingTxIndex, state.commitments.localCommitIndex + 1).second ) } else -> null 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 011966d8f..85dd6c5b3 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,7 +157,6 @@ 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 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 @@ -147,6 +166,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: } } 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) { @@ -154,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() + nextRemoteNonces.firstOrNull() ) + 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}" } @@ -203,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/crypto/KeyManager.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 2c2f07edd..1896a55c2 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -87,6 +87,7 @@ interface KeyManager { val fundingPrivateKey = fundingKey(fundingTxIndex) val sessionId = Bolt3Derivation.perCommitSecret(nonceSeed(fundingTxIndex), commitIndex).value val nonce = Musig2.generateNonce(sessionId, fundingPrivateKey, listOf(fundingPrivateKey.publicKey())) + println("fundingTxIndex = $fundingTxIndex commitIndex = $commitIndex nonce = ${nonce.second}") return nonce } 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..dc0dfa59f 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,30 @@ 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 } + println("inputIndex = $inputIndex") + val session = Musig2.taprootSession( + this.tx, + inputIndex, + listOf(this.input.txOut), + Scripts.sort(listOf(localPubKey, remotePubKey)), + listOf(localNonce, psig.nonce), + null + ) + if (session.isLeft) { + println(session) + } + val result = session.map { it.verify(psig.partialSig, psig.nonce, remotePubKey) }.getOrElse { + false + } + if(!result) { + println("checkPartialSignature failed for $psig with local nonce = $localNonce") + } + return result + } + } @Serializable sealed class HtlcTx : TransactionWithInputInfo() { 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 acd8db6de..5199a1782 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 @@ -928,6 +928,7 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(bob2.state.spliceStatus, SpliceStatus.None) val (alice2, bob3, channelReestablishAlice) = disconnect(alice1, bob2) + println("\ndisconnected") assertEquals(channelReestablishAlice.nextFundingTxId, spliceTxId) val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) assertEquals(actionsBob4.size, 5)