Skip to content

Commit

Permalink
Add specific splicing nonces to channel_reestablish
Browse files Browse the repository at this point in the history
  • Loading branch information
sstone committed Nov 20, 2024
1 parent e225e66 commit 3179d2f
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -332,14 +331,38 @@ 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
else -> null
}
val tlvs: TlvStream<ChannelReestablishTlv> = 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
@@ -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.*
Expand Down Expand Up @@ -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<ChannelAction>()

Expand All @@ -137,24 +157,33 @@ 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) {
when (val localFundingStatus = state.commitments.latest.localFundingStatus) {
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}" }
Expand Down Expand Up @@ -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))
Expand Down
Loading

0 comments on commit 3179d2f

Please sign in to comment.