From 0082aea68970dca7e54d6fcc608c18aeb6339da2 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 24 Apr 2024 20:21:13 +0200 Subject: [PATCH] Update splicing protocol The current "simple taproot channels" proposal is not compatible with splices. Supporting splices means supporting multiple commitment transactions that are valid at the same time, with the same commitment index but with different funding transactions. We need to extend the taproot proposal to include a list of musig2 nonces (one for each active commitment transaction). Similar to how commitment points are handled, `firstRemoteNonce` and `secondRemoteNonce` fields have been added to `SpliceInit` and `SpliceAck`, encoded as a list of nonces (instead of 2 expicit nonces) We also need a for the new commit tx that is being built, here it has been added to `SpliceInit` and `SpliceAck`. The funding tx that is being built during the interactive session needs to spend the current funding tx. For this, we re-use the scheme that we developped for our custome "swaproot" musig swap-ins: we add musig2 nonces to the `TxComplete` message, one nonce for each input that requires one, ordered by serial id. The life-cycle of these nonces is tied to the life-cycle of the interactive session which is never persisted (nonces here do not have to be deterministic). --- eclair-core/pom.xml | 15 ++ .../blockchain/fee/OnChainFeeConf.scala | 2 +- .../fr/acinq/eclair/channel/ChannelData.scala | 5 +- .../eclair/channel/ChannelFeatures.scala | 18 +- .../fr/acinq/eclair/channel/Commitments.scala | 59 ++++-- .../fr/acinq/eclair/channel/Helpers.scala | 10 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 140 +++++++++---- .../channel/fsm/ChannelOpenDualFunded.scala | 26 ++- .../channel/fsm/ChannelOpenSingleFunded.scala | 8 +- .../channel/fsm/CommonFundingHandlers.scala | 2 +- .../channel/fund/InteractiveTxBuilder.scala | 148 ++++++++++--- .../crypto/keymanager/ChannelKeyManager.scala | 7 +- .../keymanager/LocalChannelKeyManager.scala | 11 +- .../eclair/transactions/Transactions.scala | 27 ++- .../channel/version0/ChannelCodecs0.scala | 2 - .../channel/version1/ChannelCodecs1.scala | 2 - .../channel/version2/ChannelCodecs2.scala | 2 - .../channel/version3/ChannelCodecs3.scala | 2 - .../channel/version4/ChannelCodecs4.scala | 4 +- .../channel/version5/ChannelCodecs5.scala | 29 +-- .../eclair/wire/protocol/ChannelTlv.scala | 17 +- .../eclair/wire/protocol/CommonCodecs.scala | 2 +- .../acinq/eclair/wire/protocol/HtlcTlv.scala | 8 +- .../wire/protocol/InteractiveTxTlv.scala | 22 +- .../wire/protocol/LightningMessageTypes.scala | 42 +++- .../channel/InteractiveTxBuilderSpec.scala | 6 +- .../states/e/NormalSplicesStateSpec.scala | 197 +++++++++++++++++- .../channel/states/h/ClosingStateSpec.scala | 30 --- .../protocol/LightningMessageCodecsSpec.scala | 29 +-- .../acinq/eclair/api/handlers/Channel.scala | 5 +- 30 files changed, 646 insertions(+), 231 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index c0331b2b76..f10411490e 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -188,6 +188,21 @@ 4.1.94.Final + + fr.acinq.bitcoin + bitcoin-kmp-jvm + 0.20.0-SNAPSHOT + + + fr.acinq.secp256k1 + secp256k1-kmp-jvm + 0.15.0 + + + fr.acinq.secp256k1 + secp256k1-kmp-jni-jvm + 0.15.0 + fr.acinq bitcoin-lib_${scala.version.short} 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 eb256da9bd..6aba8579af 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 @@ -569,14 +569,15 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32, remotePushAmount: MilliSatoshi, txBuilder: typed.ActorRef[InteractiveTxBuilder.Command], deferred: Option[CommitSig], - remoteNextLocalNonce: Option[IndividualNonce], + secondRemoteNonce_opt: Option[IndividualNonce], replyTo_opt: Option[akka.actor.typed.ActorRef[Peer.OpenChannelResponse]]) extends TransientChannelData final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelParams: ChannelParams, secondRemotePerCommitmentPoint: PublicKey, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, signingSession: InteractiveTxSigningSession.WaitingForSigs, - remoteChannelData_opt: Option[ByteVector]) extends ChannelDataWithoutCommitments + remoteChannelData_opt: Option[ByteVector], + secondRemoteNonce_opt: Option[IndividualNonce]) extends ChannelDataWithoutCommitments final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments, localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi, 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 a7ee6484f6..0cc6164242 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 @@ -1,7 +1,7 @@ package fr.acinq.eclair.channel -import akka.event.LoggingAdapter -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Musig2, Satoshi, SatoshiLong, Script, Transaction, TxId} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw, OnChainFeeConf} @@ -17,6 +17,7 @@ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, payment} +import grizzled.slf4j.Logging import scodec.bits.ByteVector /** Static channel parameters shared by all commitments. */ @@ -229,11 +230,23 @@ case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: object LocalCommit { def fromCommitSig(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxId: TxId, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, - commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey): Either[ChannelException, LocalCommit] = { + commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, localNonce_opt: Option[(SecretNonce, IndividualNonce)]): Either[ChannelException, LocalCommit] = { val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(keyManager, params.channelConfig, params.channelFeatures, localCommitIndex, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, localPerCommitmentPoint, spec) if (!localCommitTx.checkSig(commit, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) { return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx)) } + commit.sigOrPartialSig match { + case Left(_) => + if (!localCommitTx.checkSig(commit, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) { + return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx)) + } + case Right(psig) => + val fundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey + val Some(localNonce) = localNonce_opt + if (!localCommitTx.checkPartialSignature(psig, fundingPubkey, localNonce._2, remoteFundingPubKey)) { + return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx)) + } + } val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) if (commit.htlcSignatures.size != sortedHtlcTxs.size) { return Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) @@ -252,7 +265,7 @@ object LocalCommit { /** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */ case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePerCommitmentPoint: PublicKey) { - def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, remoteNonce_opt: Option[IndividualNonce]): CommitSig = { + def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, remoteNonce_opt: Option[IndividualNonce])(implicit log: LoggingAdapter): CommitSig = { val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, index, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remotePerCommitmentPoint, spec) val (sig, tlvStream) = params.commitmentFormat match { case SimpleTaprootChannelsStagingCommitmentFormat => @@ -653,6 +666,7 @@ case class Commitment(fundingTxIndex: Long, val localNonce = keyManager.signingNonce(params.localParams.fundingKeyPath, fundingTxIndex) val Some(remoteNonce) = nextRemoteNonce_opt val Right(psig) = keyManager.partialSign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce) + log.debug(s"sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint") Set(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2))) case _ => Set.empty @@ -668,11 +682,12 @@ case class Commitment(fundingTxIndex: Long, val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(Set( if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None ).flatten[CommitSigTlv] ++ partialSig)) + log.debug(s"sendCommit: setting remoteNextPerCommitmentPoint to $remoteNextPerCommitmentPoint") val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)) (copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig) } - def receiveCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, localPerCommitmentPoint: PublicKey, commit: CommitSig)(implicit log: LoggingAdapter): Either[ChannelException, Commitment] = { + def receiveCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, localPerCommitmentPoint: PublicKey, commit: CommitSig, localNonce_opt: Option[(SecretNonce, IndividualNonce)])(implicit log: LoggingAdapter): Either[ChannelException, Commitment] = { // they sent us a signature for *their* view of *our* next commit tx // so in terms of rev.hashes and indexes we have: // ourCommit.index -> our current revocation hash, which is about to become our old revocation hash @@ -683,7 +698,7 @@ case class Commitment(fundingTxIndex: Long, // and will increment our index val localCommitIndex = localCommit.index + 1 val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) - LocalCommit.fromCommitSig(keyManager, params, fundingTxId, fundingTxIndex, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec, localPerCommitmentPoint).map { localCommit1 => + LocalCommit.fromCommitSig(keyManager, params, fundingTxId, fundingTxIndex, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec, localPerCommitmentPoint, localNonce_opt).map { localCommit1 => log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommit1.commitTxAndRemoteSig.commitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(",")) copy(localCommit = localCommit1) } @@ -698,9 +713,10 @@ case class Commitment(fundingTxIndex: Long, addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig) case Right(remotePartialSigWithNonce) => val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) - val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, ChannelKeyManager.keyPath(fundingPubKey.publicKey), localCommit.index) + val channelKeyPath = ChannelKeyManager.keyPath(keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0L)) + val localNonce = keyManager.verificationNonce(params.localParams.fundingKeyPath, fundingTxIndex, channelKeyPath, 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 +1053,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), @@ -1066,7 +1088,11 @@ case class Commitments(params: ChannelParams, val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1) // Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments. val active1 = active.zip(commits).map { case (commitment, commit) => - commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit) match { + val localNonce_opt = params.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => Some(keyManager.verificationNonce(params.localParams.fundingKeyPath, commitment.fundingTxIndex, channelKeyPath, localCommitIndex + 1)) + case _ => None + } + commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit, localNonce_opt) match { case Left(f) => return Left(f) case Right(commitment1) => commitment1 } @@ -1076,9 +1102,12 @@ 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) - log.debug("generating our next local nonce with {} {} {} {}", params.localParams.fundingKeyPath, this.latest.fundingTxIndex, channelKeyPath, localCommitIndex + 2) - TlvStream(RevokeAndAckTlv.NextLocalNonceTlv(nonce)) + val nonces = this.active.map(c => { + val n = keyManager.verificationNonce(params.localParams.fundingKeyPath, c.fundingTxIndex, channelKeyPath, localCommitIndex + 2) + log.debug(s"revokeandack: creating verification nonce ${n._2} fundingIndex = ${c.fundingTxIndex} commit index = ${localCommitIndex + 2}") + n + }) + TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList)) case _ => TlvStream.empty } @@ -1103,7 +1132,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 25321f0b82..77c3ad3300 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 @@ -159,7 +159,7 @@ object Helpers { if (open.dustLimit > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimit, nodeParams.channelConf.maxRemoteDustLimit)) val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) - if (channelFeatures.hasFeature(Features.SimpleTaprootStaging) && open.tlvStream.get[ChannelTlv.NextLocalNonceTlv].isEmpty) return Left(MissingNextLocalNonce(open.temporaryChannelId)) + if (channelFeatures.hasFeature(Features.SimpleTaprootStaging) && open.tlvStream.get[ChannelTlv.NextLocalNoncesTlv].isEmpty) return Left(MissingNextLocalNonce(open.temporaryChannelId)) // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large. val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingAmount) @@ -245,7 +245,7 @@ object Helpers { if (accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay)) val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) - if (channelFeatures.hasFeature(Features.SimpleTaprootStaging) && accept.tlvStream.get[ChannelTlv.NextLocalNonceTlv].isEmpty) return Left(MissingNextLocalNonce(open.temporaryChannelId)) + if (channelFeatures.hasFeature(Features.SimpleTaprootStaging) && accept.tlvStream.get[ChannelTlv.NextLocalNoncesTlv].isEmpty) return Left(MissingNextLocalNonce(open.temporaryChannelId)) extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } @@ -456,7 +456,7 @@ object Helpers { /** * Check whether we are in sync with our peer. */ - def checkSync(keyManager: ChannelKeyManager, commitments: Commitments, remoteChannelReestablish: ChannelReestablish): SyncResult = { + def checkSync(keyManager: ChannelKeyManager, commitments: Commitments, remoteChannelReestablish: ChannelReestablish)(implicit log: LoggingAdapter): SyncResult = { // This is done in two steps: // - step 1: we check our local commitment @@ -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 26c0c10cd6..bbd499e1ec 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(info: String, n: List[IndividualNonce]): Unit = { + this.remoteNextLocalNonces = n + log.debug("{} set remoteNextLocalNonces to {}", info, 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("received RevokeAndAck", revocation.nexLocalNonces) log.debug("received a new rev, spec:\n{}", commitments1.latest.specs2String) actions.foreach { case PostRevocationAction.RelayHtlc(add) => @@ -981,18 +988,38 @@ 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 nextLocalNonces = d.commitments.latest.params.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => + val fundingIndex = parentCommitment.fundingTxIndex + 1 + val commitIndex = d.commitments.localCommitIndex + + def localNonce(commitIndex: Long) = { + val nonce = keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, fundingIndex, keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig), commitIndex) + log.info(s"splice ack: adding nonce at funding index ${fundingIndex} commit index = ${commitIndex} nonce = ${nonce._2}") + nonce._2 + } + + List(localNonce(commitIndex), localNonce(commitIndex + 1)) + case _ => + List.empty + } 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, + nextLocalNonces ) + 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 +1028,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 +1036,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with purpose = InteractiveTxBuilder.SpliceTx(parentCommitment), localPushAmount = spliceAck.pushAmount, remotePushAmount = msg.pushAmount, wallet, - None // TODO + msg.firstRemoteNonce )) 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 + log.debug(s"updating pendingRemoteNextLocalNonce $pendingRemoteNextLocalNonce with ${msg.secondRemoteNonce}") + this.pendingRemoteNextLocalNonce = msg.secondRemoteNonce stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionId, txBuilder, remoteCommitSig = None)) sending spliceAck } case SpliceStatus.SpliceAborted => @@ -1026,12 +1058,16 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case SpliceStatus.SpliceRequested(cmd, spliceInit) => 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 +1076,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 +1084,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with purpose = InteractiveTxBuilder.SpliceTx(parentCommitment), localPushAmount = cmd.pushAmount, remotePushAmount = msg.pushAmount, wallet, - None // TODO + msg.firstRemoteNonce )) 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 + log.debug(s"updating pendingRemoteNextLocalNonce $pendingRemoteNextLocalNonce with ${msg.secondRemoteNonce}") + this.pendingRemoteNextLocalNonce = msg.secondRemoteNonce stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = Some(cmd), sessionId, txBuilder, remoteCommitSig = None)) case _ => log.info(s"ignoring unexpected splice_ack=$msg") @@ -1142,6 +1183,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("received TxSignatures", this.pendingRemoteNextLocalNonce.toList ++ this.remoteNextLocalNonces) stay() using d.copy(commitments = commitments1) storing() calling publishFundingTx(dfu1) case Left(_) => stay() @@ -1162,6 +1205,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("end of quiescence", this.pendingRemoteNextLocalNonce.toList ++ this.remoteNextLocalNonces) stay() using d1 storing() sending signingSession1.localSigs calling publishFundingTx(signingSession1.fundingTx) calling endQuiescence(d1) } case _ => @@ -1320,7 +1364,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 +1960,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 +2000,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 +2039,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("received channelReestablish", 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("received ChannelReestablish", 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("received ChannelReestablish", 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 +2094,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("received ChannelReestablish", 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("received ChannelReestablish", 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("received ChannelReestablish", channelReestablish.nextLocalNonces) Syncing.checkSync(keyManager, d.commitments, channelReestablish) match { case syncFailure: SyncResult.Failure => @@ -2105,7 +2148,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 +2158,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 +2264,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("received ChannelReestablish", channelReestablish.nextLocalNonces) Syncing.checkSync(keyManager, d.commitments, channelReestablish) match { case syncFailure: SyncResult.Failure => handleSyncFailure(channelReestablish, syncFailure, d) @@ -2237,10 +2280,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("received ChannelReestablish", 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 @@ -2853,9 +2896,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with 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) @@ -2872,13 +2919,28 @@ 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 nextLocalNonces = d.commitments.latest.params.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => + val fundingIndex = parentCommitment.fundingTxIndex + 1 + val commitIndex = d.commitments.localCommitIndex + + def localNonce(commitIndex: Long) = { + val nonce = keyManager.verificationNonce(d.commitments.params.localParams.fundingKeyPath, fundingIndex, keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig), commitIndex) + log.info(s"splice init: adding nonce at funding index ${fundingIndex} commit index = ${commitIndex} nonce = ${nonce._2}") + nonce._2 + } + + List(localNonce(commitIndex), localNonce(commitIndex + 1)) + case _ => List.empty + } 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, + nextLocalNonces ) Right(spliceInit) } 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 ac7bfef95e..8ddb4f0c7a 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,12 @@ 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.NextLocalNoncesTlv( + List( + keyManager.verificationNonce(input.localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0)._2, + keyManager.verificationNonce(input.localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 1)._2 + ) + )) else None ).flatten val open = OpenDualFundedChannel( chainHash = nodeParams.chainHash, @@ -181,8 +186,13 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { Some(ChannelTlv.ChannelTypeTlv(d.init.channelType)), d.init.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)), if (nodeParams.channelConf.requireConfirmedInputsForDualFunding) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None, - if (channelParams.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat) Some(ChannelTlv.NextLocalNonceTlv(keyManager.verificationNonce(localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0)._2)) else None + if (channelParams.commitmentFormat == SimpleTaprootChannelsStagingCommitmentFormat) Some(ChannelTlv.NextLocalNoncesTlv( + List( + keyManager.verificationNonce(localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 0)._2, + keyManager.verificationNonce(localParams.fundingKeyPath, fundingTxIndex = 0, channelKeyPath, 1)._2 + ))) else None ).flatten + log.debug("sending AcceptDualFundedChannel with {}", tlvs) val accept = AcceptDualFundedChannel( temporaryChannelId = open.temporaryChannelId, fundingAmount = localAmount, @@ -224,9 +234,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { channelParams, purpose, localPushAmount = accept.pushAmount, remotePushAmount = open.pushAmount, wallet, - open.nexLocalNonce_opt)) + open.firstRemoteNonce)) txBuilder ! InteractiveTxBuilder.Start(self) - goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, open.secondPerCommitmentPoint, accept.pushAmount, open.pushAmount, txBuilder, deferred = None, remoteNextLocalNonce = open.nexLocalNonce_opt, replyTo_opt = None) sending accept + setRemoteNextLocalNonces("received OpenDualFundedChannel", open.secondRemoteNonce.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, open.secondRemoteNonce, replyTo_opt = None) sending accept } case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) @@ -288,9 +299,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { channelParams, purpose, localPushAmount = d.lastSent.pushAmount, remotePushAmount = accept.pushAmount, wallet, - accept.nexLocalNonce_opt)) + accept.firstRemoteNonce)) txBuilder ! InteractiveTxBuilder.Start(self) - goto(WAIT_FOR_DUAL_FUNDING_CREATED) using DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId, channelParams, accept.secondPerCommitmentPoint, d.lastSent.pushAmount, accept.pushAmount, txBuilder, deferred = None, remoteNextLocalNonce = accept.nexLocalNonce_opt, replyTo_opt = Some(d.init.replyTo)) + setRemoteNextLocalNonces("received AcceptDualFundedChannel", accept.secondRemoteNonce.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, accept.secondRemoteNonce, replyTo_opt = Some(d.init.replyTo)) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => @@ -343,7 +355,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case InteractiveTxBuilder.Succeeded(status, commitSig) => d.deferred.foreach(self ! _) d.replyTo_opt.foreach(_ ! OpenChannelResponse.Created(d.channelId, status.fundingTx.txId, status.fundingTx.tx.localFees.truncateToSatoshi)) - val d1 = DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(d.channelParams, d.secondRemotePerCommitmentPoint, d.localPushAmount, d.remotePushAmount, status, None) + val d1 = DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(d.channelParams, d.secondRemotePerCommitmentPoint, d.localPushAmount, d.remotePushAmount, status, None, d.secondRemoteNonce_opt) goto(WAIT_FOR_DUAL_FUNDING_SIGNED) using d1 storing() sending commitSig case f: InteractiveTxBuilder.Failed => d.replyTo_opt.foreach(_ ! OpenChannelResponse.Rejected(f.cause.getMessage)) 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 a0b00a7d9c..7bc65a284a 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("received AcceptChannel", 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..a25849def8 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("received ChannelReady", 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 fba75f9f06..931c20f283 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 { @@ -463,17 +488,25 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon 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 { @@ -482,7 +515,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)) } @@ -513,7 +546,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] = { @@ -545,12 +588,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 => @@ -562,7 +600,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) } @@ -571,7 +609,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 => @@ -583,7 +621,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 => @@ -591,7 +629,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 { @@ -621,7 +659,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)) @@ -749,7 +787,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, @@ -776,6 +814,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case SimpleTaprootChannelsStagingCommitmentFormat => val localNonce = keyManager.signingNonce(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex) val Right(psig) = keyManager.partialSign(remoteCommitTx, fundingPubKey, fundingParams.remoteFundingPubKey, TxOwner.Remote, localNonce, nextRemoteNonce_opt.get) + log.debug(s"signCommitTx: creating partial signature $psig for commit tx ${remoteCommitTx.tx.txid} with local nonce ${localNonce._2} remote nonce ${nextRemoteNonce_opt.get}") TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2))) case _ => TlvStream.empty @@ -784,13 +823,14 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val htlcSignatures = sortedHtlcTxs.map(keyManager.sign(_, localPerCommitmentPoint, purpose.remotePerCommitmentPoint, TxOwner.Remote, channelParams.commitmentFormat)).toList val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, tlvStream) val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil) + log.debug(s"signCommitTx: setting remotePerCommitmentPoint to ${purpose.remotePerCommitmentPoint}") 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) @@ -809,13 +849,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) + val Right(psig) = keyManager.partialSign(tx, inputIndex, unsignedTx.spentOutputs, fundingKey, m.remoteFundingPubkey, TxOwner.Local, localNonce, remoteNonce) + log.debug(s"signTx: creating partial sig $psig 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}") + 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 { @@ -843,7 +902,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) @@ -942,6 +1001,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) @@ -984,7 +1068,11 @@ object InteractiveTxSigningSession { case Left(unsignedLocalCommit) => val channelKeyPath = nodeParams.channelKeyManager.keyPath(channelParams.localParams, channelParams.channelConfig) val localPerCommitmentPoint = nodeParams.channelKeyManager.commitmentPoint(channelKeyPath, localCommitIndex) - LocalCommit.fromCommitSig(nodeParams.channelKeyManager, channelParams, fundingTx.txId, fundingTxIndex, fundingParams.remoteFundingPubKey, commitInput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec, localPerCommitmentPoint).map { signedLocalCommit => + val localNonce_opt = channelParams.commitmentFormat match { + case SimpleTaprootChannelsStagingCommitmentFormat => Some(nodeParams.channelKeyManager.verificationNonce(channelParams.localParams.fundingKeyPath, fundingTxIndex, channelKeyPath, localCommitIndex)) + case _ => None + } + LocalCommit.fromCommitSig(nodeParams.channelKeyManager, channelParams, fundingTx.txId, fundingTxIndex, fundingParams.remoteFundingPubKey, commitInput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec, localPerCommitmentPoint, localNonce_opt).map { signedLocalCommit => if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) { val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, nodeParams.currentBlockHeight, fundingParams) val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None) 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 fbfa2864db..9584b18e0d 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 @@ -16,6 +16,7 @@ package fr.acinq.eclair.crypto.keymanager +import akka.event.LoggingAdapter import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPublicKey @@ -75,7 +76,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..fcf3392e5e 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 @@ -107,7 +107,8 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha override def verificationNonce(fundingKeyPath: KeyPath, fundingTxIndex: Long, channelKeyPath: KeyPath, index: Long): (SecretNonce, IndividualNonce) = { val fundingPrivateKey = privateKeys.get(internalKeyPath(fundingKeyPath, hardened(fundingTxIndex))) val sessionId = Generators.perCommitSecret(nonceSeed(channelKeyPath), index).value - Musig2.generateNonce(sessionId, fundingPrivateKey.privateKey, Seq(fundingPrivateKey.publicKey)) + val nonce = Musig2.generateNonce(sessionId, fundingPrivateKey.privateKey, Seq(fundingPrivateKey.publicKey)) + nonce } override def signingNonce(fundingKeyPath: KeyPath, fundingTxIndex: Long): (SecretNonce, IndividualNonce) = { @@ -139,16 +140,16 @@ 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) + val psig = Transactions.partialSign(privateKey, tx, inputIndex, spentOutputs, localPublicKey.publicKey, remotePublicKey, localNonce, remoteNextLocalNonce) + psig } } - + /** * 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 04105ceff8..c9547c08c0 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 @@ -25,6 +25,7 @@ import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} +import fr.acinq.eclair.channel.PartialSignatureWithNonce import fr.acinq.eclair.transactions.CommitmentOutput._ import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.wire.protocol.{CommitSig, UpdateAddHtlc} @@ -173,7 +174,21 @@ object Transactions { case SimpleTaprootChannelsStagingCommitmentFormat => commitSig.sigOrPartialSig.isRight // TODO: export necessary methods case _ => super.checkSig(commitSig, pubKey, txOwner, commitmentFormat) } + + def checkPartialSignature(psig: PartialSignatureWithNonce, localPubKey: PublicKey, localNonce: IndividualNonce, remotePubKey: PublicKey): Boolean = { + import KotlinUtils._ + val session = fr.acinq.bitcoin.crypto.musig2.Musig2.taprootSession( + this.tx, + 0, + java.util.List.of(this.input.txOut), + Scripts.sort(Seq(localPubKey, remotePubKey)).map(scala2kmp).asJava, + java.util.List.of(localNonce, psig.nonce), + null + ).getRight + session.verify(psig.partialSig, psig.nonce, remotePubKey) + } } + /** * It's important to note that htlc transactions with the default commitment format are not actually replaceable: only * anchor outputs htlc transactions are replaceable. We should have used different types for these different kinds of @@ -222,7 +237,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 +1256,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 b9d5f52592..77b100a4cb 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,8 +17,6 @@ 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.{ByteVector64, Crypto, 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/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index c154ca1431..d0798fcb22 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 130d33d0ec..2b2dc31e27 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 a2bfe74c65..48c7dd692f 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 c3992bad96..b64079b507 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,6 +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 @@ -627,7 +626,8 @@ private[channel] object ChannelCodecs4 { ("localPushAmount" | millisatoshi) :: ("remotePushAmount" | millisatoshi) :: ("status" | interactiveTxWaitingForSigsCodec) :: - ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) :: + ("secondRemoteNonce_opt" | provide[Option[IndividualNonce]](None))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_02_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: 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 91078555be..ba534368da 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 @@ -110,24 +110,9 @@ private[channel] object ChannelCodecs5 { val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))) - def scriptTreeAndInternalKey: Codec[ScriptTreeAndInternalKey] = { - import fr.acinq.bitcoin.scalacompat.KotlinUtils._ - import shapeless.{::, HNil} - - val leafCodec: Codec[ScriptTree.Leaf] = (int32 :: varsizebinarydata :: int32).xmap[ScriptTree.Leaf]({ - case a :: b :: c :: HNil => new ScriptTree.Leaf(a, scala2kmp(b), c) - }, { - l => l.getId :: kmp2scala(l.getScript) :: l.getLeafVersion :: HNil - }) - - def scriptTreeCodec: Codec[ScriptTree] = lazily { - discriminated[ScriptTree].by(bool8) - .typecase(false, leafCodec) - .typecase(true, (scriptTreeCodec ~ scriptTreeCodec).xmap[ScriptTree.Branch]({ a => new ScriptTree.Branch(a._1, a._2) }, { n: ScriptTree.Branch => n.getLeft -> n.getRight })) - } + val scriptTreeCodec: Codec[ScriptTree] = lengthDelimited(bytes.xmap(d => ScriptTree.read(d.toArray), d => ByteVector.view(d.write()))) - (scriptTreeCodec :: xonlyPublicKey).as[ScriptTreeAndInternalKey] - } + val scriptTreeAndInternalKey: Codec[ScriptTreeAndInternalKey] = (scriptTreeCodec :: xonlyPublicKey).as[ScriptTreeAndInternalKey] val inputInfoCodec: Codec[InputInfo] = ( ("outPoint" | outPointCodec) :: @@ -252,8 +237,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] @@ -645,7 +637,8 @@ private[channel] object ChannelCodecs5 { ("localPushAmount" | millisatoshi) :: ("remotePushAmount" | millisatoshi) :: ("status" | interactiveTxWaitingForSigsCodec) :: - ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) :: + ("secondRemoteNonce_opt" | optional(bool8, publicNonce))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_02_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: 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..8d680d67a9 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 { @@ -107,7 +110,7 @@ object OpenDualFundedChannelTlv { .typecase(UInt64(0), upfrontShutdownScriptCodec) .typecase(UInt64(1), channelTypeCodec) .typecase(UInt64(2), requireConfirmedInputsCodec) - .typecase(UInt64(4), nexLocalNonceTlvCodec) + .typecase(UInt64(4), nexLocalNoncesTlvCodec) .typecase(UInt64(0x47000007), pushAmountCodec) ) } @@ -148,6 +151,7 @@ object SpliceInitTlv { val spliceInitTlvCodec: Codec[TlvStream[SpliceInitTlv]] = tlvStream(discriminated[SpliceInitTlv].by(varint) .typecase(UInt64(2), requireConfirmedInputsCodec) + .typecase(UInt64(4), nexLocalNoncesTlvCodec) .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), nexLocalNoncesTlvCodec) .typecase(UInt64(0x47000007), tlvField(tmillisatoshi.as[PushAmountTlv])) ) } @@ -174,7 +179,7 @@ object AcceptDualFundedChannelTlv { .typecase(UInt64(0), upfrontShutdownScriptCodec) .typecase(UInt64(1), channelTypeCodec) .typecase(UInt64(2), requireConfirmedInputsCodec) - .typecase(UInt64(4), nexLocalNonceTlvCodec) + .typecase(UInt64(4), nexLocalNoncesTlvCodec) .typecase(UInt64(0x47000007), pushAmountCodec) ) @@ -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 c7f0da3cae..a1f97c0cf2 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..02a006d0de 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, @@ -255,7 +269,9 @@ case class OpenDualFundedChannel(chainHash: BlockHash, val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat) - val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) + val nexLocalNonces: List[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNoncesTlv].map(_.nonces).getOrElse(List.empty) + val firstRemoteNonce: Option[IndividualNonce] = if (nexLocalNonces.isEmpty) None else Some(nexLocalNonces(0)) + val secondRemoteNonce: Option[IndividualNonce] = if (nexLocalNonces.isEmpty) None else Some(nexLocalNonces(1)) } // NB: this message is named accept_channel2 in the specification. @@ -279,7 +295,9 @@ case class AcceptDualFundedChannel(temporaryChannelId: ByteVector32, val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) val requireConfirmedInputs: Boolean = tlvStream.get[ChannelTlv.RequireConfirmedInputsTlv].nonEmpty val pushAmount: MilliSatoshi = tlvStream.get[ChannelTlv.PushAmountTlv].map(_.amount).getOrElse(0 msat) - val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) + val nexLocalNonces: List[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNoncesTlv].map(_.nonces).getOrElse(List.empty) + val firstRemoteNonce: Option[IndividualNonce] = if (nexLocalNonces.isEmpty) None else Some(nexLocalNonces(0)) + val secondRemoteNonce: Option[IndividualNonce] = if (nexLocalNonces.isEmpty) None else Some(nexLocalNonces(1)) } case class FundingCreated(temporaryChannelId: ByteVector32, @@ -313,13 +331,17 @@ 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 nexLocalNonces: List[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNoncesTlv].map(_.nonces).getOrElse(List.empty) + val firstRemoteNonce: Option[IndividualNonce] = if (nexLocalNonces.isEmpty) None else Some(nexLocalNonces(0)) + val secondRemoteNonce: Option[IndividualNonce] = if (nexLocalNonces.isEmpty) None else Some(nexLocalNonces(1)) } 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, nextLocalNonces: List[IndividualNonce]): SpliceInit = { val tlvs: Set[SpliceInitTlv] = Set( Some(ChannelTlv.PushAmountTlv(pushAmount)), if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None, + if (nextLocalNonces.nonEmpty) Some(ChannelTlv.NextLocalNoncesTlv(nextLocalNonces)) else None ).flatten SpliceInit(channelId, fundingContribution, feerate, lockTime, fundingPubKey, TlvStream(tlvs)) } @@ -331,13 +353,17 @@ 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 nexLocalNonces: List[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNoncesTlv].map(_.nonces).getOrElse(List.empty) + val firstRemoteNonce: Option[IndividualNonce] = if (nexLocalNonces.isEmpty) None else Some(nexLocalNonces(0)) + val secondRemoteNonce: Option[IndividualNonce] = if (nexLocalNonces.isEmpty) None else Some(nexLocalNonces(1)) } 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, nextLocalNonces: List[IndividualNonce]): SpliceAck = { val tlvs: Set[SpliceAckTlv] = Set( Some(ChannelTlv.PushAmountTlv(pushAmount)), if (requireConfirmedInputs) Some(ChannelTlv.RequireConfirmedInputsTlv()) else None, + if (nextLocalNonces.nonEmpty) Some(ChannelTlv.NextLocalNoncesTlv(nextLocalNonces)) else None ).flatten SpliceAck(channelId, fundingContribution, fundingPubKey, TlvStream(tlvs)) } @@ -414,7 +440,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 da256520f2..62deac630b 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) @@ -2604,8 +2604,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..986f561e04 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._ @@ -1475,7 +1659,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } - def testForceCloseWithMultipleSplicesSimple(f: FixtureParam): Unit = { + def testForceCloseWithMultipleSplicesSimple(f: FixtureParam, useAnchorOutputs: Boolean = false): Unit = { import f._ val htlcs = setupHtlcs(f) @@ -1495,6 +1679,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.expectMsgType[Error] val commitTx2 = assertPublished(alice2blockchain, "commit-tx") Transaction.correctlySpends(commitTx2, Seq(fundingTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + if (useAnchorOutputs) { + val claimAnchor = assertPublished(alice2blockchain, "local-anchor") + } val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") // alice publishes her htlc timeout transactions val htlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-timeout")) @@ -1503,6 +1691,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val watchConfirmedCommit2 = alice2blockchain.expectWatchTxConfirmed(commitTx2.txid) val watchConfirmedClaimMainDelayed2 = alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) // watch for all htlc outputs from local commit-tx to be spent + if (useAnchorOutputs) { + alice2blockchain.expectMsgType[WatchOutputSpent] + } val watchHtlcsOut = htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) @@ -1551,6 +1742,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik testForceCloseWithMultipleSplicesSimple(f) } + test("force-close with multiple splices (simple, simple taproot channels)", Tag(OptionSimpleTaprootStaging), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f => + testForceCloseWithMultipleSplicesSimple(f, useAnchorOutputs = true) + } + test("force-close with multiple splices (simple, quiescence)", Tag(Quiescence)) { f => testForceCloseWithMultipleSplicesSimple(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 fcb6327ec4..ba1187f8e5 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 39b39e95fe..3bc4d4716c 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", @@ -268,29 +268,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } test("decode open_channel with simple_taproot_channel extension") { - // 06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f - // 85c4f4bf75b2cb938d4c3e75bd53949f12d708b0b8d6db817e10ac3437ffb29f - // 00000000000186a0 - // 0000000000000000 - // 0000000000000162 - // 0000000005e69ec0 - // 00000000000003e8 - // 0000000000000001 - // 000009c4 - // 0090 - // 01e3 - // 03d01507c5d81a04650898e6ce017a3ed8349b83dd1f592e7ec8b9d6bdb064950c - // 02a54a8591a5fdc5f082f23d0f3e83ff74b6de433f71e40123c44b20a56a5bb9f5 - // 02a8e31e0707b1ac67b9fd938e5c9d59e3607fb84e0ab6e0824ad582e4f8f88df8 - // 02721e2a2757ff1c60a92716a366f89c3a7df6a48e71bc8824e23b1ae47d9f5965 - // 03df8191d861c265ab1f0539bdc04f8ac94847511abd6c70ed0775aea3f6c38212 - // 02c2fdb53245754e0e033a71e260e64f0c0959ac4a994e9c5159708ae05559e9ad - // 00 - // 000001171000000000000000000000000000000000000000000000 - // 04 42 03a8c947da4dae605ee05f7894e22a9d6d51e23c5523e63f8fc5dc7aea90835a9403f68dbb02e8cba1a97ea42bd6a963942187ff0da465dda3dc35cf0d260bcdcece val raw = "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f85c4f4bf75b2cb938d4c3e75bd53949f12d708b0b8d6db817e10ac3437ffb29f00000000000186a0000000000000000000000000000001620000000005e69ec000000000000003e80000000000000001000009c4009001e303d01507c5d81a04650898e6ce017a3ed8349b83dd1f592e7ec8b9d6bdb064950c02a54a8591a5fdc5f082f23d0f3e83ff74b6de433f71e40123c44b20a56a5bb9f502a8e31e0707b1ac67b9fd938e5c9d59e3607fb84e0ab6e0824ad582e4f8f88df802721e2a2757ff1c60a92716a366f89c3a7df6a48e71bc8824e23b1ae47d9f596503df8191d861c265ab1f0539bdc04f8ac94847511abd6c70ed0775aea3f6c3821202c2fdb53245754e0e033a71e260e64f0c0959ac4a994e9c5159708ae05559e9ad00000001171000000000000000000000000000000000000000000000044203a8c947da4dae605ee05f7894e22a9d6d51e23c5523e63f8fc5dc7aea90835a9403f68dbb02e8cba1a97ea42bd6a963942187ff0da465dda3dc35cf0d260bcdcece" - val decoded = openChannelCodec.decode(BitVector.fromValidHex(raw)) - println(decoded) + assert(openChannelCodec.decode(BitVector.fromValidHex(raw)).isSuccessful) } test("decode invalid open_channel") { 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 =>