Skip to content

Commit 68aa9dc

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 986fd43 commit 68aa9dc

File tree

8 files changed

+220
-270
lines changed

8 files changed

+220
-270
lines changed

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

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.SharedTransaction
1313
import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
1414
import fr.acinq.eclair.crypto.{Generators, ShaChain}
1515
import fr.acinq.eclair.payment.OutgoingPaymentPacket
16+
import fr.acinq.eclair.transactions.Transactions.TxOwner.{Local, Remote}
1617
import fr.acinq.eclair.transactions.Transactions._
1718
import fr.acinq.eclair.transactions._
1819
import fr.acinq.eclair.wire.protocol._
@@ -229,7 +230,7 @@ case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig:
229230
object LocalCommit {
230231
def fromCommitSig(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxId: TxId,
231232
fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo,
232-
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, localNonce_opt: Option[(SecretNonce, IndividualNonce)]): Either[ChannelException, LocalCommit] = {
233+
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, localNonce_opt: Option[(SecretNonce, IndividualNonce)])(implicit log: LoggingAdapter): Either[ChannelException, LocalCommit] = {
233234
val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(keyManager, params.channelConfig, params.channelFeatures, localCommitIndex, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, localPerCommitmentPoint, spec)
234235
if (!localCommitTx.checkSig(commit, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) {
235236
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
@@ -243,6 +244,10 @@ object LocalCommit {
243244
val fundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey
244245
val Some(localNonce) = localNonce_opt
245246
if (!localCommitTx.checkPartialSignature(psig, fundingPubkey, localNonce._2, remoteFundingPubKey)) {
247+
log.debug(s"fromCommitSig: invalid partial signature $psig fundingPubkey = $fundingPubkey, fundingTxIndex = $fundingTxIndex localCommitIndex = $localCommitIndex localNonce = $localNonce remoteFundingPubKey = $remoteFundingPubKey")
248+
249+
val localNonce1 = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, keyManager.keyPath(params.localParams, params.channelConfig), localCommitIndex)
250+
log.debug(s"with $localNonce1 ${localCommitTx.checkPartialSignature(psig, fundingPubkey, localNonce1._2, remoteFundingPubKey)}")
246251
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
247252
}
248253
}
@@ -267,9 +272,10 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePer
267272
def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, remoteNonce_opt: Option[IndividualNonce])(implicit log: LoggingAdapter): CommitSig = {
268273
val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, index, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remotePerCommitmentPoint, spec)
269274
val (sig, tlvStream) = if (params.commitmentFormat.useTaproot) {
270-
val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, keyManager.keyPath(params.localParams, params.channelConfig), index)
275+
val localNonce = keyManager.signingNonce(params.localParams.fundingKeyPath, fundingTxIndex)
271276
val Some(remoteNonce) = remoteNonce_opt
272277
val Right(localPartialSigOfRemoteTx) = keyManager.partialSign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce)
278+
log.debug(s"RemoteCommit.sign localPartialSigOfRemoteTx = $localPartialSigOfRemoteTx fundingTxIndex = $fundingTxIndex remote commit index = $index remote nonce = $remoteNonce")
273279
val tlvStream: TlvStream[CommitSigTlv] = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(localPartialSigOfRemoteTx, localNonce._2)))
274280
(ByteVector64.Zeroes, tlvStream)
275281
} else {
@@ -661,7 +667,7 @@ case class Commitment(fundingTxIndex: Long,
661667
val localNonce = keyManager.signingNonce(params.localParams.fundingKeyPath, fundingTxIndex)
662668
val Some(remoteNonce) = nextRemoteNonce_opt
663669
val Right(psig) = keyManager.partialSign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce)
664-
log.debug(s"sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint")
670+
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")
665671
Set(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2)))
666672
} else {
667673
Set.empty
@@ -1080,6 +1086,10 @@ case class Commitments(params: ChannelParams,
10801086
}
10811087
val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig)
10821088
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1)
1089+
1090+
val fundingIndexes = active.map(_.fundingTxIndex).toSet
1091+
if (fundingIndexes.size > 1) log.warning(s"more than 1 funding tx index")
1092+
10831093
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
10841094
val active1 = active.zip(commits).map { case (commitment, commit) =>
10851095
val localNonce_opt = if (params.commitmentFormat.useTaproot) {
@@ -1231,9 +1241,31 @@ case class Commitments(params: ChannelParams,
12311241
}
12321242

12331243
/** This function should be used to ignore a commit_sig that we've already received. */
1234-
def ignoreRetransmittedCommitSig(commitSig: CommitSig): Boolean = {
1235-
val latestRemoteSig = latest.localCommit.commitTxAndRemoteSig.remoteSig
1236-
params.channelFeatures.hasFeature(Features.DualFunding) && commitSig.batchSize == 1 && latestRemoteSig == commitSig.sigOrPartialSig
1244+
def ignoreRetransmittedCommitSig(commitSig: CommitSig, keyManager: ChannelKeyManager): Boolean = commitSig.sigOrPartialSig match {
1245+
case _ if !params.channelFeatures.hasFeature(Features.DualFunding) => false
1246+
case _ if commitSig.batchSize != 1 => false
1247+
case Left(_) =>
1248+
commitSig.sigOrPartialSig == latest.localCommit.commitTxAndRemoteSig.remoteSig
1249+
case Right(psig) if active.size > 1 =>
1250+
// we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed)
1251+
// => instead we simply check that the provided partial signature is valid for our latest commit tx
1252+
val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, latest.fundingTxIndex).publicKey
1253+
val Some(localNonce) = generateLocalNonce(keyManager, latest.fundingTxIndex, latest.localCommit.index)
1254+
val Right(oldPsig) = latest.localCommit.commitTxAndRemoteSig.remoteSig
1255+
val currentcheck = latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(psig, localFundingKey, localNonce, latest.remoteFundingPubKey)
1256+
val oldcheck = latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(oldPsig, localFundingKey, localNonce, latest.remoteFundingPubKey)
1257+
require(oldcheck)
1258+
currentcheck
1259+
case Right(psig) =>
1260+
// we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed)
1261+
// => instead we simply check that the provided partial signature is valid for our latest commit tx
1262+
val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, latest.fundingTxIndex).publicKey
1263+
val Some(localNonce) = generateLocalNonce(keyManager, latest.fundingTxIndex, latest.localCommit.index)
1264+
val Right(oldPsig) = latest.localCommit.commitTxAndRemoteSig.remoteSig
1265+
val currentcheck = latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(psig, localFundingKey, localNonce, latest.remoteFundingPubKey)
1266+
val oldcheck = latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(oldPsig, localFundingKey, localNonce, latest.remoteFundingPubKey)
1267+
require(oldcheck)
1268+
currentcheck
12371269
}
12381270

12391271
def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = {
@@ -1353,30 +1385,33 @@ case class Commitments(params: ChannelParams,
13531385
}
13541386

13551387
/**
1356-
* Create local verification nonces for the next funding tx
1357-
* @param keyManager key manager that will generate actual nonces
1358-
* @return a list of 2 verification nonces for the next funding tx: one for the current commitment index, one for the next commitment index
1388+
* Generate local verification nonces for a specific funding tx index and commit tx index
1389+
*
1390+
* @param keyManager key manager that will generate actual nonces
1391+
* @param fundingIndex funding tx index
1392+
* @param commitIndex commit tx index
1393+
* @return a public nonce for thr provided fundint tx index and commit tx index if taproot is used, None otherwise
13591394
*/
1360-
def generateLocalNonces(keyManager: ChannelKeyManager, fundingIndex: Long): List[IndividualNonce] = {
1395+
def generateLocalNonce(keyManager: ChannelKeyManager, fundingIndex: Long, commitIndex: Long): Option[IndividualNonce] = {
13611396
if (latest.params.commitmentFormat.useTaproot) {
1362-
1363-
def localNonce(commitIndex: Long) = {
1364-
val (_, nonce) = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingIndex, keyManager.keyPath(params.localParams, params.channelConfig), commitIndex)
1365-
nonce
1366-
}
1367-
1368-
List(localNonce(localCommitIndex), localNonce(localCommitIndex + 1))
1397+
Some(keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingIndex, keyManager.keyPath(params.localParams, params.channelConfig), commitIndex)._2)
13691398
} else {
1370-
List.empty
1399+
None
13711400
}
13721401
}
13731402

13741403
/**
1375-
* Create local verification nonces for the next funding tx
1404+
* Create local verification nonces a specific funding tx index and a range of commit tx indexes
1405+
*
13761406
* @param keyManager key manager that will generate actual nonces
1377-
* @return a list of 2 verification nonces for the next funding tx: one for the current commitment index, one for the next commitment index
1407+
* @param fundingIndex funding tx index
1408+
* @param commitIndexes range of commit tx indexes
1409+
* @return a list of nonces if raproot is used, or an empty list
13781410
*/
1379-
def generateLocalNonces(keyManager: ChannelKeyManager): List[IndividualNonce] = generateLocalNonces(keyManager, latest.commitment.fundingTxIndex + 1)
1411+
def generateLocalNonces(keyManager: ChannelKeyManager, fundingIndex: Long, commitIndexes: Long*): List[IndividualNonce] = {
1412+
commitIndexes.toList.flatMap(commitIndex => generateLocalNonce(keyManager, fundingIndex, commitIndex))
1413+
}
1414+
13801415
}
13811416

13821417
object Commitments {

0 commit comments

Comments
 (0)