From bc67b0504661a35e7a669ad74c165241b65c98b0 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 24 Apr 2024 20:21:13 +0200 Subject: [PATCH] [WIP] Update splicing protocol The current "simple taproot channels" proposal is not compatible with splices. Supporting splices means supporting multiple commitment transactions that are valid at the same time, with the same commitment index but with different funding transactions. We need to extend the taproot proposal to include a list of musig2 nonces (one for each active commitment transaction), see https://github.com/lightning/bolts/pull/995#discussion_r1347016228). We also need a new "next remote nonce" for the new commit tx that is being built, here it has been added to `SpliceInit` and `SpliceAck`. The funding tx that is being built needs to spend the current funding tx, for this we re-use the current remote nonce (no need to send a new one). --- .../fr/acinq/eclair/channel/ChannelData.scala | 2 +- .../eclair/channel/ChannelFeatures.scala | 12 +- .../fr/acinq/eclair/channel/Commitments.scala | 18 ++- .../fr/acinq/eclair/channel/Helpers.scala | 4 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 146 +++++++++++------ .../channel/fsm/ChannelOpenDualFunded.scala | 9 +- .../channel/fsm/ChannelOpenSingleFunded.scala | 8 +- .../channel/fsm/CommonFundingHandlers.scala | 2 +- .../channel/fund/InteractiveTxBuilder.scala | 69 +++++++- .../crypto/keymanager/ChannelKeyManager.scala | 6 +- .../keymanager/LocalChannelKeyManager.scala | 7 +- .../eclair/transactions/Transactions.scala | 12 +- .../channel/version5/ChannelCodecs5.scala | 7 + .../eclair/wire/protocol/ChannelTlv.scala | 11 +- .../acinq/eclair/wire/protocol/HtlcTlv.scala | 8 +- .../wire/protocol/InteractiveTxTlv.scala | 10 +- .../wire/protocol/LightningMessageTypes.scala | 21 ++- .../channel/InteractiveTxBuilderSpec.scala | 30 ++-- .../states/e/NormalSplicesStateSpec.scala | 152 ++++++++++++++++++ .../protocol/LightningMessageCodecsSpec.scala | 6 +- 20 files changed, 436 insertions(+), 104 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 7acd617635..01757172c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -480,7 +480,7 @@ object SpliceStatus { /** Our updates have been added to the local and remote commitments, we wait for our peer to use the now quiescent channel. */ case object NonInitiatorQuiescent extends QuiescentSpliceStatus /** We told our peer we want to splice funds in the channel. */ - case class SpliceRequested(cmd: CMD_SPLICE, init: SpliceInit) extends QuiescentSpliceStatus + case class SpliceRequested(cmd: CMD_SPLICE, init: SpliceInit, nonce_opt: Option[(SecretNonce, IndividualNonce)]) extends QuiescentSpliceStatus /** We both agreed to splice and are building the splice transaction. */ case class SpliceInProgress(cmd_opt: Option[CMD_SPLICE], sessionId: ByteVector32, splice: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends QuiescentSpliceStatus /** The splice transaction has been negotiated, we're exchanging signatures. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 9706184145..9d243bcb7d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -131,9 +131,11 @@ object ChannelTypes { override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" } - case object SimpleTaprootChannelsStaging extends SupportedChannelType { + case class SimpleTaprootChannelsStaging(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType { /** Known channel-type features */ override def features: Set[ChannelTypeFeature] = Set( + if (scidAlias) Some(Features.ScidAlias) else None, + if (zeroConf) Some(Features.ZeroConf) else None, Some(Features.SimpleTaprootStaging) ).flatten @@ -168,7 +170,11 @@ object ChannelTypes { AnchorOutputsZeroFeeHtlcTx(zeroConf = true), AnchorOutputsZeroFeeHtlcTx(scidAlias = true), AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), - SimpleTaprootChannelsStaging) + SimpleTaprootChannelsStaging(), + SimpleTaprootChannelsStaging(zeroConf = true), + SimpleTaprootChannelsStaging(scidAlias = true), + SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true), + ) .map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) .toMap @@ -184,7 +190,7 @@ object ChannelTypes { val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel val zeroConf = canUse(Features.ZeroConf) if (canUse(Features.SimpleTaprootStaging)) { - SimpleTaprootChannelsStaging + SimpleTaprootChannelsStaging(scidAlias, zeroConf) } else if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) { AnchorOutputsZeroFeeHtlcTx(scidAlias, zeroConf) } else if (canUse(Features.AnchorOutputs)) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 7563ab0328..cac1e828ed 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -700,7 +700,7 @@ case class Commitment(fundingTxIndex: Long, val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, ChannelKeyManager.keyPath(fundingPubKey.publicKey), localCommit.index) val Right(partialSig) = keyManager.partialSign(unsignedCommitTx, - keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), remoteFundingPubKey, + keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Local, localNonce, remotePartialSigWithNonce.nonce) val Right(aggSig) = Musig2.aggregateTaprootSignatures( @@ -1037,11 +1037,17 @@ case class Commitments(params: ChannelParams, } } - def sendCommit(keyManager: ChannelKeyManager, nextRemoteNonce_opt: Option[IndividualNonce] = None)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = { + def sendCommit(keyManager: ChannelKeyManager, nextRemoteNonces: List[IndividualNonce] = List.empty)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = { remoteNextCommitInfo match { case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId)) case Right(remoteNextPerCommitmentPoint) => - val (active1, sigs) = active.map(_.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, nextRemoteNonce_opt)).unzip + val (active1, sigs) = this.params.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => + require(active.size <= nextRemoteNonces.size, s"we have ${active.size} commitments but ${nextRemoteNonces.size} remote musig2 nonces") + active.zip(nextRemoteNonces).map { case (c, n) => c.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, Some(n)) } unzip + case _ => + active.map(_.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, None)).unzip + } val commitments1 = copy( changes = changes.copy( localChanges = changes.localChanges.copy(proposed = Nil, signed = changes.localChanges.proposed), @@ -1076,9 +1082,9 @@ case class Commitments(params: ChannelParams, val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 2) val tlvStream: TlvStream[RevokeAndAckTlv] = params.commitmentFormat match { case SimpleTaprootChannelsStagingCommitmentFormat => - val (_, nonce) = keyManager.verificationNonce(params.localParams.fundingKeyPath, this.latest.fundingTxIndex, channelKeyPath, localCommitIndex + 2) + val nonces = this.active.map(c => keyManager.verificationNonce(params.localParams.fundingKeyPath, c.fundingTxIndex, channelKeyPath, localCommitIndex + 2)) log.debug("generating our next local nonce with {} {} {} {}", params.localParams.fundingKeyPath, this.latest.fundingTxIndex, channelKeyPath, localCommitIndex + 2) - TlvStream(RevokeAndAckTlv.NextLocalNonceTlv(nonce)) + TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList)) case _ => TlvStream.empty } @@ -1103,7 +1109,7 @@ case class Commitments(params: ChannelParams, remoteNextCommitInfo match { case Right(_) => Left(UnexpectedRevocation(channelId)) case Left(_) if revocation.perCommitmentSecret.publicKey != active.head.remoteCommit.remotePerCommitmentPoint => Left(InvalidRevocation(channelId)) - case Left(_) if this.params.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat && revocation.nexLocalNonce_opt.isEmpty => Left(MissingNextLocalNonce(channelId)) + case Left(_) if this.params.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat && revocation.nexLocalNonces.isEmpty => Left(MissingNextLocalNonce(channelId)) case Left(_) => // Since htlcs are shared across all commitments, we generate the actions only once based on the first commitment. val receivedHtlcs = changes.remoteChanges.signed.collect { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 9bcb6bc4b2..de98af9366 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -517,8 +517,8 @@ object Helpers { val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommitIndex + 1) val tlvStream: TlvStream[RevokeAndAckTlv] = commitments.params.commitmentFormat match { case SimpleTaprootChannelsStagingCommitmentFormat => - val (_, nonce) = keyManager.verificationNonce(commitments.params.localParams.fundingKeyPath, commitments.latest.fundingTxIndex, channelKeyPath, commitments.localCommitIndex + 1) - TlvStream(RevokeAndAckTlv.NextLocalNonceTlv(nonce)) + val nonces = commitments.active.map(c => keyManager.verificationNonce(commitments.params.localParams.fundingKeyPath, c.fundingTxIndex, channelKeyPath, commitments.localCommitIndex + 1)) + TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList)) case _ => TlvStream.empty } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 57f2601cfe..059517b4c6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -50,7 +50,7 @@ import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{ClosingTx, SimpleTaprootChannelsStagingCommitmentFormat} import fr.acinq.eclair.transactions._ -import fr.acinq.eclair.wire.protocol.ChannelTlv.NextLocalNonceTlv +import fr.acinq.eclair.wire.protocol.ChannelTlv.{NextLocalNonceTlv, NextLocalNoncesTlv} import fr.acinq.eclair.wire.protocol._ import scala.collection.immutable.Queue @@ -201,7 +201,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with import Channel._ val keyManager: ChannelKeyManager = nodeParams.channelKeyManager - var remoteNextLocalNonce_opt: Option[IndividualNonce] = None // FIXME: there should be as many nonces as there are commitment txs + var remoteNextLocalNonces: List[IndividualNonce] = List.empty + var pendingRemoteNextLocalNonce: Option[IndividualNonce] = None // will be added to remoteNextLocalNonces once a splice has been completed + + def setRemoteNextLocalNonces(n: List[IndividualNonce]): Unit = { + this.remoteNextLocalNonces = n + log.debug("set remoteNextLocalNonces to {}", remoteNextLocalNonces) + } // we pass these to helpers classes so that they have the logging context implicit def implicitLog: akka.event.DiagnosticLoggingAdapter = diagLog @@ -218,7 +224,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // we aggregate sigs for splices before processing var sigStash = Seq.empty[CommitSig] - var closingNonce: Option[(SecretNonce, IndividualNonce)] = None + var closingNonce: Option[(SecretNonce, IndividualNonce)] = None // used to sign closing txs + val txPublisher = txPublisherFactory.spawnTxPublisher(context, remoteNodeId) // this will be used to detect htlc timeouts @@ -533,7 +540,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("ignoring CMD_SIGN (nothing to sign)") stay() case Right(_) => - d.commitments.sendCommit(keyManager, this.remoteNextLocalNonce_opt) match { + d.commitments.sendCommit(keyManager, this.remoteNextLocalNonces) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String) val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit @@ -636,7 +643,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with d.commitments.receiveRevocation(revocation, nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).dustTolerance.maxExposure) match { case Right((commitments1, actions)) => cancelTimer(RevocationTimeout.toString) - this.remoteNextLocalNonce_opt = revocation.nexLocalNonce_opt + setRemoteNextLocalNonces(revocation.nexLocalNonces) log.debug("received a new rev, spec:\n{}", commitments1.latest.specs2String) actions.foreach { case PostRevocationAction.RelayHtlc(add) => @@ -896,8 +903,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Left(f) => cmd.replyTo ! RES_FAILURE(cmd, f) stay() - case Right(spliceInit) => - stay() using d.copy(spliceStatus = SpliceStatus.SpliceRequested(cmd, spliceInit)) sending spliceInit + case Right((spliceInit, nonce_opt)) => + stay() using d.copy(spliceStatus = SpliceStatus.SpliceRequested(cmd, spliceInit, nonce_opt)) sending spliceInit } case _ => log.warning("cannot initiate splice, another one is already in progress") @@ -940,8 +947,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with cmd.replyTo ! RES_FAILURE(cmd, f) context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId)) stay() using d.copy(spliceStatus = SpliceStatus.NoSplice) sending Warning(d.channelId, f.getMessage) - case Right(spliceInit) => - stay() using d.copy(spliceStatus = SpliceStatus.SpliceRequested(cmd, spliceInit)) sending spliceInit + case Right((spliceInit, nonce_opt)) => + stay() using d.copy(spliceStatus = SpliceStatus.SpliceRequested(cmd, spliceInit, nonce_opt)) sending spliceInit } } else { log.warning("concurrent stfu received and our peer is the channel initiator, cancelling our splice attempt") @@ -981,18 +988,31 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } else { log.info(s"accepting splice with remote.in.amount=${msg.fundingContribution} remote.in.push=${msg.pushAmount}") val parentCommitment = d.commitments.latest.commitment + val nextLocalNonce_opt = d.commitments.latest.params.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => + val localNonce = keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1, keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig), d.commitments.localCommitIndex) + log.info(s"splice ack: adding nonce at funding index ${parentCommitment.fundingTxIndex + 1} commit index = ${parentCommitment.localCommit.index} nonce = ${localNonce._2}") + Some(localNonce) + case _ => + None + } val spliceAck = SpliceAck(d.channelId, fundingContribution = 0.sat, // only remote contributes to the splice fundingPubKey = keyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey, pushAmount = 0.msat, - requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding + requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding, + nextLocalNonce_opt.map(_._2) ) + val sharedInput = d.commitments.latest.params.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => Musig2Input(parentCommitment) + case _ => Multisig2of2Input(parentCommitment) + } val fundingParams = InteractiveTxParams( channelId = d.channelId, isInitiator = false, localContribution = spliceAck.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(parentCommitment)), + sharedInput_opt = Some(sharedInput), remoteFundingPubKey = msg.fundingPubKey, localOutputs = Nil, lockTime = msg.lockTime, @@ -1001,6 +1021,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceAck.requireConfirmedInputs) ) val sessionId = randomBytes32() + log.debug("spawning InteractiveTxBuilder with remoteNextLocalNonces {}", remoteNextLocalNonces) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( sessionId, nodeParams, fundingParams, @@ -1008,9 +1029,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with purpose = InteractiveTxBuilder.SpliceTx(parentCommitment), localPushAmount = spliceAck.pushAmount, remotePushAmount = msg.pushAmount, wallet, - None // TODO + this.remoteNextLocalNonces, + msg.nexLocalNonce_opt )) txBuilder ! InteractiveTxBuilder.Start(self) + + // README: the splice_init message contains the remote musig2 nonce for the next commit tx that will be built in the interactive tx session + this.pendingRemoteNextLocalNonce = msg.nexLocalNonce_opt stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionId, txBuilder, remoteCommitSig = None)) sending spliceAck } case SpliceStatus.SpliceAborted => @@ -1023,15 +1048,19 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(msg: SpliceAck, d: DATA_NORMAL) => d.spliceStatus match { - case SpliceStatus.SpliceRequested(cmd, spliceInit) => + case SpliceStatus.SpliceRequested(cmd, spliceInit, nextLocalNonce_opt) => log.info("our peer accepted our splice request and will contribute {} to the funding transaction", msg.fundingContribution) val parentCommitment = d.commitments.latest.commitment + val sharedInput = d.commitments.latest.params.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => Musig2Input(parentCommitment) + case _ => Multisig2of2Input(parentCommitment) + } val fundingParams = InteractiveTxParams( channelId = d.channelId, isInitiator = true, localContribution = spliceInit.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(parentCommitment)), + sharedInput_opt = Some(sharedInput), remoteFundingPubKey = msg.fundingPubKey, localOutputs = cmd.spliceOutputs, lockTime = spliceInit.lockTime, @@ -1040,6 +1069,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceInit.requireConfirmedInputs) ) val sessionId = randomBytes32() + log.debug("spawning InteractiveTxBuilder with remoteNextLocalNonces {}", remoteNextLocalNonces) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( sessionId, nodeParams, fundingParams, @@ -1047,9 +1077,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with purpose = InteractiveTxBuilder.SpliceTx(parentCommitment), localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount, wallet, - None // TODO + this.remoteNextLocalNonces, + msg.nexLocalNonce_opt )) txBuilder ! InteractiveTxBuilder.Start(self) + + // README: the splice_ack message contains the remote musig2 nonce for the next commit tx that will be built in the interactive tx session + this.pendingRemoteNextLocalNonce = msg.nexLocalNonce_opt stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = Some(cmd), sessionId, txBuilder, remoteCommitSig = None)) case _ => log.info(s"ignoring unexpected splice_ack=$msg") @@ -1077,7 +1111,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info("our peer aborted the splice attempt: ascii='{}' bin={}", msg.toAscii, msg.data) rollbackFundingAttempt(signingSession.fundingTx.tx, previousTxs = Seq.empty) // no splice rbf yet stay() using d.copy(spliceStatus = SpliceStatus.NoSplice) sending TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage) calling endQuiescence(d) - case SpliceStatus.SpliceRequested(cmd, _) => + case SpliceStatus.SpliceRequested(cmd, _, _) => log.info("our peer rejected our splice attempt: ascii='{}' bin={}", msg.toAscii, msg.data) cmd.replyTo ! RES_FAILURE(cmd, new RuntimeException(s"splice attempt rejected by our peer: ${msg.toAscii}")) stay() using d.copy(spliceStatus = SpliceStatus.NoSplice) sending TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage) calling endQuiescence(d) @@ -1142,6 +1176,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Right((commitments1, _)) => log.info("publishing funding tx for channelId={} fundingTxId={}", d.channelId, fundingTx.signedTx.txid) Metrics.recordSplice(dfu.fundingParams, fundingTx.tx) + // README: splice has been completed, update remote nonces with the one sent in splice_init/splice_ack + setRemoteNextLocalNonces(this.remoteNextLocalNonces ++ this.pendingRemoteNextLocalNonce.toList) stay() using d.copy(commitments = commitments1) storing() calling publishFundingTx(dfu1) case Left(_) => stay() @@ -1162,6 +1198,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice) log.info("publishing funding tx for channelId={} fundingTxId={}", d.channelId, signingSession1.fundingTx.sharedTx.txId) Metrics.recordSplice(signingSession1.fundingTx.fundingParams, signingSession1.fundingTx.sharedTx.tx) + setRemoteNextLocalNonces(this.remoteNextLocalNonces ++ this.pendingRemoteNextLocalNonce.toList) stay() using d1 storing() sending signingSession1.localSigs calling publishFundingTx(signingSession1.fundingTx) calling endQuiescence(d1) } case _ => @@ -1320,7 +1357,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("ignoring CMD_SIGN (nothing to sign)") stay() case Right(_) => - d.commitments.sendCommit(keyManager, this.remoteNextLocalNonce_opt) match { + d.commitments.sendCommit(keyManager, this.remoteNextLocalNonces) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String) val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit @@ -1916,8 +1953,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTx.txId)) val myNextLocalNonce = d.channelParams.commitmentFormat match { case SimpleTaprootChannelsStagingCommitmentFormat => - val (_, publicNonce) = keyManager.verificationNonce(d.channelParams.localParams.fundingKeyPath, 0, channelKeyPath, 0) - Set(NextLocalNonceTlv(publicNonce)) + val (_, publicNonce) = keyManager.verificationNonce(d.channelParams.localParams.fundingKeyPath, 0, channelKeyPath, 1) + Set(NextLocalNoncesTlv(List(publicNonce))) case _ => Set.empty } val channelReestablish = ChannelReestablish( @@ -1956,8 +1993,8 @@ 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 + 1) - Set(NextLocalNonceTlv(publicNonce)) + val nonces = d.commitments.active.map(c => keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, c.fundingTxIndex, channelKeyPath, d.commitments.localCommitIndex + 1)) + Set(NextLocalNoncesTlv(nonces.map(_._2).toList)) case _ => Set.empty } val channelReestablish = ChannelReestablish( @@ -1995,45 +2032,45 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with when(SYNCING)(handleExceptions { case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => d.commitments.params.commitmentFormat match { - case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nexLocalNonce_opt.isDefined, "missing next local nonce") + case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nextLocalNonces.size == d.commitments.active.size, "missing next local nonce") case _ => () } - this.remoteNextLocalNonce_opt = channelReestablish.nexLocalNonce_opt + setRemoteNextLocalNonces(channelReestablish.nextLocalNonces) goto(WAIT_FOR_FUNDING_CONFIRMED) case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => d.channelParams.commitmentFormat match { - case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nexLocalNonce_opt.isDefined, "missing next local nonce") + case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nextLocalNonces.size == 1, "missing next local nonce") case _ => () } - this.remoteNextLocalNonce_opt = channelReestablish.nexLocalNonce_opt + setRemoteNextLocalNonces(channelReestablish.nextLocalNonces) channelReestablish.nextFundingTxId_opt match { case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId => // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. - val commitSig = d.signingSession.remoteCommit.sign(keyManager, d.channelParams, d.signingSession.fundingTxIndex, d.signingSession.fundingParams.remoteFundingPubKey, d.signingSession.commitInput, remoteNextLocalNonce_opt) + val commitSig = d.signingSession.remoteCommit.sign(keyManager, d.channelParams, d.signingSession.fundingTxIndex, d.signingSession.fundingParams.remoteFundingPubKey, d.signingSession.commitInput, remoteNextLocalNonces.headOption) goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) } case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => d.commitments.params.commitmentFormat match { - case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nexLocalNonce_opt.isDefined, "missing next local nonce") + case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nextLocalNonces.size == d.commitments.active.size, "missing next local nonce") case _ => () } - this.remoteNextLocalNonce_opt = channelReestablish.nexLocalNonce_opt + setRemoteNextLocalNonces(channelReestablish.nextLocalNonces) channelReestablish.nextFundingTxId_opt match { case Some(fundingTxId) => d.rbfStatus match { case RbfStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. - val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput, remoteNextLocalNonce_opt) + val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput, remoteNextLocalNonces.headOption) goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig case _ if d.latestFundingTx.sharedTx.txId == fundingTxId => val toSend = d.latestFundingTx.sharedTx match { case fundingTx: InteractiveTxBuilder.PartiallySignedSharedTransaction => // We have not received their tx_signatures: we retransmit our commit_sig because we don't know if they received it. - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput, remoteNextLocalNonce_opt) + val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput, remoteNextLocalNonces.headOption) Seq(commitSig, fundingTx.localSigs) case fundingTx: InteractiveTxBuilder.FullySignedSharedTransaction => // We've already received their tx_signatures, which means they've received and stored our commit_sig, we only need to retransmit our tx_signatures. @@ -2050,30 +2087,29 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => d.commitments.params.commitmentFormat match { - case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nexLocalNonce_opt.isDefined, "missing next local nonce") + case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nextLocalNonces.size == d.commitments.active.size, "missing next local nonce") case _ => () } - this.remoteNextLocalNonce_opt = channelReestablish.nexLocalNonce_opt + setRemoteNextLocalNonces(channelReestablish.nextLocalNonces) log.debug("re-sending channelReady") val channelReady = createChannelReady(d.shortIds, d.commitments.params) goto(WAIT_FOR_CHANNEL_READY) sending channelReady case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => d.commitments.params.commitmentFormat match { - case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nexLocalNonce_opt.isDefined, "missing next local nonce") + case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nextLocalNonces.size == d.commitments.active.size, "missing next local nonce") case _ => () } - this.remoteNextLocalNonce_opt = channelReestablish.nexLocalNonce_opt - log.debug("re-sending channelReady") + setRemoteNextLocalNonces(channelReestablish.nextLocalNonces) val channelReady = createChannelReady(d.shortIds, d.commitments.params) goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => d.commitments.params.commitmentFormat match { - case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nexLocalNonce_opt.isDefined, "missing next local nonce") + case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nextLocalNonces.size == d.commitments.active.size, "missing next local nonce") case _ => () } - this.remoteNextLocalNonce_opt = channelReestablish.nexLocalNonce_opt + setRemoteNextLocalNonces(channelReestablish.nextLocalNonces) Syncing.checkSync(keyManager, d.commitments, channelReestablish) match { case syncFailure: SyncResult.Failure => @@ -2105,7 +2141,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId) - val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput, remoteNextLocalNonce_opt) + val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput, remoteNextLocalNonces.headOption) sendQueue = sendQueue :+ commitSig d.spliceStatus case _ if d.commitments.latest.fundingTxId == fundingTxId => @@ -2115,7 +2151,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case fundingTx: InteractiveTxBuilder.PartiallySignedSharedTransaction => // If we have not received their tx_signatures, we can't tell whether they had received our commit_sig, so we need to retransmit it log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput, remoteNextLocalNonce_opt) + val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput, remoteNextLocalNonces.headOption) sendQueue = sendQueue :+ commitSig :+ fundingTx.localSigs case fundingTx: InteractiveTxBuilder.FullySignedSharedTransaction => log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) @@ -2221,10 +2257,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) => d.commitments.params.commitmentFormat match { - case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nexLocalNonce_opt.isDefined, "missing next local nonce") + case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nextLocalNonces.size == d.commitments.active.size, "missing next local nonce") case _ => () } - this.remoteNextLocalNonce_opt = channelReestablish.nexLocalNonce_opt + setRemoteNextLocalNonces(channelReestablish.nextLocalNonces) Syncing.checkSync(keyManager, d.commitments, channelReestablish) match { case syncFailure: SyncResult.Failure => handleSyncFailure(channelReestablish, syncFailure, d) @@ -2237,10 +2273,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(channelReestablish: ChannelReestablish, d: DATA_NEGOTIATING) => d.commitments.params.commitmentFormat match { - case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nexLocalNonce_opt.isDefined, "missing next local nonce") + case SimpleTaprootChannelsStagingCommitmentFormat => require(channelReestablish.nextLocalNonces.size == d.commitments.active.size, "missing next local nonce") case _ => () } - this.remoteNextLocalNonce_opt = channelReestablish.nexLocalNonce_opt + setRemoteNextLocalNonces(channelReestablish.nextLocalNonces) // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. // negotiation restarts from the beginning, and is initialized by the channel initiator // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them @@ -2849,13 +2885,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with proposedTx_opt.get.unsignedTx.copy(tx = tx) } - private def initiateSplice(cmd: CMD_SPLICE, d: DATA_NORMAL): Either[ChannelException, SpliceInit] = { + private def initiateSplice(cmd: CMD_SPLICE, d: DATA_NORMAL): Either[ChannelException, (SpliceInit, Option[(SecretNonce, IndividualNonce)])] = { if (d.commitments.isQuiescent) { val parentCommitment = d.commitments.latest.commitment val targetFeerate = nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeerates) + val sharedInput = d.commitments.latest.params.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => Musig2Input(parentCommitment) + case _ => Multisig2of2Input(parentCommitment) + } val fundingContribution = InteractiveTxFunder.computeSpliceContribution( isInitiator = true, - sharedInput = Multisig2of2Input(parentCommitment), + sharedInput = sharedInput, spliceInAmount = cmd.additionalLocalFunding, spliceOut = cmd.spliceOutputs, targetFeerate = targetFeerate) @@ -2870,15 +2910,25 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with Left(InvalidSpliceRequest(d.channelId)) } else { log.info(s"initiating splice with local.in.amount=${cmd.additionalLocalFunding} local.in.push=${cmd.pushAmount} local.out.amount=${cmd.spliceOut_opt.map(_.amount).sum}") + val nextLocalNonce_opt = d.commitments.latest.params.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => + // we generate a signing nonce (i.e randomized) for the parent commitment funding key + val localNonce = keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1, keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig), d.commitments.localCommitIndex) + //val nonce = keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1) + log.info(s"splice init: adding nonce at funding index ${parentCommitment.fundingTxIndex + 1} nonce = ${localNonce._2}") + Some(localNonce) + case _ => None + } val spliceInit = SpliceInit(d.channelId, fundingContribution = fundingContribution, lockTime = nodeParams.currentBlockHeight.toLong, feerate = targetFeerate, fundingPubKey = keyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey, pushAmount = cmd.pushAmount, - requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding + requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding, + nextLocalNonce_opt.map(_._2) ) - Right(spliceInit) + Right(spliceInit -> nextLocalNonce_opt) } } else { log.warning("cannot initiate splice, channel is not quiescent") @@ -2910,7 +2960,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val cmd_opt = spliceStatus match { case SpliceStatus.QuiescenceRequested(cmd) => Some(cmd) case SpliceStatus.InitiatorQuiescent(cmd) => Some(cmd) - case SpliceStatus.SpliceRequested(cmd, _) => Some(cmd) + case SpliceStatus.SpliceRequested(cmd, _, _) => Some(cmd) case SpliceStatus.SpliceInProgress(cmd_opt, _, txBuilder, _) => txBuilder ! InteractiveTxBuilder.Abort cmd_opt diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 3aeeb31266..e5853adfbe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -114,7 +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 + if (input.channelType.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat) Some(ChannelTlv.NextLocalNonceTlv(keyManager.verificationNonce(input.localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0)._2)) else None ).flatten val open = OpenDualFundedChannel( chainHash = nodeParams.chainHash, @@ -183,6 +183,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { 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 + log.debug("sending AcceptDualFundedChannel with {}", tlvs) val accept = AcceptDualFundedChannel( temporaryChannelId = open.temporaryChannelId, fundingAmount = localAmount, @@ -224,8 +225,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { channelParams, purpose, localPushAmount = accept.pushAmount, remotePushAmount = open.pushAmount, wallet, + List.empty, open.nexLocalNonce_opt)) txBuilder ! InteractiveTxBuilder.Start(self) + setRemoteNextLocalNonces(open.nexLocalNonce_opt.toList) 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 } @@ -288,8 +291,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { channelParams, purpose, localPushAmount = d.lastSent.pushAmount, remotePushAmount = accept.pushAmount, wallet, + List.empty, accept.nexLocalNonce_opt)) txBuilder ! InteractiveTxBuilder.Start(self) + setRemoteNextLocalNonces(accept.nexLocalNonce_opt.toList) 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)) } @@ -559,6 +564,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { 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, + List.empty, None)) txBuilder ! InteractiveTxBuilder.Start(self) val toSend = Seq( @@ -598,6 +604,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { 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, + List.empty, None)) txBuilder ! InteractiveTxBuilder.Start(self) stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = Some(cmd), txBuilder, remoteCommitSig = None)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index dae11c2851..fcea46d9fa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -80,7 +80,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used // See https://github.com/lightningnetwork/lightning-rfc/pull/714. val localShutdownScript = input.localParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) - val tlvStream: TlvStream[OpenChannelTlv] = if (input.channelType == SimpleTaprootChannelsStaging) { + val tlvStream: TlvStream[OpenChannelTlv] = if (input.channelType.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat) { val localNonce = keyManager.verificationNonce(input.localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0) TlvStream( ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), @@ -145,7 +145,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. // See https://github.com/lightningnetwork/lightning-rfc/pull/714. val localShutdownScript = d.initFundee.localParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) - val tlvStream: TlvStream[AcceptChannelTlv] = if (d.initFundee.channelType == SimpleTaprootChannelsStaging) { + val tlvStream: TlvStream[AcceptChannelTlv] = if (d.initFundee.channelType.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat) { val localNonce = keyManager.verificationNonce(d.initFundee.localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0) TlvStream( ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), @@ -213,7 +213,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { } wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate, init.fundingTxFeeBudget_opt).pipeTo(self) val params = ChannelParams(init.temporaryChannelId, init.channelConfig, channelFeatures, init.localParams, remoteParams, open.channelFlags) - this.remoteNextLocalNonce_opt = accept.nexLocalNonce_opt + setRemoteNextLocalNonces(accept.nexLocalNonce_opt.toList) goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(params, init.fundingAmount, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo) } @@ -250,7 +250,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val inputIndex = remoteCommitTx.tx.txIn.zipWithIndex.find(_._1.outPoint == OutPoint(fundingTx.txid, fundingTxOutputIndex)).get._2 val Right(sig) = keyManager.partialSign(remoteCommitTx, fundingPubkey, remoteFundingPubKey, TxOwner.Remote, - localNonce, remoteNextLocalNonce_opt.get + localNonce, remoteNextLocalNonces.head ) FundingCreated( temporaryChannelId = temporaryChannelId, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala index 48be3a2289..328311716f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala @@ -145,7 +145,7 @@ trait CommonFundingHandlers extends CommonHandlers { blockchain ! WatchFundingDeeplyBuried(self, commitments.latest.fundingTxId, ANNOUNCEMENTS_MINCONF) val commitments1 = commitments .modify(_.remoteNextCommitInfo).setTo(Right(channelReady.nextPerCommitmentPoint)) - this.remoteNextLocalNonce_opt = channelReady.nexLocalNonce_opt // TODO: this is wrong, there should be a different nonce for each commitment + setRemoteNextLocalNonces(channelReady.nexLocalNonce_opt.toList) // TODO: this is wrong, there should be a different nonce for each commitment DATA_NORMAL(commitments1, shortIds1, None, initialChannelUpdate, None, None, None, SpliceStatus.NoSplice) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 774e5d3aab..564eb10eef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -23,7 +23,7 @@ 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} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, LexicographicalOrdering, Musig2, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut} import fr.acinq.eclair.blockchain.OnChainChannelFunder import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Closing.MutualClose @@ -125,6 +125,21 @@ object InteractiveTxBuilder { ) } + case class Musig2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, commitIndex: Long) extends SharedFundingInput { + override val weight: Int = 234 + + override def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction): ByteVector64 = ByteVector64.Zeroes + } + + object Musig2Input { + def apply(commitment: Commitment): Musig2Input = Musig2Input( + info = commitment.commitInput, + fundingTxIndex = commitment.fundingTxIndex, + remoteFundingPubkey = commitment.remoteFundingPubKey, + commitIndex = commitment.localCommit.index + ) + } + /** * @param channelId id of the channel. * @param isInitiator true if we initiated the protocol, in which case we will pay fees for the shared parts of the transaction. @@ -298,6 +313,9 @@ object InteractiveTxBuilder { def localOnlyNonChangeOutputs: List[Output.Local.NonChange] = localOutputs.collect { case o: Local.NonChange => o } + // outputs spent by this tx + val spentOutputs: Seq[TxOut] = (sharedInput_opt.toSeq ++ localInputs ++ remoteInputs).sortBy(_.serialId).map(_.txOut) + def buildUnsignedTx(): Transaction = { val sharedTxIn = sharedInput_opt.map(i => (i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence))).toSeq val localTxIn = localInputs.map(i => (i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence))) @@ -349,6 +367,7 @@ object InteractiveTxBuilder { localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, wallet: OnChainChannelFunder, + currentRemoteNonces: List[IndividualNonce], 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. @@ -368,7 +387,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, nextRemoteNonce_opt) + val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, wallet, stash, context, currentRemoteNonces, nextRemoteNonce_opt) actor.start() } case Abort => Behaviors.stopped @@ -394,6 +413,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon wallet: OnChainChannelFunder, stash: StashBuffer[InteractiveTxBuilder.Command], context: ActorContext[InteractiveTxBuilder.Command], + currentRemoteNonces: List[IndividualNonce], nextRemoteNonce_opt: Option[IndividualNonce])(implicit ec: ExecutionContext) { import InteractiveTxBuilder._ @@ -412,6 +432,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon } def start(): Behavior[Command] = { + log.info(s"starting funder with $fundingPubkeyScript") val txFunder = context.spawnAnonymous(InteractiveTxFunder(remoteNodeId, fundingParams, fundingPubkeyScript, purpose, wallet)) txFunder ! InteractiveTxFunder.FundTransaction(context.messageAdapter[InteractiveTxFunder.Response](r => FundTransactionResult(r))) Behaviors.receiveMessagePartial { @@ -811,8 +832,23 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val tx = unsignedTx.buildUnsignedTx() val sharedSig_opt = fundingParams.sharedInput_opt.map(_.sign(keyManager, channelParams, tx)) + val sharedPartialSig_opt = fundingParams.sharedInput_opt.collect { + case m: Musig2Input => + val channelKeyPath = keyManager.keyPath(this.channelParams.localParams, this.channelParams.channelConfig) + val localNonce = keyManager.verificationNonce(this.channelParams.localParams.fundingKeyPath, m.fundingTxIndex, channelKeyPath, m.commitIndex + 1) + val fundingKey = keyManager.fundingPublicKey(this.channelParams.localParams.fundingKeyPath, m.fundingTxIndex) + val inputIndex = tx.txIn.indexWhere(_.outPoint == m.info.outPoint) + val remoteNonce = this.currentRemoteNonces.last // nextRemoteNonce_opt.get + log.debug(s"creating partial sig for ${tx.txid} inputIndex=$inputIndex") + log.debug(s"fundingKey = ${fundingKey.publicKey} fundingTxIndex = ${m.fundingTxIndex}") + log.debug(s"remoteFundingPubkey = ${m.remoteFundingPubkey}") + log.debug(s"local nonce = ${localNonce._2} fundingTxIndex = ${m.fundingTxIndex} commitIndex = ${m.commitIndex}") + log.debug(s"remote nonce = ${remoteNonce}") + val Right(psig) = keyManager.partialSign(tx, inputIndex, unsignedTx.spentOutputs, fundingKey, m.remoteFundingPubkey, TxOwner.Local, localNonce, remoteNonce) + PartialSignatureWithNonce(psig, localNonce._2) + } if (unsignedTx.localInputs.isEmpty) { - context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt))) + context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt, sharedPartialSig_opt))) } else { val ourWalletInputs = unsignedTx.localInputs.map(i => tx.txIn.indexWhere(_.outPoint == i.outPoint)) val ourWalletOutputs = unsignedTx.localOutputs.flatMap { @@ -840,7 +876,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon }.sum require(actualLocalAmountOut == expectedLocalAmountOut, s"local output amount $actualLocalAmountOut does not match what we expect ($expectedLocalAmountOut): bitcoin core may be malicious") val sigs = partiallySignedTx.txIn.filter(txIn => localOutpoints.contains(txIn.outPoint)).map(_.witness) - PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, partiallySignedTx, sigs, sharedSig_opt)) + PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, partiallySignedTx, sigs, sharedSig_opt, sharedPartialSig_opt)) }) { case Failure(t) => WalletFailure(t) case Success(signedTx) => SignTransactionResult(signedTx) @@ -939,6 +975,31 @@ object InteractiveTxSigningSession { log.info("invalid tx_signatures: missing shared input signatures") return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) } + case Some(sharedInput: Musig2Input) => + (partiallySignedTx.localSigs.previousFundingTxPartialSig_opt, remoteSigs.previousFundingTxPartialSig_opt) match { + case (Some(localPartialSig), Some(remotePartialSig)) => + val localFundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, sharedInput.fundingTxIndex).publicKey + val unsignedTx = partiallySignedTx.tx.buildUnsignedTx() + log.debug(s"adding remote sigs for ${unsignedTx.txid}") + log.debug("local partial sig is using nonce {}", localPartialSig.nonce) + log.debug("remote partial sig is using nonce {}", remotePartialSig.nonce) + log.debug(s"local funding key = ${localFundingPubkey}") + log.debug(s"remote funding key = ${sharedInput.remoteFundingPubkey}") + log.debug(s"spent outputs = ${partiallySignedTx.tx.spentOutputs}") + val inputIndex = unsignedTx.txIn.indexWhere(_.outPoint == sharedInput.info.outPoint) + val Right(aggSig) = Musig2.aggregateTaprootSignatures( + Seq(localPartialSig.partialSig, remotePartialSig.partialSig), + unsignedTx, + inputIndex, + partiallySignedTx.tx.spentOutputs, + Scripts.sort(Seq(localFundingPubkey, sharedInput.remoteFundingPubkey)), + Seq(localPartialSig.nonce, remotePartialSig.nonce), + None) + Some(Script.witnessKeyPathPay2tr(aggSig)) + case _ => + log.info("invalid tx_signatures: missing shared input partial signatures") + return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) + } case None => None } val txWithSigs = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, remoteSigs, sharedSigs_opt) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index 442133b3b8..0e1747db69 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -75,7 +75,11 @@ trait ChannelKeyManager { */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 - def partialSign(tx: TransactionWithInputInfo, localPublicKey: ExtendedPublicKey, remotePublicKey: PublicKey, txOwner: TxOwner, localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] + def partialSign(tx: TransactionWithInputInfo, localPublicKey: ExtendedPublicKey, remotePublicKey: PublicKey, txOwner: TxOwner, localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] = { + partialSign(tx.tx, tx.tx.txIn.indexWhere(_.outPoint == tx.input.outPoint), Seq(tx.input.txOut), localPublicKey, remotePublicKey, txOwner, localNonce, remoteNextLocalNonce) + } + + def partialSign(tx: Transaction, inputIndex: Int, spentOutputs: Seq[TxOut], localPublicKey: ExtendedPublicKey, remotePublicKey: PublicKey, txOwner: TxOwner, localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] /** * This method is used to spend funds sent to htlc keys/delayed keys diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index f4b790e6f6..50e9be4af6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -139,16 +139,15 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha } } - override def partialSign(tx: TransactionWithInputInfo, localPublicKey: ExtendedPublicKey, remotePublicKey: PublicKey, txOwner: TxOwner, localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] = { - // NB: not all those transactions are actually commit txs (especially during closing), but this is good enough for monitoring purposes + override def partialSign(tx: Transaction, inputIndex: Int, spentOutputs: Seq[TxOut], localPublicKey: ExtendedPublicKey, remotePublicKey: PublicKey, txOwner: TxOwner, localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] = { val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.CommitTx) Metrics.SignTxCount.withTags(tags).increment() KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { val privateKey = privateKeys.get(localPublicKey.path).privateKey - Transactions.partialSign(tx, privateKey, localPublicKey.publicKey, remotePublicKey, localNonce, remoteNextLocalNonce) + Transactions.partialSign(privateKey, tx, inputIndex, spentOutputs, localPublicKey.publicKey, remotePublicKey, localNonce, remoteNextLocalNonce) } } - + /** * This method is used to spend funds sent to htlc keys/delayed keys * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 3b4a8eecae..17e3344a02 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -222,7 +222,7 @@ object Transactions { super.checkSig(sig, pubKey, txOwner, commitmentFormat) } } - + case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { override def desc: String = "htlc-timeout" @@ -1241,12 +1241,18 @@ object Transactions { private def sign(txinfo: TransactionWithInputInfo, key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = sign(txinfo, key, txinfo.sighash(txOwner, commitmentFormat)) + def partialSign(key: PrivateKey, tx: Transaction, inputIndex: Int, spentOutputs: Seq[TxOut], + localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, + localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] = { + val publicKeys = Scripts.sort(Seq(localFundingPublicKey, remoteFundingPublicKey)) + Musig2.signTaprootInput(key, tx, inputIndex, spentOutputs, publicKeys, localNonce._1, Seq(localNonce._2, remoteNextLocalNonce), None) + } + def partialSign(txinfo: TransactionWithInputInfo, key: PrivateKey, localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] = { val inputIndex = txinfo.tx.txIn.indexWhere(_.outPoint == txinfo.input.outPoint) - val publicKeys = Scripts.sort(Seq(localFundingPublicKey, remoteFundingPublicKey)) - Musig2.signTaprootInput(key, txinfo.tx, inputIndex, Seq(txinfo.input.txOut), publicKeys, localNonce._1, Seq(localNonce._2, remoteNextLocalNonce), None) + partialSign(key, txinfo.tx, inputIndex, Seq(txinfo.input.txOut), localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, localNonce, remoteNextLocalNonce) } def aggregatePartialSignatures(txinfo: TransactionWithInputInfo, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala index 2442ccca11..b294374df2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala @@ -251,8 +251,15 @@ private[channel] object ChannelCodecs5 { ("fundingTxIndex" | uint32) :: ("remoteFundingPubkey" | publicKey)).as[InteractiveTxBuilder.Multisig2of2Input] + private val musig2of2InputCodec: Codec[InteractiveTxBuilder.Musig2Input] = ( + ("info" | inputInfoCodec) :: + ("fundingTxIndex" | uint32) :: + ("remoteFundingPubkey" | publicKey) :: + ("commitIndex" | uint32)).as[InteractiveTxBuilder.Musig2Input] + private val sharedFundingInputCodec: Codec[InteractiveTxBuilder.SharedFundingInput] = discriminated[InteractiveTxBuilder.SharedFundingInput].by(uint16) .typecase(0x01, multisig2of2InputCodec) + .typecase(0x02, musig2of2InputCodec) private val requireConfirmedInputsCodec: Codec[InteractiveTxBuilder.RequireConfirmedInputs] = (("forLocal" | bool8) :: ("forRemote" | bool8)).as[InteractiveTxBuilder.RequireConfirmedInputs] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index 238dc1fe86..46fff4c46e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, TxId} import fr.acinq.eclair.channel.{ChannelType, ChannelTypes, PartialSignatureWithNonce} -import fr.acinq.eclair.wire.protocol.ChannelTlv.nexLocalNonceTlvCodec +import fr.acinq.eclair.wire.protocol.ChannelTlv.{nexLocalNonceTlvCodec, nexLocalNoncesTlvCodec} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tmillisatoshi} import fr.acinq.eclair.{Alias, FeatureSupport, Features, MilliSatoshi, UInt64} @@ -70,10 +70,13 @@ object ChannelTlv { val pushAmountCodec: Codec[PushAmountTlv] = tlvField(tmillisatoshi) - case class NextLocalNonceTlv(nonce: IndividualNonce) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv with ChannelReadyTlv with ChannelReestablishTlv + case class NextLocalNonceTlv(nonce: IndividualNonce) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv with ChannelReadyTlv with ChannelReestablishTlv with SpliceInitTlv with SpliceAckTlv val nexLocalNonceTlvCodec: Codec[NextLocalNonceTlv] = tlvField(publicNonce) + case class NextLocalNoncesTlv(nonces: List[IndividualNonce]) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv with ChannelReadyTlv with ChannelReestablishTlv with SpliceInitTlv with SpliceAckTlv + + val nexLocalNoncesTlvCodec: Codec[NextLocalNoncesTlv] = tlvField(list(publicNonce)) } object OpenChannelTlv { @@ -148,6 +151,7 @@ object SpliceInitTlv { val spliceInitTlvCodec: Codec[TlvStream[SpliceInitTlv]] = tlvStream(discriminated[SpliceInitTlv].by(varint) .typecase(UInt64(2), requireConfirmedInputsCodec) + .typecase(UInt64(4), nexLocalNonceTlvCodec) .typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv])) ) } @@ -158,6 +162,7 @@ object SpliceAckTlv { val spliceAckTlvCodec: Codec[TlvStream[SpliceAckTlv]] = tlvStream(discriminated[SpliceAckTlv].by(varint) .typecase(UInt64(2), requireConfirmedInputsCodec) + .typecase(UInt64(4), nexLocalNonceTlvCodec) .typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv])) ) } @@ -228,7 +233,7 @@ object ChannelReestablishTlv { val channelReestablishTlvCodec: Codec[TlvStream[ChannelReestablishTlv]] = tlvStream(discriminated[ChannelReestablishTlv].by(varint) .typecase(UInt64(0), NextFundingTlv.codec) - .typecase(UInt64(4), nexLocalNonceTlvCodec) + .typecase(UInt64(4), nexLocalNoncesTlvCodec) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala index 249da00264..b16630d9f3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala @@ -85,13 +85,13 @@ object CommitSigTlv { sealed trait RevokeAndAckTlv extends Tlv object RevokeAndAckTlv { - case class NextLocalNonceTlv(nonce: IndividualNonce) extends RevokeAndAckTlv + case class NextLocalNoncesTlv(nonces: List[IndividualNonce]) extends RevokeAndAckTlv - object NextLocalNonceTlv { - val codec: Codec[NextLocalNonceTlv] = tlvField(publicNonce) + object NextLocalNoncesTlv { + val codec: Codec[NextLocalNoncesTlv] = tlvField(list(publicNonce)) } val revokeAndAckTlvCodec: Codec[TlvStream[RevokeAndAckTlv]] = tlvStream(discriminated[RevokeAndAckTlv].by(varint) - .typecase(UInt64(4), NextLocalNonceTlv.codec) + .typecase(UInt64(4), NextLocalNoncesTlv.codec) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala index 96696d8356..22a2b3d0a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala @@ -18,7 +18,8 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.{ByteVector64, TxId} import fr.acinq.eclair.UInt64 -import fr.acinq.eclair.wire.protocol.CommonCodecs.{bytes64, txIdAsHash, varint} +import fr.acinq.eclair.channel.PartialSignatureWithNonce +import fr.acinq.eclair.wire.protocol.CommonCodecs.{bytes64, partialSignatureWithNonce, txIdAsHash, varint} import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream} import scodec.Codec import scodec.codecs.discriminated @@ -69,7 +70,14 @@ object TxSignaturesTlv { /** When doing a splice, each peer must provide their signature for the previous 2-of-2 funding output. */ case class PreviousFundingTxSig(sig: ByteVector64) extends TxSignaturesTlv + case class PreviousFundingTxPartialSig(partialSigWithNonce: PartialSignatureWithNonce) extends TxSignaturesTlv + + object PreviousFundingTxPartialSig { + val codec: Codec[PreviousFundingTxPartialSig] = tlvField(partialSignatureWithNonce) + } + val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint) + .typecase(UInt64(2), PreviousFundingTxPartialSig.codec) .typecase(UInt64(601), tlvField(bytes64.as[PreviousFundingTxSig])) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 329dd6c01a..2f4ad3824f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -124,11 +124,16 @@ case class TxSignatures(channelId: ByteVector32, witnesses: Seq[ScriptWitness], tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId { val previousFundingTxSig_opt: Option[ByteVector64] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].map(_.sig) + val previousFundingTxPartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxPartialSig].map(_.partialSigWithNonce) } object TxSignatures { - def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ByteVector64]): TxSignatures = { - TxSignatures(channelId, tx.txid, witnesses, TlvStream(previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig).toSet[TxSignaturesTlv])) + def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ByteVector64], previousFundingTxPartialSig_opt: Option[PartialSignatureWithNonce]): TxSignatures = { + val tlvs: Set[TxSignaturesTlv] = Set( + previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig), + previousFundingTxPartialSig_opt.map(p => TxSignaturesTlv.PreviousFundingTxPartialSig(p)) + ).flatten + TxSignatures(channelId, tx.txid, witnesses, TlvStream(tlvs)) } } @@ -183,7 +188,7 @@ case class ChannelReestablish(channelId: ByteVector32, myCurrentPerCommitmentPoint: PublicKey, tlvStream: TlvStream[ChannelReestablishTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId) - val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) + val nextLocalNonces: List[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNoncesTlv].map(_.nonces).getOrElse(List.empty) } case class OpenChannel(chainHash: BlockHash, @@ -313,13 +318,15 @@ case class SpliceInit(channelId: ByteVector32, tlvStream: TlvStream[SpliceInitTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { 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) } object SpliceInit { - def apply(channelId: ByteVector32, fundingContribution: Satoshi, lockTime: Long, feerate: FeeratePerKw, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean): SpliceInit = { + def apply(channelId: ByteVector32, fundingContribution: Satoshi, lockTime: Long, feerate: FeeratePerKw, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, nextLocalNonce_opt: Option[IndividualNonce]): SpliceInit = { val tlvs: Set[SpliceInitTlv] = Set( Some(ChannelTlv.PushAmountTlv(pushAmount)), if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None, + nextLocalNonce_opt.map(ChannelTlv.NextLocalNonceTlv) ).flatten SpliceInit(channelId, fundingContribution, feerate, lockTime, fundingPubKey, TlvStream(tlvs)) } @@ -331,13 +338,15 @@ case class SpliceAck(channelId: ByteVector32, tlvStream: TlvStream[SpliceAckTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { 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) } object SpliceAck { - def apply(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean): SpliceAck = { + def apply(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubKey: PublicKey, pushAmount: MilliSatoshi, requireConfirmedInputs: Boolean, nextLocalNonce_opt: Option[IndividualNonce]): SpliceAck = { val tlvs: Set[SpliceAckTlv] = Set( Some(ChannelTlv.PushAmountTlv(pushAmount)), if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None, + nextLocalNonce_opt.map(ChannelTlv.NextLocalNonceTlv) ).flatten SpliceAck(channelId, fundingContribution, fundingPubKey, TlvStream(tlvs)) } @@ -414,7 +423,7 @@ case class RevokeAndAck(channelId: ByteVector32, perCommitmentSecret: PrivateKey, nextPerCommitmentPoint: PublicKey, tlvStream: TlvStream[RevokeAndAckTlv] = TlvStream.empty) extends HtlcMessage with HasChannelId { - val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[protocol.RevokeAndAckTlv.NextLocalNonceTlv].map(_.nonce) + val nexLocalNonces: List[IndividualNonce] = tlvStream.get[protocol.RevokeAndAckTlv.NextLocalNoncesTlv].map(_.nonces).getOrElse(List.empty) } case class UpdateFee(channelId: ByteVector32, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala index 47e55795f6..db1f60b067 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala @@ -137,6 +137,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit FundingTx(commitFeerate, firstPerCommitmentPointB, feeBudget_opt = None), 0 msat, 0 msat, wallet, + List.empty, Some(nextLocalNonceB._2) )) @@ -145,21 +146,21 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit nodeParamsA, fundingParams, channelParamsA, PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, - wallet, None)) + wallet, List.empty, None)) def spawnTxBuilderSpliceAlice(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsA, fundingParams, channelParamsA, SpliceTx(commitment), 0 msat, 0 msat, - wallet, None)) + wallet, List.empty, None)) def spawnTxBuilderSpliceRbfAlice(fundingParams: InteractiveTxParams, parentCommitment: Commitment, replacedCommitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsA, fundingParams, channelParamsA, PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, - wallet, None)) + wallet, List.empty, None)) def spawnTxBuilderBob(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsB): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, @@ -167,6 +168,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit FundingTx(commitFeerate, firstPerCommitmentPointA, feeBudget_opt = None), 0 msat, 0 msat, wallet, + List.empty, Some(nextLocalNonceA._2) )) @@ -175,21 +177,21 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit nodeParamsB, fundingParams, channelParamsB, PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, - wallet, None)) + wallet, List.empty, None)) def spawnTxBuilderSpliceBob(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsB, fundingParams, channelParamsB, SpliceTx(commitment), 0 msat, 0 msat, - wallet, None)) + wallet, List.empty, None)) def spawnTxBuilderSpliceRbfBob(fundingParams: InteractiveTxParams, parentCommitment: Commitment, replacedCommitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsB, fundingParams, channelParamsB, PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, - wallet, None)) + wallet, List.empty, None)) def exchangeSigsAliceFirst(fundingParams: InteractiveTxParams, successA: InteractiveTxBuilder.Succeeded, successB: InteractiveTxBuilder.Succeeded): (FullySignedSharedTransaction, Commitment, FullySignedSharedTransaction, Commitment) = { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging @@ -227,7 +229,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit private def createFixtureParams(fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false), useTaprootChannels: Boolean = false): FixtureParams = { val channelFeatures = if (useTaprootChannels) ChannelFeatures( - ChannelTypes.SimpleTaprootChannelsStaging, + ChannelTypes.SimpleTaprootChannelsStaging(), Features[InitFeature](Features.SimpleTaprootStaging -> FeatureSupport.Optional, Features.DualFunding -> FeatureSupport.Optional), Features[InitFeature](Features.SimpleTaprootStaging -> FeatureSupport.Optional, Features.DualFunding -> FeatureSupport.Optional), announceChannel = true) @@ -317,6 +319,16 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit testFun(Fixture(alice, bob, fixtureParams, walletA, rpcClientA, walletB, rpcClientB, TestProbe(), TestProbe())) } + test("compute shared input weights") { + val fundingKeys = Seq(randomKey(), randomKey()) + val fundingScript = Scripts.multiSig2of2(fundingKeys(0).publicKey, fundingKeys(1).publicKey) + val serializedScript = Script.write(fundingScript) + val witness = Scripts.witness2of2(ByteVector64.Zeroes, ByteVector64.Zeroes, fundingKeys(0).publicKey, fundingKeys(1).publicKey) + println(witness) + val witness1 = Script.witnessKeyPathPay2tr(ByteVector64.Zeroes) + println(witness1) + } + test("initiator funds more than non-initiator") { val targetFeerate = FeeratePerKw(5000 sat) val fundingA = 120_000 sat @@ -2604,8 +2616,8 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit assert(initiatorTx.buildUnsignedTx().txid == unsignedTx.txid) assert(nonInitiatorTx.buildUnsignedTx().txid == unsignedTx.txid) - val initiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), None) - val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None) + val initiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), None, None) + val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None, None) val initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs, None) assert(initiatorSignedTx.feerate == FeeratePerKw(262 sat)) val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs, None) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 8dafe0e5bb..47eb9cae04 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -275,6 +275,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toRemote == 700_000_000.msat) } + test("recv CMD_SPLICE (splice-in, simple taproot channels)", Tag(OptionSimpleTaprootStaging), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + assert(initialState.commitments.latest.capacity == 1_500_000.sat) + assert(initialState.commitments.latest.localCommit.spec.toLocal == 800_000_000.msat) + assert(initialState.commitments.latest.localCommit.spec.toRemote == 700_000_000.msat) + + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.capacity == 2_000_000.sat) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 1_300_000_000.msat) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toRemote == 700_000_000.msat) + } + test("recv CMD_SPLICE (splice-in, non dual-funded channel)") { () => val f = init(tags = Set(DualFunding, Splicing)) import f._ @@ -368,6 +383,22 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toRemote == 700_000_000.msat) } + test("recv CMD_SPLICE (splice-out, simple taproot channels)", Tag(OptionSimpleTaprootStaging), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + assert(initialState.commitments.latest.capacity == 1_500_000.sat) + assert(initialState.commitments.latest.localCommit.spec.toLocal == 800_000_000.msat) + assert(initialState.commitments.latest.localCommit.spec.toRemote == 700_000_000.msat) + + initiateSplice(f, spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + + // initiator pays the fee + val fee = spliceOutFee(f, capacity = 1_400_000.sat) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 700_000_000.msat - fee) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toRemote == 700_000_000.msat) + } + test("recv CMD_SPLICE (splice-out, would go below reserve)") { f => import f._ @@ -472,6 +503,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik testSpliceInAndOutCmd(f) } + test("recv CMD_SPLICE (splice-in + splice-out, simple taproot channels)", Tag(OptionSimpleTaprootStaging), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f => + testSpliceInAndOutCmd(f) + } + test("recv CMD_SPLICE (splice-in + splice-out, quiescence)", Tag(Quiescence)) { f => testSpliceInAndOutCmd(f) } @@ -865,6 +900,35 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) } + test("recv CMD_ADD_HTLC with multiple commitments (simple taproot channels)", Tag(OptionSimpleTaprootStaging), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + val sender = TestProbe() + alice ! CMD_ADD_HTLC(sender.ref, 500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, localOrigin(sender.ref)) + sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + alice ! CMD_SIGN() + val sigA1 = alice2bob.expectMsgType[CommitSig] + assert(sigA1.batchSize == 2) + alice2bob.forward(bob) + val sigA2 = alice2bob.expectMsgType[CommitSig] + assert(sigA2.batchSize == 2) + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + val sigB1 = bob2alice.expectMsgType[CommitSig] + assert(sigB1.batchSize == 2) + bob2alice.forward(alice) + val sigB2 = bob2alice.expectMsgType[CommitSig] + assert(sigB2.batchSize == 2) + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) + } + test("recv CMD_ADD_HTLC with multiple commitments and reconnect") { f => import f._ initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) @@ -992,6 +1056,30 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.head.localCommit.spec.htlcs.size == 1) } + test("recv UpdateAddHtlc before splice confirms (zero-conf, simple taproot channels)", Tag(OptionSimpleTaprootStaging), Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + val spliceTx = initiateSplice(f, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey))) + alice ! WatchPublishedTriggered(spliceTx) + val spliceLockedAlice = alice2bob.expectMsgType[SpliceLocked] + bob ! WatchPublishedTriggered(spliceTx) + val spliceLockedBob = bob2alice.expectMsgType[SpliceLocked] + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 2) + val (preimage, htlc) = addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + alice2bob.forward(bob, spliceLockedAlice) + bob2alice.forward(alice, spliceLockedBob) + + fulfillHtlc(htlc.id, preimage, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.spec.htlcs.isEmpty) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.size == 1) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.head.localCommit.spec.htlcs.size == 1) + } + test("recv UpdateAddHtlc while splice is being locked", Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ @@ -1056,6 +1144,70 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bobCommitments.inactive.foreach(c => assert(c.localCommit.index < bobCommitments.localCommitIndex)) } + test("recv UpdateAddHtlc while splice is being locked (simple taproot channels)", Tag(OptionSimpleTaprootStaging), Tag(ZeroConf), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + initiateSplice(f, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey))) + val spliceTx = initiateSplice(f, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey))) + alice ! WatchPublishedTriggered(spliceTx) + val spliceLockedAlice = alice2bob.expectMsgType[SpliceLocked] + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 3) + + // Alice adds a new HTLC, and sends commit_sigs before receiving Bob's splice_locked. + // + // Alice Bob + // | splice_locked | + // |----------------------------->| + // | update_add_htlc | + // |----------------------------->| + // | commit_sig | batch_size = 3 + // |----------------------------->| + // | splice_locked | + // |<-----------------------------| + // | commit_sig | batch_size = 3 + // |----------------------------->| + // | commit_sig | batch_size = 3 + // |----------------------------->| + // | revoke_and_ack | + // |<-----------------------------| + // | commit_sig | batch_size = 1 + // |<-----------------------------| + // | revoke_and_ack | + // |----------------------------->| + + alice2bob.forward(bob, spliceLockedAlice) + val (preimage, htlc) = addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN() + val commitSigsAlice = (1 to 3).map(_ => alice2bob.expectMsgType[CommitSig]) + alice2bob.forward(bob, commitSigsAlice(0)) + bob ! WatchPublishedTriggered(spliceTx) + val spliceLockedBob = bob2alice.expectMsgType[SpliceLocked] + bob2alice.forward(alice, spliceLockedBob) + alice2bob.forward(bob, commitSigsAlice(1)) + alice2bob.forward(bob, commitSigsAlice(2)) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + assert(bob2alice.expectMsgType[CommitSig].batchSize == 1) + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.size == 2) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.size == 2) + + // Bob fulfills the HTLC. + fulfillHtlc(htlc.id, preimage, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments + assert(aliceCommitments.active.head.localCommit.spec.htlcs.isEmpty) + aliceCommitments.inactive.foreach(c => assert(c.localCommit.index < aliceCommitments.localCommitIndex)) + val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments + assert(bobCommitments.active.head.localCommit.spec.htlcs.isEmpty) + bobCommitments.inactive.foreach(c => assert(c.localCommit.index < bobCommitments.localCommitIndex)) + } + private def disconnect(f: FixtureParam): Unit = { import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 439fdc1fbe..5c1f985bf6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -190,9 +190,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TxRemoveOutput(channelId1, UInt64(1)) -> hex"0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001", TxComplete(channelId1) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", TxComplete(channelId1, TlvStream(Set.empty[TxCompleteTlv], Set(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", - TxSignatures(channelId1, tx2, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87")), ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484", - TxSignatures(channelId2, tx1, Nil, None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000", - TxSignatures(channelId2, tx1, Nil, Some(signature)) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxSignatures(channelId1, tx2, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87")), ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None, None) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484", + TxSignatures(channelId2, tx1, Nil, None, None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000", + TxSignatures(channelId2, tx1, Nil, Some(signature), None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 1_500_000 sat, requireConfirmedInputs = true) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 0 sat, requireConfirmedInputs = false) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000",