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 =>