Skip to content

Commit 8827a04

Browse files
authored
Get ready for storing partial commit signatures (#2896)
* Get ready to store partial signatures We currently store our peer's signature for our remote commit tx, so we can publish it if needed. If we upgrade funding tx to use musig2 instead of multisig 2-of-2 we will need to store a partial signature instead. For this, we add specific types for standard signature (64 bytes) and musig2 partial signatures + nonce.
1 parent 96183a9 commit 8827a04

File tree

7 files changed

+43
-11
lines changed

7 files changed

+43
-11
lines changed

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fr.acinq.eclair.channel
22

33
import akka.event.LoggingAdapter
4+
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
45
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
56
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Script, Transaction, TxId}
67
import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw, OnChainFeeConf}
@@ -215,7 +216,23 @@ object CommitmentChanges {
215216
case class HtlcTxAndRemoteSig(htlcTx: HtlcTx, remoteSig: ByteVector64)
216217

217218
/** We don't store the fully signed transaction, otherwise someone with read access to our database could force-close our channels. */
218-
case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: ByteVector64)
219+
sealed trait RemoteSignature
220+
221+
object RemoteSignature {
222+
case class FullSignature(sig: ByteVector64) extends RemoteSignature
223+
224+
case class PartialSignatureWithNonce(partialSig: ByteVector32, nonce: IndividualNonce) extends RemoteSignature
225+
226+
def apply(sig: ByteVector64): RemoteSignature = FullSignature(sig)
227+
228+
def apply(partialSig: ByteVector32, nonce: IndividualNonce): RemoteSignature = PartialSignatureWithNonce(partialSig: ByteVector32, nonce: IndividualNonce)
229+
}
230+
231+
case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: RemoteSignature)
232+
233+
object CommitTxAndRemoteSig {
234+
def apply(commitTx: CommitTx, remoteSig: ByteVector64): CommitTxAndRemoteSig = CommitTxAndRemoteSig(commitTx, RemoteSignature(remoteSig))
235+
}
219236

220237
/** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */
221238
case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: CommitTxAndRemoteSig, htlcTxsAndRemoteSigs: List[HtlcTxAndRemoteSig])
@@ -240,7 +257,7 @@ object LocalCommit {
240257
}
241258
HtlcTxAndRemoteSig(htlcTx, remoteSig)
242259
}
243-
Right(LocalCommit(localCommitIndex, spec, CommitTxAndRemoteSig(localCommitTx, commit.signature), htlcTxsAndRemoteSigs))
260+
Right(LocalCommit(localCommitIndex, spec, CommitTxAndRemoteSig(localCommitTx, RemoteSignature.FullSignature(commit.signature)), htlcTxsAndRemoteSigs))
244261
}
245262
}
246263

@@ -663,7 +680,7 @@ case class Commitment(fundingTxIndex: Long,
663680
def fullySignedLocalCommitTx(params: ChannelParams, keyManager: ChannelKeyManager): CommitTx = {
664681
val unsignedCommitTx = localCommit.commitTxAndRemoteSig.commitTx
665682
val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Local, params.commitmentFormat)
666-
val remoteSig = localCommit.commitTxAndRemoteSig.remoteSig
683+
val RemoteSignature.FullSignature(remoteSig) = localCommit.commitTxAndRemoteSig.remoteSig
667684
val commitTx = addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig)
668685
// We verify the remote signature when receiving their commit_sig, so this check should always pass.
669686
require(checkSpendable(commitTx).isSuccess, "commit signatures are invalid")
@@ -1151,14 +1168,14 @@ case class Commitments(params: ChannelParams,
11511168

11521169
/** This function should be used to ignore a commit_sig that we've already received. */
11531170
def ignoreRetransmittedCommitSig(commitSig: CommitSig): Boolean = {
1154-
val latestRemoteSig = latest.localCommit.commitTxAndRemoteSig.remoteSig
1171+
val RemoteSignature.FullSignature(latestRemoteSig) = latest.localCommit.commitTxAndRemoteSig.remoteSig
11551172
params.channelFeatures.hasFeature(Features.DualFunding) && commitSig.batchSize == 1 && latestRemoteSig == commitSig.signature
11561173
}
11571174

11581175
def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = {
11591176
all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.localSigs_opt)
11601177
}
1161-
1178+
11621179
def liquidityPurchase(fundingTxId: TxId): Option[LiquidityAds.PurchaseBasicInfo] = {
11631180
all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.liquidityPurchase_opt)
11641181
}

eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath
2222
import fr.acinq.bitcoin.scalacompat.{BlockHash, BlockId, Btc, ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction, TxId}
2323
import fr.acinq.eclair.balance.CheckBalance.{CorrectedOnChainBalance, GlobalBalance, OffChainBalance}
2424
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
25+
import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce
2526
import fr.acinq.eclair.channel._
2627
import fr.acinq.eclair.crypto.{ShaChain, Sphinx}
2728
import fr.acinq.eclair.db.FailureType.FailureType
@@ -332,6 +333,17 @@ object ColorSerializer extends MinimalSerializer({
332333
case c: Color => JString(c.toString)
333334
})
334335

336+
// @formatter:off
337+
private case class CommitTxAndRemoteSigJson(commitTx: CommitTx, remoteSig: ByteVector64)
338+
private case class CommitTxAndRemotePartialSigJson(commitTx: CommitTx, remoteSig: RemoteSignature.PartialSignatureWithNonce)
339+
object CommitTxAndRemoteSigSerializer extends ConvertClassSerializer[CommitTxAndRemoteSig](
340+
i => i.remoteSig match {
341+
case f: RemoteSignature.FullSignature => CommitTxAndRemoteSigJson(i.commitTx, f.sig)
342+
case p: RemoteSignature.PartialSignatureWithNonce => CommitTxAndRemotePartialSigJson(i.commitTx, p)
343+
}
344+
)
345+
// @formatter:on
346+
335347
// @formatter:off
336348
private sealed trait HopJson
337349
private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: HopRelayParams) extends HopJson
@@ -711,6 +723,7 @@ object JsonSerializers {
711723
OpenChannelResponseSerializer +
712724
CommandResponseSerializer +
713725
InputInfoSerializer +
726+
CommitTxAndRemoteSigSerializer +
714727
ColorSerializer +
715728
ThrowableSerializer +
716729
FailureMessageSerializer +

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ private[channel] object ChannelCodecs3 {
203203

204204
val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = (
205205
("commitTx" | commitTxCodec) ::
206-
("remoteSig" | bytes64)).as[CommitTxAndRemoteSig]
206+
("remoteSig" | bytes64.as[RemoteSignature.FullSignature].upcast[RemoteSignature])).as[CommitTxAndRemoteSig]
207207

208208
val localCommitCodec: Codec[LocalCommit] = (
209209
("index" | uint64overflow) ::

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,10 @@ private[channel] object ChannelCodecs4 {
183183
val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = (
184184
("txinfo" | htlcTxCodec) ::
185185
("remoteSig" | bytes64)).as[HtlcTxAndRemoteSig]
186-
186+
187187
val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = (
188188
("commitTx" | commitTxCodec) ::
189-
("remoteSig" | bytes64)).as[CommitTxAndRemoteSig]
189+
("remoteSig" | bytes64.as[RemoteSignature.FullSignature].upcast[RemoteSignature])).as[CommitTxAndRemoteSig]
190190

191191
val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g))
192192

eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
445445
val revokedCommitTx = {
446446
val commitTx = localCommitF.commitTxAndRemoteSig.commitTx
447447
val localSig = keyManagerF.sign(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex), TxOwner.Local, commitmentFormat)
448-
Transactions.addSigs(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex).publicKey, commitmentsF.latest.remoteFundingPubKey, localSig, localCommitF.commitTxAndRemoteSig.remoteSig).tx
448+
val RemoteSignature.FullSignature(remoteSig) = localCommitF.commitTxAndRemoteSig.remoteSig
449+
Transactions.addSigs(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex).publicKey, commitmentsF.latest.remoteFundingPubKey, localSig, remoteSig).tx
449450
}
450451
val htlcSuccess = htlcSuccessTxs.zip(Seq(preimage1, preimage2)).map {
451452
case (htlcTxAndSigs, preimage) =>

eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ object PaymentPacketSpec {
720720
val localParams = LocalParams(null, null, null, Long.MaxValue.msat, Some(channelReserve), null, null, 0, isChannelOpener = true, paysCommitTxFees = true, None, None, null)
721721
val remoteParams = RemoteParams(randomKey().publicKey, null, UInt64.MaxValue, Some(channelReserve), null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, None)
722722
val commitInput = InputInfo(OutPoint(randomTxId(), 1), TxOut(testCapacity, Nil), Nil)
723-
val localCommit = LocalCommit(0, null, CommitTxAndRemoteSig(Transactions.CommitTx(commitInput, null), null), Nil)
723+
val localCommit = LocalCommit(0, null, CommitTxAndRemoteSig(Transactions.CommitTx(commitInput, null), RemoteSignature.FullSignature(null)), Nil)
724724
val remoteCommit = RemoteCommit(0, null, null, randomKey().publicKey)
725725
val localChanges = LocalChanges(Nil, Nil, Nil)
726726
val remoteChanges = RemoteChanges(Nil, Nil, Nil)

eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,8 @@ class ChannelCodecsSpec extends AnyFunSuite {
242242
assert(newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.forall(_.witness.stack.isEmpty))
243243
assert(newnormal.commitments.latest.localCommit.htlcTxsAndRemoteSigs.forall(_.htlcTx.tx.txIn.forall(_.witness.stack.isEmpty)))
244244
// make sure that we have extracted the remote sig of the local tx
245-
newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.checkSig(newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.remoteSig, newnormal.commitments.remoteNodeId, TxOwner.Remote, newnormal.commitments.params.commitmentFormat)
245+
val RemoteSignature.FullSignature(remoteSig) = newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.remoteSig
246+
newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.checkSig(remoteSig, newnormal.commitments.remoteNodeId, TxOwner.Remote, newnormal.commitments.params.commitmentFormat)
246247
}
247248
}
248249

0 commit comments

Comments
 (0)