Skip to content

Commit 452c0e0

Browse files
committed
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.
1 parent be8de50 commit 452c0e0

File tree

12 files changed

+42
-34
lines changed

12 files changed

+42
-34
lines changed

eclair-core/pom.xml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -188,21 +188,6 @@
188188
<version>4.1.94.Final</version>
189189
</dependency>
190190
<!-- BITCOIN -->
191-
<dependency>
192-
<groupId>fr.acinq.bitcoin</groupId>
193-
<artifactId>bitcoin-kmp-jvm</artifactId>
194-
<version>0.20.0-SNAPSHOT</version>
195-
</dependency>
196-
<dependency>
197-
<groupId>fr.acinq.secp256k1</groupId>
198-
<artifactId>secp256k1-kmp-jvm</artifactId>
199-
<version>0.15.0</version>
200-
</dependency>
201-
<dependency>
202-
<groupId>fr.acinq.secp256k1</groupId>
203-
<artifactId>secp256k1-kmp-jni-jvm</artifactId>
204-
<version>0.15.0</version>
205-
</dependency>
206191
<dependency>
207192
<groupId>fr.acinq</groupId>
208193
<artifactId>bitcoin-lib_${scala.version.short}</artifactId>

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

Lines changed: 7 additions & 4 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}
@@ -217,8 +218,10 @@ object CommitmentChanges {
217218

218219
case class HtlcTxAndRemoteSig(htlcTx: HtlcTx, remoteSig: ByteVector64)
219220

221+
case class PartialSignatureWithNonce(partialSig: ByteVector32, nonce: IndividualNonce)
222+
220223
/** We don't store the fully signed transaction, otherwise someone with read access to our database could force-close our channels. */
221-
case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: ByteVector64)
224+
case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: Either[ByteVector64, PartialSignatureWithNonce])
222225

223226
/** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */
224227
case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: CommitTxAndRemoteSig, htlcTxsAndRemoteSigs: List[HtlcTxAndRemoteSig])
@@ -243,7 +246,7 @@ object LocalCommit {
243246
}
244247
HtlcTxAndRemoteSig(htlcTx, remoteSig)
245248
}
246-
Right(LocalCommit(localCommitIndex, spec, CommitTxAndRemoteSig(localCommitTx, commit.signature), htlcTxsAndRemoteSigs))
249+
Right(LocalCommit(localCommitIndex, spec, CommitTxAndRemoteSig(localCommitTx, Left(commit.signature)), htlcTxsAndRemoteSigs))
247250
}
248251
}
249252

@@ -666,7 +669,7 @@ case class Commitment(fundingTxIndex: Long,
666669
def fullySignedLocalCommitTx(params: ChannelParams, keyManager: ChannelKeyManager): CommitTx = {
667670
val unsignedCommitTx = localCommit.commitTxAndRemoteSig.commitTx
668671
val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Local, params.commitmentFormat)
669-
val remoteSig = localCommit.commitTxAndRemoteSig.remoteSig
672+
val Left(remoteSig) = localCommit.commitTxAndRemoteSig.remoteSig
670673
val commitTx = addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig)
671674
// We verify the remote signature when receiving their commit_sig, so this check should always pass.
672675
require(checkSpendable(commitTx).isSuccess, "commit signatures are invalid")
@@ -1149,7 +1152,7 @@ case class Commitments(params: ChannelParams,
11491152

11501153
/** This function should be used to ignore a commit_sig that we've already received. */
11511154
def ignoreRetransmittedCommitSig(commitSig: CommitSig): Boolean = {
1152-
val latestRemoteSig = latest.localCommit.commitTxAndRemoteSig.remoteSig
1155+
val Left(latestRemoteSig) = latest.localCommit.commitTxAndRemoteSig.remoteSig
11531156
params.channelFeatures.hasFeature(Features.DualFunding) && commitSig.batchSize == 1 && latestRemoteSig == commitSig.signature
11541157
}
11551158

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
281281
remoteFundingPubKey = remoteFundingPubKey,
282282
localFundingStatus = SingleFundedUnconfirmedFundingTx(None),
283283
remoteFundingStatus = RemoteFundingStatus.NotLocked,
284-
localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil),
284+
localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, Left(remoteSig)), htlcTxsAndRemoteSigs = Nil),
285285
remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint),
286286
nextRemoteCommit_opt = None)
287287
val commitments = Commitments(
@@ -328,7 +328,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
328328
remoteFundingPubKey = remoteFundingPubKey,
329329
localFundingStatus = SingleFundedUnconfirmedFundingTx(Some(fundingTx)),
330330
remoteFundingStatus = RemoteFundingStatus.NotLocked,
331-
localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil),
331+
localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, Left(remoteSig)), htlcTxsAndRemoteSigs = Nil),
332332
remoteCommit = remoteCommit,
333333
nextRemoteCommit_opt = None
334334
)

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ private[channel] object ChannelTypes0 {
121121
def migrate(remoteFundingPubKey: PublicKey): channel.LocalCommit = {
122122
val remoteSig = extractRemoteSig(publishableTxs.commitTx, remoteFundingPubKey)
123123
val unsignedCommitTx = publishableTxs.commitTx.modify(_.tx.txIn.each.witness).setTo(ScriptWitness.empty)
124-
val commitTxAndRemoteSig = CommitTxAndRemoteSig(unsignedCommitTx, remoteSig)
124+
val commitTxAndRemoteSig = CommitTxAndRemoteSig(unsignedCommitTx, Left(remoteSig))
125125
val htlcTxsAndRemoteSigs = publishableTxs.htlcTxsAndSigs map {
126126
case HtlcTxAndSigs(htlcTx: HtlcSuccessTx, _, remoteSig) =>
127127
val unsignedHtlcTx = htlcTx.modify(_.tx.txIn.each.witness).setTo(ScriptWitness.empty)

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
@@ -207,7 +207,7 @@ private[channel] object ChannelCodecs3 {
207207

208208
val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = (
209209
("commitTx" | commitTxCodec) ::
210-
("remoteSig" | bytes64)).as[CommitTxAndRemoteSig]
210+
("remoteSig" | either(provide(false), bytes64, partialSignatureWithNonce))).as[CommitTxAndRemoteSig]
211211

212212
val localCommitCodec: Codec[LocalCommit] = (
213213
("index" | uint64overflow) ::

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fr.acinq.bitcoin.ScriptTree
44
import fr.acinq.bitcoin.io.ByteArrayInput
55
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
66
import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath
7-
import fr.acinq.bitcoin.scalacompat.{OutPoint, ScriptWitness, Transaction, TxOut}
7+
import fr.acinq.bitcoin.scalacompat.{ByteVector64, OutPoint, ScriptWitness, Transaction, TxOut}
88
import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, ConfirmationTarget}
99
import fr.acinq.eclair.channel.LocalFundingStatus._
1010
import fr.acinq.eclair.channel._
@@ -201,9 +201,19 @@ private[channel] object ChannelCodecs4 {
201201
("txinfo" | htlcTxCodec) ::
202202
("remoteSig" | bytes64)).as[HtlcTxAndRemoteSig]
203203

204-
val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = (
204+
private case class CommitTxAndRemoteSigEx(commitTx: CommitTx, remoteSig: ByteVector64, partialSig: Either[ByteVector64, PartialSignatureWithNonce], dummy: Boolean)
205+
206+
// remoteSig is now either a signature or a partial signature with nonce. To retain compatibility with the previous codec, we use remoteSig as a left/write indicator,
207+
// a value of all zeroes meaning right (a valid signature cannot be all zeroes)
208+
private val commitTxAndRemoteSigExCodec: Codec[CommitTxAndRemoteSigEx] = (
205209
("commitTx" | commitTxCodec) ::
206-
("remoteSig" | bytes64)).as[CommitTxAndRemoteSig]
210+
(("remoteSig" | bytes64) >>:~ { remoteSig => either(provide(remoteSig == ByteVector64.Zeroes), provide(remoteSig), partialSignatureWithNonce) :: ("dummy" | provide(false)) })
211+
).as[CommitTxAndRemoteSigEx]
212+
213+
val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = commitTxAndRemoteSigExCodec.xmap(
214+
ce => CommitTxAndRemoteSig(ce.commitTx, ce.partialSig),
215+
c => CommitTxAndRemoteSigEx(c.commitTx, c.remoteSig.swap.toOption.getOrElse(fr.acinq.bitcoin.scalacompat.ByteVector64.Zeroes), c.remoteSig, false)
216+
)
207217

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

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616

1717
package fr.acinq.eclair.wire.protocol
1818

19+
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
1920
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey}
2021
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Satoshi, Transaction, TxHash, TxId}
2122
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
22-
import fr.acinq.eclair.channel.{ChannelFlags, RealScidStatus, ShortIds}
23+
import fr.acinq.eclair.channel.{ChannelFlags, PartialSignatureWithNonce, RealScidStatus, ShortIds}
2324
import fr.acinq.eclair.crypto.Mac32
2425
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId}
2526
import org.apache.commons.codec.binary.Base32
@@ -168,6 +169,13 @@ object CommonCodecs {
168169

169170
val xonlyPublicKey: Codec[XonlyPublicKey] = publicKey.xmap(p => p.xOnly, x => x.publicKey)
170171

172+
val publicNonce: Codec[IndividualNonce] = Codec[IndividualNonce](
173+
(pub: IndividualNonce) => bytes(66).encode(ByteVector.view(pub.toByteArray)),
174+
(wire: BitVector) => bytes(66).decode(wire).map(_.map(b => new IndividualNonce(b.toArray)))
175+
)
176+
177+
val partialSignatureWithNonce: Codec[PartialSignatureWithNonce] = (bytes32 :: publicNonce).as[PartialSignatureWithNonce]
178+
171179
val rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b))
172180

173181
val txCodec: Codec[Transaction] = bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))

eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ object CommitmentsSpec {
491491
val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, Some(channelReserve), 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None)
492492
val remoteFundingPubKey = randomKey().publicKey
493493
val commitmentInput = Funding.makeFundingInputInfo(randomTxId(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteFundingPubKey)
494-
val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil)
494+
val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Left(ByteVector64.Zeroes)), Nil)
495495
val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toRemote, toLocal), randomTxId(), randomKey().publicKey)
496496
Commitments(
497497
ChannelParams(randomBytes32(), ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, ChannelFlags(announceChannel = announceChannel)),
@@ -510,7 +510,7 @@ object CommitmentsSpec {
510510
val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, Some(channelReserve), 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None)
511511
val remoteFundingPubKey = randomKey().publicKey
512512
val commitmentInput = Funding.makeFundingInputInfo(randomTxId(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteFundingPubKey)
513-
val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil)
513+
val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Left(ByteVector64.Zeroes)), Nil)
514514
val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toRemote, toLocal), randomTxId(), randomKey().publicKey)
515515
Commitments(
516516
ChannelParams(randomBytes32(), ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, ChannelFlags(announceChannel = announceChannel)),

eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike {
146146
localParams.dustLimit.returns(1000 sat)
147147
commitment.localParams.returns(localParams)
148148
val localCommit = mock[LocalCommit]
149-
localCommit.commitTxAndRemoteSig.returns(CommitTxAndRemoteSig(commitTx, PlaceHolderSig))
149+
localCommit.commitTxAndRemoteSig.returns(CommitTxAndRemoteSig(commitTx, Left(PlaceHolderSig)))
150150
commitment.localCommit.returns(localCommit)
151151

152152
// We can handle a small feerate update by lowering the change output.

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
@@ -447,7 +447,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
447447
val revokedCommitTx = {
448448
val commitTx = localCommitF.commitTxAndRemoteSig.commitTx
449449
val localSig = keyManagerF.sign(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex), TxOwner.Local, commitmentFormat)
450-
Transactions.addSigs(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex).publicKey, commitmentsF.latest.remoteFundingPubKey, localSig, localCommitF.commitTxAndRemoteSig.remoteSig).tx
450+
val Left(remoteSig) = localCommitF.commitTxAndRemoteSig.remoteSig
451+
Transactions.addSigs(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex).publicKey, commitmentsF.latest.remoteFundingPubKey, localSig, remoteSig).tx
451452
}
452453
val htlcSuccess = htlcSuccessTxs.zip(Seq(preimage1, preimage2)).map {
453454
case (htlcTxAndSigs, preimage) =>

0 commit comments

Comments
 (0)