Skip to content

Commit ca706c7

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 fcaae44 commit ca706c7

File tree

16 files changed

+61
-38
lines changed

16 files changed

+61
-38
lines changed

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}
@@ -216,8 +217,10 @@ object CommitmentChanges {
216217

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

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

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

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

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

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/channel/fund/InteractiveTxBuilder.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1008,7 +1008,7 @@ object InteractiveTxSigningSession {
10081008
case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, commitTx: CommitTx, htlcTxs: List[HtlcTx])
10091009

10101010
private def shouldSignFirst(isInitiator: Boolean, channelParams: ChannelParams, tx: SharedTransaction): Boolean = {
1011-
val sharedAmountIn = tx.sharedInput_opt.map(_.txOut.amount).getOrElse(0 sat)
1011+
val sharedAmountIn = tx.sharedInput_opt.map(i => i.localAmount + i.remoteAmount + i.htlcAmount).getOrElse(0 msat).truncateToSatoshi
10121012
val (localAmountIn, remoteAmountIn) = if (isInitiator) {
10131013
(sharedAmountIn + tx.localInputs.map(i => i.txOut.amount).sum, tx.remoteInputs.map(i => i.txOut.amount).sum)
10141014
} else {

eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,8 @@ class Peer(val nodeParams: NodeParams,
276276
proposal.createFulfillCommands(status.preimage).foreach { case (channelId, cmd) => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) }
277277
pending.copy(proposed = pending.proposed :+ proposal)
278278
case status: OnTheFlyFunding.Status.Funded =>
279-
log.info("rejecting extra payment for on-the-fly funding that has already been funded with txId={} (payment_hash={}, amount={})", status.txId, cmd.paymentHash, cmd.amount)
280-
// The payer is buggy and is paying the same payment_hash multiple times. We could simply claim that
281-
// extra payment for ourselves, but we're nice and instead immediately fail it.
282-
val proposal = OnTheFlyFunding.Proposal(htlc, cmd.upstream)
283-
proposal.createFailureCommands(None).foreach { case (channelId, cmd) => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) }
284-
pending
279+
log.info("received extra payment for on-the-fly funding that has already been funded with txId={} (payment_hash={}, amount={})", status.txId, cmd.paymentHash, cmd.amount)
280+
pending.copy(proposed = pending.proposed :+ OnTheFlyFunding.Proposal(htlc, cmd.upstream))
285281
}
286282
case None =>
287283
self ! Peer.OutgoingMessage(htlc, d.peerConnection)

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,9 @@ object OnTheFlyFunding {
292292
// This lets us detect that this HTLC is an on-the-fly funded HTLC.
293293
val htlcFees = LiquidityAds.FundingFee(remainingFees.min(p.maxFees(htlcMinimum)), cmd.status.txId)
294294
val origin = Origin.Hot(htlcSettledAdapter.toClassic, p.upstream)
295-
val add = CMD_ADD_HTLC(cmdAdapter.toClassic, p.htlc.amount - htlcFees.amount, paymentHash, p.htlc.expiry, p.htlc.finalPacket, p.htlc.blinding_opt, 1.0, Some(htlcFees), origin, commit = true)
295+
// We only sign at the end of the whole batch.
296+
val commit = p.htlc.id == cmd.proposed.last.htlc.id
297+
val add = CMD_ADD_HTLC(cmdAdapter.toClassic, p.htlc.amount - htlcFees.amount, paymentHash, p.htlc.expiry, p.htlc.finalPacket, p.htlc.blinding_opt, 1.0, Some(htlcFees), origin, commit)
296298
cmd.channel ! add
297299
remainingFees - htlcFees.amount
298300
}

eclair-core/src/main/scala/fr/acinq/eclair/remote/LightningMessageSerializer.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616

1717
package fr.acinq.eclair.remote
1818

19-
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.lightningMessageCodecWithFallback
19+
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.lightningMessageCodec
2020

21-
class LightningMessageSerializer extends ScodecSerializer(42, lightningMessageCodecWithFallback)
21+
class LightningMessageSerializer extends ScodecSerializer(42, lightningMessageCodec)

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/right 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
@@ -169,6 +170,13 @@ object CommonCodecs {
169170

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

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

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

0 commit comments

Comments
 (0)