Skip to content

Commit

Permalink
Update dual-funding protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
sstone committed May 22, 2024
1 parent 31be1b0 commit faf8e85
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32,
remotePushAmount: MilliSatoshi,
txBuilder: typed.ActorRef[InteractiveTxBuilder.Command],
deferred: Option[CommitSig],
remoteNextLocalNonce: Option[IndividualNonce],
replyTo_opt: Option[akka.actor.typed.ActorRef[Peer.OpenChannelResponse]]) extends TransientChannelData
final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelParams: ChannelParams,
secondRemotePerCommitmentPoint: PublicKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment),
localPushAmount = spliceAck.pushAmount, remotePushAmount = msg.pushAmount,
wallet
wallet,
None // TODO
))
txBuilder ! InteractiveTxBuilder.Start(self)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionId, txBuilder, remoteCommitSig = None)) sending spliceAck
Expand Down Expand Up @@ -1045,7 +1046,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.SpliceTx(parentCommitment),
localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount,
wallet
wallet,
None // TODO
))
txBuilder ! InteractiveTxBuilder.Start(self)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = Some(cmd), sessionId, txBuilder, remoteCommitSig = None))
Expand Down Expand Up @@ -1954,7 +1956,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
}
val myNextLocalNonce = d.commitments.params.commitmentFormat match {
case SimpleTaprootChannelsStagingCommitmentFormat =>
val (_, publicNonce) = keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex, channelKeyPath, d.commitments.localCommitIndex)
val (_, publicNonce) = keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex, channelKeyPath, d.commitments.localCommitIndex + 1)
Set(NextLocalNonceTlv(publicNonce))
case _ => Set.empty
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapte
import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt}
import fr.acinq.bitcoin.scalacompat.SatoshiLong
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.channel.ChannelTypes.SimpleTaprootChannelsStaging
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.fsm.Channel._
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs}
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession}
import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.io.Peer.OpenChannelResponse
import fr.acinq.eclair.transactions.Transactions.SimpleTaprootChannelsStagingCommitmentFormat
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{MilliSatoshiLong, RealShortChannelId, ToMilliSatoshiConversion, UInt64, randomBytes32}

Expand Down Expand Up @@ -112,6 +114,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
Some(ChannelTlv.ChannelTypeTlv(input.channelType)),
input.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)),
if (input.requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
if (input.channelType == SimpleTaprootChannelsStaging) Some(ChannelTlv.NextLocalNonceTlv(keyManager.verificationNonce(input.localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0)._2)) else None
).flatten
val open = OpenDualFundedChannel(
chainHash = nodeParams.chainHash,
Expand Down Expand Up @@ -159,6 +162,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
initFeatures = remoteInit.features,
upfrontShutdownScript_opt = remoteShutdownScript)
log.debug("remote params: {}", remoteParams)
log.info(s"received $open")
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex = 0).publicKey
val channelKeyPath = keyManager.keyPath(localParams, d.init.channelConfig)
val revocationBasePoint = keyManager.revocationPoint(channelKeyPath).publicKey
Expand All @@ -177,6 +181,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
Some(ChannelTlv.ChannelTypeTlv(d.init.channelType)),
d.init.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)),
if (nodeParams.channelConf.requireConfirmedInputsForDualFunding) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None,
if (channelParams.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat) Some(ChannelTlv.NextLocalNonceTlv(keyManager.verificationNonce(localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0)._2)) else None
).flatten
val accept = AcceptDualFundedChannel(
temporaryChannelId = open.temporaryChannelId,
Expand Down Expand Up @@ -218,9 +223,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
nodeParams, fundingParams,
channelParams, purpose,
localPushAmount = accept.pushAmount, remotePushAmount = open.pushAmount,
wallet))
wallet,
open.nexLocalNonce_opt))
txBuilder ! InteractiveTxBuilder.Start(self)
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
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
}

case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId)
Expand Down Expand Up @@ -281,9 +287,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
nodeParams, fundingParams,
channelParams, purpose,
localPushAmount = d.lastSent.pushAmount, remotePushAmount = accept.pushAmount,
wallet))
wallet,
accept.nexLocalNonce_opt))
txBuilder ! InteractiveTxBuilder.Start(self)
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))
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))
}

case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) =>
Expand Down Expand Up @@ -551,7 +558,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = None),
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
wallet))
wallet,
None))
txBuilder ! InteractiveTxBuilder.Start(self)
val toSend = Seq(
Some(TxAckRbf(d.channelId, fundingParams.localContribution, nodeParams.channelConf.requireConfirmedInputsForDualFunding)),
Expand Down Expand Up @@ -589,7 +597,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
channelParams = d.commitments.params,
purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = Some(cmd.fundingFeeBudget)),
localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount,
wallet))
wallet,
None))
txBuilder ! InteractiveTxBuilder.Start(self)
stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = Some(cmd), txBuilder, remoteCommitSig = None))
case _ =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior}
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ScriptFlags
import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce}
import fr.acinq.bitcoin.psbt.Psbt
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, LexicographicalOrdering, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut}
Expand All @@ -32,7 +33,7 @@ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Output.Local
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Purpose
import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit
import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, TxOwner}
import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, SimpleTaprootChannelsStagingCommitmentFormat, TxOwner}
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, Scripts, Transactions}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, UInt64}
Expand Down Expand Up @@ -347,7 +348,8 @@ object InteractiveTxBuilder {
purpose: Purpose,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
wallet: OnChainChannelFunder)(implicit ec: ExecutionContext): Behavior[Command] = {
wallet: OnChainChannelFunder,
nextRemoteNonce_opt: Option[IndividualNonce])(implicit ec: ExecutionContext): Behavior[Command] = {
Behaviors.setup { context =>
// The stash is used to buffer messages that arrive while we're funding the transaction.
// Since the interactive-tx protocol is turn-based, we should not have more than one stashed lightning message.
Expand All @@ -366,7 +368,7 @@ object InteractiveTxBuilder {
replyTo ! LocalFailure(InvalidFundingBalances(channelParams.channelId, fundingParams.fundingAmount, nextLocalBalance, nextRemoteBalance))
Behaviors.stopped
} else {
val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, wallet, stash, context)
val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, wallet, stash, context, nextRemoteNonce_opt)
actor.start()
}
case Abort => Behaviors.stopped
Expand All @@ -391,14 +393,18 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
remotePushAmount: MilliSatoshi,
wallet: OnChainChannelFunder,
stash: StashBuffer[InteractiveTxBuilder.Command],
context: ActorContext[InteractiveTxBuilder.Command])(implicit ec: ExecutionContext) {
context: ActorContext[InteractiveTxBuilder.Command],
nextRemoteNonce_opt: Option[IndividualNonce])(implicit ec: ExecutionContext) {

import InteractiveTxBuilder._

private val log = context.log
private val keyManager = nodeParams.channelKeyManager
private val localFundingPubKey: PublicKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex).publicKey
private val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubKey, fundingParams.remoteFundingPubKey)))
private val fundingPubkeyScript: ByteVector = channelParams.commitmentFormat match {
case SimpleTaprootChannelsStagingCommitmentFormat => Script.write(Scripts.musig2FundingScript(localFundingPubKey, fundingParams.remoteFundingPubKey))
case _ => Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubKey, fundingParams.remoteFundingPubKey)))
}
private val remoteNodeId = channelParams.remoteParams.nodeId
private val previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction] = purpose match {
case rbf: PreviousTxRbf => rbf.previousTransactions
Expand Down Expand Up @@ -581,7 +587,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
replyTo ! RemoteFailure(UnknownSerialId(fundingParams.channelId, removeOutput.serialId))
unlockAndStop(session)
}
case _: TxComplete =>
case txComplete: TxComplete =>
val next = session.copy(txCompleteReceived = true)
if (next.isComplete) {
validateAndSign(next)
Expand Down Expand Up @@ -759,10 +765,21 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)) =>
require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!")
val fundingPubKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex)
val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat)
val localSigOfRemoteTx = channelParams.commitmentFormat match {
case SimpleTaprootChannelsStagingCommitmentFormat => ByteVector64.Zeroes
case _ => keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat)
}
val tlvStream: TlvStream[CommitSigTlv] = channelParams.commitmentFormat match {
case SimpleTaprootChannelsStagingCommitmentFormat =>
val localNonce = keyManager.signingNonce(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex)
val Right(psig) = keyManager.partialSign(remoteCommitTx, fundingPubKey, fundingParams.remoteFundingPubKey, TxOwner.Remote, localNonce, nextRemoteNonce_opt.get)
TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2)))
case _ =>
TlvStream.empty
}
val localPerCommitmentPoint = keyManager.htlcPoint(keyManager.keyPath(channelParams.localParams, channelParams.channelConfig))
val htlcSignatures = sortedHtlcTxs.map(keyManager.sign(_, localPerCommitmentPoint, purpose.remotePerCommitmentPoint, TxOwner.Remote, channelParams.commitmentFormat)).toList
val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures)
val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, tlvStream)
val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil)
val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint)
signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ object Transactions {
override def desc: String = "commit-tx"

override def checkSig(commitSig: CommitSig, pubKey: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): Boolean = commitmentFormat match {
case SimpleTaprootChannelsStagingCommitmentFormat => true // TODO: export necessary methods
case SimpleTaprootChannelsStagingCommitmentFormat => commitSig.sigOrPartialSig.isRight // TODO: export necessary methods
case _ => super.checkSig(commitSig, pubKey, txOwner, commitmentFormat)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ object OpenDualFundedChannelTlv {
.typecase(UInt64(0), upfrontShutdownScriptCodec)
.typecase(UInt64(1), channelTypeCodec)
.typecase(UInt64(2), requireConfirmedInputsCodec)
.typecase(UInt64(4), nexLocalNonceTlvCodec)
.typecase(UInt64(0x47000007), pushAmountCodec)
)
}
Expand Down Expand Up @@ -173,6 +174,7 @@ object AcceptDualFundedChannelTlv {
.typecase(UInt64(0), upfrontShutdownScriptCodec)
.typecase(UInt64(1), channelTypeCodec)
.typecase(UInt64(2), requireConfirmedInputsCodec)
.typecase(UInt64(4), nexLocalNonceTlvCodec)
.typecase(UInt64(0x47000007), pushAmountCodec)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ case class TxRemoveOutput(channelId: ByteVector32,
tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId

case class TxComplete(channelId: ByteVector32,
tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId
tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId {

}

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

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

case class FundingCreated(temporaryChannelId: ByteVector32,
Expand Down
Loading

0 comments on commit faf8e85

Please sign in to comment.