From f556839c92e77304fecf08dccc2d6a6e76034819 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 24 Apr 2024 20:21:13 +0200 Subject: [PATCH] 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 during the interactive session needs to spend the current funding tx. For this, we re-use the scheme that we developped for our custome "swaproot" musig swap-ins: we add musig2 nonces to the `TxComplete` message, one nonce for each input that requires one, ordered by serial id. --- .../blockchain/fee/OnChainFeeConf.scala | 2 +- .../fr/acinq/eclair/channel/ChannelData.scala | 2 +- .../eclair/channel/ChannelFeatures.scala | 18 +- .../fr/acinq/eclair/channel/Commitments.scala | 18 +- .../fr/acinq/eclair/channel/Helpers.scala | 4 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 144 +++++++++----- .../channel/fsm/ChannelOpenDualFunded.scala | 5 +- .../channel/fsm/ChannelOpenSingleFunded.scala | 8 +- .../channel/fsm/CommonFundingHandlers.scala | 2 +- .../channel/fund/InteractiveTxBuilder.scala | 140 ++++++++++--- .../crypto/keymanager/ChannelKeyManager.scala | 6 +- .../keymanager/LocalChannelKeyManager.scala | 7 +- .../eclair/transactions/Transactions.scala | 12 +- .../channel/version0/ChannelCodecs0.scala | 4 +- .../channel/version1/ChannelCodecs1.scala | 2 - .../channel/version2/ChannelCodecs2.scala | 2 - .../channel/version3/ChannelCodecs3.scala | 2 - .../channel/version4/ChannelCodecs4.scala | 2 - .../channel/version5/ChannelCodecs5.scala | 7 + .../eclair/wire/protocol/ChannelTlv.scala | 13 +- .../eclair/wire/protocol/CommonCodecs.scala | 2 +- .../acinq/eclair/wire/protocol/HtlcTlv.scala | 8 +- .../wire/protocol/InteractiveTxTlv.scala | 22 ++- .../wire/protocol/LightningMessageTypes.scala | 30 ++- .../channel/InteractiveTxBuilderSpec.scala | 16 +- .../states/e/NormalSplicesStateSpec.scala | 184 ++++++++++++++++++ .../channel/states/h/ClosingStateSpec.scala | 30 --- .../protocol/LightningMessageCodecsSpec.scala | 6 +- .../acinq/eclair/api/handlers/Channel.scala | 5 +- 29 files changed, 528 insertions(+), 175 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala index 7db343ebcf..602298132b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala @@ -121,7 +121,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets, commitmentFormat match { case Transactions.DefaultCommitmentFormat => networkFeerate - case _: Transactions.AnchorOutputsCommitmentFormat => + case _: Transactions.AnchorOutputsCommitmentFormat => val targetFeerate = networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate) // We make sure the feerate is always greater than the propagation threshold. targetFeerate.max(networkMinFee * 1.25) 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..7cb03a144a 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,18 +131,16 @@ 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 - - /** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */ override def paysDirectlyToWallet: Boolean = false - /** Format of the channel transactions. */ override def commitmentFormat: CommitmentFormat = SimpleTaprootChannelsStagingCommitmentFormat - - override def toString: String = "simple_taproot_channel_staging" + override def toString: String = s"simple_taproot_channel_staging${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" } case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { @@ -168,7 +166,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 +186,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..fe4321ef79 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,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with purpose = InteractiveTxBuilder.SpliceTx(parentCommitment), localPushAmount = spliceAck.pushAmount, remotePushAmount = msg.pushAmount, wallet, - None // TODO + 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 +1047,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 +1068,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 +1076,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with purpose = InteractiveTxBuilder.SpliceTx(parentCommitment), localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount, wallet, - None // TODO + 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 +1109,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 +1174,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 +1196,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 +1355,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 +1951,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 +1991,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 +2030,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 +2085,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 +2139,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 +2149,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 +2255,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 +2271,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 +2883,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 +2908,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 +2958,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..6623383bc2 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, @@ -226,6 +227,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { wallet, 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 } @@ -290,6 +292,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { wallet, 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)) } 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..316584add9 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 @@ -37,6 +37,7 @@ import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, S import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, UInt64} +import kotlin.Pair import scodec.bits.ByteVector import scala.concurrent.{ExecutionContext, Future} @@ -125,6 +126,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. @@ -275,11 +291,12 @@ object InteractiveTxBuilder { remoteInputs: Seq[IncomingInput] = Nil, localOutputs: Seq[OutgoingOutput] = Nil, remoteOutputs: Seq[IncomingOutput] = Nil, - txCompleteSent: Boolean = false, - txCompleteReceived: Boolean = false, + txCompleteSent: Option[TxComplete] = None, + txCompleteReceived: Option[TxComplete] = None, inputsReceivedCount: Int = 0, - outputsReceivedCount: Int = 0) { - val isComplete: Boolean = txCompleteSent && txCompleteReceived + outputsReceivedCount: Int = 0, + secretNonces: Map[UInt64, (SecretNonce, IndividualNonce)] = Map.empty) { + val isComplete: Boolean = txCompleteSent.isDefined && txCompleteReceived.isDefined } /** Unsigned transaction created collaboratively. */ @@ -298,6 +315,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))) @@ -410,8 +430,13 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case rbf: PreviousTxRbf => rbf.previousTransactions case _ => Nil } + private val localNonce = fundingParams.sharedInput_opt.collect { + case s: Musig2Input => keyManager.signingNonce(channelParams.localParams.fundingKeyPath, s.fundingTxIndex) + } + log.debug("creating local nonce {} for fundingTxIndex {}", localNonce, purpose.fundingTxIndex) 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 { @@ -460,17 +485,25 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case i: Input.Local => TxAddInput(fundingParams.channelId, i.serialId, Some(i.previousTx), i.previousTxOutput, i.sequence) case i: Input.Shared => TxAddInput(fundingParams.channelId, i.serialId, i.outPoint, i.sequence) } + val nextSecretNonces = addInput match { + case i: Input.Shared if localNonce.isDefined => session.secretNonces + (i.serialId -> localNonce.get) + case _ => session.secretNonces + } replyTo ! SendMessage(sessionId, message) - val next = session.copy(toSend = tail, localInputs = session.localInputs :+ addInput, txCompleteSent = false) + val next = session.copy(toSend = tail, localInputs = session.localInputs :+ addInput, txCompleteSent = None, secretNonces = nextSecretNonces) receive(next) case (addOutput: Output) +: tail => val message = TxAddOutput(fundingParams.channelId, addOutput.serialId, addOutput.amount, addOutput.pubkeyScript) replyTo ! SendMessage(sessionId, message) - val next = session.copy(toSend = tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = false) + val next = session.copy(toSend = tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = None) receive(next) case Nil => - replyTo ! SendMessage(sessionId, TxComplete(fundingParams.channelId)) - val next = session.copy(txCompleteSent = true) + val publicNonces = (session.remoteInputs ++ session.localInputs).sortBy(_.serialId).collect { + case i: Input.Shared if this.channelParams.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat => session.secretNonces.get(i.serialId).map(_._2).getOrElse(throw new RuntimeException("missing secret nonce")) + } + val txComplete = TxComplete(fundingParams.channelId, publicNonces.toList) + replyTo ! SendMessage(sessionId, txComplete) + val next = session.copy(txCompleteSent = Some(txComplete)) if (next.isComplete) { validateAndSign(next) } else { @@ -479,7 +512,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon } } - private def receiveInput(session: InteractiveTxSession, addInput: TxAddInput): Either[ChannelException, IncomingInput] = { + private def receiveInput(session: InteractiveTxSession, addInput: TxAddInput): Either[ChannelException, InteractiveTxSession] = { if (session.inputsReceivedCount + 1 >= MAX_INPUTS_OUTPUTS_RECEIVED) { return Left(TooManyInteractiveTxRounds(fundingParams.channelId)) } @@ -510,7 +543,17 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon if (input.sequence > 0xfffffffdL) { return Left(NonReplaceableInput(fundingParams.channelId, addInput.serialId, input.outPoint.txid, input.outPoint.index, addInput.sequence)) } - Right(input) + val session1 = session.copy( + remoteInputs = session.remoteInputs :+ input, + inputsReceivedCount = session.inputsReceivedCount + 1, + txCompleteReceived = None, + ) + val session2 = input match { + case i: Input.Shared if this.localNonce.isDefined => + session1.copy(secretNonces = session1.secretNonces + (i.serialId -> localNonce.get)) + case _ => session1 + } + Right(session2) } private def receiveOutput(session: InteractiveTxSession, addOutput: TxAddOutput): Either[ChannelException, IncomingOutput] = { @@ -542,12 +585,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Left(f) => replyTo ! RemoteFailure(f) unlockAndStop(session) - case Right(input) => - val next = session.copy( - remoteInputs = session.remoteInputs :+ input, - inputsReceivedCount = session.inputsReceivedCount + 1, - txCompleteReceived = false, - ) + case Right(next) => send(next) } case addOutput: TxAddOutput => @@ -559,7 +597,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val next = session.copy( remoteOutputs = session.remoteOutputs :+ output, outputsReceivedCount = session.outputsReceivedCount + 1, - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) } @@ -568,7 +606,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Some(_) => val next = session.copy( remoteInputs = session.remoteInputs.filterNot(_.serialId == removeInput.serialId), - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) case None => @@ -580,7 +618,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Some(_) => val next = session.copy( remoteOutputs = session.remoteOutputs.filterNot(_.serialId == removeOutput.serialId), - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) case None => @@ -588,7 +626,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon unlockAndStop(session) } case txComplete: TxComplete => - val next = session.copy(txCompleteReceived = true) + val next = session.copy(txCompleteReceived = Some(txComplete)) if (next.isComplete) { validateAndSign(next) } else { @@ -618,7 +656,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon replyTo ! RemoteFailure(cause) unlockAndStop(session) case Right(completeTx) => - signCommitTx(completeTx) + signCommitTx(session, completeTx) } case _: WalletFailure => replyTo ! RemoteFailure(UnconfirmedInteractiveTxInputs(fundingParams.channelId)) @@ -746,7 +784,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon Right(sharedTx) } - private def signCommitTx(completeTx: SharedTransaction): Behavior[Command] = { + private def signCommitTx(session: InteractiveTxSession, completeTx: SharedTransaction): Behavior[Command] = { val fundingTx = completeTx.buildUnsignedTx() val fundingOutputIndex = fundingTx.txOut.indexWhere(_.publicKeyScript == fundingPubkeyScript) Funding.makeCommitTxs(keyManager, channelParams, @@ -782,12 +820,12 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, tlvStream) val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil) val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) - signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit) + signFundingTx(session, completeTx, localCommitSig, localCommit, remoteCommit) } } - private def signFundingTx(completeTx: SharedTransaction, commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { - signTx(completeTx) + private def signFundingTx(session: InteractiveTxSession, completeTx: SharedTransaction, commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { + signTx(session, completeTx) Behaviors.receiveMessagePartial { case SignTransactionResult(signedTx) => log.info(s"interactive-tx txid=${signedTx.txId} partially signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", signedTx.tx.localInputs.length, signedTx.tx.remoteInputs.length, signedTx.tx.localOutputs.length, signedTx.tx.remoteOutputs.length) @@ -806,13 +844,32 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon } } - private def signTx(unsignedTx: SharedTransaction): Unit = { + private def signTx(session: InteractiveTxSession, unsignedTx: SharedTransaction): Unit = { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ 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 sharedInputs = (session.localInputs ++ session.remoteInputs).collect { case i: Input.Shared => i } + // there should be a single shared input + val serialId = sharedInputs.head.serialId + val localNonce = session.secretNonces(serialId) + val fundingKey = keyManager.fundingPublicKey(this.channelParams.localParams.fundingKeyPath, m.fundingTxIndex) + val inputIndex = tx.txIn.indexWhere(_.outPoint == m.info.outPoint) + // there should be one remote nonce for each shared input ordered by serial id + val remoteNonces = sharedInputs.sortBy(_.serialId).zip(session.txCompleteReceived.get.publicNonces).map { case (i, n) => i.serialId -> n }.toMap + val remoteNonce = remoteNonces(serialId) + 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 +897,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 +996,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/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index fa30546d0f..d6a89aeff6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -17,10 +17,8 @@ package fr.acinq.eclair.wire.internal.channel.version0 import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} -import fr.acinq.bitcoin.ScriptTree -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxId, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, OutPoint, Transaction, TxId, TxOut} import fr.acinq.eclair.blockchain.fee.ConfirmationTarget import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index c5666c88ff..d5353e6bf3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -17,8 +17,6 @@ package fr.acinq.eclair.wire.internal.channel.version1 import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} -import fr.acinq.bitcoin.ScriptTree -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxId, TxOut} import fr.acinq.eclair.blockchain.fee.ConfirmationTarget diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index a4d89a6c86..ae33482c66 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -17,8 +17,6 @@ package fr.acinq.eclair.wire.internal.channel.version2 import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} -import fr.acinq.bitcoin.ScriptTree -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} import fr.acinq.eclair.blockchain.fee.ConfirmationTarget diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 0e1400508e..dbab45bba6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -17,8 +17,6 @@ package fr.acinq.eclair.wire.internal.channel.version3 import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} -import fr.acinq.bitcoin.ScriptTree -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} import fr.acinq.eclair.blockchain.fee.ConfirmationTarget diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala index 5a1501750a..a49ad52ade 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala @@ -1,7 +1,5 @@ package fr.acinq.eclair.wire.internal.channel.version4 -import fr.acinq.bitcoin.ScriptTree -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath import fr.acinq.bitcoin.scalacompat.{OutPoint, ScriptWitness, Transaction, TxOut} 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..56acbcdd8d 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,12 +19,12 @@ 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} import scodec.Codec -import scodec.bits.{BitVector, ByteVector} +import scodec.bits.ByteVector import scodec.codecs._ sealed trait OpenChannelTlv extends Tlv @@ -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/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index e1345df5ed..d2d511dbab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey} -import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, KotlinUtils, Satoshi, Transaction, TxHash, TxId} +import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Satoshi, Transaction, TxHash, TxId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, PartialSignatureWithNonce, RealScidStatus, ShortIds} import fr.acinq.eclair.crypto.Mac32 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..e7526a97df 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 @@ -16,12 +16,14 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce 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, publicNonce, txIdAsHash, varint} import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream} import scodec.Codec -import scodec.codecs.discriminated +import scodec.codecs.{discriminated, list} /** * Created by t-bast on 08/04/2022. @@ -60,7 +62,14 @@ object TxRemoveOutputTlv { sealed trait TxCompleteTlv extends Tlv object TxCompleteTlv { - val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint)) + /** musig2 nonces for musig2 swap-in inputs, ordered by serial id */ + case class Nonces(nonces: List[IndividualNonce]) extends TxCompleteTlv + + val noncesCodec: Codec[Nonces] = list(publicNonce).xmap(l => Nonces(l), _.nonces.toList) + + val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint) + .typecase(UInt64(101), tlvField(noncesCodec)) + ) } sealed trait TxSignaturesTlv extends Tlv @@ -69,7 +78,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..00d0663f30 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 @@ -116,7 +116,16 @@ case class TxRemoveOutput(channelId: ByteVector32, case class TxComplete(channelId: ByteVector32, tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId { + // there should be a musig2 nonce for each input that requires one, ordered by serial id + val publicNonces: List[IndividualNonce] = tlvStream.get[TxCompleteTlv.Nonces].map(_.nonces).getOrElse(List.empty[IndividualNonce]) +} + +object TxComplete { + def apply(channelId: ByteVector32) = new TxComplete(channelId, TlvStream.empty) + def apply(channelId: ByteVector32, tlvStream: TlvStream[TxCompleteTlv]) = new TxComplete(channelId, tlvStream) + + def apply(channelId: ByteVector32, publicNonces: List[IndividualNonce]) = new TxComplete(channelId, TlvStream(TxCompleteTlv.Nonces(publicNonces))) } case class TxSignatures(channelId: ByteVector32, @@ -124,11 +133,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 +197,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 +327,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 +347,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 +432,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..529e54f053 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 @@ -227,7 +227,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 +317,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 +2614,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..3a829a13d5 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))) @@ -897,6 +961,38 @@ 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 and reconnect (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() + assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) + assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) + // Bob disconnects before receiving Alice's commit_sig. + disconnect(f) + reconnect(f, interceptFundingDeeplyBuried = false) + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) + alice2bob.forward(bob) + assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + assert(bob2alice.expectMsgType[CommitSig].batchSize == 2) + bob2alice.forward(alice) + assert(bob2alice.expectMsgType[CommitSig].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 while a splice is requested") { f => import f._ val sender = TestProbe() @@ -992,6 +1088,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 +1176,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/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 12940f4620..4bcaabf1ae 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -407,36 +407,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData == initialState) // this was a no-op } - test("recv WatchOutputSpentTriggered (simple taproot channels", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => - import f._ - // alice sends an htlc to bob - val (ra1, htlca1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - bob2relayer.expectMsgType[RelayForward] - localClose(alice, alice2blockchain) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.localCommitPublished.isDefined) - - // actual test starts here - channelUpdateListener.expectMsgType[LocalChannelDown] - - // scenario 1: bob claims the htlc output from the commit tx using its preimage - val claimHtlcSuccessFromCommitTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint(randomTxId(), 0), signatureScript = ByteVector.empty, sequence = 0, witness = Scripts.witnessClaimHtlcSuccessFromCommitTx(Transactions.PlaceHolderSig, ra1, ByteVector.fill(130)(33))) :: Nil, txOut = Nil, lockTime = 0) - alice ! WatchOutputSpentTriggered(claimHtlcSuccessFromCommitTx) - val fulfill1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] - assert(fulfill1.htlc == htlca1) - assert(fulfill1.result.paymentPreimage == ra1) - - // scenario 2: bob claims the htlc output from his own commit tx using its preimage (let's assume both parties had published their commitment tx) - val claimHtlcSuccessTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint(randomTxId(), 0), signatureScript = ByteVector.empty, sequence = 0, witness = Scripts.witnessHtlcSuccess(Transactions.PlaceHolderSig, Transactions.PlaceHolderSig, ra1, ByteVector.fill(130)(33), Transactions.DefaultCommitmentFormat)) :: Nil, txOut = Nil, lockTime = 0) - alice ! WatchOutputSpentTriggered(claimHtlcSuccessTx) - val fulfill2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] - assert(fulfill2.htlc == htlca1) - assert(fulfill2.result.paymentPreimage == ra1) - - assert(alice.stateData == initialState) // this was a no-op - } - test("recv WatchOutputSpentTriggered (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ // alice sends an htlc to bob 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", diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index cfe2a5d5ea..5198c124d5 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -49,7 +49,10 @@ trait Channel { ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), - ChannelTypes.SimpleTaprootChannelsStaging + ChannelTypes.SimpleTaprootChannelsStaging(), + ChannelTypes.SimpleTaprootChannelsStaging(zeroConf = true), + ChannelTypes.SimpleTaprootChannelsStaging(scidAlias = true), + ChannelTypes.SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true), ).map(ct => ct.toString -> ct).toMap // we use the toString method as name in the api val open: Route = postRequest("open") { implicit t =>