Skip to content

Commit d503971

Browse files
committed
Fix splicing issues
Upon re-connection, when a splice has not been fully completed, nodes will re-send signatures for the previous remote commit tx. This signature will be ignored by the receiving nodes if it has already received it before it was disconnected, simply by comparing them (signatures are deterministic). With taproot channels, we also need to attach musig2 nonces for splices in progress to channel_reestablish, which are needed to re-generate the signature for the old commit tx.
1 parent 0adcd41 commit d503971

File tree

8 files changed

+164
-241
lines changed

8 files changed

+164
-241
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig:
246246
object LocalCommit {
247247
def fromCommitSig(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxId: TxId,
248248
fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo,
249-
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, localNonce_opt: Option[(SecretNonce, IndividualNonce)]): Either[ChannelException, LocalCommit] = {
249+
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, localNonce_opt: Option[(SecretNonce, IndividualNonce)])(implicit log: LoggingAdapter): Either[ChannelException, LocalCommit] = {
250250
val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(keyManager, params.channelConfig, params.channelFeatures, localCommitIndex, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, localPerCommitmentPoint, spec)
251251
commit.sigOrPartialSig match {
252252
case Left(sig) =>
@@ -257,6 +257,10 @@ object LocalCommit {
257257
val fundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey
258258
val Some(localNonce) = localNonce_opt
259259
if (!localCommitTx.checkPartialSignature(psig, fundingPubkey, localNonce._2, remoteFundingPubKey)) {
260+
log.debug(s"fromCommitSig: invalid partial signature $psig fundingPubkey = $fundingPubkey, fundingTxIndex = $fundingTxIndex localCommitIndex = $localCommitIndex localNonce = $localNonce remoteFundingPubKey = $remoteFundingPubKey")
261+
262+
val localNonce1 = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, keyManager.keyPath(params.localParams, params.channelConfig), localCommitIndex)
263+
log.debug(s"with $localNonce1 ${localCommitTx.checkPartialSignature(psig, fundingPubkey, localNonce1._2, remoteFundingPubKey)}")
260264
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
261265
}
262266
}
@@ -285,9 +289,10 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePer
285289
def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, remoteNonce_opt: Option[IndividualNonce])(implicit log: LoggingAdapter): CommitSig = {
286290
val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, index, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remotePerCommitmentPoint, spec)
287291
val (sig, tlvStream) = if (params.commitmentFormat.useTaproot) {
288-
val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, keyManager.keyPath(params.localParams, params.channelConfig), index)
292+
val localNonce = keyManager.signingNonce(params.localParams.fundingKeyPath, fundingTxIndex)
289293
val Some(remoteNonce) = remoteNonce_opt
290294
val Right(localPartialSigOfRemoteTx) = keyManager.partialSign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce)
295+
log.debug(s"RemoteCommit.sign localPartialSigOfRemoteTx = $localPartialSigOfRemoteTx fundingTxIndex = $fundingTxIndex remote commit index = $index remote nonce = $remoteNonce")
291296
val tlvStream: TlvStream[CommitSigTlv] = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(localPartialSigOfRemoteTx, localNonce._2)))
292297
(ByteVector64.Zeroes, tlvStream)
293298
} else {
@@ -679,7 +684,7 @@ case class Commitment(fundingTxIndex: Long,
679684
val localNonce = keyManager.signingNonce(params.localParams.fundingKeyPath, fundingTxIndex)
680685
val Some(remoteNonce) = nextRemoteNonce_opt
681686
val Right(psig) = keyManager.partialSign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce)
682-
log.debug(s"sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint")
687+
log.debug(s"sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with fundingTxIndex = $fundingTxIndex remoteCommit.index (should add +1) = ${remoteCommit.index} remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint")
683688
Set(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2)))
684689
} else {
685690
Set.empty
@@ -1101,6 +1106,10 @@ case class Commitments(params: ChannelParams,
11011106
}
11021107
val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig)
11031108
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1)
1109+
1110+
val fundingIndexes = active.map(_.fundingTxIndex).toSet
1111+
if (fundingIndexes.size > 1) log.warning(s"more than 1 funding tx index")
1112+
11041113
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
11051114
val active1 = active.zip(commits).map { case (commitment, commit) =>
11061115
val localNonce_opt = if (params.commitmentFormat.useTaproot) {
@@ -1247,12 +1256,20 @@ case class Commitments(params: ChannelParams,
12471256
}
12481257

12491258
/** This function should be used to ignore a commit_sig that we've already received. */
1250-
def ignoreRetransmittedCommitSig(commitSig: CommitSig): Boolean = {
1251-
val latestRemoteSigOrPartialSig = latest.localCommit.commitTxAndRemoteSig.remoteSig match {
1252-
case RemoteSignature.FullSignature(sig) => Left(sig)
1253-
case _: RemoteSignature.PartialSignatureWithNonce => Right(latest.localCommit.commitTxAndRemoteSig.remoteSig)
1254-
}
1255-
params.channelFeatures.hasFeature(Features.DualFunding) && commitSig.batchSize == 1 && latestRemoteSigOrPartialSig == commitSig.sigOrPartialSig
1259+
def ignoreRetransmittedCommitSig(commitSig: CommitSig, keyManager: ChannelKeyManager): Boolean = commitSig.sigOrPartialSig match {
1260+
case _ if !params.channelFeatures.hasFeature(Features.DualFunding) => false
1261+
case _ if commitSig.batchSize != 1 => false
1262+
case Left(sig) =>
1263+
latest.localCommit.commitTxAndRemoteSig.remoteSig match {
1264+
case f: RemoteSignature.FullSignature => f.sig == sig
1265+
case _: RemoteSignature.PartialSignatureWithNonce => false
1266+
}
1267+
case Right(psig) =>
1268+
// we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed)
1269+
// => instead we simply check that the provided partial signature is valid for our latest commit tx
1270+
val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, latest.fundingTxIndex).publicKey
1271+
val Some(localNonce) = generateLocalNonce(keyManager, latest.fundingTxIndex, latest.localCommit.index)
1272+
latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(psig, localFundingKey, localNonce, latest.remoteFundingPubKey)
12561273
}
12571274

12581275
def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = {
@@ -1372,32 +1389,33 @@ case class Commitments(params: ChannelParams,
13721389
}
13731390

13741391
/**
1375-
* Create local verification nonces for the next funding tx
1392+
* Generate local verification nonces for a specific funding tx index and commit tx index
13761393
*
1377-
* @param keyManager key manager that will generate actual nonces
1378-
* @return a list of 2 verification nonces for the next funding tx: one for the current commitment index, one for the next commitment index
1394+
* @param keyManager key manager that will generate actual nonces
1395+
* @param fundingIndex funding tx index
1396+
* @param commitIndex commit tx index
1397+
* @return a public nonce for thr provided fundint tx index and commit tx index if taproot is used, None otherwise
13791398
*/
1380-
def generateLocalNonces(keyManager: ChannelKeyManager, fundingIndex: Long): List[IndividualNonce] = {
1399+
def generateLocalNonce(keyManager: ChannelKeyManager, fundingIndex: Long, commitIndex: Long): Option[IndividualNonce] = {
13811400
if (latest.params.commitmentFormat.useTaproot) {
1382-
1383-
def localNonce(commitIndex: Long) = {
1384-
val (_, nonce) = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingIndex, keyManager.keyPath(params.localParams, params.channelConfig), commitIndex)
1385-
nonce
1386-
}
1387-
1388-
List(localNonce(localCommitIndex), localNonce(localCommitIndex + 1))
1401+
Some(keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingIndex, keyManager.keyPath(params.localParams, params.channelConfig), commitIndex)._2)
13891402
} else {
1390-
List.empty
1403+
None
13911404
}
13921405
}
13931406

13941407
/**
1395-
* Create local verification nonces for the next funding tx
1408+
* Create local verification nonces a specific funding tx index and a range of commit tx indexes
13961409
*
13971410
* @param keyManager key manager that will generate actual nonces
1398-
* @return a list of 2 verification nonces for the next funding tx: one for the current commitment index, one for the next commitment index
1411+
* @param fundingIndex funding tx index
1412+
* @param commitIndexes range of commit tx indexes
1413+
* @return a list of nonces if raproot is used, or an empty list
13991414
*/
1400-
def generateLocalNonces(keyManager: ChannelKeyManager): List[IndividualNonce] = generateLocalNonces(keyManager, latest.commitment.fundingTxIndex + 1)
1415+
def generateLocalNonces(keyManager: ChannelKeyManager, fundingIndex: Long, commitIndexes: Long*): List[IndividualNonce] = {
1416+
commitIndexes.toList.flatMap(commitIndex => generateLocalNonce(keyManager, fundingIndex, commitIndex))
1417+
}
1418+
14011419
}
14021420

14031421
object Commitments {

0 commit comments

Comments
 (0)