Skip to content

Commit 3380a07

Browse files
committed
Update dual-funding protocol
1 parent 9e51e9b commit 3380a07

File tree

11 files changed

+143
-39
lines changed

11 files changed

+143
-39
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32,
575575
remotePushAmount: MilliSatoshi,
576576
txBuilder: typed.ActorRef[InteractiveTxBuilder.Command],
577577
deferred: Option[CommitSig],
578+
remoteNextLocalNonce: Option[IndividualNonce],
578579
replyTo_opt: Option[akka.actor.typed.ActorRef[Peer.OpenChannelResponse]]) extends TransientChannelData
579580
final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelParams: ChannelParams,
580581
secondRemotePerCommitmentPoint: PublicKey,

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,7 +1008,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
10081008
channelParams = d.commitments.params,
10091009
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment),
10101010
localPushAmount = spliceAck.pushAmount, remotePushAmount = msg.pushAmount,
1011-
wallet
1011+
wallet,
1012+
None // TODO
10121013
))
10131014
txBuilder ! InteractiveTxBuilder.Start(self)
10141015
stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionId, txBuilder, remoteCommitSig = None)) sending spliceAck
@@ -1046,7 +1047,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
10461047
channelParams = d.commitments.params,
10471048
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment),
10481049
localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount,
1049-
wallet
1050+
wallet,
1051+
None // TODO
10501052
))
10511053
txBuilder ! InteractiveTxBuilder.Start(self)
10521054
stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = Some(cmd), sessionId, txBuilder, remoteCommitSig = None))
@@ -1955,7 +1957,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
19551957
}
19561958
val myNextLocalNonce = d.commitments.params.commitmentFormat match {
19571959
case SimpleTaprootChannelsStagingCommitmentFormat =>
1958-
val (_, publicNonce) = keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex, channelKeyPath, d.commitments.localCommitIndex)
1960+
val (_, publicNonce) = keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex, channelKeyPath, d.commitments.localCommitIndex + 1)
19591961
Set(NextLocalNonceTlv(publicNonce))
19601962
case _ => Set.empty
19611963
}

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapte
2020
import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt}
2121
import fr.acinq.bitcoin.scalacompat.SatoshiLong
2222
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
23+
import fr.acinq.eclair.channel.ChannelTypes.SimpleTaprootChannelsStaging
2324
import fr.acinq.eclair.channel._
2425
import fr.acinq.eclair.channel.fsm.Channel._
2526
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs}
2627
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession}
2728
import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId
2829
import fr.acinq.eclair.crypto.ShaChain
2930
import fr.acinq.eclair.io.Peer.OpenChannelResponse
31+
import fr.acinq.eclair.transactions.Transactions.SimpleTaprootChannelsStagingCommitmentFormat
3032
import fr.acinq.eclair.wire.protocol._
3133
import fr.acinq.eclair.{MilliSatoshiLong, RealShortChannelId, ToMilliSatoshiConversion, UInt64, randomBytes32}
3234

@@ -112,6 +114,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
112114
Some(ChannelTlv.ChannelTypeTlv(input.channelType)),
113115
input.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)),
114116
if (input.requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
117+
if (input.channelType == SimpleTaprootChannelsStaging) Some(ChannelTlv.NextLocalNonceTlv(keyManager.verificationNonce(input.localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0)._2)) else None
115118
).flatten
116119
val open = OpenDualFundedChannel(
117120
chainHash = nodeParams.chainHash,
@@ -159,6 +162,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
159162
initFeatures = remoteInit.features,
160163
upfrontShutdownScript_opt = remoteShutdownScript)
161164
log.debug("remote params: {}", remoteParams)
165+
log.info(s"received $open")
162166
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex = 0).publicKey
163167
val channelKeyPath = keyManager.keyPath(localParams, d.init.channelConfig)
164168
val revocationBasePoint = keyManager.revocationPoint(channelKeyPath).publicKey
@@ -177,6 +181,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
177181
Some(ChannelTlv.ChannelTypeTlv(d.init.channelType)),
178182
d.init.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)),
179183
if (nodeParams.channelConf.requireConfirmedInputsForDualFunding) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
184+
if (channelParams.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat) Some(ChannelTlv.NextLocalNonceTlv(keyManager.verificationNonce(localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0)._2)) else None
180185
).flatten
181186
val accept = AcceptDualFundedChannel(
182187
temporaryChannelId = open.temporaryChannelId,
@@ -218,9 +223,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
218223
nodeParams, fundingParams,
219224
channelParams, purpose,
220225
localPushAmount = accept.pushAmount, remotePushAmount = open.pushAmount,
221-
wallet))
226+
wallet,
227+
open.nexLocalNonce_opt))
222228
txBuilder ! InteractiveTxBuilder.Start(self)
223-
goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, open.secondPerCommitmentPoint, accept.pushAmount, open.pushAmount, txBuilder, deferred = None, replyTo_opt = None) sending accept
229+
goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, open.secondPerCommitmentPoint, accept.pushAmount, open.pushAmount, txBuilder, deferred = None, remoteNextLocalNonce = open.nexLocalNonce_opt, replyTo_opt = None) sending accept
224230
}
225231

226232
case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId)
@@ -281,9 +287,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
281287
nodeParams, fundingParams,
282288
channelParams, purpose,
283289
localPushAmount = d.lastSent.pushAmount, remotePushAmount = accept.pushAmount,
284-
wallet))
290+
wallet,
291+
accept.nexLocalNonce_opt))
285292
txBuilder ! InteractiveTxBuilder.Start(self)
286-
goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, accept.secondPerCommitmentPoint, d.lastSent.pushAmount, accept.pushAmount, txBuilder, deferred = None, replyTo_opt = Some(d.init.replyTo))
293+
goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, accept.secondPerCommitmentPoint, d.lastSent.pushAmount, accept.pushAmount, txBuilder, deferred = None, remoteNextLocalNonce = accept.nexLocalNonce_opt, replyTo_opt = Some(d.init.replyTo))
287294
}
288295

289296
case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) =>
@@ -551,7 +558,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
551558
channelParams = d.commitments.params,
552559
purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = None),
553560
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
554-
wallet))
561+
wallet,
562+
None))
555563
txBuilder ! InteractiveTxBuilder.Start(self)
556564
val toSend = Seq(
557565
Some(TxAckRbf(d.channelId, fundingParams.localContribution, nodeParams.channelConf.requireConfirmedInputsForDualFunding)),
@@ -589,7 +597,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
589597
channelParams = d.commitments.params,
590598
purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = Some(cmd.fundingFeeBudget)),
591599
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
592-
wallet))
600+
wallet,
601+
None))
593602
txBuilder ! InteractiveTxBuilder.Start(self)
594603
stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = Some(cmd), txBuilder, remoteCommitSig = None))
595604
case _ =>

eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
2020
import akka.actor.typed.{ActorRef, Behavior}
2121
import akka.event.LoggingAdapter
2222
import fr.acinq.bitcoin.ScriptFlags
23+
import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce}
2324
import fr.acinq.bitcoin.psbt.Psbt
2425
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2526
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, LexicographicalOrdering, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut}
@@ -32,7 +33,7 @@ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Output.Local
3233
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Purpose
3334
import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit
3435
import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
35-
import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, TxOwner}
36+
import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, SimpleTaprootChannelsStagingCommitmentFormat, TxOwner}
3637
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, Scripts, Transactions}
3738
import fr.acinq.eclair.wire.protocol._
3839
import fr.acinq.eclair.{Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, UInt64}
@@ -347,7 +348,8 @@ object InteractiveTxBuilder {
347348
purpose: Purpose,
348349
localPushAmount: MilliSatoshi,
349350
remotePushAmount: MilliSatoshi,
350-
wallet: OnChainChannelFunder)(implicit ec: ExecutionContext): Behavior[Command] = {
351+
wallet: OnChainChannelFunder,
352+
nextRemoteNonce_opt: Option[IndividualNonce])(implicit ec: ExecutionContext): Behavior[Command] = {
351353
Behaviors.setup { context =>
352354
// The stash is used to buffer messages that arrive while we're funding the transaction.
353355
// Since the interactive-tx protocol is turn-based, we should not have more than one stashed lightning message.
@@ -366,7 +368,7 @@ object InteractiveTxBuilder {
366368
replyTo ! LocalFailure(InvalidFundingBalances(channelParams.channelId, fundingParams.fundingAmount, nextLocalBalance, nextRemoteBalance))
367369
Behaviors.stopped
368370
} else {
369-
val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, wallet, stash, context)
371+
val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, wallet, stash, context, nextRemoteNonce_opt)
370372
actor.start()
371373
}
372374
case Abort => Behaviors.stopped
@@ -391,14 +393,18 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
391393
remotePushAmount: MilliSatoshi,
392394
wallet: OnChainChannelFunder,
393395
stash: StashBuffer[InteractiveTxBuilder.Command],
394-
context: ActorContext[InteractiveTxBuilder.Command])(implicit ec: ExecutionContext) {
396+
context: ActorContext[InteractiveTxBuilder.Command],
397+
nextRemoteNonce_opt: Option[IndividualNonce])(implicit ec: ExecutionContext) {
395398

396399
import InteractiveTxBuilder._
397400

398401
private val log = context.log
399402
private val keyManager = nodeParams.channelKeyManager
400403
private val localFundingPubKey: PublicKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex).publicKey
401-
private val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubKey, fundingParams.remoteFundingPubKey)))
404+
private val fundingPubkeyScript: ByteVector = channelParams.commitmentFormat match {
405+
case SimpleTaprootChannelsStagingCommitmentFormat => Script.write(Scripts.musig2FundingScript(localFundingPubKey, fundingParams.remoteFundingPubKey))
406+
case _ => Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubKey, fundingParams.remoteFundingPubKey)))
407+
}
402408
private val remoteNodeId = channelParams.remoteParams.nodeId
403409
private val previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction] = purpose match {
404410
case rbf: PreviousTxRbf => rbf.previousTransactions
@@ -584,7 +590,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
584590
replyTo ! RemoteFailure(UnknownSerialId(fundingParams.channelId, removeOutput.serialId))
585591
unlockAndStop(session)
586592
}
587-
case _: TxComplete =>
593+
case txComplete: TxComplete =>
588594
val next = session.copy(txCompleteReceived = true)
589595
if (next.isComplete) {
590596
validateAndSign(next)
@@ -767,10 +773,21 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
767773
case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)) =>
768774
require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!")
769775
val fundingPubKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex)
770-
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat)
776+
val localSigOfRemoteTx = channelParams.commitmentFormat match {
777+
case SimpleTaprootChannelsStagingCommitmentFormat => ByteVector64.Zeroes
778+
case _ => keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat)
779+
}
780+
val tlvStream: TlvStream[CommitSigTlv] = channelParams.commitmentFormat match {
781+
case SimpleTaprootChannelsStagingCommitmentFormat =>
782+
val localNonce = keyManager.signingNonce(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex)
783+
val Right(psig) = keyManager.partialSign(remoteCommitTx, fundingPubKey, fundingParams.remoteFundingPubKey, TxOwner.Remote, localNonce, nextRemoteNonce_opt.get)
784+
TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2)))
785+
case _ =>
786+
TlvStream.empty
787+
}
771788
val localPerCommitmentPoint = keyManager.htlcPoint(keyManager.keyPath(channelParams.localParams, channelParams.channelConfig))
772789
val htlcSignatures = sortedHtlcTxs.map(keyManager.sign(_, localPerCommitmentPoint, purpose.remotePerCommitmentPoint, TxOwner.Remote, channelParams.commitmentFormat)).toList
773-
val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures)
790+
val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, tlvStream)
774791
val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil)
775792
val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint)
776793
signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit)

eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ object Transactions {
175175
override def desc: String = "commit-tx"
176176

177177
override def checkSig(commitSig: CommitSig, pubKey: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): Boolean = commitmentFormat match {
178-
case SimpleTaprootChannelsStagingCommitmentFormat => true // TODO: export necessary methods
178+
case SimpleTaprootChannelsStagingCommitmentFormat => commitSig.sigOrPartialSig.isRight // TODO: export necessary methods
179179
case _ => super.checkSig(commitSig, pubKey, txOwner, commitmentFormat)
180180
}
181181
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ object OpenDualFundedChannelTlv {
107107
.typecase(UInt64(0), upfrontShutdownScriptCodec)
108108
.typecase(UInt64(1), channelTypeCodec)
109109
.typecase(UInt64(2), requireConfirmedInputsCodec)
110+
.typecase(UInt64(4), nexLocalNonceTlvCodec)
110111
.typecase(UInt64(0x47000007), pushAmountCodec)
111112
)
112113
}
@@ -173,6 +174,7 @@ object AcceptDualFundedChannelTlv {
173174
.typecase(UInt64(0), upfrontShutdownScriptCodec)
174175
.typecase(UInt64(1), channelTypeCodec)
175176
.typecase(UInt64(2), requireConfirmedInputsCodec)
177+
.typecase(UInt64(4), nexLocalNonceTlvCodec)
176178
.typecase(UInt64(0x47000007), pushAmountCodec)
177179
)
178180

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ case class TxRemoveOutput(channelId: ByteVector32,
115115
tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId
116116

117117
case class TxComplete(channelId: ByteVector32,
118-
tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId
118+
tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId {
119+
120+
}
119121

120122
case class TxSignatures(channelId: ByteVector32,
121123
txId: TxId,
@@ -253,6 +255,7 @@ case class OpenDualFundedChannel(chainHash: BlockHash,
253255
val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType)
254256
val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty
255257
val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat)
258+
val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce)
256259
}
257260

258261
// NB: this message is named accept_channel2 in the specification.
@@ -276,6 +279,7 @@ case class AcceptDualFundedChannel(temporaryChannelId: ByteVector32,
276279
val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType)
277280
val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty
278281
val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat)
282+
val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce)
279283
}
280284

281285
case class FundingCreated(temporaryChannelId: ByteVector32,

0 commit comments

Comments
 (0)