diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 59a8facfe1..27164347cf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -342,7 +342,7 @@ object Features { } case object SimpleTaprootChannelsPhoenix extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { - val rfcName = "option_simple_taproot_phoenix_tweaked" + val rfcName = "option_simple_taproot_phoenix" val mandatory = 564 } 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 6ba1dd6b74..0c8b958546 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 @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, LegacySimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions._ // @formatter:off sealed trait ConfirmationPriority extends Ordered[ConfirmationPriority] { @@ -76,8 +76,8 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax def isProposedFeerateTooHigh(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = { commitmentFormat match { - case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | LegacySimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate + case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate } } @@ -85,7 +85,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax commitmentFormat match { case Transactions.DefaultCommitmentFormat => proposedFeerate < networkFeerate * ratioLow // When using anchor outputs, we allow low feerates: fees will be set with CPFP and RBF at broadcast time. - case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | LegacySimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => false + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => false } } } @@ -122,7 +122,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets, commitmentFormat match { case Transactions.DefaultCommitmentFormat => networkFeerate - case _: Transactions.AnchorOutputsCommitmentFormat | _: Transactions.SimpleTaprootChannelCommitmentFormat=> + case _: Transactions.AnchorOutputsCommitmentFormat | _: Transactions.SimpleTaprootChannelCommitmentFormat => 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/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 4a99d76424..74cd7c0f2b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -153,9 +153,9 @@ case class ForbiddenDuringQuiescence (override val channelId: Byte case class ConcurrentRemoteSplice (override val channelId: ByteVector32) extends ChannelException(channelId, "splice attempt canceled, remote initiated splice before us") case class TooManySmallHtlcs (override val channelId: ByteVector32, number: Long, below: MilliSatoshi) extends ChannelJammingException(channelId, s"too many small htlcs: $number HTLCs below $below") case class ConfidenceTooLow (override val channelId: ByteVector32, confidence: Double, occupancy: Double) extends ChannelJammingException(channelId, s"confidence too low: confidence=$confidence occupancy=$occupancy") -case class MissingNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"next nonce for funding tx $fundingTxId is missing") -case class InvalidNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"next nonce for funding tx $fundingTxId is not valid") -case class MissingFundingNonce (override val channelId: ByteVector32) extends ChannelException(channelId, "missing funding nonce") -case class InvalidFundingNonce (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid funding nonce") -case class MissingShutdownNonce (override val channelId: ByteVector32) extends ChannelException(channelId, "missing shutdown nonce") +case class MissingCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is missing") +case class InvalidCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is not valid") +case class MissingFundingNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"funding nonce for funding tx $fundingTxId is missing") +case class InvalidFundingNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"funding nonce for funding tx $fundingTxId is not valid") +case class MissingClosingNonce (override val channelId: ByteVector32) extends ChannelException(channelId, "closing nonce is missing") // @formatter:on \ No newline at end of file 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 0ca53a7ea8..392683640c 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 @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, LegacySimpleTaprootChannelCommitmentFormat, SimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature} /** @@ -122,7 +122,7 @@ 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 class SimpleTaprootChannelsStagingLegacy(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType { + case class SimpleTaprootChannelsPhoenix(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, @@ -130,8 +130,8 @@ object ChannelTypes { Some(Features.SimpleTaprootChannelsPhoenix), ).flatten override def paysDirectlyToWallet: Boolean = false - override def commitmentFormat: CommitmentFormat = LegacySimpleTaprootChannelCommitmentFormat - override def toString: String = s"simple_taproot_channel_staging_legacy${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" + override def commitmentFormat: CommitmentFormat = PhoenixSimpleTaprootChannelCommitmentFormat + override def toString: String = s"simple_taproot_channel_phoenix${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" } case class SimpleTaprootChannelsStaging(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType { /** Known channel-type features */ @@ -168,10 +168,10 @@ object ChannelTypes { AnchorOutputsZeroFeeHtlcTx(zeroConf = true), AnchorOutputsZeroFeeHtlcTx(scidAlias = true), AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), - SimpleTaprootChannelsStagingLegacy(), - SimpleTaprootChannelsStagingLegacy(zeroConf = true), - SimpleTaprootChannelsStagingLegacy(scidAlias = true), - SimpleTaprootChannelsStagingLegacy(scidAlias = true, zeroConf = true), + SimpleTaprootChannelsPhoenix(), + SimpleTaprootChannelsPhoenix(zeroConf = true), + SimpleTaprootChannelsPhoenix(scidAlias = true), + SimpleTaprootChannelsPhoenix(scidAlias = true, zeroConf = true), SimpleTaprootChannelsStaging(), SimpleTaprootChannelsStaging(zeroConf = true), SimpleTaprootChannelsStaging(scidAlias = true), @@ -192,7 +192,7 @@ object ChannelTypes { if (canUse(Features.SimpleTaprootChannelsStaging)) { SimpleTaprootChannelsStaging(scidAlias, zeroConf) } else if (canUse(Features.SimpleTaprootChannelsPhoenix)) { - SimpleTaprootChannelsStagingLegacy(scidAlias, zeroConf) + SimpleTaprootChannelsPhoenix(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 46a5298139..eb0f35e60d 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 @@ -4,8 +4,8 @@ import akka.event.LoggingAdapter import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxId} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Musig2, Satoshi, SatoshiLong, Script, Transaction, TxId} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw, OnChainFeeConf} +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} import fr.acinq.eclair.channel.fsm.Channel.ChannelConf @@ -171,13 +171,14 @@ object LocalCommit { fundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitInput: InputInfo, commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): Either[ChannelException, LocalCommit] = { val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(channelParams, commitParams, commitKeys, localCommitIndex, fundingKey, remoteFundingPubKey, commitInput, commitmentFormat, spec) - val remoteCommitSigOk = commit.sigOrPartialSig match { - case ChannelSpendSignature.IndividualSignature(sig) if !commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - localCommitTx.checkRemoteSig(fundingKey.publicKey, remoteFundingPubKey, ChannelSpendSignature.IndividualSignature(sig)) - case psig: ChannelSpendSignature.PartialSignatureWithNonce if commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - val localNonce = NonceGenerator.verificationNonce(localCommitTx.input.outPoint.txid, fundingKey, localCommitIndex) - localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, remoteFundingPubKey, psig, localNonce.publicNonce) - case _ => false + val remoteCommitSigOk = commitmentFormat match { + case _: SegwitV0CommitmentFormat => localCommitTx.checkRemoteSig(fundingKey.publicKey, remoteFundingPubKey, commit.signature) + case _: SimpleTaprootChannelCommitmentFormat => commit.sigOrPartialSig match { + case _: IndividualSignature => false + case remoteSig: PartialSignatureWithNonce => + val localNonce = NonceGenerator.verificationNonce(fundingTxId, fundingKey, remoteFundingPubKey, localCommitIndex) + localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, remoteFundingPubKey, remoteSig, localNonce.publicNonce) + } } if (!remoteCommitSigOk) { return Left(InvalidCommitmentSignature(channelParams.channelId, fundingTxId, localCommitIndex, localCommitTx.tx)) @@ -207,15 +208,17 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txId: TxId, remotePer val htlcSigs = sortedHtlcTxs.map(_.localSig(commitKeys)) commitmentFormat match { case _: SegwitV0CommitmentFormat => - val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig + val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey) Right(CommitSig(channelParams.channelId, sig, htlcSigs.toList)) - case _: SimpleTaprootChannelCommitmentFormat if remoteNonce_opt.isEmpty => - Left(MissingNonce(channelParams.channelId, commitInput.outPoint.txid)) case _: SimpleTaprootChannelCommitmentFormat => - val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey) - remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, Map.empty, localNonce, Seq(localNonce.publicNonce, remoteNonce_opt.get)) match { - case Left(t) => Left(InvalidNonce(channelParams.channelId, commitInput.outPoint.txid)) - case Right(psig) => Right(CommitSig(channelParams.channelId, ByteVector64.Zeroes, htlcSigs.toList, TlvStream[CommitSigTlv](CommitSigTlv.PartialSignatureWithNonceTlv(psig)))) + remoteNonce_opt match { + case Some(remoteNonce) => + val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey, remoteFundingPubKey, commitInput.outPoint.txid) + remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => Left(InvalidCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index)) + case Right(psig) => Right(CommitSig(channelParams.channelId, psig, htlcSigs.toList, batchSize = 1)) + } + case None => Left(MissingCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index)) } } } @@ -660,34 +663,23 @@ case class Commitment(fundingTxIndex: Long, val fundingKey = localFundingKey(channelKeys) val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(params, remoteCommitParams, commitKeys, remoteCommit.index + 1, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commitmentFormat, spec) val htlcSigs = htlcTxs.sortBy(_.input.outPoint.index).map(_.localSig(commitKeys)) - // NB: IN/OUT htlcs are inverted because this is the remote commit log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(",")) Metrics.recordHtlcsInFlight(spec, remoteCommit.spec) - val partialSig: Option[CommitSigTlv] = commitmentFormat match { + val sig = commitmentFormat match { + case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, remoteFundingPubKey) case _: SimpleTaprootChannelCommitmentFormat => - val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey) - if (nextRemoteNonce_opt.isEmpty) - return Left(MissingNonce(params.channelId, remoteCommitTx.input.outPoint.txid)) - val Some(remoteNonce) = nextRemoteNonce_opt - val psig = remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, Map.empty, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { - case Left(t) => return Left(InvalidNonce(params.channelId, remoteCommitTx.input.outPoint.txid)) - case Right(psig) => psig + nextRemoteNonce_opt match { + case Some(remoteNonce) => + val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey, remoteFundingPubKey, fundingTxId) + remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => return Left(InvalidCommitNonce(params.channelId, fundingTxId, remoteCommit.index + 1)) + case Right(psig) => psig + } + case None => return Left(MissingCommitNonce(params.channelId, fundingTxId, remoteCommit.index + 1)) } - log.debug(s"sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with fundingTxIndex = $fundingTxIndex remoteCommit.index (should add +1) = ${remoteCommit.index} remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint") - Some(CommitSigTlv.PartialSignatureWithNonceTlv(psig)) - case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => - None - } - val sig = commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => ByteVector64.Zeroes - case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, remoteFundingPubKey).sig } - val tlvs = Set( - if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None, - partialSig - ).flatten[CommitSigTlv] - val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(tlvs)) + val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, batchSize) val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)) Right((copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig)) } @@ -711,36 +703,26 @@ case class Commitment(fundingTxIndex: Long, } /** Return a fully signed commit tx, that can be published as-is. */ - def fullySignedLocalCommitTx(params: ChannelParams, channelKeys: ChannelKeys): Either[ChannelException, Transaction] = { + def fullySignedLocalCommitTx(params: ChannelParams, channelKeys: ChannelKeys): Transaction = { val fundingKey = localFundingKey(channelKeys) val commitKeys = localKeys(params, channelKeys) val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, localCommitParams, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput(fundingKey), commitmentFormat, localCommit.spec) localCommit.remoteSig match { - case remoteSig: ChannelSpendSignature.IndividualSignature if !this.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => + case remoteSig: IndividualSignature => val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubKey) - Right(unsignedCommitTx.aggregateSigs(fundingKey.publicKey, remoteFundingPubKey, localSig, remoteSig)) - case ChannelSpendSignature.PartialSignatureWithNonce(remotePsig, remoteNonce) if this.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - val fundingTxId = if (this.fundingTxIndex == 0 && localCommit.index == 0 && !params.channelFeatures.hasFeature(Features.DualFunding)) { - TxId(ByteVector32.Zeroes) // special case because for channel establishment v1 we exchange the first nonce before the funding tx id is known + unsignedCommitTx.aggregateSigs(fundingKey.publicKey, remoteFundingPubKey, localSig, remoteSig) + case remoteSig: PartialSignatureWithNonce => + val localNonce = if (fundingTxIndex == 0 && localCommit.index == 0 && !params.channelFeatures.hasFeature(Features.DualFunding)) { + // With channel establishment v1, we exchange the first nonce before the funding tx and remote funding key are known. + NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, localCommit.index) } else { - unsignedCommitTx.input.outPoint.txid + NonceGenerator.verificationNonce(fundingTxId, fundingKey, remoteFundingPubKey, localCommit.index) } - val localNonce = NonceGenerator.verificationNonce(fundingTxId, fundingKey, localCommit.index) - (for { - partialSig <- unsignedCommitTx.partialSign(fundingKey, remoteFundingPubKey, Map.empty, localNonce, Seq(localNonce.publicNonce, remoteNonce)) - inputIndex = unsignedCommitTx.tx.txIn.indexWhere(_.outPoint == unsignedCommitTx.input.outPoint) - aggSig <- Musig2.aggregateTaprootSignatures( - Seq(partialSig.partialSig, remotePsig), - unsignedCommitTx.tx, inputIndex, Seq(unsignedCommitTx.input.txOut), - Scripts.sort(Seq(fundingKey.publicKey, remoteFundingPubKey)), - Seq(localNonce.publicNonce, remoteNonce), - None) - signedCommitTx = unsignedCommitTx.copy(tx = unsignedCommitTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggSig))) - } yield signedCommitTx.tx) match { - case Left(t) => Left(InvalidCommitmentSignature(params.channelId, unsignedCommitTx.input.outPoint.txid, localCommit.index, unsignedCommitTx.tx)) - case Right(tx) => Right(tx) - } - case _ => Left(InvalidCommitmentSignature(params.channelId, unsignedCommitTx.input.outPoint.txid, localCommit.index, unsignedCommitTx.tx)) + // We have already validated the remote nonce and partial signature when we received it, so we're guaranteed + // that the following code cannot produce an error. + val Right(localSig) = unsignedCommitTx.partialSign(fundingKey, remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteSig.nonce)) + val Right(signedTx) = unsignedCommitTx.aggregateSigs(fundingKey.publicKey, remoteFundingPubKey, localSig, remoteSig) + signedTx } } @@ -828,7 +810,7 @@ case class FullCommitment(channelParams: ChannelParams, changes: CommitmentChang def remoteChannelReserve: Satoshi = commitment.remoteChannelReserve(channelParams) - def fullySignedLocalCommitTx(channelKeys: ChannelKeys): Either[ChannelException, Transaction] = commitment.fullySignedLocalCommitTx(channelParams, channelKeys) + def fullySignedLocalCommitTx(channelKeys: ChannelKeys): Transaction = commitment.fullySignedLocalCommitTx(channelParams, channelKeys) def htlcTxs(channelKeys: ChannelKeys): Seq[(UnsignedHtlcTx, ByteVector64)] = commitment.htlcTxs(channelParams, channelKeys) @@ -1100,19 +1082,13 @@ case class Commitments(channelParams: ChannelParams, } } - def sendCommit(channelKeys: ChannelKeys, nextRemoteNonces: Map[TxId, IndividualNonce] = Map.empty)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, CommitSigs)] = { + def sendCommit(channelKeys: ChannelKeys, nextRemoteCommitNonces: Map[TxId, IndividualNonce])(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, CommitSigs)] = { remoteNextCommitInfo match { case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId)) case Right(remoteNextPerCommitmentPoint) => - - def remoteNonce(c: Commitment) = c.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => nextRemoteNonces.get(c.fundingTxId) - case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => None - } - val (active1, sigs) = active.map(c => { val commitKeys = RemoteCommitmentKeys(channelParams, channelKeys, remoteNextPerCommitmentPoint, c.commitmentFormat) - c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, remoteNonce(c)) match { + c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, nextRemoteCommitNonces.get(c.fundingTxId)) match { case Left(e) => return Left(e) case Right((c, cs)) => (c, cs) } @@ -1151,26 +1127,17 @@ case class Commitments(channelParams: ChannelParams, // we will send our revocation preimage + our next revocation hash val localPerCommitmentSecret = channelKeys.commitmentSecret(localCommitIndex) val localNextPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex + 2) - val nonces = this.active.collect { - case c: Commitment if c.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - val fundingKey = channelKeys.fundingKey(c.fundingTxIndex) - val n = NonceGenerator.verificationNonce(c.fundingTxId, fundingKey, localCommitIndex + 2).publicNonce - log.debug(s"revokeandack: creating verification nonce $n fundingIndex = ${c.fundingTxIndex} commit index = ${localCommitIndex + 2}") - c.fundingTxId -> n - } - val nextLocalNonce = if (latest.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat]) - Some(RevokeAndAckTlv.NextLocalNonceTlv(NonceGenerator.verificationNonce(latest.fundingTxId, channelKeys.fundingKey(latest.fundingTxIndex), localCommitIndex + 2).publicNonce)) - else None - - val tlvStream: TlvStream[RevokeAndAckTlv] = TlvStream( - Set(if (nonces.nonEmpty) Some(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.toList)) else None, nextLocalNonce).flatten[RevokeAndAckTlv] - ) - + val localCommitNonces = active.flatMap(c => c.commitmentFormat match { + case _: SegwitV0CommitmentFormat => None + case _: SimpleTaprootChannelCommitmentFormat => + val localNonce = NonceGenerator.verificationNonce(c.fundingTxId, c.localFundingKey(channelKeys), c.remoteFundingPubKey, localCommitIndex + 2) + Some(c.fundingTxId -> localNonce.publicNonce) + }) val revocation = RevokeAndAck( channelId = channelId, perCommitmentSecret = localPerCommitmentSecret, nextPerCommitmentPoint = localNextPerCommitmentPoint, - tlvStream + nextCommitNonces = localCommitNonces, ) val commitments1 = copy( changes = changes.copy( @@ -1187,8 +1154,9 @@ case class Commitments(channelParams: ChannelParams, remoteNextCommitInfo match { case Right(_) => Left(UnexpectedRevocation(channelId)) case Left(_) if revocation.perCommitmentSecret.publicKey != active.head.remoteCommit.remotePerCommitmentPoint => Left(InvalidRevocation(channelId)) - case Left(_) if revocation.nexLocalNonce_opt.isEmpty && this.active.exists(c => c.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] && !revocation.nexLocalNonces.contains(c.fundingTxId)) => - Left(MissingNonce(channelId, this.latest.fundingTxId)) + case Left(_) if active.exists(c => c.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !revocation.nextCommitNonces.contains(c.fundingTxId)) => + val missingNonce = active.find(c => c.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !revocation.nextCommitNonces.contains(c.fundingTxId)).get + Left(MissingCommitNonce(channelId, missingNonce.fundingTxId, remoteCommitIndex + 1)) 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 { @@ -1289,12 +1257,6 @@ case class Commitments(channelParams: ChannelParams, } } - /** This function should be used to ignore a commit_sig that we've already received. */ - def ignoreRetransmittedCommitSig(channelKeys: ChannelKeys, commitSig: CommitSig): Boolean = { - val isLatestSig = false // TODO ?? - channelParams.channelFeatures.hasFeature(Features.DualFunding) && isLatestSig - } - def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = { all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.localSigs_opt) } 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 fbcd54efef..a1643e37cd 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 @@ -17,24 +17,24 @@ package fr.acinq.eclair.channel import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainPubkeyCache import fr.acinq.eclair.blockchain.fee._ +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL -import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain} +import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys} +import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain} import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ -import fr.acinq.eclair.wire.protocol.ChannelTlv.NextLocalNonceTlv import fr.acinq.eclair.wire.protocol._ import scodec.bits.ByteVector @@ -135,7 +135,7 @@ object Helpers { val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) channelType.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => if (open.nexLocalNonce_opt.isEmpty) return Left(MissingNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes))) + case _: SimpleTaprootChannelCommitmentFormat => if (open.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0)) case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => () } @@ -246,7 +246,7 @@ object Helpers { val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) channelType.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => if (accept.nexLocalNonce_opt.isEmpty) return Left(MissingNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes))) + case _: SimpleTaprootChannelCommitmentFormat => if (accept.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0)) case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => () } extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) @@ -291,10 +291,6 @@ object Helpers { } } - def hasMissingNonce(channelReestablish: ChannelReestablish, commitments: Commitments): Option[TxId] = { - commitments.active.find(c => c.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] && !channelReestablish.nextLocalNonces.contains(c.fundingTxId)).map(_.fundingTxId) - } - /** * @param remoteFeeratePerKw remote fee rate per kiloweight * @return true if the remote fee rate is too small @@ -551,18 +547,18 @@ object Helpers { // they just sent a new commit_sig, we have received it but they didn't receive our revocation val localPerCommitmentSecret = channelKeys.commitmentSecret(commitments.localCommitIndex - 1) val localNextPerCommitmentPoint = channelKeys.commitmentPoint(commitments.localCommitIndex + 1) - val nonces = commitments.active.collect { - case c: Commitment if c.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => + val localCommitNonces = commitments.active.flatMap(c => c.commitmentFormat match { + case _: SegwitV0CommitmentFormat => None + case _: SimpleTaprootChannelCommitmentFormat => val fundingKey = channelKeys.fundingKey(c.fundingTxIndex) - val n = NonceGenerator.verificationNonce(c.fundingTxId, fundingKey, commitments.localCommitIndex + 1).publicNonce - c.fundingTxId -> n - } - val tlvStream: TlvStream[RevokeAndAckTlv] = if (nonces.nonEmpty) TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.toList)) else TlvStream.empty + val n = NonceGenerator.verificationNonce(c.fundingTxId, fundingKey, c.remoteFundingPubKey, commitments.localCommitIndex + 1).publicNonce + Some(c.fundingTxId -> n) + }) val revocation = RevokeAndAck( channelId = commitments.channelId, perCommitmentSecret = localPerCommitmentSecret, nextPerCommitmentPoint = localNextPerCommitmentPoint, - tlvStream + nextCommitNonces = localCommitNonces, ) checkRemoteCommit(remoteChannelReestablish, retransmitRevocation_opt = Some(revocation)) } else if (commitments.localCommitIndex > remoteChannelReestablish.nextRemoteRevocationNumber + 1) { @@ -586,6 +582,17 @@ object Helpers { } } + def checkCommitNonces(channelReestablish: ChannelReestablish, commitments: Commitments, pendingSig_opt: Option[InteractiveTxSigningSession.WaitingForSigs]): Option[ChannelException] = { + pendingSig_opt match { + case Some(pendingSig) if pendingSig.fundingParams.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !channelReestablish.nextCommitNonces.contains(pendingSig.fundingTxId) => + Some(MissingCommitNonce(commitments.channelId, pendingSig.fundingTxId, commitments.remoteCommitIndex + 1)) + case _ => + commitments.active + .find(c => c.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && !channelReestablish.nextCommitNonces.contains(c.fundingTxId)) + .map(c => MissingCommitNonce(commitments.channelId, c.fundingTxId, commitments.remoteCommitIndex + 1)) + } + } + } object Closing { @@ -697,7 +704,7 @@ object Helpers { // this is just to estimate the weight, it depends on size of the pubkey scripts val dummyClosingTx = ClosingTx.createUnsignedTx(commitment.commitInput(channelKeys), localScriptPubkey, remoteScriptPubkey, commitment.localChannelParams.paysClosingFees, 0 sat, 0 sat, commitment.localCommit.spec) val dummyPubkey = commitment.remoteFundingPubKey - val dummySig = ChannelSpendSignature.IndividualSignature(Transactions.PlaceHolderSig) + val dummySig = IndividualSignature(Transactions.PlaceHolderSig) val closingWeight = dummyClosingTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig).weight() log.info(s"using feerates=$feerates for initial closing tx") feerates.computeFees(closingWeight) @@ -742,8 +749,8 @@ object Helpers { val (closingTx, closingSigned) = makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee)) if (checkClosingDustAmounts(closingTx)) { val fundingPubkey = channelKeys.fundingKey(commitment.fundingTxIndex).publicKey - if (closingTx.checkRemoteSig(fundingPubkey, commitment.remoteFundingPubKey, ChannelSpendSignature.IndividualSignature(remoteClosingSig))) { - val signedTx = closingTx.aggregateSigs(fundingPubkey, commitment.remoteFundingPubKey, ChannelSpendSignature.IndividualSignature(closingSigned.signature), ChannelSpendSignature.IndividualSignature(remoteClosingSig)) + if (closingTx.checkRemoteSig(fundingPubkey, commitment.remoteFundingPubKey, IndividualSignature(remoteClosingSig))) { + val signedTx = closingTx.aggregateSigs(fundingPubkey, commitment.remoteFundingPubKey, IndividualSignature(closingSigned.signature), IndividualSignature(remoteClosingSig)) Right(closingTx.copy(tx = signedTx), closingSigned) } else { Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) @@ -753,10 +760,8 @@ object Helpers { } } - case class ClosingCompleteNonces(closerAndCloseeOutputsNonce: Option[LocalNonce], closerOutputOnlyNonce: Option[LocalNonce], closeeOutputOnlyNonce: Option[LocalNonce]) - /** We are the closer: we sign closing transactions for which we pay the fees. */ - def makeSimpleClosingTx(currentBlockHeight: BlockHeight, channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw, remoteClosingNonce_opt: Option[IndividualNonce] = None): Either[ChannelException, (ClosingTxs, ClosingComplete, ClosingCompleteNonces)] = { + def makeSimpleClosingTx(currentBlockHeight: BlockHeight, channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw, remoteNonce_opt: Option[IndividualNonce]): Either[ChannelException, (ClosingTxs, ClosingComplete, CloserNonces)] = { // We must convert the feerate to a fee: we must build dummy transactions to compute their weight. val commitInput = commitment.commitInput(channelKeys) val closingFee = { @@ -766,12 +771,11 @@ object Helpers { commitment.commitmentFormat match { case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => val dummyPubkey = commitment.remoteFundingPubKey - val dummySig = ChannelSpendSignature.IndividualSignature(Transactions.PlaceHolderSig) + val dummySig = IndividualSignature(Transactions.PlaceHolderSig) val dummySignedTx = dummyTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig) SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight())) case _: SimpleTaprootChannelCommitmentFormat => - val dummySig = ChannelSpendSignature.IndividualSignature(Transactions.PlaceHolderSig) - val dummySignedTx = dummyTx.tx.updateWitness(dummyTx.inputIndex, Script.witnessKeyPathPay2tr(dummySig.sig)) + val dummySignedTx = dummyTx.tx.updateWitness(dummyTx.inputIndex, Script.witnessKeyPathPay2tr(Transactions.PlaceHolderSig)) SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight())) } case None => return Left(CannotGenerateClosingTx(commitment.channelId)) @@ -784,38 +788,24 @@ object Helpers { case _ => return Left(CannotGenerateClosingTx(commitment.channelId)) } val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - - var closingCompleteNonces = ClosingCompleteNonces(None, None, None) + val localNonces = CloserNonces.generate(localFundingKey.publicKey, commitment.remoteFundingPubKey, commitment.fundingTxId) val tlvs: TlvStream[ClosingCompleteTlv] = commitment.commitmentFormat match { case _: SimpleTaprootChannelCommitmentFormat => - if (remoteClosingNonce_opt.isEmpty) return Left(MissingShutdownNonce(commitment.channelId)) - try { - // generate a partial signature to send to our peer, using a random signing nonce and their closing nonce - def partialSign(tx: ClosingTx, localNonce: LocalNonce): ChannelSpendSignature.PartialSignatureWithNonce = { - val Right(psig) = tx.partialSign(localFundingKey, commitment.remoteFundingPubKey, Map.empty, localNonce, Seq(localNonce.publicNonce, remoteClosingNonce_opt.get)) - psig - } + remoteNonce_opt match { + case None => return Left(MissingClosingNonce(commitment.channelId)) + case Some(remoteNonce) => + // If we cannot create our partial signature for one of our closing txs, we just skip it. + // It will only happen if our peer sent an invalid nonce, in which case we cannot do anything anyway + // apart from eventually force-closing. + def localSig(tx: ClosingTx, localNonce: LocalNonce): Option[PartialSignatureWithNonce] = { + tx.partialSign(localFundingKey, commitment.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)).toOption + } - TlvStream(Set( - closingTxs.localAndRemote_opt.map(tx => { - val localNonce = NonceGenerator.signingNonce(localFundingKey.publicKey) - closingCompleteNonces = closingCompleteNonces.copy(closerAndCloseeOutputsNonce = Some(localNonce)) - ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature(partialSign(tx, localNonce)) - }), - closingTxs.localOnly_opt.map(tx => { - val localNonce = NonceGenerator.signingNonce(localFundingKey.publicKey) - closingCompleteNonces = closingCompleteNonces.copy(closerOutputOnlyNonce = Some(localNonce)) - ClosingCompleteTlv.CloserOutputOnlyPartialSignature(partialSign(tx, localNonce)) - }), - closingTxs.remoteOnly_opt.map(tx => { - val localNonce = NonceGenerator.signingNonce(localFundingKey.publicKey) - closingCompleteNonces = closingCompleteNonces.copy(closeeOutputOnlyNonce = Some(localNonce)) - ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(partialSign(tx, localNonce)) - }), - ).flatten[ClosingCompleteTlv]) - } - catch { - case _: Throwable => return Left(CannotGenerateClosingTx(commitment.channelId)) + TlvStream(Set( + closingTxs.localAndRemote_opt.flatMap(tx => localSig(tx, localNonces.localAndRemote)).map(ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature(_)), + closingTxs.localOnly_opt.flatMap(tx => localSig(tx, localNonces.localOnly)).map(ClosingCompleteTlv.CloserOutputOnlyPartialSignature(_)), + closingTxs.remoteOnly_opt.flatMap(tx => localSig(tx, localNonces.remoteOnly)).map(ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(_)), + ).flatten[ClosingCompleteTlv]) } case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => TlvStream(Set( closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)), @@ -823,9 +813,8 @@ object Helpers { closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)), ).flatten[ClosingCompleteTlv]) } - val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, currentBlockHeight.toLong, tlvs) - Right(closingTxs, closingComplete, closingCompleteNonces) + Right(closingTxs, closingComplete, localNonces) } /** @@ -834,43 +823,44 @@ object Helpers { * Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that they * are not using our latest script (race condition between our closing_complete and theirs). */ - def signSimpleClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete, localClosingNonce_opt: Option[LocalNonce]): Either[ChannelException, (ClosingTx, ClosingSig, Option[LocalNonce])] = { + def signSimpleClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete, localNonce_opt: Option[LocalNonce]): Either[ChannelException, (ClosingTx, ClosingSig, Option[LocalNonce])] = { val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees) val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput(channelKeys), commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) // If our output isn't dust, they must provide a signature for a transaction that includes it. // Note that we're the closee, so we look for signatures including the closee output. - commitment.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat if localClosingNonce_opt.isEmpty => Left(MissingShutdownNonce(commitment.channelId)) - case _: SimpleTaprootChannelCommitmentFormat => - (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { - case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty && closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) - case (Some(_), None) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) - case (None, Some(_)) if closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) - case _ => () - } - // We choose the closing signature that matches our preferred closing transaction. - val closingTxsWithSigs = Seq( - closingComplete.closerAndCloseeOutputsPartialSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloserAndCloseeOutputsPartialSignature(localSig)))), - closingComplete.closeeOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloserOutputOnlyPartialSignature(localSig)))), - closingComplete.closerOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloserOutputOnlyPartialSignature(localSig)))), - ).flatten - closingTxsWithSigs.headOption match { - case Some((closingTx, remoteSig, sigToTlv)) => - val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - (for { - // generate a local partial signature using our closing nonce (the one we sent to our peer in our Shutdown message) - localSig <- closingTx.partialSign(localFundingKey, commitment.remoteFundingPubKey, Map.empty, localClosingNonce_opt.get, Seq(localClosingNonce_opt.get.publicNonce, remoteSig.nonce)) - tx <- closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig, Map.empty) - } yield (closingTx.copy(tx = tx), localSig)) match { - case Right((signedClosingTx, localSig)) if signedClosingTx.validate(Map.empty) => - val nextClosingNonce = NonceGenerator.signingNonce(localFundingKey.publicKey) - Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig.partialSig), ClosingSigTlv.NextCloseeNonce(nextClosingNonce.publicNonce))), Some(nextClosingNonce)) - case _ => - Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) - } - case None => Left(MissingCloseSignature(commitment.channelId)) - } + case _: SimpleTaprootChannelCommitmentFormat => localNonce_opt match { + case None => Left(MissingClosingNonce(commitment.channelId)) + case Some(localNonce) => + (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { + case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty && closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (Some(_), None) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (None, Some(_)) if closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case _ => () + } + // We choose the closing signature that matches our preferred closing transaction. + val closingTxsWithSigs = Seq( + closingComplete.closerAndCloseeOutputsPartialSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloserAndCloseeOutputsPartialSignature(localSig)))), + closingComplete.closeeOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloseeOutputOnlyPartialSignature(localSig)))), + closingComplete.closerOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingSigTlv.CloserOutputOnlyPartialSignature(localSig)))), + ).flatten + closingTxsWithSigs.headOption match { + case Some((closingTx, remoteSig, sigToTlv)) => + val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val signedClosingTx_opt = for { + localSig <- closingTx.partialSign(localFundingKey, commitment.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteSig.nonce)).toOption + signedTx <- closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig).toOption + } yield (closingTx.copy(tx = signedTx), localSig.partialSig) + signedClosingTx_opt match { + case Some((signedClosingTx, localSig)) if signedClosingTx.validate(extraUtxos = Map.empty) => + val nextLocalNonce = NonceGenerator.signingNonce(localFundingKey.publicKey, commitment.remoteFundingPubKey, commitment.fundingTxId) + val tlvs = TlvStream[ClosingSigTlv](sigToTlv(localSig), ClosingSigTlv.NextCloseeNonce(nextLocalNonce.publicNonce)) + Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, tlvs), Some(nextLocalNonce)) + case _ => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + } + case None => Left(MissingCloseSignature(commitment.channelId)) + } + } case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) @@ -888,7 +878,7 @@ object Helpers { case Some((closingTx, remoteSig, sigToTlv)) => val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubKey) - val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig)) + val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, IndividualSignature(remoteSig)) val signedClosingTx = closingTx.copy(tx = signedTx) if (signedClosingTx.validate(extraUtxos = Map.empty)) { Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig.sig))), None) @@ -907,50 +897,40 @@ object Helpers { * sent another closing_complete before receiving their closing_sig, which is now obsolete: we ignore it and wait * for their next closing_sig that will match our latest closing_complete. */ - def receiveSimpleClosingSig(channelKeys: ChannelKeys, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig, remoteNonce_opt: Option[IndividualNonce], closingCompleteNonces: ClosingCompleteNonces): Either[ChannelException, ClosingTx] = { - val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - commitment.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => - val closingTxsWithSig = Seq( - closingSig.closerAndCloseeOutputsPartialSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig, closingCompleteNonces.closerAndCloseeOutputsNonce))), - closingSig.closerOutputOnlyPartialSig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig, closingCompleteNonces.closerOutputOnlyNonce))), - closingSig.closeeOutputOnlyPartialSig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig, closingCompleteNonces.closeeOutputOnlyNonce))), - ).flatten - closingTxsWithSig.headOption match { - case Some((closingTx, remoteSig, localNonce)) if !closingTx.checkRemotePartialSignature( - localFundingKey.publicKey, commitment.remoteFundingPubKey, - ChannelSpendSignature.PartialSignatureWithNonce(remoteSig, remoteNonce_opt.get), localNonce.get.publicNonce) => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) - case Some((closingTx, remoteSig, localNonce)) => - (for { - localSig <- closingTx.partialSign(localFundingKey, commitment.remoteFundingPubKey, Map.empty, localNonce.get, Seq(localNonce.get.publicNonce, remoteNonce_opt.get)) - tx <- closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, ChannelSpendSignature.PartialSignatureWithNonce(remoteSig, remoteNonce_opt.get), Map.empty) - signedClosingTx = closingTx.copy(tx = tx) - } yield signedClosingTx) match { - case Left(_) => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) - case Right(signedClosing) if !signedClosing.validate(Map.empty) => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) - case Right(signedClosing) => Right(signedClosing) - } - case None => Left(MissingCloseSignature(commitment.channelId)) - } - case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => - val closingTxsWithSig = Seq( - closingSig.closerAndCloseeOutputsSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))), - closingSig.closerOutputOnlySig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))), - closingSig.closeeOutputOnlySig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))), - ).flatten - closingTxsWithSig.headOption match { - case Some((closingTx, remoteSig)) => - val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + def receiveSimpleClosingSig(channelKeys: ChannelKeys, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig, localNonces_opt: Option[CloserNonces], remoteNonce_opt: Option[IndividualNonce]): Either[ChannelException, ClosingTx] = { + val closingTxsWithSig = Seq( + closingSig.closerAndCloseeOutputsSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, IndividualSignature(sig)))), + closingSig.closerAndCloseeOutputsPartialSig_opt.flatMap(sig => remoteNonce_opt.flatMap(nonce => closingTxs.localAndRemote_opt.map(tx => (tx, PartialSignatureWithNonce(sig, nonce))))), + closingSig.closerOutputOnlySig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, IndividualSignature(sig)))), + closingSig.closerOutputOnlyPartialSig_opt.flatMap(sig => remoteNonce_opt.flatMap(nonce => closingTxs.localOnly_opt.map(tx => (tx, PartialSignatureWithNonce(sig, nonce))))), + closingSig.closeeOutputOnlySig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, IndividualSignature(sig)))), + closingSig.closeeOutputOnlyPartialSig_opt.flatMap(sig => remoteNonce_opt.flatMap(nonce => closingTxs.remoteOnly_opt.map(tx => (tx, PartialSignatureWithNonce(sig, nonce))))) + ).flatten + closingTxsWithSig.headOption match { + case Some((closingTx, remoteSig)) => + val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val signedClosingTx_opt = remoteSig match { + case remoteSig: IndividualSignature => val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubKey) - val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig)) - val signedClosingTx = closingTx.copy(tx = signedTx) - if (signedClosingTx.validate(extraUtxos = Map.empty)) { - Right(signedClosingTx) - } else { - Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) + Some(closingTx.copy(tx = signedTx)) + case remoteSig: PartialSignatureWithNonce => + val localNonce = localNonces_opt match { + case Some(localNonces) if closingTx.tx.txOut.size == 2 => localNonces.localAndRemote + case Some(localNonces) if closingTx.toLocalOutput_opt.nonEmpty => localNonces.localOnly + case Some(localNonces) => localNonces.remoteOnly + case None => return Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) } - case None => Left(MissingCloseSignature(commitment.channelId)) + for { + localSig <- closingTx.partialSign(localFundingKey, commitment.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteSig.nonce)).toOption + signedTx <- closingTx.aggregateSigs(localFundingKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig).toOption + } yield closingTx.copy(tx = signedTx) + } + signedClosingTx_opt match { + case Some(signedClosingTx) if signedClosingTx.validate(extraUtxos = Map.empty) => Right(signedClosingTx) + case _ => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) } + case None => Left(MissingCloseSignature(commitment.channelId)) } } 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 c8c0d4218d..5b42cefcb0 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 @@ -32,7 +32,6 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.Commitments.PostRevocationAction import fr.acinq.eclair.channel.Helpers.Closing.MutualClose -import fr.acinq.eclair.channel.Helpers.Closing.MutualClose.ClosingCompleteNonces import fr.acinq.eclair.channel.Helpers.Syncing.SyncResult import fr.acinq.eclair.channel.Helpers._ import fr.acinq.eclair.channel.Monitoring.Metrics.ProcessMessage @@ -51,10 +50,9 @@ import fr.acinq.eclair.io.Peer.LiquidityPurchaseSigned import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, ClosingTx, DefaultCommitmentFormat, LocalNonce, SimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ -import scodec.bits.ByteVector import scala.collection.immutable.Queue import scala.concurrent.ExecutionContext @@ -223,34 +221,14 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall import Channel._ - var remoteNextLocalNonces: Map[TxId, IndividualNonce] = Map.empty - - // closing nonces are first exchanged in shutdown messages - // to generate closing_complete, nodes use random local nonces and their peer's closing nonce - // when they receive closing_complete, nodes - // - combine the chosen received partial signature with a local partial signature created with their closing nonce to build a complete signature for their closing transaction - // - reply with a closing_sig message using their closing nonce and their peer's closing nonce - // when they receive closing_sig, nodes update their remote closing nonce and can send closing_complete again - var localClosingNonce_opt: Option[LocalNonce] = None - var remoteClosingNonce_opt: Option[IndividualNonce] = None - var closingCompleteNonces: ClosingCompleteNonces = ClosingCompleteNonces(None, None, None) - - def createLocalShutdown(channelId: ByteVector32, finalScriptPubKey: ByteVector, commitments: Commitments): Shutdown = { - val tlvs: TlvStream[ShutdownTlv] = commitments.latest.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => - val localFundingPubKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex).publicKey - localClosingNonce_opt = Some(NonceGenerator.signingNonce(localFundingPubKey)) - TlvStream(ShutdownTlv.ShutdownNonce(localClosingNonce_opt.get.publicNonce)) - case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => - TlvStream.empty - } - Shutdown(channelId, finalScriptPubKey, tlvs) - } + // Remote nonces that must be used when signing the next remote commitment transaction (one per active commitment). + var remoteNextCommitNonces: Map[TxId, IndividualNonce] = Map.empty - def setRemoteNextLocalNonces(info: String, n: Map[TxId, IndividualNonce]): Unit = { - this.remoteNextLocalNonces = n - log.debug("{} set remoteNextLocalNonces to {}", info, this.remoteNextLocalNonces) - } + // Closee nonces are first exchanged in shutdown messages, and replaced by a new nonce after each closing_sig. + var localCloseeNonce_opt: Option[LocalNonce] = None + var remoteCloseeNonce_opt: Option[IndividualNonce] = None + // Closer nonces are randomly generated when sending our closing_complete. + var localCloserNonces_opt: Option[CloserNonces] = None // we pass these to helpers classes so that they have the logging context implicit def implicitLog: akka.event.DiagnosticLoggingAdapter = diagLog @@ -655,7 +633,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall log.debug("ignoring CMD_SIGN (nothing to sign)") stay() case Right(_) => - d.commitments.sendCommit(channelKeys, this.remoteNextLocalNonces) match { + d.commitments.sendCommit(channelKeys, remoteNextCommitNonces) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String) val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit @@ -710,14 +688,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall stay() using d1 storing() sending signingSession1.localSigs calling endQuiescence(d1) } } - case (_, sig: CommitSig) if d.commitments.ignoreRetransmittedCommitSig(channelKeys, sig) => - // If our peer hasn't implemented https://github.com/lightning/bolts/pull/1214, they may retransmit commit_sig - // even though we've already received it and haven't requested a retransmission. It is safe to simply ignore - // this commit_sig while we wait for peers to correctly implemented commit_sig retransmission, at which point - // we should be able to get rid of this edge case. - // Note that the funding transaction may have confirmed while we were reconnecting. - log.info("ignoring commit_sig, we're still waiting for tx_signatures") - stay() case _ => // NB: in all other cases we process the commit_sigs normally. We could do a full pattern matching on all // splice statuses, but it would force us to handle every corner case where our peer doesn't behave correctly @@ -757,7 +727,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.receiveRevocation(revocation, nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).dustTolerance.maxExposure) match { case Right((commitments1, actions)) => cancelTimer(RevocationTimeout.toString) - setRemoteNextLocalNonces("received RevokeAndAck", revocation.nexLocalNonces) + remoteNextCommitNonces = revocation.nextCommitNonces log.debug("received a new rev, spec:\n{}", commitments1.latest.specs2String) actions.foreach { case PostRevocationAction.RelayHtlc(add) => @@ -778,7 +748,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall if (d.remoteShutdown.isDefined && !commitments1.changes.localHasUnsignedOutgoingHtlcs) { // we were waiting for our pending htlcs to be signed before replying with our local shutdown val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) - val localShutdown = createLocalShutdown(d.channelId, finalScriptPubKey, d.commitments) + val localShutdown = createShutdown(d.commitments, finalScriptPubKey) // this should always be defined, we provide a fallback for backward compat with older channels val closeStatus = d.closeStatus_opt.getOrElse(CloseStatus.NonInitiator(None)) // note: it means that we had pending htlcs to sign, therefore we go to SHUTDOWN, not to NEGOTIATING @@ -805,7 +775,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.channelParams.validateLocalShutdownScript(localScriptPubKey) match { case Left(e) => handleCommandError(e, c) case Right(localShutdownScript) => - val shutdown = createLocalShutdown(d.channelId, localShutdownScript, d.commitments) + val shutdown = createShutdown(d.commitments, localShutdownScript) handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown), closeStatus_opt = Some(CloseStatus.Initiator(c.feerates)))) storing() sending shutdown } } @@ -831,6 +801,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // we did not send a shutdown message // there are pending signed changes => go to SHUTDOWN // there are no htlcs => go to NEGOTIATING + remoteCloseeNonce_opt = remoteShutdown.closeeNonce_opt if (d.commitments.changes.remoteHasUnsignedOutgoingHtlcs) { handleLocalError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), d, Some(remoteShutdown)) } else if (d.commitments.changes.remoteHasUnsignedOutgoingUpdateFee) { @@ -848,17 +819,14 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } // in the meantime we won't send new changes stay() using d.copy(remoteShutdown = Some(remoteShutdown), closeStatus_opt = Some(CloseStatus.NonInitiator(None))) - } else if (d.commitments.latest.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] && remoteShutdown.shutdownNonce_opt.isEmpty) { - handleLocalError(MissingShutdownNonce(d.channelId), d, Some(remoteShutdown)) + } else if (d.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] && remoteShutdown.closeeNonce_opt.isEmpty) { + handleLocalError(MissingClosingNonce(d.channelId), d, Some(remoteShutdown)) } else { - remoteClosingNonce_opt = remoteShutdown.shutdownNonce_opt - // so we don't have any unsigned outgoing changes val (localShutdown, sendList) = d.localShutdown match { - case Some(localShutdown) => - (localShutdown, Nil) + case Some(localShutdown) => (localShutdown, Nil) case None => - val localShutdown = createLocalShutdown(d.channelId, getOrGenerateFinalScriptPubKey(d), d.commitments) + val localShutdown = createShutdown(d.commitments, getOrGenerateFinalScriptPubKey(d)) // we need to send our shutdown if we didn't previously (localShutdown, localShutdown :: Nil) } @@ -873,8 +841,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // there are no pending signed changes, let's directly negotiate a closing transaction if (Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose)) { val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus) - closingComplete_opt.foreach { case (_, closingCompleteNonces) => this.closingCompleteNonces = closingCompleteNonces } - goto(NEGOTIATING_SIMPLE) using d1 storing() sending sendList ++ closingComplete_opt.map(_._1).toSeq + goto(NEGOTIATING_SIMPLE) using d1 storing() sending sendList ++ closingComplete_opt.toSeq } else if (d.commitments.localChannelParams.paysClosingFees) { // we pay the closing fees, so we initiate the negotiation by sending the first closing_signed val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(channelKeys, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, closeStatus.feerates_opt) @@ -1136,8 +1103,17 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, t.getMessage) case Right(willFund_opt) => log.info(s"accepting splice with remote.in.amount=${msg.fundingContribution} remote.in.push=${msg.pushAmount}") - val nextCommitmentFormat_opt = msg.channelType_opt.collect { - case s: SupportedChannelType => s.commitmentFormat // TODO: validate msg.channelType_opt + // We only support updating phoenix channels to taproot: we ignore other attempts at upgrading the + // commitment format and will simply apply the previous commitment format. + val nextCommitmentFormat = msg.channelType_opt match { + case Some(channelType: ChannelTypes.SimpleTaprootChannelsPhoenix) if parentCommitment.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat => + log.info(s"accepting upgrade to $channelType during splice from commitment format ${parentCommitment.commitmentFormat}") + PhoenixSimpleTaprootChannelCommitmentFormat + case Some(channelType) => + log.info(s"rejecting upgrade to $channelType during splice from commitment format ${parentCommitment.commitmentFormat}") + parentCommitment.commitmentFormat + case _ => + parentCommitment.commitmentFormat } val spliceAck = SpliceAck(d.channelId, fundingContribution = willFund_opt.map(_.purchase.amount).getOrElse(0 sat), @@ -1146,9 +1122,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall requireConfirmedInputs = nodeParams.channelConf.requireConfirmedInputsForDualFunding, willFund_opt = willFund_opt.map(_.willFund), feeCreditUsed_opt = msg.useFeeCredit_opt, - channelType_opt = msg.channelType_opt // TODO: validate msg.channelType_opt + channelType_opt = msg.channelType_opt ) - val commitmentFormat = nextCommitmentFormat_opt.getOrElse(parentCommitment.commitmentFormat) val fundingParams = InteractiveTxParams( channelId = d.channelId, isInitiator = false, @@ -1157,7 +1132,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall sharedInput_opt = Some(SharedFundingInput(channelKeys, parentCommitment)), remoteFundingPubKey = msg.fundingPubKey, localOutputs = Nil, - commitmentFormat = commitmentFormat, + commitmentFormat = nextCommitmentFormat, lockTime = msg.lockTime, dustLimit = parentCommitment.localCommitParams.dustLimit.max(parentCommitment.remoteCommitParams.dustLimit), targetFeerate = msg.feerate, @@ -1196,10 +1171,12 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall 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 nextCommitmentFormat_opt = msg.channelType_opt.collect { - case s: SupportedChannelType => s.commitmentFormat // TODO: validate msg.channelType_opt + // We only support updating phoenix channels to taproot: we ignore other attempts at upgrading the + // commitment format and will simply apply the previous commitment format. + val nextCommitmentFormat = msg.channelType_opt match { + case Some(_: ChannelTypes.SimpleTaprootChannelsPhoenix) if parentCommitment.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat => PhoenixSimpleTaprootChannelCommitmentFormat + case _ => parentCommitment.commitmentFormat } - val nextCommitmentFormat = nextCommitmentFormat_opt.getOrElse(parentCommitment.commitmentFormat) val fundingParams = InteractiveTxParams( channelId = d.channelId, isInitiator = true, @@ -1422,9 +1399,9 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall log.info("ignoring outgoing interactive-tx message {} from previous session", msg.getClass.getSimpleName) stay() } - case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteNonce_opt) => + case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) => log.info(s"splice tx created with fundingTxIndex=${signingSession.fundingTxIndex} fundingTxId=${signingSession.fundingTx.txId}") - nextRemoteNonce_opt.foreach { case (t, n) => setRemoteNextLocalNonces("swap completed", this.remoteNextLocalNonces + (t -> n)) } + nextRemoteCommitNonce_opt.foreach { case (txId, nonce) => remoteNextCommitNonces = remoteNextCommitNonces + (txId -> nonce) } cmd_opt.foreach(cmd => cmd.replyTo ! RES_SPLICE(fundingTxIndex = signingSession.fundingTxIndex, signingSession.fundingTx.txId, signingSession.fundingParams.fundingAmount, signingSession.localCommit.fold(_.spec, _.spec).toLocal)) remoteCommitSig_opt.foreach(self ! _) liquidityPurchase_opt.collect { @@ -1676,7 +1653,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall log.debug("ignoring CMD_SIGN (nothing to sign)") stay() case Right(_) => - d.commitments.sendCommit(channelKeys) match { + d.commitments.sendCommit(channelKeys, remoteNextCommitNonces) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String) val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit @@ -1708,9 +1685,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { if (Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose)) { - val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus) - closingComplete_opt.foreach { case (_, closingCompleteNonces) => this.closingCompleteNonces = closingCompleteNonces } - goto(NEGOTIATING_SIMPLE) using d1 storing() sending revocation +: closingComplete_opt.map(_._1).toSeq + val (d1, closingComplete_opt) = startSimpleClose(commitments1, localShutdown, remoteShutdown, closeStatus) + goto(NEGOTIATING_SIMPLE) using d1 storing() sending revocation +: closingComplete_opt.toSeq } else if (d.commitments.localChannelParams.paysClosingFees) { // we pay the closing fees, so we initiate the negotiation by sending the first closing_signed val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(channelKeys, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closeStatus.feerates_opt) @@ -1735,6 +1711,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.receiveRevocation(revocation, nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).dustTolerance.maxExposure) match { case Right((commitments1, actions)) => cancelTimer(RevocationTimeout.toString) + remoteNextCommitNonces = revocation.nextCommitNonces log.debug("received a new rev, spec:\n{}", commitments1.latest.specs2String) actions.foreach { case PostRevocationAction.RelayHtlc(add) => @@ -1754,9 +1731,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String) if (Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose)) { - val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus) - closingComplete_opt.foreach { case (_, closingCompleteNonces) => this.closingCompleteNonces = closingCompleteNonces } - goto(NEGOTIATING_SIMPLE) using d1 storing() sending closingComplete_opt.map(_._1).toSeq + val (d1, closingComplete_opt) = startSimpleClose(commitments1, localShutdown, remoteShutdown, closeStatus) + goto(NEGOTIATING_SIMPLE) using d1 storing() sending closingComplete_opt.toSeq } else if (d.commitments.localChannelParams.paysClosingFees) { // we pay the closing fees, so we initiate the negotiation by sending the first closing_signed val (closingTx, closingSigned) = MutualClose.makeFirstClosingTx(channelKeys, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closeStatus.feerates_opt) @@ -1778,6 +1754,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall if (shutdown.scriptPubKey != d.remoteShutdown.scriptPubKey) { log.debug("our peer updated their shutdown script (previous={}, current={})", d.remoteShutdown.scriptPubKey, shutdown.scriptPubKey) } + remoteCloseeNonce_opt = shutdown.closeeNonce_opt stay() using d.copy(remoteShutdown = shutdown) storing() case Event(r: RevocationTimeout, d: DATA_SHUTDOWN) => handleRevocationTimeout(r, d) @@ -1788,19 +1765,20 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case Event(c: CMD_CLOSE, d: DATA_SHUTDOWN) => val useSimpleClose = Features.canUseFeature(d.commitments.localChannelParams.initFeatures, d.commitments.remoteChannelParams.initFeatures, Features.SimpleClose) - val localShutdown_opt = c.scriptPubKey match { - case Some(scriptPubKey) if scriptPubKey != d.localShutdown.scriptPubKey && useSimpleClose => Some(Shutdown(d.channelId, scriptPubKey)) + val nextScriptPubKey_opt = c.scriptPubKey match { + case Some(scriptPubKey) if scriptPubKey != d.localShutdown.scriptPubKey && useSimpleClose => Some(scriptPubKey) case _ => None } if (c.scriptPubKey.exists(_ != d.localShutdown.scriptPubKey) && !useSimpleClose) { handleCommandError(ClosingAlreadyInProgress(d.channelId), c) - } else if (localShutdown_opt.nonEmpty || c.feerates.nonEmpty) { + } else if (nextScriptPubKey_opt.nonEmpty || c.feerates.nonEmpty) { val closeStatus1 = d.closeStatus match { case initiator: CloseStatus.Initiator => initiator.copy(feerates_opt = c.feerates.orElse(initiator.feerates_opt)) case nonInitiator: CloseStatus.NonInitiator => nonInitiator.copy(feerates_opt = c.feerates.orElse(nonInitiator.feerates_opt)) // NB: this is the corner case where we can be non-initiator and have custom feerates } - val d1 = d.copy(localShutdown = localShutdown_opt.getOrElse(d.localShutdown), closeStatus = closeStatus1) - handleCommandSuccess(c, d1) storing() sending localShutdown_opt.toSeq + val shutdown = createShutdown(d.commitments, nextScriptPubKey_opt.getOrElse(d.localShutdown.scriptPubKey)) + val d1 = d.copy(localShutdown = shutdown, closeStatus = closeStatus1) + handleCommandSuccess(c, d1) storing() sending shutdown } else { handleCommandError(ClosingAlreadyInProgress(d.channelId), c) } @@ -1919,6 +1897,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall when(NEGOTIATING_SIMPLE)(handleExceptions { case Event(shutdown: Shutdown, d: DATA_NEGOTIATING_SIMPLE) => + remoteCloseeNonce_opt = shutdown.closeeNonce_opt if (shutdown.scriptPubKey != d.remoteScriptPubKey) { // This may lead to a signature mismatch: peers must use closing_complete to update their closing script. log.warning("received shutdown changing remote script, this may lead to a signature mismatch: previous={}, current={}", d.remoteScriptPubKey, shutdown.scriptPubKey) @@ -1934,11 +1913,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall val err = InvalidRbfFeerate(d.channelId, closingFeerate, d.lastClosingFeerate * 1.2) handleCommandError(err, c) } else { - MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, d.commitments.latest, localScript, d.remoteScriptPubKey, closingFeerate) match { + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, d.commitments.latest, localScript, d.remoteScriptPubKey, closingFeerate, remoteCloseeNonce_opt) match { case Left(f) => handleCommandError(f, c) - case Right((closingTxs, closingComplete, closingCompleteNonces)) => + case Right((closingTxs, closingComplete, closerNonces)) => log.debug("signing local mutual close transactions: {}", closingTxs) - this.closingCompleteNonces = closingCompleteNonces + localCloserNonces_opt = Some(closerNonces) handleCommandSuccess(c, d.copy(lastClosingFeerate = closingFeerate, localScriptPubKey = localScript, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs)) storing() sending closingComplete } } @@ -1951,13 +1930,13 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // No need to persist their latest script, they will re-sent it on reconnection. stay() using d.copy(remoteScriptPubKey = closingComplete.closerScriptPubKey) sending Warning(d.channelId, InvalidCloseeScript(d.channelId, closingComplete.closeeScriptPubKey, d.localScriptPubKey).getMessage) } else { - MutualClose.signSimpleClosingTx(channelKeys, d.commitments.latest, closingComplete.closeeScriptPubKey, closingComplete.closerScriptPubKey, closingComplete, this.localClosingNonce_opt) match { + MutualClose.signSimpleClosingTx(channelKeys, d.commitments.latest, closingComplete.closeeScriptPubKey, closingComplete.closerScriptPubKey, closingComplete, localCloseeNonce_opt) match { case Left(f) => log.warning("invalid closing_complete: {}", f.getMessage) stay() sending Warning(d.channelId, f.getMessage) - case Right((signedClosingTx, closingSig, nextClosingNonce_opt)) => + case Right((signedClosingTx, closingSig, nextCloseeNonce_opt)) => log.debug("signing remote mutual close transaction: {}", signedClosingTx.tx) - this.localClosingNonce_opt = nextClosingNonce_opt + localCloseeNonce_opt = nextCloseeNonce_opt val d1 = d.copy(remoteScriptPubKey = closingComplete.closerScriptPubKey, publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx) stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = false) sending closingSig } @@ -1967,15 +1946,15 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // Note that if we sent two closing_complete in a row, without waiting for their closing_sig for the first one, // this will fail because we only care about our latest closing_complete. This is fine, we should receive their // closing_sig for the last closing_complete afterwards. - MutualClose.receiveSimpleClosingSig(channelKeys, d.commitments.latest, d.proposedClosingTxs.last, closingSig, remoteClosingNonce_opt, closingCompleteNonces) match { + MutualClose.receiveSimpleClosingSig(channelKeys, d.commitments.latest, d.proposedClosingTxs.last, closingSig, localCloserNonces_opt, remoteCloseeNonce_opt) match { case Left(f) => log.warning("invalid closing_sig: {}", f.getMessage) + remoteCloseeNonce_opt = closingSig.nextCloseeNonce_opt stay() sending Warning(d.channelId, f.getMessage) case Right(signedClosingTx) => log.debug("received signatures for local mutual close transaction: {}", signedClosingTx.tx) val d1 = d.copy(publishedClosingTxs = d.publishedClosingTxs :+ signedClosingTx) - // update their closing nonce - this.remoteClosingNonce_opt = closingSig.nextClosingNonce_opt + remoteCloseeNonce_opt = closingSig.nextCloseeNonce_opt stay() using d1 storing() calling doPublish(signedClosingTx, localPaysClosingFees = true) } @@ -2424,14 +2403,21 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case Event(INPUT_RECONNECTED(r, localInit, remoteInit), d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => activeConnection = r val myFirstPerCommitmentPoint = channelKeys.commitmentPoint(0) - val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTx.txId)) - val myNextLocalNonce = d.signingSession.fundingParams.commitmentFormat match { + val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTxId)) + val nonceTlvs = d.signingSession.fundingParams.commitmentFormat match { + case _: SegwitV0CommitmentFormat => Set.empty case _: SimpleTaprootChannelCommitmentFormat => - val localNonce = NonceGenerator.verificationNonce(d.signingSession.fundingTx.txId, channelKeys.fundingKey(0), 1) - val currentCommitNonce = NonceGenerator.verificationNonce(d.signingSession.fundingTx.txId, channelKeys.fundingKey(0), d.signingSession.localCommitIndex).publicNonce - Set(ChannelTlv.NextLocalNoncesTlv(List(d.signingSession.fundingTx.txId -> localNonce.publicNonce)), ChannelReestablishTlv.CurrentCommitNonceTlv(currentCommitNonce)) - case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => - Set.empty + val localFundingKey = channelKeys.fundingKey(0) + val remoteFundingPubKey = d.signingSession.fundingParams.remoteFundingPubKey + val currentCommitNonce_opt = d.signingSession.localCommit match { + case Left(_) => Some(NonceGenerator.verificationNonce(d.signingSession.fundingTxId, localFundingKey, remoteFundingPubKey, 0)) + case Right(_) => None + } + val nextCommitNonce = NonceGenerator.verificationNonce(d.signingSession.fundingTxId, localFundingKey, remoteFundingPubKey, 1) + Set( + Some(ChannelReestablishTlv.NextLocalNoncesTlv(List(d.signingSession.fundingTxId -> nextCommitNonce.publicNonce))), + currentCommitNonce_opt.map(n => ChannelReestablishTlv.CurrentCommitNonceTlv(n.publicNonce)), + ).flatten[ChannelReestablishTlv] } val channelReestablish = ChannelReestablish( channelId = d.channelId, @@ -2439,7 +2425,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall nextRemoteRevocationNumber = 0, yourLastPerCommitmentSecret = PrivateKey(ByteVector32.Zeroes), myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint, - TlvStream(nextFundingTlv ++ myNextLocalNonce), + TlvStream(nextFundingTlv ++ nonceTlvs), ) val d1 = Helpers.updateFeatures(d, localInit, remoteInit) goto(SYNCING) using d1 sending channelReestablish @@ -2484,27 +2470,35 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.YourLastFundingLockedTlv(c.fundingTxId)).toSet } else Set.empty - val currentCommitNonce = collection.mutable.HashSet[ChannelReestablishTlv]() - val nonces = collection.mutable.HashMap[TxId, IndividualNonce]() - // send a nonce for each active commitment - d.commitments.active.collect { - case c: Commitment if c.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - val localFundingKey = channelKeys.fundingKey(c.fundingTxIndex) - nonces.addOne(c.fundingTxId -> NonceGenerator.verificationNonce(c.fundingTxId, localFundingKey, d.commitments.localCommitIndex + 1).publicNonce) - } - val spliceStatus = d match { - case d: DATA_NORMAL => Some(d.spliceStatus) - case _ => None - } - // if a splice was in progress, send a nonce for its signing session - spliceStatus.collect { - case w: SpliceStatus.SpliceWaitingForSigs => - val localFundingKey = channelKeys.fundingKey(w.signingSession.fundingTxIndex) - nonces.addOne(w.signingSession.fundingTx.txId -> NonceGenerator.verificationNonce(w.signingSession.fundingTx.txId, localFundingKey, w.signingSession.localCommitIndex + 1).publicNonce) - // include a nonce for the current commitment as well, which our peer may need to re-send a commit sig for our current commit tx - currentCommitNonce.add(ChannelReestablishTlv.CurrentCommitNonceTlv(NonceGenerator.verificationNonce(w.signingSession.fundingTx.txId, localFundingKey, w.signingSession.localCommitIndex).publicNonce)) + // We send our verification nonces for all active commitments. + val nextCommitNonces: Map[TxId, IndividualNonce] = d.commitments.active.flatMap(c => { + c.commitmentFormat match { + case _: SegwitV0CommitmentFormat => None + case _: SimpleTaprootChannelCommitmentFormat => + val localFundingKey = channelKeys.fundingKey(c.fundingTxIndex) + Some(c.fundingTxId -> NonceGenerator.verificationNonce(c.fundingTxId, localFundingKey, c.remoteFundingPubKey, d.commitments.localCommitIndex + 1).publicNonce) + } + }).toMap + // If an interactive-tx session hasn't been fully signed, we also need to include the corresponding nonces. + val (interactiveTxCurrentCommitNonce_opt, interactiveTxNextCommitNonce): (Option[IndividualNonce], Map[TxId, IndividualNonce]) = d match { + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match { + case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingParams.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] => + val nextCommitNonce = Map(signingSession.fundingTxId -> signingSession.nextCommitNonce(channelKeys).publicNonce) + (signingSession.currentCommitNonce_opt(channelKeys).map(_.publicNonce), nextCommitNonce) + case _ => (None, Map.empty) + } + case d: DATA_NORMAL => d.spliceStatus match { + case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingParams.commitmentFormat.isInstanceOf[TaprootCommitmentFormat] => + val nextCommitNonce = Map(signingSession.fundingTxId -> signingSession.nextCommitNonce(channelKeys).publicNonce) + (signingSession.currentCommitNonce_opt(channelKeys).map(_.publicNonce), nextCommitNonce) + case _ => (None, Map.empty) + } + case _ => (None, Map.empty) } - val myNextLocalNonces: Set[ChannelTlv.NextLocalNoncesTlv] = if (nonces.nonEmpty) Set(ChannelTlv.NextLocalNoncesTlv(nonces.toList)) else Set.empty[ChannelTlv.NextLocalNoncesTlv] + val nonceTlvs = Set( + interactiveTxCurrentCommitNonce_opt.map(nonce => ChannelReestablishTlv.CurrentCommitNonceTlv(nonce)), + if (nextCommitNonces.nonEmpty || interactiveTxNextCommitNonce.nonEmpty) Some(ChannelReestablishTlv.NextLocalNoncesTlv(nextCommitNonces.toSeq ++ interactiveTxNextCommitNonce.toSeq)) else None + ).flatten val channelReestablish = ChannelReestablish( channelId = d.channelId, @@ -2512,7 +2506,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall nextRemoteRevocationNumber = d.commitments.remoteCommitIndex, yourLastPerCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret), myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint, - tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlvs ++ myNextLocalNonces ++ currentCommitNonce.toSet) + tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlvs ++ nonceTlvs) ) // we update local/remote connection-local global/local features, we don't persist it right now val d1 = Helpers.updateFeatures(d, localInit, remoteInit) @@ -2544,281 +2538,212 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall }) when(SYNCING)(handleExceptions { - case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if Helpers.hasMissingNonce(channelReestablish, d.commitments).isDefined => - handleLocalError(MissingNonce(d.channelId, Helpers.hasMissingNonce(channelReestablish, d.commitments).get), d, Some(channelReestablish)) - - case Event(channelReestablish: ChannelReestablish, _: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) - goto(WAIT_FOR_FUNDING_CONFIRMED) - - case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) if d.signingSession.fundingParams.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] && channelReestablish.nextLocalNonces.isEmpty => - handleLocalError(MissingNonce(d.channelId, d.signingSession.fundingTx.txId), d, Some(channelReestablish)) + case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + goto(WAIT_FOR_FUNDING_CONFIRMED) + } case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => - setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) - channelReestablish.nextFundingTxId_opt match { - case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 => - // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received - // their commit_sig or their tx_signatures (depending on who must send tx_signatures first). - val fundingParams = d.signingSession.fundingParams - val remoteNonce_opt = channelReestablish.currentCommitNonce_opt // remoteNextLocalNonces.get(d.signingSession.fundingTx.txId) - d.signingSession.remoteCommit.sign(d.channelParams, d.signingSession.remoteCommitParams, channelKeys, d.signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, d.signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match { - case Left(e) => { - handleLocalError(e, d, None) - } - case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig + d.signingSession.fundingParams.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat if !channelReestablish.nextCommitNonces.contains(d.signingSession.fundingTxId) => + val f = MissingCommitNonce(d.channelId, d.signingSession.fundingTxId, commitmentNumber = 1) + handleLocalError(f, d, Some(channelReestablish)) + case _ => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + channelReestablish.nextFundingTxId_opt match { + case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 => + // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received + // their commit_sig or their tx_signatures (depending on who must send tx_signatures first). + val fundingParams = d.signingSession.fundingParams + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + d.signingSession.remoteCommit.sign(d.channelParams, d.signingSession.remoteCommitParams, channelKeys, d.signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, d.signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match { + case Left(e) => handleLocalError(e, d, Some(channelReestablish)) + case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig + } + case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) } - case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) } - case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) if Helpers.hasMissingNonce(channelReestablish, d.commitments).isDefined => - handleLocalError(MissingNonce(d.channelId, Helpers.hasMissingNonce(channelReestablish, d.commitments).get), d, Some(channelReestablish)) - case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) - - channelReestablish.nextFundingTxId_opt match { - case Some(fundingTxId) => - d.status match { - case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => - if (channelReestablish.nextLocalCommitmentNumber == 0) { - // They haven't received our commit_sig: we retransmit it. - // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. - val fundingParams = signingSession.fundingParams - signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNextLocalNonces.get(signingSession.fundingTx.txId)) match { - case Left(e) => handleLocalError(e, d, None) - case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig - } - } else { - // They have already received our commit_sig, but we were waiting for them to send either commit_sig or - // tx_signatures first. We wait for their message before sending our tx_signatures. - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) - } - case _ if d.latestFundingTx.sharedTx.txId == fundingTxId => - // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures - // and our commit_sig if they haven't received it already. - if (channelReestablish.nextLocalCommitmentNumber == 0) { - val remoteNonce_opt = channelReestablish.currentCommitNonce_opt - d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match { - case Left(e) => handleLocalError(e, d, None) - case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs) - } - } else { - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs + val pendingRbf_opt = d.status match { + // Note that we only consider RBF attempts that are also pending for our peer: otherwise it means we have + // disconnected before they sent their commit_sig, in which case they will abort the RBF attempt on reconnection. + case DualFundingStatus.RbfWaitingForSigs(signingSession) if channelReestablish.nextFundingTxId_opt.contains(signingSession.fundingTxId) => Some(signingSession) + case _ => None + } + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, pendingRbf_opt) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + channelReestablish.nextFundingTxId_opt match { + case Some(fundingTxId) => + d.status match { + case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => + if (channelReestablish.nextLocalCommitmentNumber == 0) { + // They haven't received our commit_sig: we retransmit it. + // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. + val fundingParams = signingSession.fundingParams + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match { + case Left(e) => handleLocalError(e, d, Some(channelReestablish)) + case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig + } + } else { + // They have already received our commit_sig, but we were waiting for them to send either commit_sig or + // tx_signatures first. We wait for their message before sending our tx_signatures. + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) + } + case _ if d.latestFundingTx.sharedTx.txId == fundingTxId => + // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures + // and our commit_sig if they haven't received it already. + if (channelReestablish.nextLocalCommitmentNumber == 0) { + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match { + case Left(e) => handleLocalError(e, d, Some(channelReestablish)) + case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs) + } + } else { + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs + } + case _ => + // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving + // their tx_complete): we tell them to abort that RBF attempt. + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, RbfAttemptAborted(d.channelId).getMessage) } - case _ => - // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving - // their tx_complete): we tell them to abort that RBF attempt. - goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, RbfAttemptAborted(d.channelId).getMessage) + case None => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) } - case None => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) } - case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) if d.commitments.active.exists(c => c.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] && !channelReestablish.nextLocalNonces.contains(c.fundingTxId)) => - handleLocalError(MissingNonce(d.channelId, d.commitments.active.find(c => c.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] && !channelReestablish.nextLocalNonces.contains(c.fundingTxId)).get.fundingTxId), d, Some(channelReestablish)) - case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => - setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) - val channelReady = createChannelReady(d.aliases, d.commitments) - goto(WAIT_FOR_CHANNEL_READY) sending channelReady - - case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) if Helpers.hasMissingNonce(channelReestablish, d.commitments).isDefined => - handleLocalError(MissingNonce(d.channelId, Helpers.hasMissingNonce(channelReestablish, d.commitments).get), d, Some(channelReestablish)) + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + val channelReady = createChannelReady(d.aliases, d.commitments) + goto(WAIT_FOR_CHANNEL_READY) sending channelReady + } case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => - log.debug("re-sending channel_ready") - setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) - val channelReady = createChannelReady(d.aliases, d.commitments) - // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures - // and our commit_sig if they haven't received it already. - channelReestablish.nextFundingTxId_opt match { - case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId => - d.commitments.latest.localFundingStatus.localSigs_opt match { - case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 => - log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - val remoteNonce_opt = channelReestablish.currentCommitNonce_opt - d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match { - case Left(e) => handleLocalError(e, d, None) - case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady) + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + val channelReady = createChannelReady(d.aliases, d.commitments) + // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures + // and our commit_sig if they haven't received it already. + channelReestablish.nextFundingTxId_opt match { + case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId => + d.commitments.latest.localFundingStatus.localSigs_opt match { + case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 => + log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match { + case Left(e) => handleLocalError(e, d, Some(channelReestablish)) + case Right(commitSig) => goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady) + } + case Some(txSigs) => + log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(txSigs, channelReady) + case None => + log.warning("cannot retransmit tx_signatures, we don't have them (status={})", d.commitments.latest.localFundingStatus) + goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady } - case Some(txSigs) => - log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(txSigs, channelReady) - case None => - log.warning("cannot retransmit tx_signatures, we don't have them (status={})", d.commitments.latest.localFundingStatus) - goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady + case _ => goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady } - case _ => goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady } case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => - setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) Syncing.checkSync(channelKeys, d.commitments, channelReestablish) match { case syncFailure: SyncResult.Failure => handleSyncFailure(channelReestablish, syncFailure, d) case syncSuccess: SyncResult.Success => - var sendQueue = Queue.empty[LightningMessage] // normal case, our data is up-to-date - - // re-send channel_ready and announcement_signatures if necessary - d.commitments.lastLocalLocked_opt match { - case None => () - // We only send channel_ready for initial funding transactions. - case Some(c) if c.fundingTxIndex != 0 => () - case Some(c) => - val remoteSpliceSupport = d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype) - // If our peer has not received our channel_ready, we retransmit it. - val notReceivedByRemote = remoteSpliceSupport && channelReestablish.yourLastFundingLocked_opt.isEmpty - // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node - // MUST retransmit channel_ready, otherwise it MUST NOT - val notReceivedByRemoteLegacy = !remoteSpliceSupport && channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0 - // If this is a public channel and we haven't announced the channel, we retransmit our channel_ready and - // will also send announcement_signatures. - val notAnnouncedYet = d.commitments.announceChannel && c.shortChannelId_opt.nonEmpty && d.lastAnnouncement_opt.isEmpty - if (notAnnouncedYet || notReceivedByRemote || notReceivedByRemoteLegacy) { - log.debug("re-sending channel_ready") - val nextPerCommitmentPoint = channelKeys.commitmentPoint(1) - sendQueue = sendQueue :+ ChannelReady(d.commitments.channelId, nextPerCommitmentPoint) - } - if (notAnnouncedYet) { - // The funding transaction is confirmed, so we've already sent our announcement_signatures. - // We haven't announced the channel yet, which means we haven't received our peer's announcement_signatures. - // We retransmit our announcement_signatures to let our peer know that we're ready to announce the channel. - val localAnnSigs = c.signAnnouncement(nodeParams, d.commitments.channelParams, channelKeys.fundingKey(c.fundingTxIndex)) - localAnnSigs.foreach(annSigs => { - announcementSigsSent += annSigs.shortChannelId - sendQueue = sendQueue :+ annSigs - }) - } - } - - // resume splice signing session if any - val spliceStatus1 = channelReestablish.nextFundingTxId_opt match { - case Some(fundingTxId) => - d.spliceStatus match { - case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => - if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { - // They haven't received our commit_sig: we retransmit it. - // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. - log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId) - val fundingParams = signingSession.fundingParams - signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, channelReestablish.currentCommitNonce_opt) match { - case Left(channelException: ChannelException) => throw channelException // our exception handler will call handleLocalError() which understands ChannelException and will close the channel - case Right(commitSig) => sendQueue = sendQueue :+ commitSig - } - } - d.spliceStatus - case _ if d.commitments.latest.fundingTxId == fundingTxId => - d.commitments.latest.localFundingStatus match { - case dfu: LocalFundingStatus.DualFundedUnconfirmedFundingTx => - // We've already received their commit_sig and sent our tx_signatures. We retransmit our - // tx_signatures and our commit_sig if they haven't received it already. - if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { - log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, channelReestablish.currentCommitNonce_opt) match { - case Left(channelException: ChannelException) => throw channelException // our exception handler will call handleLocalError() which understands ChannelException and will close the channel - case Right(commitSig) => sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs - } - } else { - log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - sendQueue = sendQueue :+ dfu.sharedTx.localSigs - } - case fundingStatus => - // They have not received our tx_signatures, but they must have received our commit_sig, otherwise we would be in the case above. - log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={} (already published or confirmed)", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - sendQueue = sendQueue ++ fundingStatus.localSigs_opt.toSeq - } - d.spliceStatus - case _ => - // The fundingTxId must be for a splice attempt that we didn't store (we got disconnected before receiving - // their tx_complete): we tell them to abort that splice attempt. - log.info(s"aborting obsolete splice attempt for fundingTxId=$fundingTxId") - sendQueue = sendQueue :+ TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage) - SpliceStatus.SpliceAborted - } - case None => d.spliceStatus - } + var sendQueue = Queue.empty[LightningMessage] + // We re-send channel_ready and announcement_signatures for the initial funding transaction if necessary. + val (channelReady_opt, announcementSigs_opt) = resendChannelReadyIfNeeded(channelReestablish, d) + sendQueue = sendQueue ++ channelReady_opt.toSeq ++ announcementSigs_opt.toSeq + // If we disconnected in the middle of a signing a splice transaction, we re-send our signatures or abort. + val (spliceStatus1, spliceMessages) = resumeSpliceSigningSessionIfNeeded(channelReestablish, d) + sendQueue = sendQueue ++ spliceMessages // Prune previous funding transactions and RBF attempts if we already sent splice_locked for the last funding // transaction that is also locked by our counterparty; we either missed their splice_locked or it confirmed // while disconnected. - val commitments1: Commitments = channelReestablish.myCurrentFundingLocked_opt + val commitments1 = channelReestablish.myCurrentFundingLocked_opt .flatMap(remoteFundingTxLocked => d.commitments.updateRemoteFundingStatus(remoteFundingTxLocked, d.lastAnnouncedFundingTxId_opt).toOption.map(_._1)) .getOrElse(d.commitments) // We then clean up unsigned updates that haven't been received before the disconnection. .discardUnsignedUpdates() - commitments1.lastLocalLocked_opt match { - case None => () - // We only send splice_locked for splice transactions. - case Some(c) if c.fundingTxIndex == 0 => () - case Some(c) => - // If our peer has not received our splice_locked, we retransmit it. - val notReceivedByRemote = !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId) - // If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and - // will exchange announcement_signatures afterwards. - val notAnnouncedYet = commitments1.announceChannel && d.lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId)) - if (notReceivedByRemote || notAnnouncedYet) { - // Retransmission of local announcement_signatures for splices are done when receiving splice_locked, no need - // to retransmit here. - log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId) - spliceLockedSent += (c.fundingTxId -> c.fundingTxIndex) - trimSpliceLockedSentIfNeeded() - sendQueue = sendQueue :+ SpliceLocked(d.channelId, c.fundingTxId) - } + // If there is a pending splice, we need to receive nonces for the corresponding transaction if we're using taproot. + val pendingSplice_opt = spliceStatus1 match { + // Note that we only consider splices that are also pending for our peer: otherwise it means we have disconnected + // before they sent their commit_sig, in which case they will abort the splice attempt on reconnection. + case SpliceStatus.SpliceWaitingForSigs(signingSession) if channelReestablish.nextFundingTxId_opt.contains(signingSession.fundingTxId) => Some(signingSession) + case _ => None } + Helpers.Syncing.checkCommitNonces(channelReestablish, commitments1, pendingSplice_opt) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + // We re-send our latest splice_locked if needed. + val spliceLocked_opt = resendSpliceLockedIfNeeded(channelReestablish, commitments1, d.lastAnnouncement_opt) + sendQueue = sendQueue ++ spliceLocked_opt.toSeq + // We may need to retransmit updates and/or commit_sig and/or revocation to resume the channel. + sendQueue = sendQueue ++ syncSuccess.retransmit + + commitments1.remoteNextCommitInfo match { + case Left(_) => + // we expect them to (re-)send the revocation immediately + startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout) + case _ => () + } - // we may need to retransmit updates and/or commit_sig and/or revocation - sendQueue = sendQueue ++ syncSuccess.retransmit - - commitments1.remoteNextCommitInfo match { - case Left(_) => - // we expect them to (re-)send the revocation immediately - startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout) - case _ => () - } + // do I have something to sign? + if (commitments1.changes.localHasChanges) { + self ! CMD_SIGN() + } - // do I have something to sign? - if (commitments1.changes.localHasChanges) { - self ! CMD_SIGN() - } + // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. + d.localShutdown.foreach { + localShutdown => + log.debug("re-sending local_shutdown") + sendQueue = sendQueue :+ localShutdown + } - // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. - d.localShutdown.foreach { - localShutdown => - log.debug("re-sending local_shutdown") - sendQueue = sendQueue :+ localShutdown - } + if (d.commitments.announceChannel) { + // we will re-enable the channel after some delay to prevent flappy updates in case the connection is unstable + startSingleTimer(Reconnected.toString, BroadcastChannelUpdate(Reconnected), 10 seconds) + } else { + // except for private channels where our peer is likely a mobile wallet: they will stay online only for a short period of time, + // so we need to re-enable them immediately to ensure we can route payments to them. It's also less of a problem to frequently + // refresh the channel update for private channels, since we won't broadcast it to the rest of the network. + self ! BroadcastChannelUpdate(Reconnected) + } - if (d.commitments.announceChannel) { - // we will re-enable the channel after some delay to prevent flappy updates in case the connection is unstable - startSingleTimer(Reconnected.toString, BroadcastChannelUpdate(Reconnected), 10 seconds) - } else { - // except for private channels where our peer is likely a mobile wallet: they will stay online only for a short period of time, - // so we need to re-enable them immediately to ensure we can route payments to them. It's also less of a problem to frequently - // refresh the channel update for private channels, since we won't broadcast it to the rest of the network. - self ! BroadcastChannelUpdate(Reconnected) - } + // We usually handle feerate updates once per block (~10 minutes), but when our remote is a mobile wallet that + // only briefly connects and then disconnects, we may never have the opportunity to send our `update_fee`, so + // we send it (if needed) when reconnected. + val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty + if (d.commitments.localChannelParams.paysCommitTxFees && !shutdownInProgress) { + val currentFeeratePerKw = d.commitments.latest.localCommit.spec.commitTxFeerate + val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat, d.commitments.latest.capacity) + if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) { + self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) + } + } - // We usually handle feerate updates once per block (~10 minutes), but when our remote is a mobile wallet that - // only briefly connects and then disconnects, we may never have the opportunity to send our `update_fee`, so - // we send it (if needed) when reconnected. - val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty - if (d.commitments.localChannelParams.paysCommitTxFees && !shutdownInProgress) { - val currentFeeratePerKw = d.commitments.latest.localCommit.spec.commitTxFeerate - val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat, d.commitments.latest.capacity) - if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) { - self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) - } - } + // We tell the peer that the channel is ready to process payments that may be queued. + if (!shutdownInProgress) { + val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min + peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex) + } - // We tell the peer that the channel is ready to process payments that may be queued. - if (!shutdownInProgress) { - val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min - peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex) + goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1) sending sendQueue } - - goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1) sending sendQueue } case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) @@ -2833,19 +2758,20 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => handleUpdateRelayFeeDisconnected(c, d) - case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) if Helpers.hasMissingNonce(channelReestablish, d.commitments).isDefined => - handleLocalError(MissingNonce(d.channelId, Helpers.hasMissingNonce(channelReestablish, d.commitments).get), d, Some(channelReestablish)) - case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) => - setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) - Syncing.checkSync(channelKeys, d.commitments, channelReestablish) match { - case syncFailure: SyncResult.Failure => - handleSyncFailure(channelReestablish, syncFailure, d) - case syncSuccess: SyncResult.Success => - val commitments1 = d.commitments.discardUnsignedUpdates() - val sendQueue = Queue.empty[LightningMessage] ++ syncSuccess.retransmit :+ d.localShutdown - // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. - goto(SHUTDOWN) using d.copy(commitments = commitments1) sending sendQueue + Helpers.Syncing.checkCommitNonces(channelReestablish, d.commitments, None) match { + case Some(f) => handleLocalError(f, d, Some(channelReestablish)) + case None => + remoteNextCommitNonces = channelReestablish.nextCommitNonces + Syncing.checkSync(channelKeys, d.commitments, channelReestablish) match { + case syncFailure: SyncResult.Failure => + handleSyncFailure(channelReestablish, syncFailure, d) + case syncSuccess: SyncResult.Success => + val commitments1 = d.commitments.discardUnsignedUpdates() + val sendQueue = Queue.empty[LightningMessage] ++ syncSuccess.retransmit :+ d.localShutdown + // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. + goto(SHUTDOWN) using d.copy(commitments = commitments1) sending sendQueue + } } case Event(_: ChannelReestablish, d: DATA_NEGOTIATING) => @@ -2865,7 +2791,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case Event(_: ChannelReestablish, d: DATA_NEGOTIATING_SIMPLE) => // We retransmit our shutdown: we may have updated our script and they may not have received it. - val localShutdown = Shutdown(d.channelId, d.localScriptPubKey) + val localShutdown = createShutdown(d.commitments, d.localScriptPubKey) goto(NEGOTIATING_SIMPLE) using d sending localShutdown // This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send @@ -3135,7 +3061,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } context.system.eventStream.publish(ChannelStateChanged(self, nextStateData.channelId, peer, remoteNodeId, state, nextState, commitments_opt)) } - if (nextState == CLOSED) { // channel is closed, scheduling this actor for self destruction context.system.scheduler.scheduleOnce(1 minute, self, Symbol("shutdown")) @@ -3255,12 +3180,16 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } } - /** On disconnection we clear up stashes. */ + /** On disconnection we clear up temporary mutable state that applies to the previous connection. */ onTransition { case _ -> OFFLINE => announcementSigsStash = Map.empty announcementSigsSent = Set.empty spliceLockedSent = Map.empty[TxId, Long] + remoteNextCommitNonces = Map.empty + localCloseeNonce_opt = None + remoteCloseeNonce_opt = None + localCloserNonces_opt = None } /* @@ -3478,6 +3407,117 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } } + private def resendChannelReadyIfNeeded(channelReestablish: ChannelReestablish, d: DATA_NORMAL): (Option[ChannelReady], Option[AnnouncementSignatures]) = { + d.commitments.lastLocalLocked_opt match { + case None => (None, None) + // We only send channel_ready for initial funding transactions. + case Some(c) if c.fundingTxIndex != 0 => (None, None) + case Some(c) => + val remoteSpliceSupport = d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype) + // If our peer has not received our channel_ready, we retransmit it. + val notReceivedByRemote = remoteSpliceSupport && channelReestablish.yourLastFundingLocked_opt.isEmpty + // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node + // MUST retransmit channel_ready, otherwise it MUST NOT + val notReceivedByRemoteLegacy = !remoteSpliceSupport && channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0 + // If this is a public channel and we haven't announced the channel, we retransmit our channel_ready and + // will also send announcement_signatures. + val notAnnouncedYet = d.commitments.announceChannel && c.shortChannelId_opt.nonEmpty && d.lastAnnouncement_opt.isEmpty + val channelReady_opt = if (notAnnouncedYet || notReceivedByRemote || notReceivedByRemoteLegacy) { + log.debug("re-sending channel_ready") + Some(createChannelReady(d.aliases, d.commitments)) + } else { + None + } + val announcementSigs_opt = if (notAnnouncedYet) { + // The funding transaction is confirmed, so we've already sent our announcement_signatures. + // We haven't announced the channel yet, which means we haven't received our peer's announcement_signatures. + // We retransmit our announcement_signatures to let our peer know that we're ready to announce the channel. + val localAnnSigs = c.signAnnouncement(nodeParams, d.commitments.channelParams, channelKeys.fundingKey(c.fundingTxIndex)) + localAnnSigs.foreach(annSigs => announcementSigsSent += annSigs.shortChannelId) + localAnnSigs + } else { + None + } + (channelReady_opt, announcementSigs_opt) + } + } + + private def resumeSpliceSigningSessionIfNeeded(channelReestablish: ChannelReestablish, d: DATA_NORMAL): (SpliceStatus, Queue[LightningMessage]) = { + var sendQueue = Queue.empty[LightningMessage] + val spliceStatus1 = channelReestablish.nextFundingTxId_opt match { + case Some(fundingTxId) => + d.spliceStatus match { + case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => + if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { + // They haven't received our commit_sig: we retransmit it. + // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. + log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId) + val fundingParams = signingSession.fundingParams + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + signingSession.remoteCommit.sign(d.commitments.channelParams, signingSession.remoteCommitParams, channelKeys, signingSession.fundingTxIndex, fundingParams.remoteFundingPubKey, signingSession.commitInput(channelKeys), fundingParams.commitmentFormat, remoteNonce_opt) match { + case Left(f) => sendQueue = sendQueue :+ Warning(d.channelId, f.getMessage) + case Right(commitSig) => sendQueue = sendQueue :+ commitSig + } + } + d.spliceStatus + case _ if d.commitments.latest.fundingTxId == fundingTxId => + d.commitments.latest.localFundingStatus match { + case dfu: LocalFundingStatus.DualFundedUnconfirmedFundingTx => + // We've already received their commit_sig and sent our tx_signatures. We retransmit our + // tx_signatures and our commit_sig if they haven't received it already. + if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { + log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + val remoteNonce_opt = channelReestablish.currentCommitNonce_opt + d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match { + case Left(f) => sendQueue = sendQueue :+ Warning(d.channelId, f.getMessage) + case Right(commitSig) => sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs + } + } else { + log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + sendQueue = sendQueue :+ dfu.sharedTx.localSigs + } + case fundingStatus => + // They have not received our tx_signatures, but they must have received our commit_sig, otherwise we would be in the case above. + log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={} (already published or confirmed)", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) + sendQueue = sendQueue ++ fundingStatus.localSigs_opt.toSeq + } + d.spliceStatus + case _ => + // The fundingTxId must be for a splice attempt that we didn't store (we got disconnected before receiving + // their tx_complete): we tell them to abort that splice attempt. + log.info(s"aborting obsolete splice attempt for fundingTxId=$fundingTxId") + sendQueue = sendQueue :+ TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage) + SpliceStatus.SpliceAborted + } + case None => d.spliceStatus + } + (spliceStatus1, sendQueue) + } + + private def resendSpliceLockedIfNeeded(channelReestablish: ChannelReestablish, commitments: Commitments, lastAnnouncement_opt: Option[ChannelAnnouncement]): Option[SpliceLocked] = { + commitments.lastLocalLocked_opt match { + case None => None + // We only send splice_locked for splice transactions. + case Some(c) if c.fundingTxIndex == 0 => None + case Some(c) => + // If our peer has not received our splice_locked, we retransmit it. + val notReceivedByRemote = !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId) + // If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and + // will exchange announcement_signatures afterwards. + val notAnnouncedYet = commitments.announceChannel && lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId)) + if (notReceivedByRemote || notAnnouncedYet) { + // Retransmission of local announcement_signatures for splices are done when receiving splice_locked, no need + // to retransmit here. + log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId) + spliceLockedSent += (c.fundingTxId -> c.fundingTxIndex) + trimSpliceLockedSentIfNeeded() + Some(SpliceLocked(commitments.channelId, c.fundingTxId)) + } else { + None + } + } + } + /** * Return full information about a known closing tx. */ 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 3a9fc04933..3cc1fce723 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 @@ -322,8 +322,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(msg: InteractiveTxBuilder.Response, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => msg match { case InteractiveTxBuilder.SendMessage(_, msg) => stay() sending msg - case InteractiveTxBuilder.Succeeded(status, commitSig, liquidityPurchase_opt, nextRemoteNonce_opt) => - nextRemoteNonce_opt.foreach { case (t, n) => setRemoteNextLocalNonces("dual funding completed", this.remoteNextLocalNonces + (t -> n)) } + case InteractiveTxBuilder.Succeeded(status, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) => + nextRemoteCommitNonce_opt.foreach { case (txId, nonce) => remoteNextCommitNonces = remoteNextCommitNonces + (txId -> nonce) } d.deferred.foreach(self ! _) d.replyTo_opt.foreach(_ ! OpenChannelResponse.Created(d.channelId, status.fundingTx.txId, status.fundingTx.tx.localFees.truncateToSatoshi)) liquidityPurchase_opt.collect { @@ -692,8 +692,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case DualFundingStatus.RbfInProgress(cmd_opt, _, remoteCommitSig_opt) => msg match { case InteractiveTxBuilder.SendMessage(_, msg) => stay() sending msg - case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteNonce_opt) => - nextRemoteNonce_opt.foreach { case (t, n) => setRemoteNextLocalNonces("rbf completed", this.remoteNextLocalNonces + (t -> n)) } + case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) => + nextRemoteCommitNonce_opt.foreach { case (txId, nonce) => remoteNextCommitNonces = remoteNextCommitNonces + (txId -> nonce) } cmd_opt.foreach(cmd => cmd.replyTo ! RES_BUMP_FUNDING_FEE(rbfIndex = d.previousFundingTxs.length, signingSession.fundingTx.txId, signingSession.fundingTx.tx.localFees.truncateToSatoshi)) remoteCommitSig_opt.foreach(self ! _) liquidityPurchase_opt.collect { 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 9b16472fbf..23651e6a3d 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 @@ -19,10 +19,10 @@ package fr.acinq.eclair.channel.fsm import akka.actor.Status import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.pattern.pipe -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, SatoshiLong, TxId} +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx import fr.acinq.eclair.channel._ @@ -31,10 +31,10 @@ import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys} import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain} import fr.acinq.eclair.io.Peer.OpenChannelResponse -import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat} -import fr.acinq.eclair.wire.protocol.{AcceptChannel, AcceptChannelTlv, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, OpenChannelTlv, PartialSignatureWithNonceTlv, TlvStream} -import fr.acinq.eclair.{MilliSatoshiLong, UInt64, randomKey, toLongId} +import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, AcceptChannelTlv, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, OpenChannelTlv, TlvStream} +import fr.acinq.eclair.{MilliSatoshiLong, randomKey, toLongId} import scodec.bits.ByteVector /** @@ -73,12 +73,12 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { when(WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL)(handleExceptions { case Event(input: INPUT_INIT_CHANNEL_INITIATOR, _) => - val fundingPubKey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) // 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.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) val localNonce = input.channelType.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(TxId(ByteVector32.Zeroes), channelKeys.fundingKey(fundingTxIndex = 0), 0).publicNonce) + case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce) case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => None } val open = OpenChannel( @@ -93,7 +93,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { feeratePerKw = input.commitTxFeerate, toSelfDelay = input.proposedCommitParams.toRemoteDelay, maxAcceptedHtlcs = input.proposedCommitParams.localMaxAcceptedHtlcs, - fundingPubkey = fundingPubKey, + fundingPubkey = fundingKey.publicKey, revocationBasepoint = channelKeys.revocationBasePoint, paymentBasepoint = input.localChannelParams.walletStaticPaymentBasepoint.getOrElse(channelKeys.paymentBasePoint), delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint, @@ -125,7 +125,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { htlcBasepoint = open.htlcBasepoint, initFeatures = d.initFundee.remoteInit.features, upfrontShutdownScript_opt = remoteShutdownScript) - val fundingPubkey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) val channelParams = ChannelParams(d.initFundee.temporaryChannelId, d.initFundee.channelConfig, channelFeatures, d.initFundee.localChannelParams, remoteChannelParams, open.channelFlags) val localCommitParams = CommitParams(d.initFundee.proposedCommitParams.localDustLimit, d.initFundee.proposedCommitParams.localHtlcMinimum, d.initFundee.proposedCommitParams.localMaxHtlcValueInFlight, d.initFundee.proposedCommitParams.localMaxAcceptedHtlcs, open.toSelfDelay) val remoteCommitParams = CommitParams(open.dustLimitSatoshis, open.htlcMinimumMsat, open.maxHtlcValueInFlightMsat, open.maxAcceptedHtlcs, d.initFundee.proposedCommitParams.toRemoteDelay) @@ -133,7 +133,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { // See https://github.com/lightningnetwork/lightning-rfc/pull/714. val localShutdownScript = d.initFundee.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) val localNonce = d.initFundee.channelType.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(TxId(ByteVector32.Zeroes), channelKeys.fundingKey(fundingTxIndex = 0), 0).publicNonce) + case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce) case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => None } val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, @@ -144,7 +144,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { htlcMinimumMsat = localCommitParams.htlcMinimum, toSelfDelay = remoteCommitParams.toSelfDelay, maxAcceptedHtlcs = localCommitParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubkey, + fundingPubkey = fundingKey.publicKey, revocationBasepoint = channelKeys.revocationBasePoint, paymentBasepoint = d.initFundee.localChannelParams.walletStaticPaymentBasepoint.getOrElse(channelKeys.paymentBasePoint), delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint, @@ -155,7 +155,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { Some(ChannelTlv.ChannelTypeTlv(d.initFundee.channelType)), localNonce.map(n => ChannelTlv.NextLocalNonceTlv(n)) ).flatten[AcceptChannelTlv])) - setRemoteNextLocalNonces("from their open_channel", open.nexLocalNonce_opt.map(n => Map(TxId(ByteVector32.Zeroes) -> n)).getOrElse(Map.empty)) + remoteNextCommitNonces = open.commitNonce_opt.map(n => NonceGenerator.dummyFundingTxId -> n).toMap goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(channelParams, d.initFundee.channelType, localCommitParams, remoteCommitParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.fundingPubkey, open.firstPerCommitmentPoint) sending accept } @@ -189,7 +189,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val channelParams = ChannelParams(d.initFunder.temporaryChannelId, d.initFunder.channelConfig, channelFeatures, d.initFunder.localChannelParams, remoteChannelParams, d.lastSent.channelFlags) val localCommitParams = CommitParams(d.initFunder.proposedCommitParams.localDustLimit, d.initFunder.proposedCommitParams.localHtlcMinimum, d.initFunder.proposedCommitParams.localMaxHtlcValueInFlight, d.initFunder.proposedCommitParams.localMaxAcceptedHtlcs, accept.toSelfDelay) val remoteCommitParams = CommitParams(accept.dustLimitSatoshis, accept.htlcMinimumMsat, accept.maxHtlcValueInFlightMsat, accept.maxAcceptedHtlcs, d.initFunder.proposedCommitParams.toRemoteDelay) - setRemoteNextLocalNonces("received AcceptChannel", accept.nexLocalNonce_opt.map(n => Map(TxId(ByteVector32.Zeroes) -> n)).getOrElse(Map.empty)) + remoteNextCommitNonces = accept.commitNonce_opt.map(n => NonceGenerator.dummyFundingTxId -> n).toMap goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams, d.initFunder.channelType, localCommitParams, remoteCommitParams, d.initFunder.fundingAmount, d.initFunder.pushAmount_opt.getOrElse(0 msat), d.initFunder.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo) } @@ -217,45 +217,36 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) val localCommitmentKeys = LocalCommitmentKeys(d.channelParams, channelKeys, localCommitIndex = 0, d.commitmentFormat) val remoteCommitmentKeys = RemoteCommitmentKeys(d.channelParams, channelKeys, d.remoteFirstPerCommitmentPoint, d.commitmentFormat) - Funding.makeFirstCommitTxs(d.channelParams, d.localCommitParams, d.remoteCommitParams, localFundingAmount = d.fundingAmount, remoteFundingAmount = 0 sat, localPushAmount = d.pushAmount, remotePushAmount = 0 msat, d.commitTxFeerate, d.commitmentFormat, fundingTx.txid, fundingTxOutputIndex, fundingKey, d.remoteFundingPubKey, localCommitmentKeys, remoteCommitmentKeys) match { case Left(ex) => handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => - require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") + require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!") val remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint) - // signature of their initial commitment tx that pays remote pushMsat - val fundingCreated = d.commitmentFormat match { + val localSigOfRemoteTx = d.commitmentFormat match { case _: SimpleTaprootChannelCommitmentFormat => - val fakeFundingTxId = TxId(ByteVector32.Zeroes) - val localNonce = NonceGenerator.verificationNonce(fakeFundingTxId, fundingKey, 0) - if (!remoteNextLocalNonces.contains(fakeFundingTxId)) throw MissingNonce(d.channelId, fakeFundingTxId) - val psig = remoteCommitTx.partialSign(fundingKey, d.remoteFundingPubKey, Map.empty, localNonce, Seq(localNonce.publicNonce, remoteNextLocalNonces(fakeFundingTxId))) match { - case Left(t) => throw InvalidNonce(d.channelId, fakeFundingTxId) - case Right(psig) => psig + val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0) + remoteNextCommitNonces.get(NonceGenerator.dummyFundingTxId) match { + case Some(remoteNonce) => + remoteCommitTx.partialSign(fundingKey, d.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => Left(InvalidCommitNonce(d.channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0)) + case Right(psig) => Right(psig) + } + case None => Left(MissingCommitNonce(d.channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0)) } - FundingCreated( - temporaryChannelId = temporaryChannelId, - fundingTxId = fundingTx.txid, - fundingOutputIndex = fundingTxOutputIndex, - signature = ByteVector64.Zeroes, - tlvStream = TlvStream(PartialSignatureWithNonceTlv(psig)) - ) - case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => - val localSigOfRemoteTx = remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey).sig - FundingCreated( - temporaryChannelId = temporaryChannelId, - fundingTxId = fundingTx.txid, - fundingOutputIndex = fundingTxOutputIndex, - signature = localSigOfRemoteTx - ) + case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey)) + } + localSigOfRemoteTx match { + case Left(f) => handleLocalError(f, d, None) + case Right(localSig) => + val fundingCreated = FundingCreated(temporaryChannelId, fundingTx.txid, fundingTxOutputIndex, localSig) + val channelId = toLongId(fundingTx.txid, fundingTxOutputIndex) + val channelParams1 = d.channelParams.copy(channelId = channelId) + peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) + // NB: we don't send a ChannelSignatureSent for the first commit + goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelParams1, d.channelType, d.localCommitParams, d.remoteCommitParams, d.remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, d.replyTo) sending fundingCreated } - val channelId = toLongId(fundingTx.txid, fundingTxOutputIndex) - val channelParams1 = d.channelParams.copy(channelId = channelId) - peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages - txPublisher ! SetChannelId(remoteNodeId, channelId) - context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) - // NB: we don't send a ChannelSignatureSent for the first commit - goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelParams1, d.channelType, d.localCommitParams, d.remoteCommitParams, d.remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, d.replyTo) sending fundingCreated } case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => @@ -281,70 +272,73 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(fc@FundingCreated(_, fundingTxId, fundingTxOutputIndex, remoteSig, _), d: DATA_WAIT_FOR_FUNDING_CREATED) => + case Event(fc@FundingCreated(_, fundingTxId, fundingTxOutputIndex, _, _), d: DATA_WAIT_FOR_FUNDING_CREATED) => val temporaryChannelId = d.channelParams.channelId val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) val localCommitmentKeys = LocalCommitmentKeys(d.channelParams, channelKeys, localCommitIndex = 0, d.commitmentFormat) val remoteCommitmentKeys = RemoteCommitmentKeys(d.channelParams, channelKeys, d.remoteFirstPerCommitmentPoint, d.commitmentFormat) - // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) Funding.makeFirstCommitTxs(d.channelParams, d.localCommitParams, d.remoteCommitParams, localFundingAmount = 0 sat, remoteFundingAmount = d.fundingAmount, localPushAmount = 0 msat, remotePushAmount = d.pushAmount, d.commitTxFeerate, d.commitmentFormat, fundingTxId, fundingTxOutputIndex, fundingKey, d.remoteFundingPubKey, localCommitmentKeys, remoteCommitmentKeys) match { - case Left(ex) => handleLocalError(ex, d, None) + case Left(ex) => handleLocalError(ex, d, Some(fc)) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => // check remote signature validity - val isRemoveSigValid = fc.sigOrPartialSig match { - case psig: ChannelSpendSignature.PartialSignatureWithNonce if d.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - val localNonce = NonceGenerator.verificationNonce(TxId(ByteVector32.Zeroes), fundingKey, 0) + val isRemoteSigValid = fc.sigOrPartialSig match { + case psig: PartialSignatureWithNonce => + val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0) localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, d.remoteFundingPubKey, psig, localNonce.publicNonce) - case sig: ChannelSpendSignature.IndividualSignature if !d.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => + case sig: IndividualSignature => localCommitTx.checkRemoteSig(fundingKey.publicKey, d.remoteFundingPubKey, sig) - case _ => false } - isRemoveSigValid match { - case false => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, fundingTxId, commitmentNumber = 0, localCommitTx.tx), d, None) + isRemoteSigValid match { + case false => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, fundingTxId, commitmentNumber = 0, localCommitTx.tx), d, Some(fc)) case true => val channelId = toLongId(fundingTxId, fundingTxOutputIndex) - val fundingSigned = d.commitmentFormat match { + val localSigOfRemoteTx = d.commitmentFormat match { case _: SimpleTaprootChannelCommitmentFormat => - val localNonce = NonceGenerator.verificationNonce(TxId(ByteVector32.Zeroes), fundingKey, 0) - if (!remoteNextLocalNonces.contains(TxId(ByteVector32.Zeroes))) throw MissingNonce(d.channelId, TxId(ByteVector32.Zeroes)) - val localPartialSigOfRemoteTx = remoteCommitTx.partialSign(fundingKey, d.remoteFundingPubKey, Map.empty, localNonce, Seq(localNonce.publicNonce, remoteNextLocalNonces(TxId(ByteVector32.Zeroes)))) match { - case Left(t) => throw InvalidNonce(d.channelId, TxId(ByteVector32.Zeroes)) - case Right(psig) => psig + val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0) + remoteNextCommitNonces.get(NonceGenerator.dummyFundingTxId) match { + case Some(remoteNonce) => + remoteCommitTx.partialSign(fundingKey, d.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => Left(InvalidCommitNonce(channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0)) + case Right(psig) => Right(psig) + } + case None => Left(MissingCommitNonce(channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0)) } - FundingSigned(channelId = channelId, signature = ByteVector64.Zeroes, TlvStream(PartialSignatureWithNonceTlv(localPartialSigOfRemoteTx))) - case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => - val localSigOfRemoteTx = remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey).sig - FundingSigned(channelId = channelId, signature = localSigOfRemoteTx) + case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey)) + } + localSigOfRemoteTx match { + case Left(f) => handleLocalError(f, d, Some(fc)) + case Right(localSig) => + val fundingSigned = FundingSigned(channelId, localSig) + val commitment = Commitment( + fundingTxIndex = 0, + firstRemoteCommitIndex = 0, + fundingInput = localCommitTx.input.outPoint, + fundingAmount = localCommitTx.input.txOut.amount, + remoteFundingPubKey = d.remoteFundingPubKey, + localFundingStatus = SingleFundedUnconfirmedFundingTx(None), + remoteFundingStatus = RemoteFundingStatus.NotLocked, + commitmentFormat = d.commitmentFormat, + localCommitParams = d.localCommitParams, + localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, fc.sigOrPartialSig, htlcRemoteSigs = Nil), + remoteCommitParams = d.remoteCommitParams, + remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint), + nextRemoteCommit_opt = None) + val commitments = Commitments( + channelParams = d.channelParams.copy(channelId = channelId), + changes = CommitmentChanges.init(), + active = List(commitment), + remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array + remotePerCommitmentSecrets = ShaChain.init, + originChannels = Map.empty) + peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) + context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) + // NB: we don't send a ChannelSignatureSent for the first commit + log.info("waiting for them to publish the funding tx for channelId={} fundingTxid={}", channelId, commitment.fundingTxId) + watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned } - val commitment = Commitment( - fundingTxIndex = 0, - firstRemoteCommitIndex = 0, - fundingInput = localCommitTx.input.outPoint, - fundingAmount = localCommitTx.input.txOut.amount, - remoteFundingPubKey = d.remoteFundingPubKey, - localFundingStatus = SingleFundedUnconfirmedFundingTx(None), - remoteFundingStatus = RemoteFundingStatus.NotLocked, - commitmentFormat = d.commitmentFormat, - localCommitParams = d.localCommitParams, - localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, fc.sigOrPartialSig, htlcRemoteSigs = Nil), - remoteCommitParams = d.remoteCommitParams, - remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint), - nextRemoteCommit_opt = None) - val commitments = Commitments( - channelParams = d.channelParams.copy(channelId = channelId), - changes = CommitmentChanges.init(), - active = List(commitment), - remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array - remotePerCommitmentSecrets = ShaChain.init, - originChannels = Map.empty) - peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages - txPublisher ! SetChannelId(remoteNodeId, channelId) - context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) - context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) - // NB: we don't send a ChannelSignatureSent for the first commit - log.info("waiting for them to publish the funding tx for channelId={} fundingTxid={}", channelId, commitment.fundingTxId) - watchFundingConfirmed(commitment.fundingTxId, d.channelParams.minDepth(nodeParams.channelConf.minDepth), delay_opt = None) - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned } } @@ -358,18 +352,15 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { case Event(fundingSigned: FundingSigned, d: DATA_WAIT_FOR_FUNDING_SIGNED) => // we make sure that their sig checks out and that our first commit tx is spendable - val fundingPubkey = channelKeys.fundingKey(fundingTxIndex = 0).publicKey - - val isRemoveSigValid = fundingSigned.sigOrPartialSig match { - case psig: ChannelSpendSignature.PartialSignatureWithNonce if d.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - val localNonce = NonceGenerator.verificationNonce(TxId(ByteVector32.Zeroes), channelKeys.fundingKey(fundingTxIndex = 0), 0) - d.localCommitTx.checkRemotePartialSignature(fundingPubkey, d.remoteFundingPubKey, psig, localNonce.publicNonce) - case sig: ChannelSpendSignature.IndividualSignature if !d.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - d.localCommitTx.checkRemoteSig(fundingPubkey, d.remoteFundingPubKey, sig) - case _ => false + val fundingKey = channelKeys.fundingKey(fundingTxIndex = 0) + val isRemoteSigValid = fundingSigned.sigOrPartialSig match { + case psig: PartialSignatureWithNonce => + val localNonce = NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0) + d.localCommitTx.checkRemotePartialSignature(fundingKey.publicKey, d.remoteFundingPubKey, psig, localNonce.publicNonce) + case sig: IndividualSignature => + d.localCommitTx.checkRemoteSig(fundingKey.publicKey, d.remoteFundingPubKey, sig) } - - isRemoveSigValid match { + isRemoteSigValid match { case false => // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) 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 b250cafd03..eea86d68b4 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 @@ -26,7 +26,6 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh, REFRESH_CHANNEL_UPDATE_INTERVAL} import fr.acinq.eclair.crypto.NonceGenerator import fr.acinq.eclair.db.RevokedHtlcInfoCleaner -import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{RealShortChannelId, ShortChannelId} @@ -76,12 +75,9 @@ trait CommonFundingHandlers extends CommonHandlers { case _: SingleFundedUnconfirmedFundingTx => // in the single-funding case, as fundee, it is the first time we see the full funding tx, we must verify that it is // valid (it pays the correct amount to the correct script). We also check as funder even if it's not really useful - d.commitments.latest.fullySignedLocalCommitTx(channelKeys).map(signedTx => Try(Transaction.correctlySpends(signedTx, Seq(w.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))) match { - case Right(Success(_)) => () - case Right(Failure(t)) => - log.error(t, s"rejecting channel with invalid funding tx: ${w.tx.bin}") - throw InvalidFundingTx(d.channelId) - case Left(t) => + Try(Transaction.correctlySpends(d.commitments.latest.fullySignedLocalCommitTx(channelKeys), Seq(w.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { + case Success(_) => () + case Failure(t) => log.error(t, s"rejecting channel with invalid funding tx: ${w.tx.bin}") throw InvalidFundingTx(d.channelId) } @@ -130,16 +126,15 @@ trait CommonFundingHandlers extends CommonHandlers { def createChannelReady(aliases: ShortIdAliases, commitments: Commitments): ChannelReady = { val params = commitments.channelParams val nextPerCommitmentPoint = channelKeys.commitmentPoint(1) - val nextLocalNonce_opt = commitments.latest.commitmentFormat match { + // Note that we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway. + commitments.latest.commitmentFormat match { case _: SimpleTaprootChannelCommitmentFormat => val localFundingKey = channelKeys.fundingKey(fundingTxIndex = 0) - val nextLocalNonce = NonceGenerator.verificationNonce(commitments.latest.fundingTxId, localFundingKey, 1).publicNonce - Some(ChannelTlv.NextLocalNonceTlv(nextLocalNonce)) + val nextLocalNonce = NonceGenerator.verificationNonce(commitments.latest.fundingTxId, localFundingKey, commitments.latest.remoteFundingPubKey, 1) + ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias, nextLocalNonce.publicNonce) case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => - None + ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias) } - // we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway - ChannelReady(params.channelId, nextPerCommitmentPoint, TlvStream(Set(Some(ChannelReadyTlv.ShortChannelIdTlv(aliases.localAlias)), nextLocalNonce_opt).flatten[ChannelReadyTlv])) } def receiveChannelReady(aliases: ShortIdAliases, channelReady: ChannelReady, commitments: Commitments): DATA_NORMAL = { @@ -163,7 +158,7 @@ trait CommonFundingHandlers extends CommonHandlers { }, remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint) ) - setRemoteNextLocalNonces("received ChannelReady", channelReady.nexLocalNonce_opt.map(n => commitments.latest.fundingTxId -> n).toMap) + channelReady.nextCommitNonce_opt.foreach(nonce => remoteNextCommitNonces = remoteNextCommitNonces + (commitments.latest.fundingTxId -> nonce)) peer ! ChannelReadyForPayments(self, remoteNodeId, commitments.channelId, fundingTxIndex = 0) DATA_NORMAL(commitments1, aliases1, None, initialChannelUpdate, SpliceStatus.NoSplice, None, None, None) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala index db7039e6d5..0833816cee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -20,10 +20,11 @@ import akka.actor.FSM import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.Features import fr.acinq.eclair.channel.Helpers.Closing.MutualClose -import fr.acinq.eclair.channel.Helpers.Closing.MutualClose.ClosingCompleteNonces import fr.acinq.eclair.channel._ +import fr.acinq.eclair.crypto.NonceGenerator import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.io.Peer +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol.{ClosingComplete, HtlcSettlementMessage, LightningMessage, Shutdown, UpdateMessage} import scodec.bits.ByteVector @@ -133,19 +134,33 @@ trait CommonHandlers { finalScriptPubkey } - def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closeStatus: CloseStatus): (DATA_NEGOTIATING_SIMPLE, Option[(ClosingComplete, ClosingCompleteNonces)]) = { + def createShutdown(commitments: Commitments, finalScriptPubKey: ByteVector): Shutdown = { + commitments.latest.commitmentFormat match { + case _: SimpleTaprootChannelCommitmentFormat => + // We create a fresh local closee nonce every time we send shutdown. + val localFundingPubKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex).publicKey + val localCloseeNonce = NonceGenerator.signingNonce(localFundingPubKey, commitments.latest.remoteFundingPubKey, commitments.latest.fundingTxId) + localCloseeNonce_opt = Some(localCloseeNonce) + Shutdown(commitments.channelId, finalScriptPubKey, localCloseeNonce.publicNonce) + case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => + Shutdown(commitments.channelId, finalScriptPubKey) + } + } + + def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closeStatus: CloseStatus): (DATA_NEGOTIATING_SIMPLE, Option[ClosingComplete]) = { val localScript = localShutdown.scriptPubKey val remoteScript = remoteShutdown.scriptPubKey val closingFeerate = closeStatus.feerates_opt.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates)) - MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, commitments.latest, localScript, remoteScript, closingFeerate, remoteShutdown.shutdownNonce_opt) match { + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, channelKeys, commitments.latest, localScript, remoteScript, closingFeerate, remoteShutdown.closeeNonce_opt) match { case Left(f) => log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) val d = DATA_NEGOTIATING_SIMPLE(commitments, closingFeerate, localScript, remoteScript, Nil, Nil) (d, None) - case Right((closingTxs, closingComplete, closingCompleteNonces)) => + case Right((closingTxs, closingComplete, closerNonces)) => log.debug("signing local mutual close transactions: {}", closingTxs) + localCloserNonces_opt = Some(closerNonces) val d = DATA_NEGOTIATING_SIMPLE(commitments, closingFeerate, localScript, remoteScript, closingTxs :: Nil, Nil) - (d, Some(closingComplete, closingCompleteNonces)) + (d, Some(closingComplete)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index cfe1eb062e..90e2220dfa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -210,11 +210,7 @@ trait ErrorHandlers extends CommonHandlers { val commitment = d.commitments.latest log.error(s"force-closing with fundingIndex=${commitment.fundingTxIndex}") context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"force-closing channel ${d.channelId} with fundingIndex=${commitment.fundingTxIndex}")) - val commitTx = commitment.fullySignedLocalCommitTx(channelKeys) match { - case Right(signedTx) => signedTx - case Left(channelException: ChannelException) => - return handleLocalError(channelException, d, None) - } + val commitTx = commitment.fullySignedLocalCommitTx(channelKeys) val (localCommitPublished, closingTxs) = Closing.LocalClose.claimCommitTxOutputs(channelKeys, commitment, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) val nextData = d match { case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) 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 8832fdb1f6..ff52d0b9d2 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 @@ -25,9 +25,10 @@ import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, LexicographicalOrdering, Musig2, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, LexicographicalOrdering, 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.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ @@ -94,7 +95,7 @@ object InteractiveTxBuilder { sealed trait Response case class SendMessage(sessionId: ByteVector32, msg: LightningMessage) extends Response - case class Succeeded(signingSession: InteractiveTxSigningSession.WaitingForSigs, commitSig: CommitSig, liquidityPurchase_opt: Option[LiquidityAds.Purchase], nextRemoteNonce_opt: Option[(TxId, IndividualNonce)]) extends Response + case class Succeeded(signingSession: InteractiveTxSigningSession.WaitingForSigs, commitSig: CommitSig, liquidityPurchase_opt: Option[LiquidityAds.Purchase], nextRemoteCommitNonce_opt: Option[(TxId, IndividualNonce)]) extends Response sealed trait Failed extends Response { def cause: ChannelException } case class LocalFailure(cause: ChannelException) extends Failed case class RemoteFailure(cause: ChannelException) extends Failed @@ -106,9 +107,19 @@ object InteractiveTxBuilder { case class SharedFundingInput(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, commitmentFormat: CommitmentFormat) { val weight: Int = commitmentFormat.fundingInputWeight - def sign(channelKeys: ChannelKeys, tx: Transaction, spentUtxos: Map[OutPoint, TxOut]): ChannelSpendSignature.IndividualSignature = { + def sign(channelId: ByteVector32, channelKeys: ChannelKeys, tx: Transaction, localNonce_opt: Option[LocalNonce], remoteNonce_opt: Option[IndividualNonce], spentUtxos: Map[OutPoint, TxOut]): Either[ChannelException, ChannelSpendSignature] = { val localFundingKey = channelKeys.fundingKey(fundingTxIndex) - Transactions.SpliceTx(info, tx).sign(localFundingKey, remoteFundingPubkey, spentUtxos) + val spliceTx = Transactions.SpliceTx(info, tx) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => Right(spliceTx.sign(localFundingKey, remoteFundingPubkey, spentUtxos)) + case _: SimpleTaprootChannelCommitmentFormat => (localNonce_opt, remoteNonce_opt) match { + case (Some(localNonce), Some(remoteNonce)) => spliceTx.partialSign(localFundingKey, remoteFundingPubkey, spentUtxos, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { + case Left(_) => Left(InvalidFundingNonce(channelId, tx.txid)) + case Right(sig) => Right(sig) + } + case _ => Left(MissingFundingNonce(channelId, tx.txid)) + } + } } } @@ -333,14 +344,13 @@ object InteractiveTxBuilder { val remoteFees: MilliSatoshi = remoteAmountIn - remoteAmountOut // Note that the truncation is a no-op: sub-satoshi balances are carried over from inputs to outputs and cancel out. val fees: Satoshi = (localFees + remoteFees).truncateToSatoshi + // Outputs spent by this transaction, in the order in which they appear in the transaction inputs. + val spentOutputs: Seq[TxOut] = (sharedInput_opt.toSeq ++ localInputs ++ remoteInputs).sortBy(_.serialId).map(_.txOut) // When signing transactions that include taproot inputs, we must provide details about all of the transaction's inputs. val inputDetails: Map[OutPoint, TxOut] = (sharedInput_opt.toSeq.map(i => i.outPoint -> i.txOut) ++ localInputs.map(i => i.outPoint -> i.txOut) ++ remoteInputs.map(i => i.outPoint -> i.txOut)).toMap 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))) @@ -469,12 +479,13 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case rbf: SpliceTxRbf => rbf.previousTransactions case _ => Nil } - // nonce used to sign the shared input if there is one - private val localNonce_opt = fundingParams.sharedInput_opt.collect { - case s if s.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - val localPubKeyForSharedInput = channelKeys.fundingKey(s.fundingTxIndex).publicKey - NonceGenerator.signingNonce(localPubKeyForSharedInput) - } + // Nonce we will use to sign the shared input, if we are splicing a taproot channel. + private val localFundingNonce_opt: Option[LocalNonce] = fundingParams.sharedInput_opt.flatMap(sharedInput => sharedInput.commitmentFormat match { + case _: SegwitV0CommitmentFormat => None + case _: SimpleTaprootChannelCommitmentFormat => + val previousFundingKey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey + Some(NonceGenerator.signingNonce(previousFundingKey, sharedInput.remoteFundingPubkey, sharedInput.info.outPoint.txid)) + }) def start(): Behavior[Command] = { val txFunder = context.spawnAnonymous(InteractiveTxFunder(remoteNodeId, fundingParams, fundingPubkeyScript, purpose, wallet)) @@ -538,22 +549,27 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon receive(next) case Nil => val txComplete = fundingParams.commitmentFormat match { + case _: SegwitV0CommitmentFormat => TxComplete(fundingParams.channelId) case _: SimpleTaprootChannelCommitmentFormat => - // commit nonces are used to sign commit transactions and are sent to our peer. Once this session complete the last one we've received becomes "their next remote nonce" - // we just need the funding tx id, we don't validate its inputs/outputs, if it's invalid we'll send tx_complete again - val fundingTx = Transaction(version = 2, + // We don't have more inputs or outputs to contribute to the shared transaction. + // If our peer doesn't have anything more to contribute either, we will proceed to exchange commitment + // signatures spending this shared transaction, so we need to provide nonces to create those signatures. + // If our peer adds more inputs or outputs, we will simply send a new tx_complete message in response with + // nonces for the updated shared transaction. + // Note that we don't validate the shared transaction at that point: this will be done later once we've + // both sent tx_complete. If the shared transaction is invalid, we will abort and discard our nonces. + val fundingTxId = Transaction( + version = 2, txIn = (session.localInputs.map(i => i.serialId -> TxIn(i.outPoint, Nil, i.sequence)) ++ session.remoteInputs.map(i => i.serialId -> TxIn(i.outPoint, Nil, i.sequence))).sortBy(_._1).map(_._2), txOut = (session.localOutputs.map(o => o.serialId -> TxOut(o.amount, o.pubkeyScript)) ++ session.remoteOutputs.map(o => o.serialId -> TxOut(o.amount, o.pubkeyScript))).sortBy(_._1).map(_._2), - fundingParams.lockTime + lockTime = fundingParams.lockTime + ).txid + TxComplete( + channelId = fundingParams.channelId, + commitNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey, fundingParams.remoteFundingPubKey, purpose.localCommitIndex).publicNonce, + nextCommitNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey, fundingParams.remoteFundingPubKey, purpose.localCommitIndex + 1).publicNonce, + fundingNonce_opt = localFundingNonce_opt.map(_.publicNonce), ) - val fundingTxId = fundingTx.txid - TxComplete(fundingParams.channelId, - NonceGenerator.verificationNonce(fundingTxId, localFundingKey, purpose.localCommitIndex).publicNonce, - NonceGenerator.verificationNonce(fundingTxId, localFundingKey, purpose.localCommitIndex + 1).publicNonce, - localNonce_opt.map(_.publicNonce) - ) - case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => - TxComplete(fundingParams.channelId) } replyTo ! SendMessage(sessionId, txComplete) val next = session.copy(txCompleteSent = Some(txComplete)) @@ -565,7 +581,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon } } - private def receiveInput(session: InteractiveTxSession, addInput: TxAddInput): Either[ChannelException, InteractiveTxSession] = { + private def receiveInput(session: InteractiveTxSession, addInput: TxAddInput): Either[ChannelException, IncomingInput] = { if (session.inputsReceivedCount + 1 >= MAX_INPUTS_OUTPUTS_RECEIVED) { return Left(TooManyInteractiveTxRounds(fundingParams.channelId)) } @@ -596,11 +612,7 @@ 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(session.copy( - remoteInputs = session.remoteInputs :+ input, - inputsReceivedCount = session.inputsReceivedCount + 1, - txCompleteReceived = None - )) + Right(input) } private def receiveOutput(session: InteractiveTxSession, addOutput: TxAddOutput): Either[ChannelException, IncomingOutput] = { @@ -632,7 +644,12 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Left(f) => replyTo ! RemoteFailure(f) unlockAndStop(session) - case Right(next) => + case Right(input) => + val next = session.copy( + remoteInputs = session.remoteInputs :+ input, + inputsReceivedCount = session.inputsReceivedCount + 1, + txCompleteReceived = None, + ) send(next) } case addOutput: TxAddOutput => @@ -703,7 +720,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon replyTo ! RemoteFailure(cause) unlockAndStop(session) case Right(completeTx) => - signCommitTx(session, completeTx) + signCommitTx(completeTx, session.txCompleteReceived.flatMap(_.nonces_opt)) } case _: WalletFailure => replyTo ! RemoteFailure(UnconfirmedInteractiveTxInputs(fundingParams.channelId)) @@ -759,7 +776,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon return Left(InvalidCompleteInteractiveTx(fundingParams.channelId)) } - val sharedInput_opt = fundingParams.sharedInput_opt.map(_ => { + val sharedInput_opt = fundingParams.sharedInput_opt.map(sharedInput => { if (fundingParams.remoteContribution >= 0.sat) { // If remote has a positive contribution, we do not check their post-splice reserve level, because they are improving // their situation, even if they stay below the requirement. Note that if local splices-in some funds in the same @@ -776,6 +793,13 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon log.warn("invalid interactive tx: shared input included multiple times") return Left(InvalidCompleteInteractiveTx(fundingParams.channelId)) } + sharedInput.commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + // If we're spending a taproot channel, our peer must provide a nonce for the shared input. + val remoteFundingNonce_opt: Option[IndividualNonce] = session.txCompleteReceived.flatMap(_.nonces_opt).flatMap(_.fundingNonce_opt) + if (remoteFundingNonce_opt.isEmpty) return Left(MissingFundingNonce(fundingParams.channelId, sharedInput.info.outPoint.txid)) + } sharedInputs.headOption match { case Some(input) => input case None => @@ -791,6 +815,14 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon return Left(InvalidCompleteInteractiveTx(fundingParams.channelId)) } + // If we're using taproot, our peer must provide commit nonces for the funding transaction. + fundingParams.commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + val remoteCommitNonces_opt = session.txCompleteReceived.flatMap(_.nonces_opt) + if (remoteCommitNonces_opt.isEmpty) return Left(MissingCommitNonce(fundingParams.channelId, tx.txid, purpose.remoteCommitIndex)) + } + // The transaction isn't signed yet, and segwit witnesses can be arbitrarily low (e.g. when using an OP_1 script), // so we use empty witnesses to provide a lower bound on the transaction weight. if (tx.weight() > Transactions.MAX_STANDARD_TX_WEIGHT) { @@ -856,7 +888,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon Right(sharedTx) } - private def signCommitTx(session: InteractiveTxSession, completeTx: SharedTransaction): Behavior[Command] = { + private def signCommitTx(completeTx: SharedTransaction, remoteNonces_opt: Option[TxCompleteTlv.Nonces]): Behavior[Command] = { val fundingTx = completeTx.buildUnsignedTx() val fundingOutputIndex = fundingTx.txOut.indexWhere(_.publicKeyScript == fundingPubkeyScript) val liquidityFee = fundingParams.liquidityFees(liquidityPurchase_opt) @@ -880,44 +912,34 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)) => require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!") val localSigOfRemoteTx = fundingParams.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => ByteVector64.Zeroes - case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(localFundingKey, fundingParams.remoteFundingPubKey).sig - } - - def makeTlvs(): Either[ChannelException, TlvStream[CommitSigTlv]] = fundingParams.commitmentFormat match { + case _: SegwitV0CommitmentFormat => Right(remoteCommitTx.sign(localFundingKey, fundingParams.remoteFundingPubKey)) case _: SimpleTaprootChannelCommitmentFormat => - val localNonce = NonceGenerator.signingNonce(localFundingKey.publicKey) - val remoteNonce = session.txCompleteReceived.flatMap(_.nonces_opt).map(_.remoteNonce) match { - case Some(n) => n - case None => return Left(MissingNonce(channelParams.channelId, fundingTx.txid)) - } - val psig = remoteCommitTx.partialSign(localFundingKey, fundingParams.remoteFundingPubKey, Map.empty, localNonce, Seq(localNonce.publicNonce, remoteNonce)) match { - case Left(e) => - println(e) - return Left(MissingNonce(channelParams.channelId, fundingTx.txid)) - case Right(psig) => psig + remoteNonces_opt match { + case Some(remoteNonces) => + val localNonce = NonceGenerator.signingNonce(localFundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingTx.txid) + remoteCommitTx.partialSign(localFundingKey, fundingParams.remoteFundingPubKey, localNonce, Seq(localNonce.publicNonce, remoteNonces.commitNonce)) match { + case Left(_) => Left(InvalidCommitNonce(channelParams.channelId, fundingTx.txid, purpose.remoteCommitIndex)) + case Right(localSig) => Right(localSig) + } + case None => Left(MissingCommitNonce(fundingParams.channelId, fundingTx.txid, purpose.remoteCommitIndex)) } - log.debug(s"signCommitTx: creating partial signature $psig for commit tx ${remoteCommitTx.tx.txid} with local nonce ${localNonce.publicNonce} remote nonce $remoteNonce") - Right(TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(psig))) - case _: SegwitV0CommitmentFormat => Right(TlvStream.empty[CommitSigTlv]) } - - makeTlvs() match { + localSigOfRemoteTx match { case Left(cause) => replyTo ! RemoteFailure(cause) unlockAndStop(completeTx) - case Right(tlvs) => + case Right(localSigOfRemoteTx) => val htlcSignatures = sortedHtlcTxs.map(_.localSig(remoteCommitmentKeys)).toList - val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, tlvs) + val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, batchSize = 1) val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx.tx.txid) val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) - signFundingTx(session, completeTx, localCommitSig, localCommit, remoteCommit) + signFundingTx(completeTx, remoteNonces_opt, localCommitSig, localCommit, remoteCommit) } } } - private def signFundingTx(session: InteractiveTxSession, completeTx: SharedTransaction, commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { - signTx(session, completeTx) + private def signFundingTx(completeTx: SharedTransaction, remoteNonces_opt: Option[TxCompleteTlv.Nonces], commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { + signTx(completeTx, remoteNonces_opt.flatMap(_.fundingNonce_opt)) 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) @@ -954,10 +976,8 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon remoteCommit, liquidityPurchase_opt.map(_.basicInfo(isBuyer = fundingParams.isInitiator)) ) - // the last nonce they've sent becomes their "next remote nonce" - val fundingTxId = validateTx(session).map(_.buildUnsignedTx().txid).getOrElse(throw new RuntimeException("invalid signing session")) - val theirNextRemoteNonce = session.txCompleteReceived.flatMap(_.nonces_opt).map(n => fundingTxId -> n.nextRemoteNonce) - replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt, theirNextRemoteNonce) + val nextRemoteCommitNonce_opt = remoteNonces_opt.map(n => signedTx.txId -> n.nextCommitNonce) + replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteCommitNonce_opt) Behaviors.stopped case WalletFailure(t) => log.error("could not sign funding transaction: ", t) @@ -972,76 +992,56 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon } } - private def signTx(session: InteractiveTxSession, unsignedTx: SharedTransaction): Unit = { + private def signTx(unsignedTx: SharedTransaction, remoteFundingNonce_opt: Option[IndividualNonce]): Unit = { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ val tx = unsignedTx.buildUnsignedTx() - val sharedSig_opt = fundingParams.sharedInput_opt.map { i => - i.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => ByteVector64.Zeroes - case _: AnchorOutputsCommitmentFormat | DefaultCommitmentFormat => i.sign(channelKeys, tx, unsignedTx.inputDetails).sig - } + val sharedSig_opt = fundingParams.sharedInput_opt match { + case Some(i) => i.sign(fundingParams.channelId, channelKeys, tx, localFundingNonce_opt, remoteFundingNonce_opt, unsignedTx.inputDetails).map(sig => Some(sig)) + case None => Right(None) } - val sharedPartialSig_opt: Option[ChannelSpendSignature.PartialSignatureWithNonce] = fundingParams.sharedInput_opt.collect { - case i if i.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat] => - // there should be a single shared input - val localNonce = this.localNonce_opt.get - val fundingKey = channelKeys.fundingKey(i.fundingTxIndex) - val inputIndex = tx.txIn.indexWhere(_.outPoint == i.info.outPoint) - // there should be one remote nonce for each shared input ordered by serial id - val remoteNonce = session.txCompleteReceived.flatMap(_.nonces_opt).flatMap(_.fundingNonce_opt) match { - case Some(n) => n - case None => - context.self ! WalletFailure(MissingFundingNonce(this.channelParams.channelId)) - return + sharedSig_opt match { + case Left(f) => + context.self ! WalletFailure(f) + case Right(sharedSig_opt) if unsignedTx.localInputs.isEmpty => + context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt))) + case Right(sharedSig_opt) => + // We track our wallet inputs and outputs, so we can verify them when we sign the transaction: if Eclair is managing bitcoin core wallet keys, it will + // only sign our wallet inputs, and check that it can re-compute private keys for our wallet outputs. + val ourWalletInputs = unsignedTx.localInputs.map(i => tx.txIn.indexWhere(_.outPoint == i.outPoint)) + val ourWalletOutputs = unsignedTx.localOutputs.flatMap { + case Output.Local.Change(_, amount, pubkeyScript) => Some(tx.txOut.indexWhere(output => output.amount == amount && output.publicKeyScript == pubkeyScript)) + // Non-change outputs may go to an external address (typically during a splice-out). + // Here we only keep outputs which are ours i.e explicitly go back into our wallet. + // We trust that non-change outputs are valid: this only works if the entry point for creating such outputs is trusted (for example, a secure API call). + case _: Output.Local.NonChange => None } - val psig = Musig2.signTaprootInput(fundingKey, tx, inputIndex, unsignedTx.spentOutputs, Scripts.sort(Seq(fundingKey.publicKey, i.remoteFundingPubkey)), localNonce.secretNonce, Seq(localNonce.publicNonce, remoteNonce), None) match { - case Left(_) => - context.self ! WalletFailure(InvalidFundingNonce(this.channelParams.channelId)) - return - case Right(psig) => psig + // If this is a splice, the PSBT we create must contain the shared input, because if we use taproot wallet inputs + // we need information about *all* of the transaction's inputs, not just the one we're signing. + val psbt = unsignedTx.sharedInput_opt.flatMap { + si => new Psbt(tx).updateWitnessInput(si.outPoint, si.txOut, null, null, null, java.util.Map.of(), null, null, java.util.Map.of()).toOption + }.getOrElse(new Psbt(tx)) + context.pipeToSelf(wallet.signPsbt(psbt, ourWalletInputs, ourWalletOutputs).map { + response => + val localOutpoints = unsignedTx.localInputs.map(_.outPoint).toSet + val partiallySignedTx = response.partiallySignedTx + // Partially signed PSBT must include spent amounts for all inputs that were signed, and we can "trust" these amounts because they are included + // in the hash that we signed (see BIP143). If our bitcoin node lied about them, then our signatures are invalid. + val actualLocalAmountIn = ourWalletInputs.map(i => kmp2scala(response.psbt.getInput(i).getWitnessUtxo.amount)).sum + val expectedLocalAmountIn = unsignedTx.localInputs.map(i => i.txOut.amount).sum + require(actualLocalAmountIn == expectedLocalAmountIn, s"local spent amount $actualLocalAmountIn does not match what we expect ($expectedLocalAmountIn): bitcoin core may be malicious") + val actualLocalAmountOut = ourWalletOutputs.map(i => partiallySignedTx.txOut(i).amount).sum + val expectedLocalAmountOut = unsignedTx.localOutputs.map { + case c: Output.Local.Change => c.amount + case _: Output.Local.NonChange => 0.sat + }.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)) + }) { + case Failure(t) => WalletFailure(t) + case Success(signedTx) => SignTransactionResult(signedTx) } - ChannelSpendSignature.PartialSignatureWithNonce(psig, localNonce.publicNonce) - } - if (unsignedTx.localInputs.isEmpty) { - context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt, sharedPartialSig_opt))) - } else { - // We track our wallet inputs and outputs, so we can verify them when we sign the transaction: if Eclair is managing bitcoin core wallet keys, it will - // only sign our wallet inputs, and check that it can re-compute private keys for our wallet outputs. - val ourWalletInputs = unsignedTx.localInputs.map(i => tx.txIn.indexWhere(_.outPoint == i.outPoint)) - val ourWalletOutputs = unsignedTx.localOutputs.flatMap { - case Output.Local.Change(_, amount, pubkeyScript) => Some(tx.txOut.indexWhere(output => output.amount == amount && output.publicKeyScript == pubkeyScript)) - // Non-change outputs may go to an external address (typically during a splice-out). - // Here we only keep outputs which are ours i.e explicitly go back into our wallet. - // We trust that non-change outputs are valid: this only works if the entry point for creating such outputs is trusted (for example, a secure API call). - case _: Output.Local.NonChange => None - } - // If this is a splice, the PSBT we create must contain the shared input, because if we use taproot wallet inputs - // we need information about *all* of the transaction's inputs, not just the one we're signing. - val psbt = unsignedTx.sharedInput_opt.flatMap { - si => new Psbt(tx).updateWitnessInput(si.outPoint, si.txOut, null, null, null, java.util.Map.of(), null, null, java.util.Map.of()).toOption - }.getOrElse(new Psbt(tx)) - context.pipeToSelf(wallet.signPsbt(psbt, ourWalletInputs, ourWalletOutputs).map { - response => - val localOutpoints = unsignedTx.localInputs.map(_.outPoint).toSet - val partiallySignedTx = response.partiallySignedTx - // Partially signed PSBT must include spent amounts for all inputs that were signed, and we can "trust" these amounts because they are included - // in the hash that we signed (see BIP143). If our bitcoin node lied about them, then our signatures are invalid. - val actualLocalAmountIn = ourWalletInputs.map(i => kmp2scala(response.psbt.getInput(i).getWitnessUtxo.amount)).sum - val expectedLocalAmountIn = unsignedTx.localInputs.map(i => i.txOut.amount).sum - require(actualLocalAmountIn == expectedLocalAmountIn, s"local spent amount $actualLocalAmountIn does not match what we expect ($expectedLocalAmountIn): bitcoin core may be malicious") - val actualLocalAmountOut = ourWalletOutputs.map(i => partiallySignedTx.txOut(i).amount).sum - val expectedLocalAmountOut = unsignedTx.localOutputs.map { - case c: Output.Local.Change => c.amount - case _: Output.Local.NonChange => 0.sat - }.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, sharedPartialSig_opt)) - }) { - case Failure(t) => WalletFailure(t) - case Success(signedTx) => SignTransactionResult(signedTx) - } } } @@ -1127,40 +1127,24 @@ object InteractiveTxSigningSession { return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) } val sharedSigs_opt = fundingParams.sharedInput_opt.map(sharedInput => { - sharedInput.commitmentFormat match { - case _: SegwitV0CommitmentFormat => (partiallySignedTx.localSigs.previousFundingTxSig_opt, remoteSigs.previousFundingTxSig_opt) match { - case (Some(localSig), Some(remoteSig)) => - val localFundingPubkey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey - Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, sharedInput.remoteFundingPubkey) - case _ => - log.info("invalid tx_signatures: missing shared input signatures") - return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) - } + val localFundingPubkey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey + val spliceTx = Transactions.SpliceTx(sharedInput.info, partiallySignedTx.tx.buildUnsignedTx()) + val signedTx_opt = sharedInput.commitmentFormat match { + case _: SegwitV0CommitmentFormat => + (partiallySignedTx.localSigs.previousFundingTxSig_opt, remoteSigs.previousFundingTxSig_opt) match { + case (Some(localSig), Some(remoteSig)) => Right(spliceTx.aggregateSigs(localFundingPubkey, sharedInput.remoteFundingPubkey, IndividualSignature(localSig), IndividualSignature(remoteSig))) + case _ => Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) + } case _: SimpleTaprootChannelCommitmentFormat => (partiallySignedTx.localSigs.previousFundingTxPartialSig_opt, remoteSigs.previousFundingTxPartialSig_opt) match { - case (Some(localPartialSig), Some(remotePartialSig)) => - val localFundingPubkey = channelKeys.fundingKey(sharedInput.fundingTxIndex).publicKey - val unsignedTx = partiallySignedTx.tx.buildUnsignedTx() - val inputIndex = unsignedTx.txIn.indexWhere(_.outPoint == sharedInput.info.outPoint) - val 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) - aggSig match { - case Right(sig) => Script.witnessKeyPathPay2tr(sig) - case Left(error) => - log.warning(s"adding remote sigs for ${unsignedTx.txid} local partial sig ${localPartialSig.partialSig} is using nonce ${localPartialSig.nonce} remote partial sig ${remotePartialSig.partialSig} is using nonce ${remotePartialSig.nonce} local funding key = ${localFundingPubkey} remote funding key = ${sharedInput.remoteFundingPubkey} spent outputs = ${partiallySignedTx.tx.spentOutputs} failed with $error") - return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) - } - case _ => - log.info("invalid tx_signatures: missing shared input partial signatures") - return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) + case (Some(localSig), Some(remoteSig)) => spliceTx.aggregateSigs(localFundingPubkey, sharedInput.remoteFundingPubkey, localSig, remoteSig, partiallySignedTx.tx.inputDetails) + case _ => Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) } } + signedTx_opt match { + case Left(_) => return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) + case Right(signedTx) => signedTx.txIn(spliceTx.inputIndex).witness + } }) val txWithSigs = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, remoteSigs, sharedSigs_opt) if (remoteSigs.txId != txWithSigs.signedTx.txid) { @@ -1201,6 +1185,7 @@ object InteractiveTxSigningSession { remoteCommitParams: CommitParams, remoteCommit: RemoteCommit, liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession { + val fundingTxId: TxId = fundingTx.txId val localCommitIndex: Long = localCommit.fold(_.index, _.index) // This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not. val nextLocalCommitmentNumber: Long = localCommit match { @@ -1212,12 +1197,21 @@ object InteractiveTxSigningSession { def commitInput(fundingKey: PrivateKey): InputInfo = { val fundingScript = Transactions.makeFundingScript(fundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingParams.commitmentFormat).pubkeyScript - val fundingOutput = OutPoint(fundingTx.txId, fundingTx.tx.buildUnsignedTx().txOut.indexWhere(txOut => txOut.amount == fundingParams.fundingAmount && txOut.publicKeyScript == fundingScript)) + val fundingOutput = OutPoint(fundingTxId, fundingTx.tx.buildUnsignedTx().txOut.indexWhere(txOut => txOut.amount == fundingParams.fundingAmount && txOut.publicKeyScript == fundingScript)) InputInfo(fundingOutput, TxOut(fundingParams.fundingAmount, fundingScript)) } def commitInput(channelKeys: ChannelKeys): InputInfo = commitInput(localFundingKey(channelKeys)) + /** Nonce for the current commitment, which our peer will need if they must re-send their commit_sig for our current commitment transaction. */ + def currentCommitNonce_opt(channelKeys: ChannelKeys): Option[LocalNonce] = localCommit match { + case Left(_) => Some(NonceGenerator.verificationNonce(fundingTxId, localFundingKey(channelKeys), fundingParams.remoteFundingPubKey, localCommitIndex)) + case Right(_) => None + } + + /** Nonce for the next commitment, which our peer will need to sign our next commitment transaction. */ + def nextCommitNonce(channelKeys: ChannelKeys): LocalNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey(channelKeys), fundingParams.remoteFundingPubKey, localCommitIndex + 1) + def receiveCommitSig(channelParams: ChannelParams, channelKeys: ChannelKeys, remoteCommitSig: CommitSig, currentBlockHeight: BlockHeight)(implicit log: LoggingAdapter): Either[ChannelException, InteractiveTxSigningSession] = { localCommit match { case Left(unsignedLocalCommit) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala index 7883159b97..02bc3a1a10 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/NonceGenerator.scala @@ -1,34 +1,33 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{Musig2, TxId} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Musig2, TxId} import fr.acinq.eclair.randomBytes32 import fr.acinq.eclair.transactions.Transactions.LocalNonce import grizzled.slf4j.Logging object NonceGenerator extends Logging { + + // When using single-funding, we don't have access to the funding tx and remote funding key when creating our first + // verification nonce, so we use placeholder values instead. Note that this is fixed with dual-funding. + val dummyFundingTxId: TxId = TxId(ByteVector32.Zeroes) + val dummyRemoteFundingPubKey: PublicKey = PrivateKey(ByteVector32.One.bytes).publicKey + /** - * Deterministic nonce, used to sign our commit tx. Its public part is sent to our peer - * - * @param fundingTxId funding transaction id - * @param fundingPrivKey funding private key - * @param index commitment index - * @return a deterministic nonce + * @return a deterministic nonce used to sign our local commit tx: its public part is sent to our peer. */ - def verificationNonce(fundingTxId: TxId, fundingPrivKey: PrivateKey, index: Long): LocalNonce = { - val nonces = Musig2.generateNonceWithCounter(index, fundingPrivKey, Seq(fundingPrivKey.publicKey), None, Some(fundingTxId.value)) + def verificationNonce(fundingTxId: TxId, fundingPrivKey: PrivateKey, remoteFundingPubKey: PublicKey, commitIndex: Long): LocalNonce = { + val nonces = Musig2.generateNonceWithCounter(commitIndex, fundingPrivKey, Seq(fundingPrivKey.publicKey, remoteFundingPubKey), None, Some(fundingTxId.value)) LocalNonce(nonces._1, nonces._2) } /** - * Random nonce used to sign our peer's commit tx. - * - * @param fundingPubKey funding public key which matches the private that will be used with this nonce - * @return a random nonce + * @return a random nonce used to sign our peer's commit tx. */ - def signingNonce(fundingPubKey: PublicKey): LocalNonce = { + def signingNonce(localFundingPubKey: PublicKey, remoteFundingPubKey: PublicKey, fundingTxId: TxId): LocalNonce = { val sessionId = randomBytes32() - val nonces = Musig2.generateNonce(sessionId, Right(fundingPubKey), Seq(fundingPubKey), None, None) + val nonces = Musig2.generateNonce(sessionId, Right(localFundingPubKey), Seq(localFundingPubKey, remoteFundingPubKey), None, Some(fundingTxId.value)) LocalNonce(nonces._1, nonces._2) } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index f414d8d4ef..eb517841ef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.scalacompat.{LexicographicalOrdering, SatoshiLong, TxOut} import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, PhoenixSimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol._ /** @@ -94,7 +94,8 @@ final case class CommitmentSpec(htlcs: Set[DirectedHtlc], commitTxFeerate: Feera def htlcTxFeerate(commitmentFormat: CommitmentFormat): FeeratePerKw = commitmentFormat match { case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => FeeratePerKw(0 sat) - case _ => commitTxFeerate + case UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat => commitTxFeerate + case DefaultCommitmentFormat => commitTxFeerate } def findIncomingHtlcById(id: Long): Option[IncomingHtlc] = htlcs.collectFirst { case htlc: IncomingHtlc if htlc.add.id == id => htlc } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala index 67a56f8b2c..f2a93939f6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala @@ -17,15 +17,14 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Script.LOCKTIME_THRESHOLD -import fr.acinq.bitcoin.{ScriptTree, SigHash} +import fr.acinq.bitcoin.ScriptTree import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.TxIn.{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_MASK, SEQUENCE_LOCKTIME_TYPE_FLAG} -import fr.acinq.bitcoin.io.Output import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, XonlyPublicKey} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.crypto.keymanager.{CommitmentPublicKeys, LocalCommitmentKeys, RemoteCommitmentKeys} -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta} import scodec.bits.ByteVector @@ -242,7 +241,7 @@ object Scripts { /** Extract the payment preimage from a 2nd-stage HTLC Success transaction's witness script */ def extractPreimageFromHtlcSuccess: PartialFunction[ScriptWitness, ByteVector32] = { case ScriptWitness(Seq(ByteVector.empty, _, _, paymentPreimage, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) - case ScriptWitness(Seq(_, sig, paymentPreimage, _, _)) if sig.size == 64 && paymentPreimage.size == 32 => ByteVector32(paymentPreimage) + case ScriptWitness(Seq(_, _, paymentPreimage, _, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) } /** Extract payment preimages from a (potentially batched) 2nd-stage HTLC transaction's witnesses. */ @@ -258,7 +257,7 @@ object Scripts { /** Extract the payment preimage from from a fulfilled offered htlc. */ def extractPreimageFromClaimHtlcSuccess: PartialFunction[ScriptWitness, ByteVector32] = { case ScriptWitness(Seq(_, paymentPreimage, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) - case ScriptWitness(Seq(sig, paymentPreimage, _, _)) if sig.size == 64 && paymentPreimage.size == 32 => ByteVector32(paymentPreimage) + case ScriptWitness(Seq(_, paymentPreimage, _, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) } /** Extract payment preimages from a (potentially batched) claim HTLC transaction's witnesses. */ @@ -326,7 +325,7 @@ object Scripts { /** * Taproot signatures are usually 64 bytes, unless a non-default sighash is used, in which case it is appended. */ - def encodeSig(sig: ByteVector64, sighashType: Int = SIGHASH_DEFAULT): ByteVector = sighashType match { + private def encodeSig(sig: ByteVector64, sighashType: Int = SIGHASH_DEFAULT): ByteVector = sighashType match { case SIGHASH_DEFAULT | SIGHASH_ALL => sig case _ => sig :+ sighashType.toByte } 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 e9e7f5b3ba..6c62891f0e 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 @@ -27,6 +27,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelSpendSignature import fr.acinq.eclair.channel.ChannelSpendSignature._ +import fr.acinq.eclair.crypto.NonceGenerator import fr.acinq.eclair.crypto.keymanager.{CommitmentPublicKeys, LocalCommitmentKeys, RemoteCommitmentKeys} import fr.acinq.eclair.transactions.CommitmentOutput._ import fr.acinq.eclair.transactions.Scripts.Taproot.NUMS_POINT @@ -191,8 +192,9 @@ object Transactions { override val claimHtlcPenaltyWeight = 396 } - case object LegacySimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat { - override def toString: String = "unsafe_simple_taproot" + /** For Phoenix users we sign HTLC transactions with the same feerate as the commit tx to allow broadcasting without wallet inputs. */ + case object PhoenixSimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat { + override def toString: String = "simple_taproot_phoenix" } case object ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat extends SimpleTaprootChannelCommitmentFormat { @@ -362,6 +364,10 @@ object Transactions { override val desc: String = "commit-tx" def sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey): ChannelSpendSignature.IndividualSignature = sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty) + + def partialSign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, localNonce: LocalNonce, publicNonces: Seq[IndividualNonce]): Either[Throwable, ChannelSpendSignature.PartialSignatureWithNonce] = partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce, publicNonces) + + def aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: PartialSignatureWithNonce, remoteSig: PartialSignatureWithNonce): Either[Throwable, Transaction] = aggregateSigs(localFundingPubkey, remoteFundingPubkey, localSig, remoteSig, extraUtxos = Map.empty) } /** This transaction collaboratively spends the channel funding output (mutual-close). */ @@ -370,6 +376,10 @@ object Transactions { val toLocalOutput_opt: Option[TxOut] = toLocalOutputIndex_opt.map(i => tx.txOut(i.toInt)) def sign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey): ChannelSpendSignature.IndividualSignature = sign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty) + + def partialSign(localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, localNonce: LocalNonce, publicNonces: Seq[IndividualNonce]): Either[Throwable, ChannelSpendSignature.PartialSignatureWithNonce] = partialSign(localFundingKey, remoteFundingPubkey, extraUtxos = Map.empty, localNonce, publicNonces) + + def aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: PartialSignatureWithNonce, remoteSig: PartialSignatureWithNonce): Either[Throwable, Transaction] = aggregateSigs(localFundingPubkey, remoteFundingPubkey, localSig, remoteSig, extraUtxos = Map.empty) } object ClosingTx { @@ -1537,6 +1547,21 @@ object Transactions { } // @formatter:on + /** + * When sending [[fr.acinq.eclair.wire.protocol.ClosingComplete]], we use a different nonce for each closing transaction we create. + * We generate nonces for all variants of the closing transaction for simplicity, even though we never use them all. + */ + case class CloserNonces(localAndRemote: LocalNonce, localOnly: LocalNonce, remoteOnly: LocalNonce) + + object CloserNonces { + /** Generate a set of random signing nonces for our closing transactions. */ + def generate(localFundingKey: PublicKey, remoteFundingKey: PublicKey, fundingTxId: TxId): CloserNonces = CloserNonces( + NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId), + NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId), + NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId), + ) + } + /** Each closing attempt can result in multiple potential closing transactions, depending on which outputs are included. */ case class ClosingTxs(localAndRemote_opt: Option[ClosingTx], localOnly_opt: Option[ClosingTx], remoteOnly_opt: Option[ClosingTx]) { /** Preferred closing transaction for this closing attempt. */ 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 1ecc72f104..0762700dea 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 @@ -233,7 +233,7 @@ private[channel] object ChannelCodecs0 { val commitSigCodec: Codec[CommitSig] = ( ("channelId" | bytes32) :: - ("signature" | bytes64) :: + ("signature" | bytes64.as[ChannelSpendSignature.IndividualSignature]) :: ("htlcSignatures" | listofsignatures) :: ("tlvStream" | provide(TlvStream.empty[CommitSigTlv]))).as[CommitSig] 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 559284300f..75e9de1043 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 @@ -7,8 +7,6 @@ import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, ConfirmationTarget, import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, PartiallySignedSharedTransaction} -import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit -import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys} import fr.acinq.eclair.transactions.Transactions._ 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 6c7f7fa62b..956db33f3f 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 @@ -48,7 +48,7 @@ private[channel] object ChannelCodecs5 { private val channelSpendSignatureCodec: Codec[ChannelSpendSignature] = discriminated[ChannelSpendSignature].by(uint8) .typecase(0x01, bytes64.as[ChannelSpendSignature.IndividualSignature]) - .typecase(0x02, (("partialSig" | bytes32) :: ("nonce" | publicNonce)).as[ChannelSpendSignature.PartialSignatureWithNonce]) + .typecase(0x02, partialSignatureWithNonce) private def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList) @@ -81,7 +81,7 @@ private[channel] object ChannelCodecs5 { .typecase(0x00, provide(Transactions.DefaultCommitmentFormat)) .typecase(0x01, provide(Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) .typecase(0x02, provide(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat)) - .typecase(0x03, provide(Transactions.LegacySimpleTaprootChannelCommitmentFormat)) + .typecase(0x03, provide(Transactions.PhoenixSimpleTaprootChannelCommitmentFormat)) .typecase(0x04, provide(Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)) private val localChannelParamsCodec: Codec[LocalChannelParams] = ( 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 5e06063a0a..1e9b7cdb52 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 @@ -20,8 +20,6 @@ import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, TxId} import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel.{ChannelType, ChannelTypes} -import fr.acinq.eclair.transactions.Transactions.CommitmentFormat -import fr.acinq.eclair.wire.protocol.ChannelTlv.{nextLocalNonceTlvCodec, nextLocalNoncesTlvCodec} 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} @@ -93,13 +91,16 @@ object ChannelTlv { */ case class UseFeeCredit(amount: MilliSatoshi) extends OpenDualFundedChannelTlv with SpliceInitTlv + /** Verification nonce used for the next commitment transaction that will be signed (when using taproot channels). */ case class NextLocalNonceTlv(nonce: IndividualNonce) extends OpenChannelTlv with AcceptChannelTlv with ChannelReadyTlv with ClosingTlv - val nextLocalNonceTlvCodec: Codec[NextLocalNonceTlv] = tlvField(publicNonce) + val nextLocalNonceCodec: Codec[NextLocalNonceTlv] = tlvField(publicNonce) - case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends ChannelReestablishTlv + /** Partial signature along with the signer's nonce, which is usually randomly created at signing time (when using taproot channels). */ + case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends FundingCreatedTlv with FundingSignedTlv with ClosingTlv + + val partialSignatureWithNonceCodec: Codec[PartialSignatureWithNonceTlv] = tlvField(partialSignatureWithNonce) - val nextLocalNoncesTlvCodec: Codec[NextLocalNoncesTlv] = tlvField(list(txId ~ publicNonce).xmap[Seq[(TxId, IndividualNonce)]](_.toSeq, _.toList)) } object OpenChannelTlv { @@ -109,7 +110,7 @@ object OpenChannelTlv { val openTlvCodec: Codec[TlvStream[OpenChannelTlv]] = tlvStream(discriminated[OpenChannelTlv].by(varint) .typecase(UInt64(0), upfrontShutdownScriptCodec) .typecase(UInt64(1), channelTypeCodec) - .typecase(UInt64(4), nextLocalNonceTlvCodec) + .typecase(UInt64(4), nextLocalNonceCodec) ) } @@ -121,7 +122,7 @@ object AcceptChannelTlv { val acceptTlvCodec: Codec[TlvStream[AcceptChannelTlv]] = tlvStream(discriminated[AcceptChannelTlv].by(varint) .typecase(UInt64(0), upfrontShutdownScriptCodec) .typecase(UInt64(1), channelTypeCodec) - .typecase(UInt64(4), nextLocalNonceTlvCodec) + .typecase(UInt64(4), nextLocalNonceCodec) ) } @@ -220,17 +221,11 @@ object AcceptDualFundedChannelTlv { } -case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends FundingCreatedTlv with FundingSignedTlv with ClosingTlv - -object PartialSignatureWithNonceTlv { - val codec: Codec[PartialSignatureWithNonceTlv] = tlvField(partialSignatureWithNonce) -} - sealed trait FundingCreatedTlv extends Tlv object FundingCreatedTlv { val fundingCreatedTlvCodec: Codec[TlvStream[FundingCreatedTlv]] = tlvStream(discriminated[FundingCreatedTlv].by(varint) - .typecase(UInt64(2), PartialSignatureWithNonceTlv.codec) + .typecase(UInt64(2), ChannelTlv.partialSignatureWithNonceCodec) ) } @@ -238,7 +233,7 @@ sealed trait FundingSignedTlv extends Tlv object FundingSignedTlv { val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint) - .typecase(UInt64(2), PartialSignatureWithNonceTlv.codec) + .typecase(UInt64(2), ChannelTlv.partialSignatureWithNonceCodec) ) } @@ -252,7 +247,7 @@ object ChannelReadyTlv { val channelReadyTlvCodec: Codec[TlvStream[ChannelReadyTlv]] = tlvStream(discriminated[ChannelReadyTlv].by(varint) .typecase(UInt64(1), channelAliasTlvCodec) - .typecase(UInt64(4), nextLocalNonceTlvCodec) + .typecase(UInt64(4), ChannelTlv.nextLocalNonceCodec) ) } @@ -265,12 +260,18 @@ object ChannelReestablishTlv { case class MyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv /** - * When disconnected during an interactive tx session, we'll include a verification nonce for our *current* commitment (using the - * session's commitment index) which our peer may need to re-send a commit sig for our current commit tx - * + * When disconnected during an interactive tx session, we'll include a verification nonce for our *current* commitment + * which our peer will need to re-send a commit sig for our current commitment transaction spending the interactive tx. */ case class CurrentCommitNonceTlv(nonce: IndividualNonce) extends ChannelReestablishTlv + /** + * Verification nonces used for the next commitment transaction, when using taproot channels. + * There must be a nonce for each active commitment (when there are pending splices or RBF attempts), indexed by the + * corresponding fundingTxId. + */ + case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends ChannelReestablishTlv + object NextFundingTlv { val codec: Codec[NextFundingTlv] = tlvField(txIdAsHash) } @@ -287,11 +288,15 @@ object ChannelReestablishTlv { val codec: Codec[CurrentCommitNonceTlv] = tlvField("current_commit_nonce" | publicNonce) } + object NextLocalNoncesTlv { + val codec: Codec[NextLocalNoncesTlv] = tlvField(list(txIdAsHash ~ publicNonce).xmap[Seq[(TxId, IndividualNonce)]](_.toSeq, _.toList)) + } + val channelReestablishTlvCodec: Codec[TlvStream[ChannelReestablishTlv]] = tlvStream(discriminated[ChannelReestablishTlv].by(varint) .typecase(UInt64(0), NextFundingTlv.codec) .typecase(UInt64(1), YourLastFundingLockedTlv.codec) .typecase(UInt64(3), MyCurrentFundingLockedTlv.codec) - .typecase(UInt64(4), nextLocalNoncesTlvCodec) + .typecase(UInt64(4), NextLocalNoncesTlv.codec) .typecase(UInt64(6), CurrentCommitNonceTlv.codec) ) } @@ -305,6 +310,7 @@ object UpdateFeeTlv { sealed trait ShutdownTlv extends Tlv object ShutdownTlv { + /** When closing taproot channels, local nonce that will be used to sign the remote closing transaction. */ case class ShutdownNonce(nonce: IndividualNonce) extends ShutdownTlv private val shutdownNonceCodec: Codec[ShutdownNonce] = tlvField(publicNonce) @@ -339,19 +345,18 @@ object ClosingTlv { /** Signature for a closing transaction containing the closer and closee's outputs. */ case class CloserAndCloseeOutputs(sig: ByteVector64) extends ClosingTlv with ClosingCompleteTlv with ClosingSigTlv - - /** Signature for a closing transaction containing only the closer's output. */ } sealed trait ClosingCompleteTlv extends ClosingTlv object ClosingCompleteTlv { + /** When closing taproot channels, partial signature for a closing transaction containing only the closer's output. */ case class CloserOutputOnlyPartialSignature(partialSignature: PartialSignatureWithNonce) extends ClosingCompleteTlv - /** Signature for a closing transaction containing only the closee's output. */ + /** When closing taproot channels, partial signature for a closing transaction containing only the closee's output. */ case class CloseeOutputOnlyPartialSignature(partialSignature: PartialSignatureWithNonce) extends ClosingCompleteTlv - /** Signature for a closing transaction containing the closer and closee's outputs. */ + /** When closing taproot channels, partial signature for a closing transaction containing the closer and closee's outputs. */ case class CloserAndCloseeOutputsPartialSignature(partialSignature: PartialSignatureWithNonce) extends ClosingCompleteTlv val closingCompleteTlvCodec: Codec[TlvStream[ClosingCompleteTlv]] = tlvStream(discriminated[ClosingCompleteTlv].by(varint) @@ -367,15 +372,17 @@ object ClosingCompleteTlv { sealed trait ClosingSigTlv extends ClosingTlv object ClosingSigTlv { + /** When closing taproot channels, partial signature for a closing transaction containing only the closer's output. */ case class CloserOutputOnlyPartialSignature(partialSignature: ByteVector32) extends ClosingSigTlv - /** Signature for a closing transaction containing only the closee's output. */ + /** When closing taproot channels, partial signature for a closing transaction containing only the closee's output. */ case class CloseeOutputOnlyPartialSignature(partialSignature: ByteVector32) extends ClosingSigTlv - /** Signature for a closing transaction containing the closer and closee's outputs. */ + /** When closing taproot channels, partial signature for a closing transaction containing the closer and closee's outputs. */ case class CloserAndCloseeOutputsPartialSignature(partialSignature: ByteVector32) extends ClosingSigTlv - case class NextCloseeNonce(closeeNonce: IndividualNonce) extends ClosingSigTlv + /** When closing taproot channels, local nonce that will be used to sign the next remote closing transaction. */ + case class NextCloseeNonce(nonce: IndividualNonce) extends ClosingSigTlv val closingSigTlvCodec: Codec[TlvStream[ClosingSigTlv]] = tlvStream(discriminated[ClosingSigTlv].by(varint) .typecase(UInt64(1), tlvField(bytes64.as[ClosingTlv.CloserOutputOnly])) 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 5ba4cba830..29894f80f1 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 @@ -23,7 +23,6 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel.{ChannelFlags, ShortIdAliases} import fr.acinq.eclair.crypto.Mac32 -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, LegacySimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId} import fr.acinq.secp256k1.Secp256k1 import org.apache.commons.codec.binary.Base32 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 029d9ec77c..5482c68fb4 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 @@ -20,8 +20,8 @@ import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.TxId import fr.acinq.eclair.UInt64 -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tu16} import scodec.bits.{ByteVector, HexStringSyntax} @@ -97,6 +97,7 @@ object CommitSigTlv { val codec: Codec[BatchTlv] = tlvField(tu16) } + /** Partial signature signature for the current commitment transaction, along with the signing nonce used (when using taproot channels). */ case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends CommitSigTlv object PartialSignatureWithNonceTlv { @@ -113,20 +114,19 @@ object CommitSigTlv { sealed trait RevokeAndAckTlv extends Tlv object RevokeAndAckTlv { + + /** + * Verification nonces used for the next commitment transaction, when using taproot channels. + * There must be a nonce for each active commitment (when there are pending splices or RBF attempts), indexed by the + * corresponding fundingTxId. + */ case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends RevokeAndAckTlv object NextLocalNoncesTlv { - val codec: Codec[NextLocalNoncesTlv] = tlvField(list(txId ~ publicNonce).xmap[Seq[(TxId, IndividualNonce)]](_.toSeq, _.toList)) - } - - case class NextLocalNonceTlv(nonce: IndividualNonce) extends RevokeAndAckTlv - - object NextLocalNonceTlv { - val codec: Codec[NextLocalNonceTlv] = tlvField(publicNonce) + val codec: Codec[NextLocalNoncesTlv] = tlvField(list(txIdAsHash ~ publicNonce).xmap[Seq[(TxId, IndividualNonce)]](_.toSeq, _.toList)) } val revokeAndAckTlvCodec: Codec[TlvStream[RevokeAndAckTlv]] = tlvStream(discriminated[RevokeAndAckTlv].by(varint) - .typecase(UInt64(4), NextLocalNonceTlv.codec) .typecase(UInt64(6), 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 908d5a9bf6..d13c38b9b9 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 @@ -63,20 +63,21 @@ sealed trait TxCompleteTlv extends Tlv object TxCompleteTlv { /** - * Musig2 nonces exchanged during an interactive tx session + * Musig2 nonces exchanged during an interactive tx session, when using a taproot channel or upgrading a channel to + * use taproot. * - * @param remoteNonce a verification nonce for the session commitment transaction - * @param nextRemoteNonce a verification nonce that will be added to the channel's nonce map once the session completes - * @param fundingNonce_opt an optional nonce to spend the session's shared input + * @param commitNonce the sender's verification nonce for the current commit tx spending the interactive tx. + * @param nextCommitNonce the sender's verification nonce for the next commit tx spending the interactive tx. + * @param fundingNonce_opt when splicing a taproot channel, the sender's random signing nonce for the previous funding output. */ - case class Nonces(remoteNonce: IndividualNonce, nextRemoteNonce: IndividualNonce, fundingNonce_opt: Option[IndividualNonce]) extends TxCompleteTlv + case class Nonces(commitNonce: IndividualNonce, nextCommitNonce: IndividualNonce, fundingNonce_opt: Option[IndividualNonce]) extends TxCompleteTlv object Nonces { - val codec: Codec[Nonces] = (publicNonce :: publicNonce :: optional(bitsRemaining, publicNonce)).as[Nonces] + val codec: Codec[Nonces] = tlvField((publicNonce :: publicNonce :: optional(bitsRemaining, publicNonce)).as[Nonces]) } val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint) - .typecase(UInt64(4), tlvField[Nonces, Nonces](Nonces.codec)) + .typecase(UInt64(4), Nonces.codec) ) } @@ -86,14 +87,11 @@ 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 + /** When doing a splice for a taproot channel, each peer must provide their partial signature for the previous musig2 funding output. */ 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(2), tlvField(partialSignatureWithNonce.as[PreviousFundingTxPartialSig])) .typecase(UInt64(601), tlvField(bytes64.as[PreviousFundingTxSig])) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index cc796d2655..f506a3cc57 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.ScriptWitness +import fr.acinq.eclair.channel.ChannelSpendSignature import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.{Features, InitFeature, KamonExt} @@ -273,7 +274,7 @@ object LightningMessageCodecs { val commitSigCodec: Codec[CommitSig] = ( ("channelId" | bytes32) :: - ("signature" | bytes64) :: + ("signature" | bytes64.as[ChannelSpendSignature.IndividualSignature]) :: ("htlcSignatures" | listofsignatures) :: ("tlvStream" | CommitSigTlv.commitSigTlvCodec)).as[CommitSig] 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 633fc52f97..74129f5312 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 @@ -22,12 +22,10 @@ import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, OutPoint, Satoshi, SatoshiLong, ScriptWitness, Transaction, TxId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.{ChannelFlags, ChannelSpendSignature, ChannelType} import fr.acinq.eclair.payment.relay.Relayer -import fr.acinq.eclair.transactions.Transactions.CommitmentFormat import fr.acinq.eclair.wire.protocol.ChannelReadyTlv.ShortChannelIdTlv -import fr.acinq.eclair.wire.protocol.ChannelTlv.NextLocalNonceTlv import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, isAsciiPrintable} import scodec.bits.ByteVector @@ -125,10 +123,8 @@ case class TxComplete(channelId: ByteVector32, } object TxComplete { - def apply(channelId: ByteVector32): TxComplete = TxComplete(channelId, TlvStream.empty) - - def apply(channelId: ByteVector32, remoteNonce: IndividualNonce, nextRemoteNonce: IndividualNonce, fundingNonce_opt: Option[IndividualNonce]): TxComplete = - TxComplete(channelId, TlvStream(TxCompleteTlv.Nonces(remoteNonce, nextRemoteNonce, fundingNonce_opt))) + def apply(channelId: ByteVector32, commitNonce: IndividualNonce, nextCommitNonce: IndividualNonce, fundingNonce_opt: Option[IndividualNonce]): TxComplete = + TxComplete(channelId, TlvStream(TxCompleteTlv.Nonces(commitNonce, nextCommitNonce, fundingNonce_opt))) } case class TxSignatures(channelId: ByteVector32, @@ -140,10 +136,12 @@ case class TxSignatures(channelId: ByteVector32, } object TxSignatures { - def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ByteVector64], previousFundingTxPartialSig_opt: Option[PartialSignatureWithNonce]): TxSignatures = { + def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ChannelSpendSignature]): TxSignatures = { val tlvs: Set[TxSignaturesTlv] = Set( - previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig), - previousFundingTxPartialSig_opt.map(p => TxSignaturesTlv.PreviousFundingTxPartialSig(p)) + previousFundingSig_opt.map { + case IndividualSignature(sig) => TxSignaturesTlv.PreviousFundingTxSig(sig) + case partialSig: PartialSignatureWithNonce => TxSignaturesTlv.PreviousFundingTxPartialSig(partialSig) + } ).flatten TxSignatures(channelId, tx.txid, witnesses, TlvStream(tlvs)) } @@ -205,7 +203,7 @@ case class ChannelReestablish(channelId: ByteVector32, val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId) val myCurrentFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].map(_.txId) val yourLastFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.YourLastFundingLockedTlv].map(_.txId) - val nextLocalNonces: Map[TxId, IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNoncesTlv].map(_.nonces.toMap).getOrElse(Map.empty) + val nextCommitNonces: Map[TxId, IndividualNonce] = tlvStream.get[ChannelReestablishTlv.NextLocalNoncesTlv].map(_.nonces.toMap).getOrElse(Map.empty) val currentCommitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelReestablishTlv.CurrentCommitNonceTlv].map(_.nonce) } @@ -230,7 +228,7 @@ case class OpenChannel(chainHash: BlockHash, tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) - val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) + val commitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) } case class AcceptChannel(temporaryChannelId: ByteVector32, @@ -250,7 +248,7 @@ case class AcceptChannel(temporaryChannelId: ByteVector32, tlvStream: TlvStream[AcceptChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) - val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) + val commitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) } // NB: this message is named open_channel2 in the specification. @@ -312,20 +310,63 @@ case class FundingCreated(temporaryChannelId: ByteVector32, fundingOutputIndex: Int, signature: ByteVector64, tlvStream: TlvStream[FundingCreatedTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { - val sigOrPartialSig: ChannelSpendSignature = tlvStream.get[PartialSignatureWithNonceTlv].map(_.partialSigWithNonce).getOrElse(ChannelSpendSignature.IndividualSignature(signature)) + val sigOrPartialSig: ChannelSpendSignature = tlvStream.get[ChannelTlv.PartialSignatureWithNonceTlv].map(_.partialSigWithNonce).getOrElse(IndividualSignature(signature)) +} + +object FundingCreated { + def apply(temporaryChannelId: ByteVector32, fundingTxId: TxId, fundingOutputIndex: Int, sig: ChannelSpendSignature): FundingCreated = { + val individualSig = sig match { + case IndividualSignature(sig) => sig + case _: PartialSignatureWithNonce => ByteVector64.Zeroes + } + val tlvs = sig match { + case _: IndividualSignature => TlvStream.empty[FundingCreatedTlv] + case psig: PartialSignatureWithNonce => TlvStream[FundingCreatedTlv](ChannelTlv.PartialSignatureWithNonceTlv(psig)) + } + FundingCreated(temporaryChannelId, fundingTxId, fundingOutputIndex, individualSig, tlvs) + } } case class FundingSigned(channelId: ByteVector32, signature: ByteVector64, tlvStream: TlvStream[FundingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { - val sigOrPartialSig: ChannelSpendSignature = tlvStream.get[PartialSignatureWithNonceTlv].map(_.partialSigWithNonce).getOrElse(ChannelSpendSignature.IndividualSignature(signature)) + val sigOrPartialSig: ChannelSpendSignature = tlvStream.get[ChannelTlv.PartialSignatureWithNonceTlv].map(_.partialSigWithNonce).getOrElse(IndividualSignature(signature)) +} + +object FundingSigned { + def apply(channelId: ByteVector32, sig: ChannelSpendSignature): FundingSigned = { + val individualSig = sig match { + case IndividualSignature(sig) => sig + case _: PartialSignatureWithNonce => ByteVector64.Zeroes + } + val tlvs = sig match { + case _: IndividualSignature => TlvStream.empty[FundingSignedTlv] + case psig: PartialSignatureWithNonce => TlvStream[FundingSignedTlv](ChannelTlv.PartialSignatureWithNonceTlv(psig)) + } + FundingSigned(channelId, individualSig, tlvs) + } } case class ChannelReady(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, tlvStream: TlvStream[ChannelReadyTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { val alias_opt: Option[Alias] = tlvStream.get[ShortChannelIdTlv].map(_.alias) - val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) + val nextCommitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) +} + +object ChannelReady { + def apply(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, alias: Alias): ChannelReady = { + val tlvs = TlvStream[ChannelReadyTlv](ChannelReadyTlv.ShortChannelIdTlv(alias)) + ChannelReady(channelId, nextPerCommitmentPoint, tlvs) + } + + def apply(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, alias: Alias, nextCommitNonce: IndividualNonce): ChannelReady = { + val tlvs = TlvStream[ChannelReadyTlv]( + ChannelReadyTlv.ShortChannelIdTlv(alias), + ChannelTlv.NextLocalNonceTlv(nextCommitNonce), + ) + ChannelReady(channelId, nextPerCommitmentPoint, tlvs) + } } case class Stfu(channelId: ByteVector32, initiator: Boolean) extends SetupMessage with HasChannelId @@ -393,7 +434,11 @@ case class SpliceLocked(channelId: ByteVector32, case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector, tlvStream: TlvStream[ShutdownTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId with ForbiddenMessageWhenQuiescent { - val shutdownNonce_opt: Option[IndividualNonce] = tlvStream.get[ShutdownTlv.ShutdownNonce].map(_.nonce) + val closeeNonce_opt: Option[IndividualNonce] = tlvStream.get[ShutdownTlv.ShutdownNonce].map(_.nonce) +} + +object Shutdown { + def apply(channelId: ByteVector32, scriptPubKey: ByteVector, closeeNonce: IndividualNonce): Shutdown = Shutdown(channelId, scriptPubKey, TlvStream[ShutdownTlv](ShutdownTlv.ShutdownNonce(closeeNonce))) } case class ClosingSigned(channelId: ByteVector32, @@ -419,7 +464,7 @@ case class ClosingSig(channelId: ByteVector32, closerScriptPubKey: ByteVector, c val closerOutputOnlyPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingSigTlv.CloserOutputOnlyPartialSignature].map(_.partialSignature) val closeeOutputOnlyPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingSigTlv.CloseeOutputOnlyPartialSignature].map(_.partialSignature) val closerAndCloseeOutputsPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingSigTlv.CloserAndCloseeOutputsPartialSignature].map(_.partialSignature) - val nextClosingNonce_opt: Option[IndividualNonce] = tlvStream.get[ClosingSigTlv.NextCloseeNonce].map(_.closeeNonce) + val nextCloseeNonce_opt: Option[IndividualNonce] = tlvStream.get[ClosingSigTlv.NextCloseeNonce].map(_.nonce) } case class UpdateAddHtlc(channelId: ByteVector32, @@ -488,11 +533,25 @@ object CommitSigs { } case class CommitSig(channelId: ByteVector32, - signature: ByteVector64, + signature: IndividualSignature, htlcSignatures: List[ByteVector64], tlvStream: TlvStream[CommitSigTlv] = TlvStream.empty) extends CommitSigs { val partialSignature_opt: Option[PartialSignatureWithNonce] = tlvStream.get[CommitSigTlv.PartialSignatureWithNonceTlv].map(_.partialSigWithNonce) - val sigOrPartialSig: ChannelSpendSignature = partialSignature_opt.getOrElse(ChannelSpendSignature.IndividualSignature(signature)) + val sigOrPartialSig: ChannelSpendSignature = partialSignature_opt.getOrElse(signature) +} + +object CommitSig { + def apply(channelId: ByteVector32, signature: ChannelSpendSignature, htlcSignatures: List[ByteVector64], batchSize: Int): CommitSig = { + val (individualSig, partialSig_opt) = signature match { + case sig: IndividualSignature => (sig, None) + case psig: PartialSignatureWithNonce => (IndividualSignature(ByteVector64.Zeroes), Some(psig)) + } + val tlvs = Set( + if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None, + partialSig_opt.map(CommitSigTlv.PartialSignatureWithNonceTlv(_)) + ).flatten[CommitSigTlv] + CommitSig(channelId, individualSig, htlcSignatures, TlvStream(tlvs)) + } } case class CommitSigBatch(messages: Seq[CommitSig]) extends CommitSigs { @@ -505,8 +564,16 @@ case class RevokeAndAck(channelId: ByteVector32, perCommitmentSecret: PrivateKey, nextPerCommitmentPoint: PublicKey, tlvStream: TlvStream[RevokeAndAckTlv] = TlvStream.empty) extends HtlcMessage with HasChannelId { - val nexLocalNonces: Map[TxId, IndividualNonce] = tlvStream.get[RevokeAndAckTlv.NextLocalNoncesTlv].map(_.nonces.toMap).getOrElse(Map.empty) - val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[RevokeAndAckTlv.NextLocalNonceTlv].map(_.nonce) + val nextCommitNonces: Map[TxId, IndividualNonce] = tlvStream.get[RevokeAndAckTlv.NextLocalNoncesTlv].map(_.nonces.toMap).getOrElse(Map.empty) +} + +object RevokeAndAck { + def apply(channelId: ByteVector32, perCommitmentSecret: PrivateKey, nextPerCommitmentPoint: PublicKey, nextCommitNonces: Seq[(TxId, IndividualNonce)]): RevokeAndAck = { + val tlvs = Set( + if (nextCommitNonces.nonEmpty) Some(RevokeAndAckTlv.NextLocalNoncesTlv(nextCommitNonces)) else None + ).flatten[RevokeAndAckTlv] + RevokeAndAck(channelId, perCommitmentSecret, nextPerCommitmentPoint, TlvStream(tlvs)) + } } case class UpdateFee(channelId: ByteVector32, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index 4c867e8026..27a919600b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala @@ -5,11 +5,12 @@ import fr.acinq.eclair.balance.CheckBalance.{MainAndHtlcBalance, OffChainBalance import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{apply => _, _} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ import fr.acinq.eclair.db.pg.PgUtils.using import fr.acinq.eclair.testutils.PimpTestProbe.convert -import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, ClaimHtlcTimeoutTx, ClaimLocalAnchorTx, ClaimRemoteAnchorTx} +import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, ClaimHtlcTimeoutTx, ClaimRemoteAnchorTx} import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} @@ -87,7 +88,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with fulfillHtlc(htlcb.id, rb, alice, bob, alice2bob, bob2alice) // Bob publishes his current commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 8) // two anchor outputs, two main outputs and 4 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) // In response to that, alice publishes her claim txs. @@ -136,7 +137,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[RevokeAndAck] // Bob publishes his next commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 7) // two anchor outputs, two main outputs and 3 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) // In response to that, alice publishes her claim txs diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 0f56624625..28611e0c5b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -91,7 +91,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc1.availableBalanceForSend == b) assert(bc1.availableBalanceForReceive == a - p - htlcOutputFee) - val Right((ac2, commit1)) = ac1.sendCommit(alice.underlyingActor.channelKeys) + val Right((ac2, commit1)) = ac1.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac2.availableBalanceForSend == a - p - htlcOutputFee) assert(ac2.availableBalanceForReceive == b) @@ -103,7 +103,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac3.availableBalanceForSend == a - p - htlcOutputFee) assert(ac3.availableBalanceForReceive == b) - val Right((bc3, commit2)) = bc2.sendCommit(bob.underlyingActor.channelKeys) + val Right((bc3, commit2)) = bc2.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc3.availableBalanceForSend == b) assert(bc3.availableBalanceForReceive == a - p - htlcOutputFee) @@ -124,7 +124,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac5.availableBalanceForSend == a - p - htlcOutputFee) assert(ac5.availableBalanceForReceive == b + p) - val Right((bc6, commit3)) = bc5.sendCommit(bob.underlyingActor.channelKeys) + val Right((bc6, commit3)) = bc5.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc6.availableBalanceForSend == b + p) assert(bc6.availableBalanceForReceive == a - p - htlcOutputFee) @@ -136,7 +136,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc7.availableBalanceForSend == b + p) assert(bc7.availableBalanceForReceive == a - p) - val Right((ac7, commit4)) = ac6.sendCommit(alice.underlyingActor.channelKeys) + val Right((ac7, commit4)) = ac6.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac7.availableBalanceForSend == a - p) assert(ac7.availableBalanceForReceive == b + p) @@ -176,7 +176,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc1.availableBalanceForSend == b) assert(bc1.availableBalanceForReceive == a - p - htlcOutputFee) - val Right((ac2, commit1)) = ac1.sendCommit(alice.underlyingActor.channelKeys) + val Right((ac2, commit1)) = ac1.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac2.availableBalanceForSend == a - p - htlcOutputFee) assert(ac2.availableBalanceForReceive == b) @@ -188,7 +188,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac3.availableBalanceForSend == a - p - htlcOutputFee) assert(ac3.availableBalanceForReceive == b) - val Right((bc3, commit2)) = bc2.sendCommit(bob.underlyingActor.channelKeys) + val Right((bc3, commit2)) = bc2.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc3.availableBalanceForSend == b) assert(bc3.availableBalanceForReceive == a - p - htlcOutputFee) @@ -209,7 +209,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac5.availableBalanceForSend == a - p - htlcOutputFee) assert(ac5.availableBalanceForReceive == b) - val Right((bc6, commit3)) = bc5.sendCommit(bob.underlyingActor.channelKeys) + val Right((bc6, commit3)) = bc5.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc6.availableBalanceForSend == b) assert(bc6.availableBalanceForReceive == a - p - htlcOutputFee) @@ -221,7 +221,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc7.availableBalanceForSend == b) assert(bc7.availableBalanceForReceive == a) - val Right((ac7, commit4)) = ac6.sendCommit(alice.underlyingActor.channelKeys) + val Right((ac7, commit4)) = ac6.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac7.availableBalanceForSend == a) assert(ac7.availableBalanceForReceive == b) @@ -282,7 +282,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac3.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee) assert(ac3.availableBalanceForReceive == b - p3) - val Right((ac4, commit1)) = ac3.sendCommit(alice.underlyingActor.channelKeys) + val Right((ac4, commit1)) = ac3.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac4.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee) assert(ac4.availableBalanceForReceive == b - p3) @@ -294,7 +294,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac5.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee) assert(ac5.availableBalanceForReceive == b - p3) - val Right((bc5, commit2)) = bc4.sendCommit(bob.underlyingActor.channelKeys) + val Right((bc5, commit2)) = bc4.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc5.availableBalanceForSend == b - p3) assert(bc5.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee) @@ -306,7 +306,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc6.availableBalanceForSend == b - p3) assert(bc6.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) - val Right((ac7, commit3)) = ac6.sendCommit(alice.underlyingActor.channelKeys) + val Right((ac7, commit3)) = ac6.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac7.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) assert(ac7.availableBalanceForReceive == b - p3) @@ -345,7 +345,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc10.availableBalanceForSend == b + p1 - p3) assert(bc10.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) // the fee for p3 disappears - val Right((ac12, commit4)) = ac11.sendCommit(alice.underlyingActor.channelKeys) + val Right((ac12, commit4)) = ac11.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac12.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) assert(ac12.availableBalanceForReceive == b + p1 - p3) @@ -357,7 +357,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(ac13.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) assert(ac13.availableBalanceForReceive == b + p1 - p3) - val Right((bc12, commit5)) = bc11.sendCommit(bob.underlyingActor.channelKeys) + val Right((bc12, commit5)) = bc11.sendCommit(bob.underlyingActor.channelKeys, Map.empty) assert(bc12.availableBalanceForSend == b + p1 - p3) assert(bc12.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) @@ -369,7 +369,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bc13.availableBalanceForSend == b + p1 - p3) assert(bc13.availableBalanceForReceive == a - p1 + p3) - val Right((ac15, commit6)) = ac14.sendCommit(alice.underlyingActor.channelKeys) + val Right((ac15, commit6)) = ac14.sendCommit(alice.underlyingActor.channelKeys, Map.empty) assert(ac15.availableBalanceForSend == a - p1 + p3) assert(ac15.availableBalanceForReceive == b + p1 - p3) 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 999c72ef76..82ffd78545 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 @@ -21,10 +21,9 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter import akka.pattern.pipe import akka.testkit.TestProbe import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Musig2, OP_1, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxHash, TxId, TxIn, TxOut, addressToPublicKeyScript} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, OP_1, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxHash, TxId, TxIn, TxOut, addressToPublicKeyScript} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, ProcessPsbtResponse} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService @@ -32,14 +31,15 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{MempoolTx, Utx import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinCoreClient, BitcoinJsonRPCClient} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.blockchain.{OnChainWallet, SingleKeyOnChainWallet} +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} import fr.acinq.eclair.crypto.keymanager.ChannelKeys import fr.acinq.eclair.io.OpenChannelInterceptor.makeChannelParams -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, InputInfo, SimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, InputInfo, PhoenixSimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat} import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Feature, FeatureSupport, Features, InitFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, UInt64, randomBytes32, randomKey} +import fr.acinq.eclair.{Feature, FeatureSupport, Features, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, UInt64, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.{ByteVector, HexStringSyntax} @@ -51,9 +51,6 @@ import scala.reflect.ClassTag class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with BeforeAndAfterAll { - val channelType: SupportedChannelType = ChannelTypes.AnchorOutputsZeroFeeHtlcTx() - val txCompleteNonces: collection.mutable.HashSet[IndividualNonce] = collection.mutable.HashSet.empty[IndividualNonce] - override def beforeAll(): Unit = { startBitcoind() waitForBitcoindReady() @@ -97,8 +94,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit fundingParamsB: InteractiveTxParams, nodeParamsB: NodeParams, channelParamsB: ChannelParams, - commitParamsB: CommitParams, - channelFeatures: ChannelFeatures) { + commitParamsB: CommitParams) { val channelId: ByteVector32 = fundingParamsA.channelId val commitFeerate: FeeratePerKw = TestConstants.anchorOutputsFeeratePerKw val channelKeysA: ChannelKeys = nodeParamsA.channelKeyManager.channelKeys(channelParamsA.channelConfig, channelParamsA.localParams.fundingKeyPath) @@ -106,7 +102,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit private val firstPerCommitmentPointA = channelKeysA.commitmentPoint(0) private val firstPerCommitmentPointB = channelKeysB.commitmentPoint(0) - val fundingPubkeyScript: ByteVector = Transactions.makeFundingScript(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey, channelType.commitmentFormat).pubkeyScript + val fundingPubkeyScript: ByteVector = Transactions.makeFundingScript(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey, fundingParamsA.commitmentFormat).pubkeyScript def sharedInputs(commitmentA: Commitment, commitmentB: Commitment): (SharedFundingInput, SharedFundingInput) = { val sharedInputA = SharedFundingInput(channelKeysA, commitmentA) @@ -123,11 +119,12 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit SharedFundingInput(inputInfo, fundingTxIndex, fundingParamsA.remoteFundingPubKey, fundingParamsA.commitmentFormat) } - def createSpliceFixtureParams(fundingTxIndex: Long, fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, sharedInputA: SharedFundingInput, sharedInputB: SharedFundingInput, spliceOutputsA: List[TxOut] = Nil, spliceOutputsB: List[TxOut] = Nil, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false)): FixtureParams = { + def createSpliceFixtureParams(fundingTxIndex: Long, fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, sharedInputA: SharedFundingInput, sharedInputB: SharedFundingInput, nextCommitmentFormat_opt: Option[CommitmentFormat] = None, spliceOutputsA: List[TxOut] = Nil, spliceOutputsB: List[TxOut] = Nil, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false)): FixtureParams = { val fundingPubKeyA = channelKeysA.fundingKey(fundingTxIndex).publicKey val fundingPubKeyB = channelKeysB.fundingKey(fundingTxIndex).publicKey - val fundingParamsA1 = InteractiveTxParams(channelId, isInitiator = true, fundingAmountA, fundingAmountB, Some(sharedInputA), fundingPubKeyB, spliceOutputsA, fundingParamsA.commitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) - val fundingParamsB1 = InteractiveTxParams(channelId, isInitiator = false, fundingAmountB, fundingAmountA, Some(sharedInputB), fundingPubKeyA, spliceOutputsB, fundingParamsB.commitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) + val nextCommitmentFormat = nextCommitmentFormat_opt.getOrElse(fundingParamsA.commitmentFormat) + val fundingParamsA1 = InteractiveTxParams(channelId, isInitiator = true, fundingAmountA, fundingAmountB, Some(sharedInputA), fundingPubKeyB, spliceOutputsA, nextCommitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) + val fundingParamsB1 = InteractiveTxParams(channelId, isInitiator = false, fundingAmountB, fundingAmountA, Some(sharedInputB), fundingPubKeyA, spliceOutputsB, nextCommitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) copy(fundingParamsA = fundingParamsA1, fundingParamsB = fundingParamsB1) } @@ -220,9 +217,8 @@ 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), nonInitiatorPaysCommitTxFees: Boolean = false): FixtureParams = { - val channelFeatures = ChannelFeatures(channelType, Features[InitFeature](Features.DualFunding -> FeatureSupport.Optional), Features[InitFeature](Features.DualFunding -> FeatureSupport.Optional), announceChannel = true) - val Seq(nodeParamsA, nodeParamsB) = Seq(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams).map(_.copy(features = Features(channelFeatures.features.map(f => f -> FeatureSupport.Optional).toMap[Feature, FeatureSupport]))) + private def createFixtureParams(channelType: SupportedChannelType, fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false), nonInitiatorPaysCommitTxFees: Boolean = false): FixtureParams = { + val Seq(nodeParamsA, nodeParamsB) = Seq(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams).map(_.copy(features = Features(channelType.features.map(f => f -> FeatureSupport.Optional).toMap[Feature, FeatureSupport]))) val localChannelParamsA = makeChannelParams(nodeParamsA, nodeParamsA.features.initFeatures(), None, None, isChannelOpener = true, paysCommitTxFees = !nonInitiatorPaysCommitTxFees, dualFunded = true, fundingAmountA) val commitParamsA = CommitParams(nodeParamsA.channelConf.dustLimit, nodeParamsA.channelConf.htlcMinimum, nodeParamsA.channelConf.maxHtlcValueInFlight(fundingAmountA + fundingAmountB, unlimited = false), nodeParamsA.channelConf.maxAcceptedHtlcs, nodeParamsB.channelConf.toRemoteDelay) val localChannelParamsB = makeChannelParams(nodeParamsB, nodeParamsB.features.initFeatures(), None, None, isChannelOpener = false, paysCommitTxFees = nonInitiatorPaysCommitTxFees, dualFunded = true, fundingAmountB) @@ -248,10 +244,10 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val fundingPubKeyB = channelKeysB.fundingKey(fundingTxIndex = 0).publicKey val fundingParamsA = InteractiveTxParams(channelId, isInitiator = true, fundingAmountA, fundingAmountB, None, fundingPubKeyB, Nil, channelType.commitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) val fundingParamsB = InteractiveTxParams(channelId, isInitiator = false, fundingAmountB, fundingAmountA, None, fundingPubKeyA, Nil, channelType.commitmentFormat, lockTime, dustLimit, targetFeerate, requireConfirmedInputs) - val channelParamsA = ChannelParams(channelId, ChannelConfig.standard, channelFeatures, localChannelParamsA, remoteChannelParamsB, ChannelFlags(announceChannel = true)) - val channelParamsB = ChannelParams(channelId, ChannelConfig.standard, channelFeatures, localChannelParamsB, remoteChannelParamsA, ChannelFlags(announceChannel = true)) + val channelParamsA = ChannelParams(channelId, ChannelConfig.standard, ChannelFeatures(Features.DualFunding), localChannelParamsA, remoteChannelParamsB, ChannelFlags(announceChannel = true)) + val channelParamsB = ChannelParams(channelId, ChannelConfig.standard, ChannelFeatures(Features.DualFunding), localChannelParamsB, remoteChannelParamsA, ChannelFlags(announceChannel = true)) - FixtureParams(fundingParamsA, nodeParamsA, channelParamsA, commitParamsA, fundingParamsB, nodeParamsB, channelParamsB, commitParamsB, channelFeatures) + FixtureParams(fundingParamsA, nodeParamsA, channelParamsA, commitParamsA, fundingParamsB, nodeParamsB, channelParamsB, commitParamsB) } case class Fixture(alice: ActorRef[InteractiveTxBuilder.Command], @@ -281,23 +277,14 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val c = t.runtimeClass.asInstanceOf[Class[T]] assert(c.isInstance(msg), s"expected $c, found ${msg.getClass} ($msg)") msg match { - case msg: InteractiveTxConstructionMessage => - msg match { - case tc: TxComplete => - tc.nonces_opt.foreach(nonces => { - if (txCompleteNonces.contains(nonces.remoteNonce)) fail("nonce reuse") else txCompleteNonces.add(nonces.remoteNonce) - if (txCompleteNonces.contains(nonces.nextRemoteNonce)) fail("nonce reuse") else txCompleteNonces.add(nonces.nextRemoteNonce) - }) - case _ => () - } - r ! ReceiveMessage(msg) + case msg: InteractiveTxConstructionMessage => r ! ReceiveMessage(msg) case msg => fail(s"invalid message sent ($msg)") } msg.asInstanceOf[T] } } - private def withFixture(fundingAmountA: Satoshi, utxosA: Seq[Satoshi], fundingAmountB: Satoshi, utxosB: Seq[Satoshi], targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None)(testFun: Fixture => Any): Unit = { + private def withFixture(channelType: SupportedChannelType, fundingAmountA: Satoshi, utxosA: Seq[Satoshi], fundingAmountB: Satoshi, utxosB: Seq[Satoshi], targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None)(testFun: Fixture => Any): Unit = { // Initialize wallets with a few confirmed utxos. val probe = TestProbe() val rpcClientA = createWallet(UUID.randomUUID().toString) @@ -308,7 +295,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit utxosB.foreach(amount => addUtxo(walletB, amount, probe)) generateBlocks(1) - val fixtureParams = createFixtureParams(fundingAmountA, fundingAmountB, targetFeerate, dustLimit, lockTime, requireConfirmedInputs, nonInitiatorPaysCommitTxFees = liquidityPurchase_opt.nonEmpty) + val fixtureParams = createFixtureParams(channelType, fundingAmountA, fundingAmountB, targetFeerate, dustLimit, lockTime, requireConfirmedInputs, nonInitiatorPaysCommitTxFees = liquidityPurchase_opt.nonEmpty) val alice = fixtureParams.spawnTxBuilderAlice(walletA, liquidityPurchase_opt = liquidityPurchase_opt) val bob = fixtureParams.spawnTxBuilderBob(walletB, liquidityPurchase_opt = liquidityPurchase_opt) testFun(Fixture(alice, bob, fixtureParams, walletA, rpcClientA, walletB, rpcClientB, TestProbe(), TestProbe())) @@ -320,7 +307,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(50_000 sat, 35_000 sat, 60_000 sat) val fundingB = 40_000 sat val utxosB = Seq(100_000 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 42, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 42, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -397,7 +384,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(50_000 sat) val fundingB = 50_000 sat val utxosB = Seq(80_000 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputs(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ alice ! Start(alice2bob.ref) @@ -453,7 +440,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(150_000 sat) val fundingB = 50_000 sat val utxosB = Seq(200_000 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 42, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 42, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -492,7 +479,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(2500 sat) val fundingA = 150_000 sat val utxosA = Seq(80_000 sat, 120_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.SimpleTaprootChannelsStaging(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -505,17 +492,25 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice --- tx_add_input --> Bob fwd.forwardAlice2Bob[TxAddInput] // Alice <-- tx_complete --- Bob - fwd.forwardBob2Alice[TxComplete] + val txCompleteB1 = fwd.forwardBob2Alice[TxComplete] // Alice --- tx_add_output --> Bob val outputA1 = fwd.forwardAlice2Bob[TxAddOutput] // Alice <-- tx_complete --- Bob - fwd.forwardBob2Alice[TxComplete] + val txCompleteB2 = fwd.forwardBob2Alice[TxComplete] // Alice --- tx_add_output --> Bob val outputA2 = fwd.forwardAlice2Bob[TxAddOutput] // Alice <-- tx_complete --- Bob - fwd.forwardBob2Alice[TxComplete] + val txCompleteB3 = fwd.forwardBob2Alice[TxComplete] // Alice --- tx_complete --> Bob - fwd.forwardAlice2Bob[TxComplete] + val txCompleteA = fwd.forwardAlice2Bob[TxComplete] + assert(txCompleteA.nonces_opt.nonEmpty) + assert(txCompleteA.nonces_opt.flatMap(_.fundingNonce_opt).isEmpty) + Seq(txCompleteB1, txCompleteB2, txCompleteB3).foreach(txCompleteB => { + assert(txCompleteB.nonces_opt.nonEmpty) + assert(txCompleteB.nonces_opt.flatMap(_.fundingNonce_opt).isEmpty) + }) + // Nonces change every time the shared transaction changes. + assert(Seq(txCompleteB1, txCompleteB2, txCompleteB3).flatMap(_.nonces_opt).flatMap(n => Seq(n.commitNonce, n.nextCommitNonce)).toSet.size == 6) // Alice is responsible for adding the shared output. assert(aliceParams.fundingAmount == fundingA) @@ -524,8 +519,12 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Bob sends signatures first as he did not contribute at all. val successA = alice2bob.expectMsgType[Succeeded] + assert(successA.commitSig.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) val successB = bob2alice.expectMsgType[Succeeded] + assert(successB.commitSig.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) val (txA, _, txB, _) = fixtureParams.exchangeSigsBobFirst(bobParams, successA, successB) + assert(successA.nextRemoteCommitNonce_opt.contains((txA.txId, txCompleteB3.nonces_opt.get.nextCommitNonce))) + assert(successB.nextRemoteCommitNonce_opt.contains((txB.txId, txCompleteA.nonces_opt.get.nextCommitNonce))) // The resulting transaction is valid and has the right feerate. assert(txA.txId == txB.txId) assert(txA.signedTx.lockTime == aliceParams.lockTime) @@ -544,7 +543,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("initiator uses unconfirmed inputs") { - withFixture(100_000 sat, Seq(170_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, Seq(170_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ // Alice's inputs are all unconfirmed: we spent her only confirmed input to create two unconfirmed outputs. @@ -562,7 +561,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // Alice --- tx_add_input --> Bob fwd.forwardAlice2Bob[TxAddInput] // Alice <-- tx_complete --- Bob - val foo = fwd.forwardBob2Alice[TxComplete] + fwd.forwardBob2Alice[TxComplete] // Alice --- tx_add_input --> Bob fwd.forwardAlice2Bob[TxAddInput] // Alice <-- tx_complete --- Bob @@ -592,7 +591,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit // When on-the-fly funding is used, the initiator may not contribute to the funding transaction. // It will receive HTLCs later that use the purchased inbound liquidity, and liquidity fees will be deduced from those HTLCs. val purchase = LiquidityAds.Purchase.Standard(fundingB, LiquidityAds.Fees(2500 sat, 7500 sat), LiquidityAds.PaymentDetails.FromFutureHtlc(Nil)) - withFixture(0 sat, Nil, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => + withFixture(ChannelTypes.AnchorOutputs(), 0 sat, Nil, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => import f._ alice ! Start(alice2bob.ref) @@ -646,7 +645,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosB = Seq(200_000 sat) // The initiator contributes a small amount, and pays the remaining liquidity fees from its fee credit. val purchase = LiquidityAds.Purchase.WithFeeCredit(fundingB, LiquidityAds.Fees(2500 sat, 7500 sat), 7_500_000 msat, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(Nil)) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => + withFixture(ChannelTypes.AnchorOutputs(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => import f._ // Alice has enough fee credit. @@ -697,7 +696,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosB = Seq(200_000 sat) // The initiator wants to pay the liquidity fees from their fee credit, but they don't have enough of it. val purchase = LiquidityAds.Purchase.WithFeeCredit(fundingB, LiquidityAds.Fees(2500 sat, 7500 sat), 10_000_000 msat, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(Nil)) - withFixture(0 sat, Nil, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => + withFixture(ChannelTypes.AnchorOutputs(), 0 sat, Nil, fundingB, utxosB, targetFeerate, 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false), Some(purchase)) { f => import f._ // Alice doesn't have enough fee credit. @@ -733,7 +732,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(380_000 sat, 380_000 sat) val fundingB1 = 100_000 sat val utxosB = Seq(350_000 sat, 350_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ val probe = TestProbe() @@ -826,7 +825,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(150_000 sat) val fundingB1 = 90_000 sat val utxosB = Seq(130_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, FeeratePerKw(1000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.SimpleTaprootChannelsPhoenix(), fundingA1, utxosA, fundingB1, utxosB, FeeratePerKw(1000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ val probe = TestProbe() @@ -849,17 +848,21 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit fwd.forwardAlice2Bob[TxComplete] val successA1 = alice2bob.expectMsgType[Succeeded] + assert(successA1.nextRemoteCommitNonce_opt.nonEmpty) val successB1 = bob2alice.expectMsgType[Succeeded] + assert(successB1.nextRemoteCommitNonce_opt.nonEmpty) val (txA1, commitmentA1, _, commitmentB1) = fixtureParams.exchangeSigsBobFirst(bobParams, successA1, successB1) walletA.publishTransaction(txA1.signedTx).pipeTo(probe.ref) probe.expectMsg(txA1.txId) // Alice and Bob decide to splice funds out of the channel, and deduce on-chain fees from their new channel contribution. - val spliceOutputsA = List(TxOut(50_000 sat, Script.pay2wpkh(randomKey().publicKey))) - val spliceOutputsB = List(TxOut(30_000 sat, Script.pay2wpkh(randomKey().publicKey))) + val spliceOutputsA = List(TxOut(50_000 sat, Script.pay2tr(randomKey().xOnlyPublicKey()))) + val spliceOutputsB = List(TxOut(30_000 sat, Script.pay2tr(randomKey().xOnlyPublicKey()))) val subtractedFundingA = spliceOutputsA.map(_.amount).sum + 1_000.sat val subtractedFundingB = spliceOutputsB.map(_.amount).sum + 500.sat val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) + assert(sharedInputA.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(sharedInputB.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = -subtractedFundingA, fundingAmountB = -subtractedFundingB, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, spliceOutputsA = spliceOutputsA, spliceOutputsB = spliceOutputsB, requireConfirmedInputs = aliceParams.requireConfirmedInputs) val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(spliceFixtureParams.fundingParamsA, commitmentA1, walletA) @@ -887,9 +890,11 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit fwdSplice.forwardAlice2Bob[TxComplete] val successA2 = alice2bob.expectMsgType[Succeeded] - assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.nonEmpty) + assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.isEmpty) + assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxPartialSig_opt.nonEmpty) val successB2 = bob2alice.expectMsgType[Succeeded] - assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.nonEmpty) + assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.isEmpty) + assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxPartialSig_opt.nonEmpty) val (spliceTxA, commitmentA2, spliceTxB, commitmentB2) = fixtureParams.exchangeSigsBobFirst(spliceFixtureParams.fundingParamsB, successA2, successB2) assert(spliceTxA.tx.localFees == 1_000_000.msat) assert(spliceTxB.tx.localFees == 500_000.msat) @@ -914,7 +919,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(200_000 sat) val fundingB1 = 100_000 sat val utxosB = Seq(150_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, FeeratePerKw(1000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, FeeratePerKw(1000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ val probe = TestProbe() @@ -1010,7 +1015,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(480_000 sat, 130_000 sat) val fundingB1 = 100_000 sat val utxosB = Seq(340_000 sat, 70_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ val probe = TestProbe() @@ -1100,9 +1105,116 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } + test("initiator upgrades to taproot while splicing-in") { + val targetFeerate = FeeratePerKw(2000 sat) + val fundingA1 = 150_000 sat + val utxosA = Seq(480_000 sat, 130_000 sat) + val fundingB1 = 0 sat + val utxosB = Seq(70_000 sat) + withFixture(ChannelTypes.AnchorOutputs(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 750 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + import f._ + + val probe = TestProbe() + alice ! Start(alice2bob.ref) + bob ! Start(bob2alice.ref) + + // Alice --- tx_add_input --> Bob + fwd.forwardAlice2Bob[TxAddInput] + // Alice <-- tx_complete --- Bob + fwd.forwardBob2Alice[TxComplete] + // Alice --- tx_add_output --> Bob + fwd.forwardAlice2Bob[TxAddOutput] + // Alice <-- tx_complete --- Bob + fwd.forwardBob2Alice[TxComplete] + // Alice --- tx_add_output --> Bob + fwd.forwardAlice2Bob[TxAddOutput] + // Alice <-- tx_complete --- Bob + fwd.forwardBob2Alice[TxComplete] + // Alice --- tx_complete --> Bob + fwd.forwardAlice2Bob[TxComplete] + + val successA1 = alice2bob.expectMsgType[Succeeded] + val successB1 = bob2alice.expectMsgType[Succeeded] + val (txA1, commitmentA1, _, commitmentB1) = fixtureParams.exchangeSigsBobFirst(bobParams, successA1, successB1) + assert(commitmentA1.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(commitmentB1.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + walletA.publishTransaction(txA1.signedTx).pipeTo(probe.ref) + probe.expectMsg(txA1.txId) + + // Alice decides to splice funds in the channel and upgrade to taproot. + // Bob uses this opportunity to also splice some funds in the channel. + val additionalFundingA2 = 80_000.sat + val additionalFundingB2 = 55_000.sat + val (sharedInputA, sharedInputB) = fixtureParams.sharedInputs(commitmentA1, commitmentB1) + assert(sharedInputA.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(sharedInputB.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + val spliceFixtureParams = fixtureParams.createSpliceFixtureParams(fundingTxIndex = 1, fundingAmountA = additionalFundingA2, fundingAmountB = additionalFundingB2, aliceParams.targetFeerate, aliceParams.dustLimit, aliceParams.lockTime, sharedInputA = sharedInputA, sharedInputB = sharedInputB, nextCommitmentFormat_opt = Some(PhoenixSimpleTaprootChannelCommitmentFormat), requireConfirmedInputs = aliceParams.requireConfirmedInputs) + val aliceSplice = fixtureParams.spawnTxBuilderSpliceAlice(spliceFixtureParams.fundingParamsA, commitmentA1, walletA) + val bobSplice = fixtureParams.spawnTxBuilderSpliceBob(spliceFixtureParams.fundingParamsB, commitmentB1, walletB) + val fwdSplice = TypeCheckedForwarder(aliceSplice, bobSplice, alice2bob, bob2alice) + + aliceSplice ! Start(alice2bob.ref) + bobSplice ! Start(bob2alice.ref) + + // Alice --- tx_add_input --> Bob + fwdSplice.forwardAlice2Bob[TxAddInput] + // Alice <-- tx_add_input --- Bob + fwdSplice.forwardBob2Alice[TxAddInput] + // Alice --- tx_add_input --> Bob + fwdSplice.forwardAlice2Bob[TxAddInput] + // Alice <-- tx_add_output --- Bob + fwdSplice.forwardBob2Alice[TxAddOutput] + // Alice --- tx_add_output --> Bob + fwdSplice.forwardAlice2Bob[TxAddOutput] + // Alice <-- tx_complete --- Bob + fwdSplice.forwardBob2Alice[TxComplete] + // Alice --- tx_add_output --> Bob + fwdSplice.forwardAlice2Bob[TxAddOutput] + // Alice <-- tx_complete --- Bob + val txCompleteB = fwdSplice.forwardBob2Alice[TxComplete] + // Alice --- tx_complete --> Bob + val txCompleteA = fwdSplice.forwardAlice2Bob[TxComplete] + Seq(txCompleteA, txCompleteB).foreach(txComplete => { + assert(txComplete.nonces_opt.nonEmpty) + assert(txComplete.nonces_opt.flatMap(_.fundingNonce_opt).isEmpty) // the previous commitment didn't use taproot + assert(txComplete.nonces_opt.map(n => Seq(n.commitNonce, n.nextCommitNonce)).get.size == 2) + }) + + val successA2 = alice2bob.expectMsgType[Succeeded] + assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.nonEmpty) + assert(successA2.signingSession.fundingTx.localSigs.previousFundingTxPartialSig_opt.isEmpty) + assert(successA2.commitSig.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + val successB2 = bob2alice.expectMsgType[Succeeded] + assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxSig_opt.nonEmpty) + assert(successB2.signingSession.fundingTx.localSigs.previousFundingTxPartialSig_opt.isEmpty) + assert(successB2.commitSig.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + val (spliceTxA, commitmentA2, spliceTxB, commitmentB2) = fixtureParams.exchangeSigsBobFirst(spliceFixtureParams.fundingParamsB, successA2, successB2) + assert(successA2.nextRemoteCommitNonce_opt.contains((spliceTxA.txId, txCompleteB.nonces_opt.get.nextCommitNonce))) + assert(successB2.nextRemoteCommitNonce_opt.contains((spliceTxB.txId, txCompleteA.nonces_opt.get.nextCommitNonce))) + assert(spliceTxA.tx.localAmountIn > spliceTxA.tx.remoteAmountIn) + assert(spliceTxA.signedTx.txIn.exists(_.outPoint == commitmentA1.fundingInput)) + assert(0.msat < spliceTxA.tx.localFees) + assert(0.msat < spliceTxA.tx.remoteFees) + assert(spliceTxB.tx.localFees == spliceTxA.tx.remoteFees) + assert(spliceTxA.tx.sharedOutput.amount == fundingA1 + fundingB1 + additionalFundingA2 + additionalFundingB2) + + assert(commitmentA2.localCommit.spec.toLocal == (fundingA1 + additionalFundingA2).toMilliSatoshi) + assert(commitmentA2.localCommit.spec.toRemote == (fundingB1 + additionalFundingB2).toMilliSatoshi) + assert(commitmentB2.localCommit.spec.toLocal == (fundingB1 + additionalFundingB2).toMilliSatoshi) + assert(commitmentB2.localCommit.spec.toRemote == (fundingA1 + additionalFundingA2).toMilliSatoshi) + + // The resulting transaction is valid and has the right feerate. + walletA.publishTransaction(spliceTxA.signedTx).pipeTo(probe.ref) + probe.expectMsg(spliceTxA.txId) + walletA.getMempoolTx(spliceTxA.txId).pipeTo(probe.ref) + val mempoolTx = probe.expectMsgType[MempoolTx] + assert(mempoolTx.fees == spliceTxA.tx.fees) + assert(targetFeerate <= spliceTxA.feerate && spliceTxA.feerate <= targetFeerate * 1.25, s"unexpected feerate (target=$targetFeerate actual=${spliceTxA.feerate})") + } + } + test("remove input/output") { - assume(!channelType.commitmentFormat.isInstanceOf[SimpleTaprootChannelCommitmentFormat]) // TODO: make this test work with taproot channels ? - withFixture(100_000 sat, Seq(150_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, Seq(150_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -1146,7 +1258,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("not enough funds (unconfirmed utxos not allowed)") { - withFixture(100_000 sat, Seq(250_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, Seq(250_000 sat), 0 sat, Nil, FeeratePerKw(2500 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => import f._ // Alice's inputs are all unconfirmed. @@ -1172,7 +1284,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("not enough funds (unusable utxos)") { val fundingA = 140_000 sat val utxosA = Seq(75_000 sat, 60_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, FeeratePerKw(5000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, FeeratePerKw(5000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ import fr.acinq.bitcoin.scalacompat.KotlinUtils._ @@ -1220,7 +1332,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("skip unusable utxos") { val fundingA = 140_000 sat val utxosA = Seq(55_000 sat, 65_000 sat, 50_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, FeeratePerKw(5000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, FeeratePerKw(5000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ // Add some unusable utxos to Alice's wallet. @@ -1285,7 +1397,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(7500 sat) val fundingA = 85_000 sat val utxosA = Seq(120_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -1354,7 +1466,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(10_000 sat) val fundingA = 100_000 sat val utxosA = Seq(55_000 sat, 55_000 sat, 55_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -1436,7 +1548,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(70_000 sat, 60_000 sat) val fundingB = 25_000 sat val utxosB = Seq(27_500 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, initialFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, fundingB, utxosB, initialFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -1520,7 +1632,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(480_000 sat, 75_000 sat) val fundingB1 = 100_000 sat val utxosB = Seq(325_000 sat, 60_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -1647,7 +1759,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(340_000 sat, 40_000 sat, 35_000 sat) val fundingB1 = 80_000 sat val utxosB = Seq(280_000 sat, 20_000 sat, 15_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -1782,7 +1894,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(150_000 sat) val fundingB = 92_000 sat val utxosB = Seq(50_000 sat, 50_000 sat, 50_000 sat, 50_000 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -1818,10 +1930,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val purchase = LiquidityAds.Purchase.Standard(50_000 sat, LiquidityAds.Fees(1000 sat, 1500 sat), LiquidityAds.PaymentDetails.FromChannelBalance) // Alice pays fees for the common fields of the transaction, by decreasing her balance in the shared output. val spliceFeeA = { - val dummyWitness: ScriptWitness = channelType.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => Script.witnessKeyPathPay2tr(Transactions.PlaceHolderSig) - case _ => Scripts.witness2of2(Transactions.PlaceHolderSig, Transactions.PlaceHolderSig, randomKey().publicKey, randomKey().publicKey) - } + val dummyWitness = Scripts.witness2of2(Transactions.PlaceHolderSig, Transactions.PlaceHolderSig, randomKey().publicKey, randomKey().publicKey) val dummySpliceTx = Transaction( version = 2, txIn = Seq(TxIn(commitmentA1.fundingInput, ByteVector.empty, 0, dummyWitness)), @@ -1861,7 +1970,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val (spliceTxA1, commitmentA2, _, commitmentB2) = fixtureParams.exchangeSigsBobFirst(fundingParamsB1, successA2, successB2) assert(commitmentA2.localCommit.spec.toLocal == commitmentA1.localCommit.spec.toLocal - spliceFeeA - purchase.fees.total) assert(commitmentB2.localCommit.spec.toLocal == commitmentB1.localCommit.spec.toLocal + fundingB + purchase.fees.total) - assert(targetFeerate * 0.9 <= spliceTxA1.feerate && spliceTxA1.feerate <= targetFeerate * 1.27) + assert(targetFeerate * 0.9 <= spliceTxA1.feerate && spliceTxA1.feerate <= targetFeerate * 1.25) walletA.publishTransaction(spliceTxA1.signedTx).pipeTo(probe.ref) probe.expectMsg(spliceTxA1.txId) @@ -1878,7 +1987,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val utxosA = Seq(340_000 sat, 40_000 sat, 35_000 sat) val fundingB1 = 80_000 sat val utxosB = Seq(290_000 sat, 20_000 sat, 15_000 sat) - withFixture(fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.SimpleTaprootChannelsStaging(), fundingA1, utxosA, fundingB1, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -2032,7 +2141,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(10_000 sat) val fundingA = 80_000 sat val utxosA = Seq(85_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -2061,7 +2170,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("allow unconfirmed remote inputs") { - withFixture(120_000 sat, Seq(150_000 sat), 50_000 sat, Seq(100_000 sat), FeeratePerKw(4000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 120_000 sat, Seq(150_000 sat), 50_000 sat, Seq(100_000 sat), FeeratePerKw(4000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ // Bob's available utxo is unconfirmed. @@ -2097,7 +2206,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("reject unconfirmed remote inputs") { - withFixture(120_000 sat, Seq(150_000 sat), 50_000 sat, Seq(100_000 sat), FeeratePerKw(4000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = true)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 120_000 sat, Seq(150_000 sat), 50_000 sat, Seq(100_000 sat), FeeratePerKw(4000 sat), 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = true)) { f => import f._ // Bob's available utxo is unconfirmed. @@ -2129,7 +2238,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("funding amount drops below reserve") { - withFixture(500_000 sat, Seq(600_000 sat), 400_000 sat, Seq(450_000 sat), FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 500_000 sat, Seq(600_000 sat), 400_000 sat, Seq(450_000 sat), FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -2191,8 +2300,8 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } - test("invalid tx_signatures (missing shared input signature)") { - withFixture(150_000 sat, Seq(200_000 sat), 0 sat, Nil, FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + private def testTxSignaturesMissingSharedInputSigs(channelType: SupportedChannelType): Unit = { + withFixture(channelType, 150_000 sat, Seq(200_000 sat), 0 sat, Nil, FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -2258,8 +2367,16 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } + test("invalid tx_signatures (missing shared input signature)") { + testTxSignaturesMissingSharedInputSigs(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + } + + test("invalid tx_signatures (missing shared input signature, taproot)") { + testTxSignaturesMissingSharedInputSigs(ChannelTypes.SimpleTaprootChannelsStaging()) + } + test("invalid commitment index") { - withFixture(150_000 sat, Seq(200_000 sat), 0 sat, Nil, FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 150_000 sat, Seq(200_000 sat), 0 sat, Nil, FeeratePerKw(1000 sat), 330 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ val probe = TestProbe() @@ -2335,7 +2452,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("invalid funding contributions") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(75_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 500 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 75_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 500 sat, 0) val previousCommitment = CommitmentsSpec.makeCommitments(25_000_000 msat, 75_000_000 msat).active.head val sharedInput = params.dummySharedInputB(100_000 sat) val testCases = Seq( @@ -2355,7 +2472,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() val purchase = LiquidityAds.Purchase.Standard(500_000 sat, LiquidityAds.Fees(5000 sat, 20_000 sat), LiquidityAds.PaymentDetails.FromChannelBalance) - val params = createFixtureParams(24_000 sat, 500_000 sat, FeeratePerKw(5000 sat), 500 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 24_000 sat, 500_000 sat, FeeratePerKw(5000 sat), 500 sat, 0) // Bob will reject Alice's proposal, since she doesn't have enough funds to pay the liquidity fees. val bob = params.spawnTxBuilderBob(wallet, params.fundingParamsB, Some(purchase)) bob ! Start(probe.ref) @@ -2392,7 +2509,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit ) val previousTx = Transaction(2, Nil, previousOutputs, 0) val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val testCases = Seq( TxAddInput(params.channelId, UInt64(0), Some(previousTx), 0, 0) -> InvalidSerialId(params.channelId, UInt64(0)), TxAddInput(params.channelId, UInt64(1), Some(previousTx), 0, 0) -> DuplicateSerialId(params.channelId, UInt64(1)), @@ -2421,7 +2538,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("allow standard output types") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val testCases = Seq( TxAddOutput(params.channelId, UInt64(1), 25_000 sat, Script.write(Script.pay2pkh(randomKey().publicKey))), TxAddOutput(params.channelId, UInt64(1), 25_000 sat, Script.write(Script.pay2sh(OP_1 :: Nil))), @@ -2444,7 +2561,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("invalid output") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val testCases = Seq( TxAddOutput(params.channelId, UInt64(0), 25_000 sat, validScript) -> InvalidSerialId(params.channelId, UInt64(0)), @@ -2470,7 +2587,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("remove unknown input/output") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val testCases = Seq( TxRemoveOutput(params.channelId, UInt64(53)) -> UnknownSerialId(params.channelId, UInt64(53)), TxRemoveInput(params.channelId, UInt64(57)) -> UnknownSerialId(params.channelId, UInt64(57)), @@ -2490,7 +2607,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("too many protocol rounds") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val alice = params.spawnTxBuilderAlice(wallet) alice ! Start(probe.ref) @@ -2508,7 +2625,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("too many inputs") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val alice = params.spawnTxBuilderAlice(wallet) alice ! Start(probe.ref) (1 to 252).foreach(i => { @@ -2525,7 +2642,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("too many outputs") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val alice = params.spawnTxBuilderAlice(wallet) alice ! Start(probe.ref) @@ -2543,7 +2660,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("missing funding output") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) @@ -2563,7 +2680,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("multiple funding outputs") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) // Alice --- tx_add_input --> Bob @@ -2586,7 +2703,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("missing shared input") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(1000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(1000 sat), 330 sat, 0) val commitment = CommitmentsSpec.makeCommitments(250_000_000 msat, 150_000_000 msat).active.head val fundingParamsB = params.fundingParamsB.copy(sharedInput_opt = Some(params.dummySharedInputB(commitment.capacity))) val bob = params.spawnTxBuilderSpliceBob(fundingParamsB, commitment, wallet) @@ -2607,7 +2724,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("invalid funding amount") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) // Alice --- tx_add_input --> Bob @@ -2622,7 +2739,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("missing previous tx") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val previousCommitment = CommitmentsSpec.makeCommitments(25_000_000 msat, 50_000_000 msat).active.head val fundingParams = params.fundingParamsB.copy(sharedInput_opt = Some(SharedFundingInput(previousCommitment.commitInput(params.channelKeysB), 0, randomKey().publicKey, previousCommitment.commitmentFormat))) val bob = params.spawnTxBuilderSpliceBob(fundingParams, previousCommitment, wallet) @@ -2637,7 +2754,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("invalid shared input") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val previousCommitment = CommitmentsSpec.makeCommitments(25_000_000 msat, 50_000_000 msat).active.head val fundingTx = Transaction(2, Nil, Seq(TxOut(50_000 sat, Script.pay2wpkh(randomKey().publicKey)), TxOut(20_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) val sharedInput = SharedFundingInput(InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head), 0, randomKey().publicKey, previousCommitment.commitmentFormat) @@ -2653,7 +2770,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("total input amount too low") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) @@ -2677,7 +2794,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("minimum fee not met") { val probe = TestProbe() val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) val bob = params.spawnTxBuilderBob(wallet) bob ! Start(probe.ref) @@ -2702,7 +2819,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val targetFeerate = FeeratePerKw(7500 sat) val fundingA = 85_000 sat val utxosA = Seq(120_000 sat) - withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => + withFixture(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => import f._ alice ! Start(alice2bob.ref) @@ -2763,7 +2880,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("invalid commit_sig") { val (alice2bob, bob2alice) = (TestProbe(), TestProbe()) val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 330 sat, 0) val alice = params.spawnTxBuilderAlice(wallet) val bob = params.spawnTxBuilderBob(wallet) alice ! Start(alice2bob.ref) @@ -2781,13 +2898,35 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxComplete]) // Alice <-- commit_sig --- Bob val successA1 = alice2bob.expectMsgType[Succeeded] - val invalidCommitSig = channelType.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat => - val priv = randomKey() - val (_, nonce) = Musig2.generateNonce(randomBytes32(), Left(priv), Seq(priv.publicKey), None, None) - CommitSig(params.channelId, ByteVector64.Zeroes, Nil, TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(ChannelSpendSignature.PartialSignatureWithNonce(ByteVector32.Zeroes, nonce)))) - case _ => CommitSig(params.channelId, ByteVector64.Zeroes, Nil) - } + val invalidCommitSig = CommitSig(params.channelId, IndividualSignature(ByteVector64.Zeroes), Nil) + val Left(error) = successA1.signingSession.receiveCommitSig(params.channelParamsA, params.channelKeysA, invalidCommitSig, params.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) + assert(error.isInstanceOf[InvalidCommitmentSignature]) + } + + test("invalid commit_sig (taproot)") { + val (alice2bob, bob2alice) = (TestProbe(), TestProbe()) + val wallet = new SingleKeyOnChainWallet() + val params = createFixtureParams(ChannelTypes.SimpleTaprootChannelsPhoenix(), 100_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val alice = params.spawnTxBuilderAlice(wallet) + val bob = params.spawnTxBuilderBob(wallet) + alice ! Start(alice2bob.ref) + bob ! Start(bob2alice.ref) + // Alice --- tx_add_input --> Bob + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddInput]) + alice ! ReceiveMessage(bob2alice.expectMsgType[SendMessage].msg.asInstanceOf[TxAddInput]) + // Alice --- tx_add_output --> Bob + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddOutput]) + alice ! ReceiveMessage(bob2alice.expectMsgType[SendMessage].msg.asInstanceOf[TxAddOutput]) + // Alice --- tx_add_output --> Bob + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxAddOutput]) + val txCompleteBob = bob2alice.expectMsgType[SendMessage].msg.asInstanceOf[TxComplete] + assert(txCompleteBob.nonces_opt.nonEmpty) + alice ! ReceiveMessage(txCompleteBob) + // Alice --- tx_complete --> Bob + bob ! ReceiveMessage(alice2bob.expectMsgType[SendMessage].msg.asInstanceOf[TxComplete]) + // Alice <-- commit_sig --- Bob + val successA1 = alice2bob.expectMsgType[Succeeded] + val invalidCommitSig = CommitSig(params.channelId, PartialSignatureWithNonce(randomBytes32(), txCompleteBob.nonces_opt.get.commitNonce), Nil, batchSize = 1) val Left(error) = successA1.signingSession.receiveCommitSig(params.channelParamsA, params.channelKeysA, invalidCommitSig, params.nodeParamsA.currentBlockHeight)(akka.event.NoLogging) assert(error.isInstanceOf[InvalidCommitmentSignature]) } @@ -2795,7 +2934,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("receive tx_signatures before commit_sig") { val (alice2bob, bob2alice) = (TestProbe(), TestProbe()) val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 0 sat, FeeratePerKw(5000 sat), 330 sat, 0) val alice = params.spawnTxBuilderAlice(wallet) val bob = params.spawnTxBuilderBob(wallet) alice ! Start(alice2bob.ref) @@ -2821,7 +2960,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("invalid tx_signatures") { val (alice2bob, bob2alice) = (TestProbe(), TestProbe()) val wallet = new SingleKeyOnChainWallet() - val params = createFixtureParams(100_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 330 sat, 0) + val params = createFixtureParams(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), 100_000 sat, 25_000 sat, FeeratePerKw(5000 sat), 330 sat, 0) val alice = params.spawnTxBuilderAlice(wallet) val bob = params.spawnTxBuilderBob(wallet) alice ! Start(alice2bob.ref) @@ -2876,8 +3015,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, None) - val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None, None) + 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 initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs, None) assert(initiatorSignedTx.feerate == FeeratePerKw(262 sat)) val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs, None) @@ -2892,7 +3031,3 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit class InteractiveTxBuilderWithEclairSignerSpec extends InteractiveTxBuilderSpec { override def useEclairSigner = true } - -class InteractiveTxBuilderWithTaprootChannelsSpec extends InteractiveTxBuilderSpec { - override val channelType: SupportedChannelType = ChannelTypes.SimpleTaprootChannelsStaging() -} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index 8782cbf201..5c1f7cc595 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -9,7 +9,7 @@ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFactory, PimpTestFSM} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.protocol.{ChannelReestablish, ChannelUpdate, CommitSig, Error, Init, RevokeAndAck} @@ -81,7 +81,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan awaitCond(newAlice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) // bob is nice and publishes its commitment - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val bobCommitTx = bob.signCommitTx() // actual tests starts here: let's see what we can do with Bob's commit tx sender.send(newAlice, WatchFundingSpentTriggered(bobCommitTx)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index c189f455e6..d4da4f2d9c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -35,6 +35,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.ReplaceableTxPublisher.{Publish, Stop, UpdateConfirmationTarget} import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ import fr.acinq.eclair.channel.publish.TxPublisher._ +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.crypto.keymanager.LocalOnChainKeyManager import fr.acinq.eclair.testutils.PimpTestProbe.convert @@ -188,8 +189,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w def closeChannelWithoutHtlcs(f: Fixture, overrideCommitTarget: BlockHeight): (PublishFinalTx, PublishReplaceableTx) = { import f._ + val commitTx = alice.signCommitTx() val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest - val commitTx = commitment.fullySignedLocalCommitTx(alice.underlyingActor.channelKeys).toTry.get val commitFee = commitment.capacity - commitTx.txOut.map(_.amount).sum probe.send(alice, CMD_FORCECLOSE(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] @@ -207,7 +208,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w def remoteCloseChannelWithoutHtlcs(f: Fixture, overrideCommitTarget: BlockHeight): (Transaction, PublishReplaceableTx) = { import f._ - val commitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val commitTx = bob.signCommitTx() wallet.publishTransaction(commitTx).pipeTo(probe.ref) probe.expectMsg(commitTx.txid) probe.send(alice, WatchFundingSpentTriggered(commitTx)) @@ -310,7 +311,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ - val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val remoteCommit = bob.signCommitTx() assert(remoteCommit.txOut.length == 4) // 2 main outputs + 2 anchor outputs val (_, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) wallet.publishTransaction(remoteCommit).pipeTo(probe.ref) @@ -340,7 +341,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.nonEmpty) val nextRemoteCommitTxId = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.get.commit.txId - val nextRemoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val nextRemoteCommit = bob.signCommitTx() assert(nextRemoteCommit.txid == nextRemoteCommitTxId) assert(nextRemoteCommit.txOut.length == 5) // 2 main outputs + 2 anchor outputs + 1 htlc val (_, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) @@ -360,7 +361,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ - val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val remoteCommit = bob.signCommitTx() val (_, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) wallet.publishTransaction(remoteCommit).pipeTo(probe.ref) probe.expectMsg(remoteCommit.txid) @@ -377,7 +378,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ - val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val remoteCommit = bob.signCommitTx() assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate == FeeratePerKw(2500 sat)) // We lower the feerate to make it easy to replace our commit tx by theirs in the mempool. @@ -609,7 +610,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ - val commitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val commitTx = bob.signCommitTx() // Note that we don't publish the remote commit, to simulate the case where the watch triggers but the remote commit is then evicted from our mempool. probe.send(alice, WatchFundingSpentTriggered(commitTx)) val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] @@ -969,7 +970,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(htlcTimeout.txInfo.isInstanceOf[HtlcTimeoutTx]) // The remote commit tx has a few confirmations, but isn't deeply confirmed yet. - val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val remoteCommitTx = bob.signCommitTx() wallet.publishTransaction(remoteCommitTx).pipeTo(probe.ref) probe.expectMsg(remoteCommitTx.txid) generateBlocks(2) @@ -1039,7 +1040,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(htlcTimeout.txInfo.isInstanceOf[HtlcTimeoutTx]) // Ensure remote commit tx confirms. - val nextRemoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val nextRemoteCommitTx = bob.signCommitTx() assert(nextRemoteCommitTx.txid == nextRemoteCommitTxId) assert(nextRemoteCommitTx.txOut.length == 6) // 2 main outputs + 2 anchor outputs + 2 htlcs wallet.publishTransaction(nextRemoteCommitTx).pipeTo(probe.ref) @@ -1069,8 +1070,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel and verify txs sent to watcher. + val commitTx = alice.signCommitTx() val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest - val commitTx = commitment.fullySignedLocalCommitTx(alice.underlyingActor.channelKeys).toTry.get val commitFee = commitment.capacity - commitTx.txOut.map(_.amount).sum assert(commitTx.txOut.size == 6) probe.send(alice, CMD_FORCECLOSE(probe.ref)) @@ -1525,8 +1526,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel. - val localCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(alice.underlyingActor.channelKeys).toTry.get - val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val localCommitTx = alice.signCommitTx() + val remoteCommitTx = bob.signCommitTx() assert(remoteCommitTx.txOut.size == 6) probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx)) alice2blockchain.expectMsgType[PublishReplaceableTx] // claim anchor @@ -1600,7 +1601,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel and verify txs sent to watcher. - val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys).toTry.get + val remoteCommitTx = bob.signCommitTx() bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat match { case Transactions.DefaultCommitmentFormat => assert(remoteCommitTx.txOut.size == 4) case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(remoteCommitTx.txOut.size == 6) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index ff65bf36ac..a06b49b55c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -99,9 +99,9 @@ object ChannelStateTestsTags { val SimpleClose = "option_simple_close" /** If set, disable option_splice for one node. */ val DisableSplice = "disable_splice" - /** If set, channels weill use option_simple_taproot_staging */ - val OptionSimpleTaprootStagingLegacy = "option_simple_taproot_staging_legacy" - val OptionSimpleTaprootStagingZeroFee = "option_simple_taproot_staging_zerofee" + /** If set, channels will use taproot. */ + val OptionSimpleTaprootPhoenix = "option_simple_taproot_phoenix" + val OptionSimpleTaproot = "option_simple_taproot" } trait ChannelStateTestsBase extends Assertions with Eventually { @@ -267,8 +267,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy))(_.updated(Features.SimpleTaprootChannelsPhoenix, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee))(_.updated(Features.SimpleTaprootChannelsStaging, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootPhoenix))(_.updated(Features.SimpleTaprootChannelsPhoenix, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaproot))(_.updated(Features.SimpleTaprootChannelsStaging, FeatureSupport.Optional)) ) val nodeParamsB1 = nodeParamsB.copy(features = nodeParamsB.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableWumbo))(_.removed(Features.Wumbo)) @@ -282,8 +282,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableSplice))(_.removed(Features.SplicePrototype)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy))(_.updated(Features.SimpleTaprootChannelsPhoenix, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee))(_.updated(Features.SimpleTaprootChannelsStaging, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootPhoenix))(_.updated(Features.SimpleTaprootChannelsPhoenix, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaproot))(_.updated(Features.SimpleTaprootChannelsStaging, FeatureSupport.Optional)) ) (nodeParamsA1, nodeParamsB1) } @@ -297,8 +297,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, announceChannel = channelFlags.announceChannel) // those features can only be enabled with AnchorOutputsZeroFeeHtlcTxs, this is to prevent incompatible test configurations - if (tags.contains(ChannelStateTestsTags.ZeroConf)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) || tags.contains(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee), "invalid test configuration") - if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) || tags.contains(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee), "invalid test configuration") + if (tags.contains(ChannelStateTestsTags.ZeroConf)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) || tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.OptionSimpleTaprootPhoenix) || tags.contains(ChannelStateTestsTags.OptionSimpleTaproot), "invalid test configuration") + if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) || tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.OptionSimpleTaprootPhoenix) || tags.contains(ChannelStateTestsTags.OptionSimpleTaproot), "invalid test configuration") implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global val aliceChannelParams = Alice.channelParams @@ -788,7 +788,7 @@ object ChannelStateTestsBase { def commitments: Commitments = channel.stateData.asInstanceOf[ChannelDataWithCommitments].commitments - def signCommitTx(): Transaction = commitments.latest.fullySignedLocalCommitTx(channel.underlyingActor.channelKeys).toTry.get + def signCommitTx(): Transaction = commitments.latest.fullySignedLocalCommitTx(channel.underlyingActor.channelKeys) def htlcTxs(): Seq[UnsignedHtlcTx] = commitments.latest.htlcTxs(channel.underlyingActor.channelKeys).map(_._1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index f364aa6f27..5a8a3d13a8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.io.Peer.OpenChannelResponse -import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, LegacySimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, PhoenixSimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, OpenChannel, TlvStream} import fr.acinq.eclair.{CltvExpiryDelta, TestConstants, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -108,24 +108,24 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOpenReplyTo.expectNoMessage() } - test("recv AcceptChannel (simple taproot channels outputs)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv AcceptChannel (simple taproot channels phoenix)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsStagingLegacy())) - assert(accept.nexLocalNonce_opt.isDefined) + assert(accept.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsPhoenix())) + assert(accept.commitNonce_opt.isDefined) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].commitmentFormat == LegacySimpleTaprootChannelCommitmentFormat) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } - test("recv AcceptChannel (simple taproot channels outputs, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv AcceptChannel (simple taproot channels outputs, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsStagingLegacy())) - assert(accept.nexLocalNonce_opt.isDefined) + assert(accept.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsPhoenix())) + assert(accept.commitNonce_opt.isDefined) bob2alice.forward(alice, accept.copy(tlvStream = accept.tlvStream.copy(records = accept.tlvStream.records.filterNot(_.isInstanceOf[ChannelTlv.NextLocalNonceTlv])))) - alice2bob.expectMsg(Error(accept.temporaryChannelId, MissingNonce(accept.temporaryChannelId, TxId(ByteVector32.Zeroes)).getMessage)) + alice2bob.expectMsg(Error(accept.temporaryChannelId, MissingCommitNonce(accept.temporaryChannelId, TxId(ByteVector32.Zeroes), 0).getMessage)) listener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 01a31edf8e..47369f4464 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -19,12 +19,12 @@ package fr.acinq.eclair.channel.states.a import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, SatoshiLong, TxId} -import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.TestConstants.Bob import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, LegacySimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, OpenChannel, TlvStream} import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -107,24 +107,24 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].commitmentFormat == DefaultCommitmentFormat) } - test("recv OpenChannel (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv OpenChannel (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsStagingLegacy())) + assert(open.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsStaging())) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].commitmentFormat == LegacySimpleTaprootChannelCommitmentFormat) - assert(open.nexLocalNonce_opt.isDefined) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].commitmentFormat == ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + assert(open.commitNonce_opt.isDefined) } - test("recv OpenChannel (simple taproot channels, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv OpenChannel (simple taproot channels, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsStagingLegacy())) - assert(open.nexLocalNonce_opt.isDefined) + assert(open.channelType_opt.contains(ChannelTypes.SimpleTaprootChannelsStaging())) + assert(open.commitNonce_opt.isDefined) alice2bob.forward(bob, open.copy(tlvStream = open.tlvStream.copy(records = open.tlvStream.records.filterNot(_.isInstanceOf[ChannelTlv.NextLocalNonceTlv])))) val error = bob2alice.expectMsgType[Error] - assert(error == Error(open.temporaryChannelId, MissingNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes)).getMessage)) + assert(error == Error(open.temporaryChannelId, MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), 0).getMessage)) listener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala index c08aa5fb92..b11293e133 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala @@ -38,14 +38,11 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], aliceOpenReplyTo: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, wallet: SingleKeyOnChainWallet, aliceListener: TestProbe, bobListener: TestProbe) - val extraTags: Set[String] = Set.empty - override def withFixture(test: OneArgTest): Outcome = { val wallet = new SingleKeyOnChainWallet() - val tags = test.tags ++ extraTags - val setup = init(wallet_opt = Some(wallet), tags = tags) + val setup = init(wallet_opt = Some(wallet), tags = test.tags) import setup._ - val channelParams = computeChannelParams(setup, tags) + val channelParams = computeChannelParams(setup, test.tags) val aliceListener = TestProbe() val bobListener = TestProbe() within(30 seconds) { @@ -113,6 +110,26 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] } + test("recv tx_complete without nonces (taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddInput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddOutput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + val txComplete = bob2alice.expectMsgType[TxComplete] + assert(txComplete.nonces_opt.isDefined) + bob2alice.forward(alice, txComplete.copy(tlvStream = txComplete.tlvStream.copy(records = txComplete.tlvStream.records.filterNot(_.isInstanceOf[TxCompleteTlv.Nonces])))) + aliceListener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + } + test("recv TxAbort", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ @@ -259,30 +276,4 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn aliceOpenReplyTo.expectMsg(OpenChannelResponse.TimedOut) } -} - -class WaitForDualFundingCreatedStateWithTaprootChannelsSpec extends WaitForDualFundingCreatedStateSpec { - override val extraTags: Set[String] = Set(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy) - - test("tx_complete is missing nonces", Tag(ChannelStateTestsTags.DualFunding)) { f => - import f._ - - bob2alice.expectNoMessage(100 millis) - alice2bob.expectMsgType[TxAddInput] - alice2bob.expectNoMessage(100 millis) - alice2bob.forward(bob) - bob2alice.expectMsgType[TxAddInput] - bob2alice.forward(alice) - alice2bob.expectMsgType[TxAddOutput] - alice2bob.forward(bob) - bob2alice.expectMsgType[TxAddOutput] - bob2alice.forward(alice) - alice2bob.expectMsgType[TxAddOutput] - alice2bob.forward(bob) - val txComplete = bob2alice.expectMsgType[TxComplete] - assert(txComplete.nonces_opt.isDefined) - bob2alice.forward(alice, txComplete.copy(tlvStream = txComplete.tlvStream.copy(records = txComplete.tlvStream.records.filterNot(_.isInstanceOf[TxCompleteTlv.Nonces])))) - aliceListener.expectMsgType[ChannelAborted] - awaitCond(alice.stateName == CLOSED) - } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala index c78ade56d9..2137dabae3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala @@ -19,17 +19,20 @@ package fr.acinq.eclair.channel.states.b import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.{ByteVector64, SatoshiLong, TxId} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.{NewTransaction, SingleKeyOnChainWallet} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchPublished, WatchPublishedTriggered} import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.blockchain.{NewTransaction, SingleKeyOnChainWallet} +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, PartiallySignedSharedTransaction} import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.crypto.NonceGenerator import fr.acinq.eclair.io.Peer.{LiquidityPurchaseSigned, OpenChannelResponse} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} +import fr.acinq.eclair.{Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -39,18 +42,14 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alicePeer: TestProbe, bobPeer: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, wallet: SingleKeyOnChainWallet, aliceListener: TestProbe, bobListener: TestProbe) - val extraTags: Set[String] = Set.empty - override def withFixture(test: OneArgTest): Outcome = { val wallet = new SingleKeyOnChainWallet() - val tags = test.tags ++ extraTags - val setup = init(wallet_opt = Some(wallet), tags = tags) + val setup = init(wallet_opt = Some(wallet), tags = test.tags) import setup._ - - val channelParams = computeChannelParams(setup, tags) + val channelParams = computeChannelParams(setup, test.tags) val bobContribution = if (channelParams.channelType.features.contains(Features.ZeroConf)) None else Some(LiquidityAds.AddFunding(TestConstants.nonInitiatorFundingSatoshis, Some(TestConstants.defaultLiquidityRates))) - val requestFunding_opt = if (tags.contains(ChannelStateTestsTags.LiquidityAds)) Some(LiquidityAds.RequestFunding(TestConstants.nonInitiatorFundingSatoshis, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)) else None - val (initiatorPushAmount, nonInitiatorPushAmount) = if (tags.contains("both_push_amount")) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None) + val requestFunding_opt = if (test.tags.contains(ChannelStateTestsTags.LiquidityAds)) Some(LiquidityAds.RequestFunding(TestConstants.nonInitiatorFundingSatoshis, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance)) else None + val (initiatorPushAmount, nonInitiatorPushAmount) = if (test.tags.contains("both_push_amount")) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None) val aliceListener = TestProbe() val bobListener = TestProbe() within(30 seconds) { @@ -161,16 +160,18 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(aliceData.latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx.txid == fundingTxId) } - def `complete interactive-tx protocol (with push amount)`(f: FixtureParam): Unit = { + test("complete interactive-tx protocol (with push amount, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag("both_push_amount"), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val listener = TestProbe() alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) + val commitSigB = bob2alice.expectMsgType[CommitSig] + assert(commitSigB.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + bob2alice.forward(alice, commitSigB) + val commitSigA = alice2bob.expectMsgType[CommitSig] + assert(commitSigA.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + alice2bob.forward(bob, commitSigA) val expectedBalanceAlice = TestConstants.fundingSatoshis.toMilliSatoshi + TestConstants.nonInitiatorPushAmount - TestConstants.initiatorPushAmount assert(expectedBalanceAlice == 900_000_000.msat) @@ -193,10 +194,6 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(aliceData.commitments.latest.localCommit.spec.toRemote == expectedBalanceBob) } - test("complete interactive-tx protocol (with push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag("both_push_amount")) { f => - `complete interactive-tx protocol (with push amount)`(f) - } - test("complete interactive-tx protocol (with liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.LiquidityAds)) { f => import f._ @@ -241,21 +238,36 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny val bobCommitSig = bob2alice.expectMsgType[CommitSig] val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - val invalidBobCommitSig = bobCommitSig.sigOrPartialSig match { - case _: ChannelSpendSignature.IndividualSignature => bobCommitSig.copy(signature = ByteVector64.Zeroes) - case psig: ChannelSpendSignature.PartialSignatureWithNonce => bobCommitSig.copy(tlvStream = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(psig.copy(partialSig = psig.partialSig.reverse)))) - } - bob2alice.forward(alice, invalidBobCommitSig) + bob2alice.forward(alice, bobCommitSig.copy(signature = IndividualSignature(ByteVector64.Zeroes))) alice2bob.expectMsgType[Error] awaitCond(wallet.rolledback.length == 1) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSED) - val invalidAliceCommitSig = aliceCommitSig.sigOrPartialSig match { - case _: ChannelSpendSignature.IndividualSignature => bobCommitSig.copy(signature = ByteVector64.Zeroes) - case psig: ChannelSpendSignature.PartialSignatureWithNonce => bobCommitSig.copy(tlvStream = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(psig.copy(partialSig = psig.partialSig.reverse)))) - } - alice2bob.forward(bob, invalidAliceCommitSig) + alice2bob.forward(bob, aliceCommitSig.copy(signature = IndividualSignature(ByteVector64.Zeroes))) + bob2alice.expectMsgType[Error] + awaitCond(wallet.rolledback.length == 2) + bobListener.expectMsgType[ChannelAborted] + awaitCond(bob.stateName == CLOSED) + } + + test("recv invalid CommitSig (taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + val bobCommitSig = bob2alice.expectMsgType[CommitSig] + assert(bobCommitSig.partialSignature_opt.nonEmpty) + val aliceCommitSig = alice2bob.expectMsgType[CommitSig] + assert(aliceCommitSig.partialSignature_opt.nonEmpty) + + val invalidSigBob = bobCommitSig.partialSignature_opt.get.copy(partialSig = randomBytes32()) + bob2alice.forward(alice, bobCommitSig.copy(tlvStream = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(invalidSigBob)))) + alice2bob.expectMsgType[Error] + awaitCond(wallet.rolledback.length == 1) + aliceListener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + + val invalidSigAlice = aliceCommitSig.partialSignature_opt.get.copy(nonce = NonceGenerator.signingNonce(randomKey().publicKey, randomKey().publicKey, randomTxId()).publicNonce) + alice2bob.forward(bob, aliceCommitSig.copy(tlvStream = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(invalidSigAlice)))) bob2alice.expectMsgType[Error] awaitCond(wallet.rolledback.length == 2) bobListener.expectMsgType[ChannelAborted] @@ -369,7 +381,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(wallet.rolledback.isEmpty) } - test("recv INPUT_DISCONNECTED (commit_sig not received)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + def testReconnectCommitSigNotReceived(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId @@ -383,10 +395,60 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceExpectsCommitSig = true, bobExpectsCommitSig = true) + reconnect(f, fundingTxId, commitmentFormat, aliceExpectsCommitSig = true, bobExpectsCommitSig = true) } - test("recv INPUT_DISCONNECTED (commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (commit_sig not received)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testReconnectCommitSigNotReceived(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (commit_sig not received, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testReconnectCommitSigNotReceived(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (commit_sig not received, missing taproot commit nonce)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId + alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishAlice.nextCommitNonces.contains(fundingTxId)) + + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishBob.nextLocalCommitmentNumber == 0) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishBob.nextCommitNonces.contains(fundingTxId)) + + // If Alice doesn't include her current commit nonce, Bob won't be able to retransmit commit_sig. + val channelReestablishAlice1 = channelReestablishAlice.copy(tlvStream = TlvStream(channelReestablishAlice.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.CurrentCommitNonceTlv]))) + alice2bob.forward(bob, channelReestablishAlice1) + assert(bob2alice.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishBob.channelId, fundingTxId, commitmentNumber = 0).getMessage) + awaitCond(bob.stateName == CLOSED) + + // If Bob doesn't include nonces for this next commit, Alice won't be able to update the channel. + val channelReestablishBob1 = channelReestablishBob.copy(tlvStream = TlvStream(channelReestablishBob.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]))) + bob2alice.forward(alice, channelReestablishBob1) + assert(alice2bob.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishBob.channelId, fundingTxId, commitmentNumber = 1).getMessage) + awaitCond(bob.stateName == CLOSED) + } + + def testReconnectCommitSigReceivedByAlice(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId @@ -401,10 +463,18 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceExpectsCommitSig = false, bobExpectsCommitSig = true) + reconnect(f, fundingTxId, commitmentFormat, aliceExpectsCommitSig = false, bobExpectsCommitSig = true) } - test("recv INPUT_DISCONNECTED (commit_sig received by Bob)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv INPUT_DISCONNECTED (commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testReconnectCommitSigReceivedByAlice(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (commit_sig received by Alice, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testReconnectCommitSigReceivedByAlice(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (commit_sig received by Bob)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId @@ -420,10 +490,10 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceExpectsCommitSig = true, bobExpectsCommitSig = false) + reconnect(f, fundingTxId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, aliceExpectsCommitSig = true, bobExpectsCommitSig = false) } - test("recv INPUT_DISCONNECTED (commit_sig received by Bob, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee)) { f => + test("recv INPUT_DISCONNECTED (commit_sig received by Bob, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ alice2bob.expectMsgType[CommitSig] @@ -439,7 +509,8 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(bob2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid) bob ! WatchPublishedTriggered(fundingTx) assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid) - bob2alice.expectMsgType[ChannelReady] + val channelReadyB = bob2alice.expectMsgType[ChannelReady] + assert(channelReadyB.nextCommitNonce_opt.nonEmpty) awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_READY) alice ! INPUT_DISCONNECTED @@ -455,15 +526,20 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishAlice.nextCommitNonces.contains(fundingTx.txid)) + assert(channelReestablishAlice.nextCommitNonces.get(fundingTx.txid) != channelReestablishAlice.currentCommitNonce_opt) assert(channelReestablishAlice.nextFundingTxId_opt.contains(fundingTx.txid)) assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) alice2bob.forward(bob, channelReestablishAlice) val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + assert(channelReestablishBob.nextCommitNonces.get(fundingTx.txid) == channelReadyB.nextCommitNonce_opt) assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) bob2alice.forward(alice, channelReestablishBob) - bob2alice.expectMsgType[CommitSig] + assert(bob2alice.expectMsgType[CommitSig].partialSignature_opt.nonEmpty) bob2alice.forward(alice) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) @@ -476,7 +552,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTx.txid) } - test("recv INPUT_DISCONNECTED (commit_sig received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (commit_sig received)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId @@ -493,7 +569,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny bob ! INPUT_DISCONNECTED awaitCond(bob.stateName == OFFLINE) - reconnect(f, fundingTxId, aliceExpectsCommitSig = false, bobExpectsCommitSig = false) + reconnect(f, fundingTxId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, aliceExpectsCommitSig = false, bobExpectsCommitSig = false) } test("recv INPUT_DISCONNECTED (tx_signatures received)", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -533,7 +609,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTxId) } - test("recv INPUT_DISCONNECTED (tx_signatures received, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv INPUT_DISCONNECTED (tx_signatures received, zero-conf)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val listener = TestProbe() @@ -553,7 +629,8 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(alice2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid) alice ! WatchPublishedTriggered(fundingTx) assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == fundingTx.txid) - alice2bob.expectMsgType[ChannelReady] + val channelReadyA1 = alice2bob.expectMsgType[ChannelReady] + assert(channelReadyA1.nextCommitNonce_opt.nonEmpty) awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY) alice ! INPUT_DISCONNECTED @@ -566,19 +643,26 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) - assert(alice2bob.expectMsgType[ChannelReestablish].nextFundingTxId_opt.isEmpty) - alice2bob.forward(bob) - assert(bob2alice.expectMsgType[ChannelReestablish].nextFundingTxId_opt.contains(fundingTx.txid)) - bob2alice.forward(alice) + val channelReestablishA = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishA.nextFundingTxId_opt.isEmpty) + assert(channelReestablishA.currentCommitNonce_opt.isEmpty) + assert(channelReestablishA.nextCommitNonces.get(fundingTx.txid) == channelReadyA1.nextCommitNonce_opt) + alice2bob.forward(bob, channelReestablishA) + val channelReestablishB = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishB.nextFundingTxId_opt.contains(fundingTx.txid)) + assert(channelReestablishA.currentCommitNonce_opt.isEmpty) + assert(channelReestablishA.nextCommitNonces.contains(fundingTx.txid)) + bob2alice.forward(alice, channelReestablishB) alice2bob.expectMsgType[TxSignatures] alice2bob.forward(bob) - alice2bob.expectMsgType[ChannelReady] - alice2bob.forward(bob) + val channelReadyA2 = alice2bob.expectMsgType[ChannelReady] + assert(channelReadyA2.nextCommitNonce_opt == channelReadyA1.nextCommitNonce_opt) + alice2bob.forward(bob, channelReadyA2) assert(bob2blockchain.expectMsgType[WatchPublished].txId == fundingTx.txid) assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTx.txid) } - private def reconnect(f: FixtureParam, fundingTxId: TxId, aliceExpectsCommitSig: Boolean, bobExpectsCommitSig: Boolean): Unit = { + private def reconnect(f: FixtureParam, fundingTxId: TxId, commitmentFormat: CommitmentFormat, aliceExpectsCommitSig: Boolean, bobExpectsCommitSig: Boolean): Unit = { import f._ val listener = TestProbe() @@ -599,13 +683,38 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(channelReestablishBob.nextLocalCommitmentNumber == nextLocalCommitmentNumberBob) bob2alice.forward(alice, channelReestablishBob) + // When using taproot, we must provide nonces for the partial signatures. + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + Seq((channelReestablishAlice, aliceExpectsCommitSig), (channelReestablishBob, bobExpectsCommitSig)).foreach { + case (channelReestablish, expectCommitSig) => + assert(channelReestablish.nextCommitNonces.size == 1) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + if (expectCommitSig) { + assert(channelReestablish.currentCommitNonce_opt.nonEmpty) + assert(channelReestablish.currentCommitNonce_opt != channelReestablish.nextCommitNonces.get(fundingTxId)) + } else { + assert(channelReestablish.currentCommitNonce_opt.isEmpty) + } + } + } + if (aliceExpectsCommitSig) { - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) + val commitSigBob = bob2alice.expectMsgType[CommitSig] + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(commitSigBob.partialSignature_opt.isEmpty) + case _: SimpleTaprootChannelCommitmentFormat => assert(commitSigBob.partialSignature_opt.nonEmpty) + } + bob2alice.forward(alice, commitSigBob) } if (bobExpectsCommitSig) { - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) + val commitSigAlice = alice2bob.expectMsgType[CommitSig] + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(commitSigAlice.partialSignature_opt.isEmpty) + case _: SimpleTaprootChannelCommitmentFormat => assert(commitSigAlice.partialSignature_opt.nonEmpty) + } + alice2bob.forward(bob, commitSigAlice) } bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) @@ -620,7 +729,3 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny } } - -class WaitForDualFundingSignedStateWithTaprootChannelsSpec extends WaitForDualFundingSignedStateSpec { - override val extraTags: Set[String] = Set(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy) -} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index cf1449acb9..cb34d82ab0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -19,16 +19,19 @@ package fr.acinq.eclair.channel.states.b import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.crypto.NonceGenerator import fr.acinq.eclair.io.Peer.OpenChannelResponse import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingSigned, OpenChannel} -import fr.acinq.eclair.{TestConstants, TestKitBaseClass} +import fr.acinq.eclair.{TestConstants, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -100,28 +103,13 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Created] } - test("recv FundingSigned with valid signature (simple taproot channels legacy)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv FundingSigned with valid signature (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val listener = TestProbe() alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - bob2alice.expectMsgType[FundingSigned] - bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - val fundingTxId = watchConfirmed.txId - assert(watchConfirmed.minDepth == 6) - val txPublished = listener.expectMsgType[TransactionPublished] - assert(txPublished.tx.txid == fundingTxId) - assert(txPublished.miningFee > 0.sat) - aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Created] - } - - test("recv FundingSigned with valid signature (simple taproot channels zero fee)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee)) { f => - import f._ - val listener = TestProbe() - alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - bob2alice.expectMsgType[FundingSigned] - bob2alice.forward(alice) + val fundingSigned = bob2alice.expectMsgType[FundingSigned] + assert(fundingSigned.sigOrPartialSig.isInstanceOf[PartialSignatureWithNonce]) + bob2alice.forward(alice, fundingSigned) awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] val fundingTxId = watchConfirmed.txId @@ -152,6 +140,16 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS listener.expectMsgType[ChannelAborted] } + test("recv FundingSigned with invalid signature (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + // sending an invalid partial sig + alice ! FundingSigned(ByteVector32.Zeroes, PartialSignatureWithNonce(randomBytes32(), NonceGenerator.signingNonce(randomKey().publicKey, randomKey().publicKey, randomTxId()).publicNonce)) + awaitCond(alice.stateName == CLOSED) + alice2bob.expectMsgType[Error] + aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] + listener.expectMsgType[ChannelAborted] + } + test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index f144173b41..29495eb81d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFactory, PimpTestFSM} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.testutils.PimpTestProbe.convert -import fr.acinq.eclair.transactions.Transactions.{ClaimLocalAnchorTx, ClaimRemoteAnchorTx} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -870,7 +870,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) } - private def initiateRbf(f: FixtureParam): Unit = { + private def initiateRbf(f: FixtureParam): TxComplete = { import f._ alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0, None) @@ -892,8 +892,9 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2alice.forward(alice) alice2bob.expectMsgType[TxAddOutput] alice2bob.forward(bob) - bob2alice.expectMsgType[TxComplete] + val txCompleteBob = bob2alice.expectMsgType[TxComplete] bob2alice.forward(alice) + txCompleteBob } private def reconnectRbf(f: FixtureParam): (ChannelReestablish, ChannelReestablish) = { @@ -915,7 +916,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture (channelReestablishAlice, channelReestablishBob) } - test("recv INPUT_DISCONNECTED (unsigned rbf attempt)", Tag(ChannelStateTestsTags.DualFunding)) { f => + def testDisconnectUnsignedRbfAttempt(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ initiateRbf(f) @@ -923,14 +924,24 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2bob.expectMsgType[CommitSig] // bob doesn't receive alice's commit_sig awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId val rbfTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx.txId assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfInProgress]) + assert(fundingTxId != rbfTxId) val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + assert(channelReestablishAlice.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablishBob.nextCommitNonces.contains(fundingTxId)) + } // Bob detects that Alice stored an old RBF attempt and tells her to abort. bob2alice.expectMsgType[TxAbort] @@ -943,24 +954,48 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2alice.expectNoMessage(100 millis) } - test("recv INPUT_DISCONNECTED (rbf commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (unsigned rbf attempt)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testDisconnectUnsignedRbfAttempt(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (unsigned rbf attempt, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectUnsignedRbfAttempt(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def testDisconnectRbfCommitSigReceivedAlice(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - initiateRbf(f) - alice2bob.expectMsgType[TxComplete] + val txCompleteBob = initiateRbf(f) + val txCompleteAlice = alice2bob.expectMsgType[TxComplete] alice2bob.forward(bob) bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId val rbfTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx.txId + assert(fundingTxId != rbfTxId) val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == 0) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(rbfTxId)) + }) + assert(channelReestablishAlice.nextCommitNonces.get(rbfTxId).contains(txCompleteAlice.nonces_opt.get.nextCommitNonce)) + assert(channelReestablishBob.nextCommitNonces.get(rbfTxId).contains(txCompleteBob.nonces_opt.get.nextCommitNonce)) + } // Alice retransmits commit_sig, and they exchange tx_signatures afterwards. bob2alice.expectNoMessage(100 millis) @@ -979,11 +1014,19 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) } - test("recv INPUT_DISCONNECTED (rbf commit_sig received by Bob)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Alice)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testDisconnectRbfCommitSigReceivedAlice(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Alice, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectRbfCommitSigReceivedAlice(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def testDisconnectRbfCommitSigReceivedBob(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - initiateRbf(f) - alice2bob.expectMsgType[TxComplete] + val txCompleteBob = initiateRbf(f) + val txCompleteAlice = alice2bob.expectMsgType[TxComplete] alice2bob.forward(bob) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) @@ -991,13 +1034,29 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId val rbfTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx.txId + assert(fundingTxId != rbfTxId) val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(rbfTxId)) + }) + assert(channelReestablishAlice.nextCommitNonces.get(rbfTxId).contains(txCompleteAlice.nonces_opt.get.nextCommitNonce)) + assert(channelReestablishBob.nextCommitNonces.get(rbfTxId).contains(txCompleteBob.nonces_opt.get.nextCommitNonce)) + } // Bob retransmits commit_sig and tx_signatures, then Alice sends her tx_signatures. bob2alice.expectMsgType[CommitSig] @@ -1016,7 +1075,15 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) } - test("recv INPUT_DISCONNECTED (rbf commit_sig received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Bob)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testDisconnectRbfCommitSigReceivedBob(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Bob, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectRbfCommitSigReceivedBob(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received by Bob, taproot, missing current commit nonce)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ initiateRbf(f) @@ -1024,18 +1091,67 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2bob.forward(bob) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig + bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures + awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + // Alice is buggy and doesn't include her current commit nonce in channel_reestablish. + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.nextFundingTxId_opt.nonEmpty) + assert(channelReestablishAlice.nextLocalCommitmentNumber == 0) + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + bob2alice.expectMsgType[ChannelReestablish] + alice2bob.forward(bob, channelReestablishAlice.copy(tlvStream = TlvStream(channelReestablishAlice.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.CurrentCommitNonceTlv])))) + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSING) + } + + def testDisconnectRbfCommitSigReceived(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { + import f._ + + val txCompleteBob = initiateRbf(f) + val txCompleteAlice = alice2bob.expectMsgType[TxComplete] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId val rbfTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx + assert(fundingTxId != rbfTx.txId) val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTx.txId)) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTx.txId)) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(rbfTx.txId)) + }) + assert(channelReestablishAlice.nextCommitNonces.get(rbfTx.txId).contains(txCompleteAlice.nonces_opt.get.nextCommitNonce)) + assert(channelReestablishBob.nextCommitNonces.get(rbfTx.txId).contains(txCompleteBob.nonces_opt.get.nextCommitNonce)) + } // Alice and Bob exchange tx_signatures and complete the RBF attempt. bob2alice.expectMsgType[TxSignatures] @@ -1051,10 +1167,17 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) } - test("recv INPUT_DISCONNECTED (rbf tx_signatures partially received)", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv INPUT_DISCONNECTED (rbf commit_sig received)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testDisconnectRbfCommitSigReceived(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectRbfCommitSigReceived(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf commit_sig received, taproot, missing next commit nonce)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ - val currentFundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId initiateRbf(f) alice2bob.expectMsgType[TxComplete] alice2bob.forward(bob) @@ -1062,6 +1185,49 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2bob.forward(bob) bob2alice.expectMsgType[CommitSig] bob2alice.forward(alice) + bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures + awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs]) + awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) + val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId + val rbfTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status.asInstanceOf[DualFundingStatus.RbfWaitingForSigs].signingSession.fundingTx.txId + assert(fundingTxId != rbfTxId) + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + // Alice is buggy and doesn't include her next commit nonce for the initial funding tx. + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + val aliceNonces = ChannelReestablishTlv.NextLocalNoncesTlv((channelReestablishAlice.nextCommitNonces - fundingTxId).toSeq) + val channelReestablishAlice1 = channelReestablishAlice.copy(tlvStream = TlvStream(channelReestablishAlice.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]) + aliceNonces)) + // Bob is buggy and doesn't include his next commit nonce for the RBF tx. + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + val bobNonces = ChannelReestablishTlv.NextLocalNoncesTlv((channelReestablishBob.nextCommitNonces - rbfTxId).toSeq) + val channelReestablishBob1 = channelReestablishBob.copy(tlvStream = TlvStream(channelReestablishBob.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]) + bobNonces)) + alice2bob.forward(bob, channelReestablishAlice1) + assert(bob2alice.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishBob.channelId, fundingTxId, commitmentNumber = 1).getMessage) + awaitCond(bob.stateName == CLOSING) + bob2alice.forward(alice, channelReestablishBob1) + assert(alice2bob.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishAlice.channelId, rbfTxId, commitmentNumber = 1).getMessage) + awaitCond(alice.stateName == CLOSING) + } + + def testDisconnectTxSigsPartiallyReceived(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { + import f._ + + val currentFundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId + val txCompleteBob = initiateRbf(f) + val txCompleteAlice = alice2bob.expectMsgType[TxComplete] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) bob2alice.expectMsgType[TxSignatures] bob2alice.forward(alice) alice2bob.expectMsgType[TxSignatures] // Bob doesn't receive Alice's tx_signatures @@ -1072,9 +1238,23 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f) assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: TaprootCommitmentFormat => + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 2) + assert(channelReestablish.nextCommitNonces.contains(currentFundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(rbfTxId)) + }) + assert(channelReestablishAlice.nextCommitNonces.get(rbfTxId).contains(txCompleteAlice.nonces_opt.get.nextCommitNonce)) + assert(channelReestablishBob.nextCommitNonces.get(rbfTxId).contains(txCompleteBob.nonces_opt.get.nextCommitNonce)) + } // Alice and Bob exchange signatures and complete the RBF attempt. bob2alice.expectNoMessage(100 millis) @@ -1089,6 +1269,14 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) } + test("recv INPUT_DISCONNECTED (rbf tx_signatures partially received)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testDisconnectTxSigsPartiallyReceived(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_DISCONNECTED (rbf tx_signatures partially received, taproot)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testDisconnectTxSigsPartiallyReceived(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val tx = alice.signCommitTx() 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 732b22ebb9..fc18f2ca9b 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 @@ -22,7 +22,7 @@ import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.NumericSatoshi.abs -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw @@ -43,7 +43,6 @@ import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ import org.scalatest.Inside.inside import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -61,11 +60,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging - val extraTags: Set[String] = Set.empty - val spliceChannelType_opt: Option[ChannelType] = None - override def withFixture(test: OneArgTest): Outcome = { - val tags = test.tags + ChannelStateTestsTags.DualFunding ++ extraTags + val tags = test.tags + ChannelStateTestsTags.DualFunding val setup = init(tags = tags) import setup._ reachNormal(setup, tags) @@ -78,9 +74,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik private val defaultSpliceOutScriptPubKey = hex"0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - private def initiateSpliceWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], sendTxComplete: Boolean): TestProbe = { + private def initiateSpliceWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], channelType_opt: Option[ChannelType], sendTxComplete: Boolean): TestProbe = { val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt, spliceOut_opt, None, this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt, spliceOut_opt, None, channelType_opt) s ! cmd exchangeStfu(s, r, s2r, r2s) s2r.expectMsgType[SpliceInit] @@ -119,7 +115,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik sender } - private def initiateSpliceWithoutSigs(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None, sendTxComplete: Boolean = true): TestProbe = initiateSpliceWithoutSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt, sendTxComplete) + private def initiateSpliceWithoutSigs(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None, channelType_opt: Option[ChannelType] = None, sendTxComplete: Boolean = true): TestProbe = { + initiateSpliceWithoutSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt, channelType_opt, sendTxComplete) + } private def initiateRbfWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, feerate: FeeratePerKw, sInputsCount: Int, sOutputsCount: Int, rInputsCount: Int, rOutputsCount: Int): TestProbe = { val sender = TestProbe() @@ -219,12 +217,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik private def exchangeSpliceSigs(f: FixtureParam, sender: TestProbe): Transaction = exchangeSpliceSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, sender) - private def initiateSplice(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]): Transaction = { - val sender = initiateSpliceWithoutSigs(s, r, s2r, r2s, spliceIn_opt, spliceOut_opt, sendTxComplete = true) + private def initiateSplice(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], channelType_opt: Option[ChannelType]): Transaction = { + val sender = initiateSpliceWithoutSigs(s, r, s2r, r2s, spliceIn_opt, spliceOut_opt, channelType_opt, sendTxComplete = true) exchangeSpliceSigs(s, r, s2r, r2s, sender) } - private def initiateSplice(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None): Transaction = initiateSplice(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt) + private def initiateSplice(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None, channelType_opt: Option[ChannelType] = None): Transaction = { + initiateSplice(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt, channelType_opt) + } private def initiateRbf(f: FixtureParam, feerate: FeeratePerKw, sInputsCount: Int, sOutputsCount: Int): Transaction = { val sender = initiateRbfWithoutSigs(f, feerate, sInputsCount, sOutputsCount) @@ -358,7 +358,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(finalState.commitments.latest.localCommit.spec.toRemote == 700_000_000.msat - settledHtlcs) } - test("recv CMD_SPLICE (splice-in)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv CMD_SPLICE (splice-in)") { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] @@ -413,7 +413,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(400_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -462,7 +462,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(400_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -489,7 +489,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(5_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -506,7 +506,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(100_000 sat, LiquidityAds.FundingRate(10_000 sat, 200_000 sat, 0, 0, 0 sat, 0 sat), LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -525,7 +525,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice requests a lot of funding, but she doesn't have enough balance to pay the corresponding fee. assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 800_000_000.msat) val fundingRequest = LiquidityAds.RequestFunding(5_000_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - val cmd = CMD_SPLICE(sender.ref, None, Some(SpliceOut(750_000 sat, defaultSpliceOutScriptPubKey)), Some(fundingRequest), this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, None, Some(SpliceOut(750_000 sat, defaultSpliceOutScriptPubKey)), Some(fundingRequest), None) alice ! cmd exchangeStfu(alice, bob, alice2bob, bob2alice) @@ -613,7 +613,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(commitFees < 15_000.sat) val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(760_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(760_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) sender.expectMsgType[RES_FAILURE[_, _]] @@ -630,13 +630,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(commitment.localCommit.spec.toLocal == 650_000_000.msat) assert(commitment.localChannelReserve == 15_000.sat) val commitFees = Transactions.commitTxTotalCost(commitment.remoteCommitParams.dustLimit, commitment.remoteCommit.spec, commitment.commitmentFormat) - commitment.commitmentFormat match { - case _: SimpleTaprootChannelCommitmentFormat | _: AnchorOutputsCommitmentFormat => assert(commitFees > 7_000.sat) - case DefaultCommitmentFormat => assert(commitFees > 20_000.sat) - } + assert(commitFees > 20_000.sat) val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(630_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(630_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) sender.expectMsgType[RES_FAILURE[_, _]] @@ -646,7 +643,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = None, requestFunding_opt = None, this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) // we tweak the feerate @@ -667,7 +664,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val bobBalance = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal - alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(100_000 sat)), spliceOut_opt = None, requestFunding_opt = None, this.spliceChannelType_opt) + alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(100_000 sat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) exchangeStfu(f) val spliceInit = alice2bob.expectMsgType[SpliceInit] alice2bob.forward(bob, spliceInit) @@ -692,7 +689,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(bob, alice, bob2alice, alice2bob) // Bob makes a large splice: Alice doesn't meet the new reserve requirements, but she met the previous one, so we allow this. - initiateSplice(bob, alice, bob2alice, alice2bob, spliceIn_opt = Some(SpliceIn(4_000_000 sat)), spliceOut_opt = None) + initiateSplice(bob, alice, bob2alice, alice2bob, spliceIn_opt = Some(SpliceIn(4_000_000 sat)), spliceOut_opt = None, channelType_opt = None) val postSpliceState = alice.stateData.asInstanceOf[DATA_NORMAL] assert(postSpliceState.commitments.latest.localCommit.spec.toLocal < postSpliceState.commitments.latest.localChannelReserve) @@ -720,7 +717,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik initiateRbf(f, FeeratePerKw(15_000 sat), sInputsCount = 2, sOutputsCount = 1) val probe = TestProbe() - alice ! CMD_SPLICE(probe.ref, Some(SpliceIn(250_000 sat)), None, None, this.spliceChannelType_opt) + alice ! CMD_SPLICE(probe.ref, Some(SpliceIn(250_000 sat)), None, None, None) assert(probe.expectMsgType[RES_FAILURE[_, ChannelException]].t.isInstanceOf[InvalidSpliceWithUnconfirmedTx]) bob2alice.forward(alice, Stfu(alice.stateData.channelId, initiator = true)) @@ -738,7 +735,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // We allow initiating such splice... val probe = TestProbe() - alice ! CMD_SPLICE(probe.ref, Some(SpliceIn(250_000 sat)), None, None, this.spliceChannelType_opt) + alice ! CMD_SPLICE(probe.ref, Some(SpliceIn(250_000 sat)), None, None, None) alice2bob.expectMsgType[Stfu] alice2bob.forward(bob) bob2alice.expectMsgType[Stfu] @@ -756,6 +753,26 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } + test("recv CMD_SPLICE (accepting upgrade channel to taproot)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + import f._ + + val htlcs = setupHtlcs(f) + initiateSplice(f, spliceIn_opt = Some(SpliceIn(400_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix())) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + resolveHtlcs(f, htlcs) + } + + test("recv CMD_SPLICE (rejecting upgrade channel to taproot)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + val htlcs = setupHtlcs(f) + initiateSplice(f, spliceIn_opt = Some(SpliceIn(400_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix())) + assert(alice.commitments.active.head.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + resolveHtlcs(f, htlcs) + } + test("recv CMD_BUMP_FUNDING_FEE (splice-in + splice-out)") { f => import f._ @@ -804,7 +821,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } // We can keep doing more splice transactions now that one of the previous transactions confirmed. - initiateSplice(bob, alice, bob2alice, alice2bob, Some(SpliceIn(100_000 sat)), None) + initiateSplice(bob, alice, bob2alice, alice2bob, Some(SpliceIn(100_000 sat)), None, None) } test("recv CMD_BUMP_FUNDING_FEE (splice-in + splice-out from non-initiator)") { f => @@ -815,7 +832,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik confirmSpliceTx(f, spliceTx1) // Bob initiates a second splice that spends the first splice. - val spliceTx2 = initiateSplice(bob, alice, bob2alice, alice2bob, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = Some(SpliceOut(25_000 sat, defaultSpliceOutScriptPubKey))) + val spliceTx2 = initiateSplice(bob, alice, bob2alice, alice2bob, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = Some(SpliceOut(25_000 sat, defaultSpliceOutScriptPubKey)), channelType_opt = None) assert(spliceTx2.txIn.exists(_.outPoint.txid == spliceTx1.txid)) // Alice cannot RBF her first splice, so she RBFs Bob's splice instead. @@ -831,7 +848,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice initiates a splice-in with a liquidity purchase. val sender = TestProbe() val fundingRequest = LiquidityAds.RequestFunding(400_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalance) - alice ! CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), this.spliceChannelType_opt) + alice ! CMD_SPLICE(sender.ref, Some(SpliceIn(500_000 sat)), None, Some(fundingRequest), None) exchangeStfu(alice, bob, alice2bob, bob2alice) inside(alice2bob.expectMsgType[SpliceInit]) { msg => assert(msg.fundingContribution == 500_000.sat) @@ -1002,7 +1019,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik import f._ val sender = TestProbe() - alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, this.spliceChannelType_opt) + alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, channelType_opt = None) exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] alice2bob.forward(bob) @@ -1027,7 +1044,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik import f._ val sender = TestProbe() - alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, this.spliceChannelType_opt) + alice ! CMD_SPLICE(sender.ref, spliceIn_opt = None, spliceOut_opt = Some(SpliceOut(50_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None, channelType_opt = None) exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] alice2bob.forward(bob) @@ -1062,10 +1079,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik test("recv TxAbort (after CommitSig)") { f => import f._ - assume(!this.extraTags.contains(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy) && !this.extraTags.contains(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee) && this.spliceChannelType_opt.isEmpty) - val sender = TestProbe() - alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None, requestFunding_opt = None, this.spliceChannelType_opt) + alice ! CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] alice2bob.forward(bob) @@ -1527,7 +1542,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik awaitAssert(assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.all.size == 1)) } - test("recv CMD_ADD_HTLC with multiple commitments", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv CMD_ADD_HTLC with multiple commitments") { f => import f._ initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) val sender = TestProbe() @@ -1550,15 +1565,43 @@ 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") { f => + test("recv CMD_ADD_HTLC with multiple commitments (missing nonces)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ - initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) val sender = TestProbe() alice ! CMD_ADD_HTLC(sender.ref, 500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) alice ! CMD_SIGN() + val sigsA = alice2bob.expectMsgType[CommitSigBatch] + assert(sigsA.batchSize == 2) + alice2bob.forward(bob, sigsA) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + val sigsB = bob2alice.expectMsgType[CommitSigBatch] + assert(sigsB.batchSize == 2) + bob2alice.forward(alice, sigsB) + val revA = alice2bob.expectMsgType[RevokeAndAck] + assert(revA.nextCommitNonces.size == 2) + val missingNonce = RevokeAndAckTlv.NextLocalNoncesTlv(revA.nextCommitNonces.toSeq.take(1)) + alice2bob.forward(bob, revA.copy(tlvStream = TlvStream(revA.tlvStream.records.filterNot(_.isInstanceOf[RevokeAndAckTlv.NextLocalNoncesTlv]) + missingNonce))) + bob2alice.expectMsgType[Error] + val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(commitTx, Seq(spliceTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + test("recv CMD_ADD_HTLC with multiple commitments and reconnect", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + val sender = TestProbe() + val preimage = randomBytes32() + alice ! CMD_ADD_HTLC(sender.ref, 500_000 msat, Crypto.sha256(preimage), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, Reputation.Score.max, None, localOrigin(sender.ref)) + sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] + val add = alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + alice ! CMD_SIGN() assert(alice2bob.expectMsgType[CommitSigBatch].batchSize == 2) // Bob disconnects before receiving Alice's commit_sig. disconnect(f) @@ -1568,21 +1611,23 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sigsA = alice2bob.expectMsgType[CommitSigBatch] assert(sigsA.batchSize == 2) alice2bob.forward(bob, sigsA) - bob2alice.expectMsgType[RevokeAndAck] + assert(bob2alice.expectMsgType[RevokeAndAck].nextCommitNonces.size == 2) bob2alice.forward(alice) val sigsB = bob2alice.expectMsgType[CommitSigBatch] assert(sigsB.batchSize == 2) bob2alice.forward(alice, sigsB) - alice2bob.expectMsgType[RevokeAndAck] + assert(alice2bob.expectMsgType[RevokeAndAck].nextCommitNonces.size == 2) 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)) + fulfillHtlc(add.id, preimage, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) } test("recv CMD_ADD_HTLC while a splice is requested") { f => import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] @@ -1594,7 +1639,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik test("recv CMD_ADD_HTLC while a splice is in progress") { f => import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] @@ -1610,7 +1655,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik test("recv UpdateAddHtlc while a splice is in progress") { f => import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] @@ -1780,7 +1825,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik import f._ val sender = TestProbe() - val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, this.spliceChannelType_opt) + val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = None) alice ! cmd exchangeStfu(f) alice2bob.expectMsgType[SpliceInit] @@ -1862,7 +1907,87 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (commit_sig received by alice)") { f => + test("disconnect (commit_sig not received, missing current nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + setupHtlcs(f) + val bobCommitIndex = bob.commitments.localCommitIndex + initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs]) + val spliceTxId = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs].signingSession.fundingTxId + + disconnect(f) + + val aliceInit = Init(alice.commitments.localChannelParams.initFeatures) + val bobInit = Init(bob.commitments.localChannelParams.initFeatures) + + alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceTxId)) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) + + bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceTxId)) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + + // If Bob doesn't provide a nonce for Alice to retransmit her commit_sig, she cannot sign. + // We sent a warning and wait for Bob to fix his node instead of force-closing. + bob2alice.forward(alice, channelReestablishBob.copy(tlvStream = TlvStream(channelReestablishBob.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.CurrentCommitNonceTlv])))) + assert(alice2bob.expectMsgType[Warning].toAscii == MissingCommitNonce(channelReestablishBob.channelId, spliceTxId, bobCommitIndex).getMessage) + alice2bob.expectNoMessage(100 millis) + assert(alice.stateName == NORMAL) + } + + test("disconnect (commit_sig not received, missing next nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + setupHtlcs(f) + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId + initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs]) + val spliceTxId = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs].signingSession.fundingTxId + + disconnect(f) + + val aliceInit = Init(alice.commitments.localChannelParams.initFeatures) + val bobInit = Init(bob.commitments.localChannelParams.initFeatures) + val aliceCommitTx = alice.signCommitTx() + val bobCommitTx = bob.signCommitTx() + + alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceTxId)) + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + + bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceTxId)) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + + // If Alice doesn't include a nonce for the previous funding transaction, Bob must force-close. + val noncesAlice1 = ChannelReestablishTlv.NextLocalNoncesTlv((channelReestablishAlice.nextCommitNonces - fundingTxId).toSeq) + val channelReestablishAlice1 = channelReestablishAlice.copy(tlvStream = TlvStream(channelReestablishAlice.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]) + noncesAlice1)) + alice2bob.forward(bob, channelReestablishAlice1) + assert(bob2alice.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishAlice.channelId, fundingTxId, aliceCommitIndex + 1).getMessage) + bob2blockchain.expectFinalTxPublished(bobCommitTx.txid) + + // If Bob doesn't include a nonce for the splice transaction, Alice must force-close. + val noncesBob1 = ChannelReestablishTlv.NextLocalNoncesTlv((channelReestablishBob.nextCommitNonces - spliceTxId).toSeq) + val channelReestablishBob1 = channelReestablishBob.copy(tlvStream = TlvStream(channelReestablishBob.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv]) + noncesBob1)) + bob2alice.forward(alice, channelReestablishBob1) + assert(alice2bob.expectMsgType[Error].toAscii == MissingCommitNonce(channelReestablishBob.channelId, spliceTxId, bobCommitIndex + 1).getMessage) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) + } + + def disconnectCommitSigReceivedAlice(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ // Disconnection with both sides sending commit_sig // alice bob @@ -1880,8 +2005,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // |----- tx_signatures -->| val htlcs = setupHtlcs(f) - val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId assert(aliceCommitIndex != bobCommitIndex) val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) @@ -1893,10 +2019,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik disconnect(f) val (channelReestablishAlice, channelReestablishBob) = reconnect(f) - assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) + assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) - assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) + assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach { channelReestablish => + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceStatus.signingSession.fundingTxId)) + } + } // Alice retransmits commit_sig, and they exchange tx_signatures afterwards. bob2alice.expectNoMessage(100 millis) @@ -1909,7 +2046,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob) sender.expectMsgType[RES_SPLICE] - val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + val spliceTx = alice.commitments.latest.localFundingStatus.signedTx_opt.get alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) @@ -1924,12 +2061,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (commit_sig received by bob)") { f => + test("disconnect (commit_sig received by alice)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + disconnectCommitSigReceivedAlice(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("disconnect (commit_sig received by alice, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + disconnectCommitSigReceivedAlice(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def disconnectCommitSigReceivedBob(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val htlcs = setupHtlcs(f) - val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId assert(aliceCommitIndex != bobCommitIndex) val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) @@ -1947,6 +2093,17 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex) assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach { channelReestablish => + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceStatus.signingSession.fundingTxId)) + } + } // Bob retransmit commit_sig and tx_signatures, Alice sends tx_signatures afterwards. bob2alice.expectMsgType[CommitSig] @@ -1958,7 +2115,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob) sender.expectMsgType[RES_SPLICE] - val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + val spliceTx = alice.commitments.latest.localFundingStatus.signedTx_opt.get alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) @@ -1973,12 +2130,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (commit_sig received)") { f => + test("disconnect (commit_sig received by bob)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + disconnectCommitSigReceivedBob(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("disconnect (commit_sig received by bob, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + disconnectCommitSigReceivedBob(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def disconnectCommitSigReceived(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val htlcs = setupHtlcs(f) - val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) alice2bob.expectMsgType[CommitSig] @@ -1992,8 +2158,18 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => Seq(channelReestablishAlice, channelReestablishBob).foreach { channelReestablish => + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceTxId)) + } + } bob2blockchain.expectWatchFundingConfirmed(spliceTxId) // Alice and Bob retransmit tx_signatures. @@ -2004,7 +2180,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.forward(bob) sender.expectMsgType[RES_SPLICE] - val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + val spliceTx = alice.commitments.latest.localFundingStatus.signedTx_opt.get alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) alice2bob.expectMsgType[SpliceLocked] @@ -2012,13 +2188,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) bob2alice.expectMsgType[SpliceLocked] bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) + awaitCond(alice.commitments.active.size == 1) + awaitCond(bob.commitments.active.size == 1) resolveHtlcs(f, htlcs) } - test("disconnect (tx_signatures received by alice)") { f => + test("disconnect (commit_sig received)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + disconnectCommitSigReceived(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("disconnect (commit_sig received, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + disconnectCommitSigReceived(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def disconnectTxSigsReceivedAlice(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ // Disconnection with both sides sending tx_signatures // alice bob @@ -2036,8 +2220,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // |----- tx_signatures -->| val htlcs = setupHtlcs(f) - val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex - val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex + val aliceCommitIndex = alice.commitments.localCommitIndex + val bobCommitIndex = bob.commitments.localCommitIndex + val fundingTxId = alice.commitments.latest.fundingTxId initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) alice2bob.expectMsgType[CommitSig] @@ -2054,14 +2239,24 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1) + assert(channelReestablishAlice.currentCommitNonce_opt.isEmpty) assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => () + case _: SimpleTaprootChannelCommitmentFormat => Seq(channelReestablishAlice, channelReestablishBob).foreach { channelReestablish => + assert(channelReestablish.nextCommitNonces.size == 2) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceTxId)) + } + } alice2blockchain.expectWatchFundingConfirmed(spliceTxId) bob2blockchain.expectWatchFundingConfirmed(spliceTxId) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 2) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 2) - val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + assert(alice.commitments.active.size == 2) + assert(bob.commitments.active.size == 2) + val spliceTx = alice.commitments.latest.localFundingStatus.signedTx_opt.get // Alice retransmits tx_signatures. alice2bob.expectMsgType[TxSignatures] @@ -2072,12 +2267,20 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx) bob2alice.expectMsgType[SpliceLocked] bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) + awaitCond(alice.commitments.active.size == 1) + awaitCond(bob.commitments.active.size == 1) resolveHtlcs(f, htlcs) } + test("disconnect (tx_signatures received by alice)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + disconnectTxSigsReceivedAlice(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("disconnect (tx_signatures received by alice, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + disconnectTxSigsReceivedAlice(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("disconnect (tx_signatures received by alice, zero-conf)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ @@ -2293,10 +2496,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik resolveHtlcs(f, htlcs) } - test("disconnect (RBF commit_sig received by bob)") { f => + test("disconnect (RBF commit_sig received by bob)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val htlcs = setupHtlcs(f) + val fundingTxId = alice.commitments.latest.fundingTxId val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == spliceTx.txid) @@ -2322,8 +2526,17 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex) + assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty) assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId)) assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1) + assert(channelReestablishBob.currentCommitNonce_opt.isEmpty) + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => { + assert(channelReestablish.nextCommitNonces.size == 3) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + assert(channelReestablish.nextCommitNonces.contains(spliceTx.txid)) + assert(channelReestablish.nextCommitNonces.contains(rbfTxId)) + assert(channelReestablish.nextCommitNonces.values.toSet.size == 3) + }) bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) // Bob retransmits commit_sig, and they exchange tx_signatures afterwards. @@ -2404,7 +2617,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2blockchain.expectWatchFundingSpent(fundingTx.txid) } - test("re-send splice_locked on reconnection") { f => + def resendSpliceLockedOnReconnection(f: FixtureParam): Unit = { import f._ val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) @@ -2492,6 +2705,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } + test("re-send splice_locked on reconnection") { f => + resendSpliceLockedOnReconnection(f) + } + + test("re-send splice_locked on reconnection (taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => + resendSpliceLockedOnReconnection(f) + } + test("disconnect before channel update and tx_signatures are received") { f => import f._ // Disconnection with both sides sending tx_signatures and channel updates @@ -2703,7 +2924,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2alice.expectNoMessage(100 millis) } - test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update; revoke_and_ack not received", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update; revoke_and_ack not received") { f => import f._ // alice bob // | ... | @@ -3014,8 +3235,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik test("force-close with multiple splices (simple)") { f => import f._ - assume(extraTags.isEmpty) - val htlcs = setupHtlcs(f) val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) @@ -3098,7 +3317,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(Helpers.Closing.isClosed(bob.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RemoteClose])) } - test("force-close with multiple splices (previous active remote)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("force-close with multiple splices (previous active remote)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => import f._ val htlcs = setupHtlcs(f) @@ -3136,7 +3355,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Bob publishes his commit tx for the first splice transaction (which double-spends the second splice transaction). val bobCommitments = bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments val previousCommitment = bobCommitments.active.find(_.fundingTxIndex == 1).get - val bobCommitTx1 = previousCommitment.fullySignedLocalCommitTx(bobCommitments.channelParams, bob.underlyingActor.channelKeys).toTry.get + val bobCommitTx1 = previousCommitment.fullySignedLocalCommitTx(bobCommitments.channelParams, bob.underlyingActor.channelKeys) val bobHtlcTxs = previousCommitment.htlcTxs(bobCommitments.channelParams, bob.underlyingActor.channelKeys).map(_._1) Transaction.correctlySpends(bobCommitTx1, Seq(fundingTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice ! WatchFundingSpentTriggered(bobCommitTx1) @@ -3353,7 +3572,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RemoteClose])) } - test("force-close with multiple splices (inactive revoked)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("force-close with multiple splices (inactive revoked)", Tag(ChannelStateTestsTags.ZeroConf), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val htlcs = setupHtlcs(f) @@ -3455,6 +3674,296 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RevokedClose])) } + test("force-close after channel type upgrade (latest active)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our first splice upgrades the channel to taproot. + val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix())) + checkWatchConfirmed(f, fundingTx1) + + // The first splice confirms on Bob's side. + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) + bob2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx1.txid) + bob2alice.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == fundingTx1.txid) + bob2alice.forward(alice) + + // The second splice preserves the taproot commitment format. + val fundingTx2 = initiateSplice(f, spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + checkWatchConfirmed(f, fundingTx2) + assert(alice.commitments.active.map(_.commitmentFormat).count(_ == UnsafeLegacyAnchorOutputsCommitmentFormat) == 1) + assert(alice.commitments.active.map(_.commitmentFormat).count(_ == PhoenixSimpleTaprootChannelCommitmentFormat) == 2) + + // From Alice's point of view, we now have two unconfirmed splices. + alice ! CMD_FORCECLOSE(ActorRef.noSender) + alice2bob.expectMsgType[Error] + val commitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(commitTx2, Seq(fundingTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val aliceAnchorTx = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val claimMainAlice = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMainAlice.tx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + // Alice publishes her htlc timeout transactions. + val aliceHtlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + aliceHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + + // Bob detects Alice's commit tx. + bob ! WatchFundingSpentTriggered(commitTx2) + val bobAnchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainBob.tx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val bobHtlcTimeout = htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + bobHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(commitTx2.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(claimMainBob.input, bobAnchorTx.input.outPoint) ++ aliceHtlcTimeout.map(_.input.outPoint) ++ bobHtlcTimeout.map(_.input.outPoint)) + alice2blockchain.expectWatchTxConfirmed(commitTx2.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMainAlice.input, aliceAnchorTx.input.outPoint) ++ aliceHtlcTimeout.map(_.input.outPoint) ++ bobHtlcTimeout.map(_.input.outPoint)) + + // The first splice transaction confirms. + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) + alice2blockchain.expectMsgType[WatchFundingSpent] + + // The second splice transaction confirms. + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx2) + alice2blockchain.expectMsgType[WatchFundingSpent] + + // Alice detects that the commit confirms, along with 2nd-stage and 3rd-stage transactions. + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMainAlice.tx) + aliceHtlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(0 sat, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + val htlcDelayed = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed.input) + alice ! WatchOutputSpentTriggered(0 sat, htlcDelayed.tx) + alice2blockchain.expectWatchTxConfirmed(htlcDelayed.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcDelayed.tx) + }) + bobHtlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + alice2blockchain.expectNoMessage(100 millis) + awaitCond(alice.stateName == CLOSED) + + // Bob also detects that the commit confirms, along with 2nd-stage transactions. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMainBob.tx) + bobHtlcTimeout.foreach(htlcTx => { + bob ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + bob2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + aliceHtlcTimeout.foreach(htlcTx => { + bob ! WatchOutputSpentTriggered(0 sat, htlcTx.tx) + bob2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + bob2blockchain.expectNoMessage(100 millis) + awaitCond(bob.stateName == CLOSED) + + checkPostSpliceState(f, spliceOutFee(f, capacity = 1_900_000.sat, signedTx_opt = Some(fundingTx2))) + assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[LocalClose])) + assert(Helpers.Closing.isClosed(bob.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RemoteClose])) + } + + test("force-close after channel type upgrade (previous active)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our splice upgrades the channel to taproot. + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix())) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + checkWatchConfirmed(f, spliceTx) + + // Alice force-closes using the non-taproot commitment. + val aliceCommitTx = alice.commitments.active.last.fullySignedLocalCommitTx(alice.commitments.channelParams, alice.underlyingActor.channelKeys) + bob ! WatchFundingSpentTriggered(aliceCommitTx) + assert(bob2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == aliceCommitTx.txid) + // Bob reacts by publishing the taproot commitment. + val bobCommitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(bobCommitTx, Seq(spliceTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val localAnchor = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = bob2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + bob2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(localMain.input) + bob2blockchain.expectWatchOutputSpent(localAnchor.input.outPoint) + (htlcs.aliceToBob.map(_._2) ++ htlcs.bobToAlice.map(_._2)).foreach(_ => bob2blockchain.expectMsgType[WatchOutputSpent]) + bob2blockchain.expectNoMessage(100 millis) + + // Alice's commit tx confirms. + bob ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(450_000), 5, aliceCommitTx) + val anchorTx = bob2blockchain.expectReplaceableTxPublished[ClaimRemoteAnchorTx] + val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val bobHtlcTimeout = htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[ClaimHtlcTimeoutTx]) + bobHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.sign(), Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(mainTx.input) + bob2blockchain.expectWatchOutputSpent(anchorTx.input.outPoint) + (htlcs.aliceToBob.map(_._2) ++ htlcs.bobToAlice.map(_._2)).foreach(_ => bob2blockchain.expectMsgType[WatchOutputSpent]) + bob2blockchain.expectNoMessage(100 millis) + } + + test("force-close after channel type upgrade (revoked previous active)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our splice upgrades the channel to taproot. + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix())) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + checkWatchConfirmed(f, spliceTx) + + // Alice will force-close using a non-taproot revoked commitment. + val aliceCommitTx = alice.commitments.active.last.fullySignedLocalCommitTx(alice.commitments.channelParams, alice.underlyingActor.channelKeys) + addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + bob ! WatchFundingSpentTriggered(aliceCommitTx) + assert(bob2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == aliceCommitTx.txid) + // Bob reacts by publishing the taproot commitment. + val bobCommitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(bobCommitTx, Seq(spliceTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val localAnchor = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = bob2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + bob2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(localMain.input) + bob2blockchain.expectWatchOutputSpent(localAnchor.input.outPoint) + (htlcs.aliceToBob.map(_._2) ++ htlcs.bobToAlice.map(_._2)).foreach(_ => bob2blockchain.expectMsgType[WatchOutputSpent]) + bob2blockchain.expectMsgType[WatchOutputSpent] // newly added HTLC + bob2blockchain.expectNoMessage(100 millis) + + // Alice's commit tx confirms. + bob ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(450_000), 5, aliceCommitTx) + val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val penaltyTx = bob2blockchain.expectFinalTxPublished("main-penalty") + Transaction.correctlySpends(penaltyTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => bob2blockchain.expectFinalTxPublished("htlc-penalty")) + htlcPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, penaltyTx.input) ++ htlcPenalty.map(_.input)) + bob2blockchain.expectNoMessage(100 millis) + + // Bob's penalty txs confirm. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceCommitTx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainTx.tx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penaltyTx.tx) + htlcPenalty.foreach { penalty => bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } + awaitCond(bob.stateName == CLOSED) + assert(Helpers.Closing.isClosed(bob.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RevokedClose])) + } + + test("force-close after channel type upgrade (revoked latest active)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our splice upgrades the channel to taproot. + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix())) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + checkWatchConfirmed(f, spliceTx) + + // Alice will force-close using a taproot revoked commitment. + val aliceCommitTx = alice.commitments.active.head.fullySignedLocalCommitTx(alice.commitments.channelParams, alice.underlyingActor.channelKeys) + addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + bob ! WatchFundingSpentTriggered(aliceCommitTx) + // Bob reacts by publishing penalty transactions. + val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val penaltyTx = bob2blockchain.expectFinalTxPublished("main-penalty") + Transaction.correctlySpends(penaltyTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => bob2blockchain.expectFinalTxPublished("htlc-penalty")) + htlcPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, penaltyTx.input) ++ htlcPenalty.map(_.input)) + bob2blockchain.expectNoMessage(100 millis) + + // Bob's penalty txs confirm. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceCommitTx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainTx.tx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penaltyTx.tx) + htlcPenalty.foreach { penalty => bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } + awaitCond(bob.stateName == CLOSED) + assert(Helpers.Closing.isClosed(bob.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RevokedClose])) + } + + test("force-close after channel type upgrade (revoked previous inactive)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + // Our splice upgrades the channel to taproot. + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), channelType_opt = Some(ChannelTypes.SimpleTaprootChannelsPhoenix())) + assert(alice.commitments.active.head.commitmentFormat == PhoenixSimpleTaprootChannelCommitmentFormat) + assert(alice.commitments.active.last.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(alice2blockchain.expectMsgType[WatchPublished].txId == spliceTx.txid) + assert(bob2blockchain.expectMsgType[WatchPublished].txId == spliceTx.txid) + + // Alice will force-close using a non-taproot revoked inactive commitment. + val aliceCommitTx = alice.commitments.active.last.fullySignedLocalCommitTx(alice.commitments.channelParams, alice.underlyingActor.channelKeys) + // Alice and Bob send splice_locked: Alice's commitment is now inactive. + alice ! WatchPublishedTriggered(spliceTx) + alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid) + alice2bob.expectMsgType[SpliceLocked] + alice2bob.forward(bob) + bob ! WatchPublishedTriggered(spliceTx) + bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid) + bob2alice.expectMsgType[SpliceLocked] + bob2alice.forward(alice) + awaitCond(bob.commitments.active.size == 1) + awaitCond(bob.commitments.inactive.size == 1) + + // Alice and Bob update the channel: Alice's commitment is now inactive and revoked. + addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + // Alice publishes her revoked commitment: Bob reacts by publishing the latest commitment. + bob ! WatchFundingSpentTriggered(aliceCommitTx) + assert(bob2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == aliceCommitTx.txid) + // Bob reacts by publishing the taproot commitment. + val bobCommitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + Transaction.correctlySpends(bobCommitTx, Seq(spliceTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val localAnchor = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] + val localMain = bob2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx]) + bob2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(localMain.input) + bob2blockchain.expectWatchOutputSpent(localAnchor.input.outPoint) + (htlcs.aliceToBob.map(_._2) ++ htlcs.bobToAlice.map(_._2)).foreach(_ => bob2blockchain.expectMsgType[WatchOutputSpent]) + bob2blockchain.expectMsgType[WatchOutputSpent] // newly added HTLC + bob2blockchain.expectNoMessage(100 millis) + + // Alice's revoked commit tx confirms. + bob ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(450_000), 5, aliceCommitTx) + val mainTx = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val penaltyTx = bob2blockchain.expectFinalTxPublished("main-penalty") + Transaction.correctlySpends(penaltyTx.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => bob2blockchain.expectFinalTxPublished("htlc-penalty")) + htlcPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, penaltyTx.input) ++ htlcPenalty.map(_.input)) + bob2blockchain.expectNoMessage(100 millis) + + // Bob's penalty txs confirm. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceCommitTx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainTx.tx) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penaltyTx.tx) + htlcPenalty.foreach { penalty => bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } + awaitCond(bob.stateName == CLOSED) + assert(Helpers.Closing.isClosed(bob.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RevokedClose])) + } + test("put back watches after restart") { f => import f._ @@ -3564,7 +4073,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2blockchain.expectNoMessage(100 millis) } - test("recv CMD_SPLICE (splice-in + splice-out) with pre and post splice htlcs") { f => + def spliceWithPreAndPostHtlcs(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val htlcs = setupHtlcs(f) @@ -3575,12 +4084,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(bob, alice, bob2alice, alice2bob) val aliceCommitments1 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments aliceCommitments1.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(aliceCommitments1.channelParams, alice.underlyingActor.channelKeys).toTry.get + assert(c.commitmentFormat == commitmentFormat) + val commitTx = c.fullySignedLocalCommitTx(aliceCommitments1.channelParams, alice.underlyingActor.channelKeys) Transaction.correctlySpends(commitTx, Map(c.fundingInput -> c.commitInput(alice.underlyingActor.channelKeys).txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } val bobCommitments1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments bobCommitments1.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(bobCommitments1.channelParams, bob.underlyingActor.channelKeys).toTry.get + assert(c.commitmentFormat == commitmentFormat) + val commitTx = c.fullySignedLocalCommitTx(bobCommitments1.channelParams, bob.underlyingActor.channelKeys) Transaction.correctlySpends(commitTx, Map(c.fundingInput -> c.commitInput(bob.underlyingActor.channelKeys).txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -3589,18 +4100,26 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(alice, bob, alice2bob, bob2alice) val aliceCommitments2 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments aliceCommitments2.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(aliceCommitments2.channelParams, alice.underlyingActor.channelKeys).toTry.get + val commitTx = c.fullySignedLocalCommitTx(aliceCommitments2.channelParams, alice.underlyingActor.channelKeys) Transaction.correctlySpends(commitTx, Map(c.fundingInput -> c.commitInput(alice.underlyingActor.channelKeys).txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } val bobCommitments2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments bobCommitments2.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(bobCommitments2.channelParams, bob.underlyingActor.channelKeys).toTry.get + val commitTx = c.fullySignedLocalCommitTx(bobCommitments2.channelParams, bob.underlyingActor.channelKeys) Transaction.correctlySpends(commitTx, Map(c.fundingInput -> c.commitInput(bob.underlyingActor.channelKeys).txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } resolveHtlcs(f, htlcs) } + test("recv CMD_SPLICE (splice-in + splice-out) with pre and post splice htlcs", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + spliceWithPreAndPostHtlcs(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv CMD_SPLICE (splice-in + splice-out) with pre and post splice htlcs (taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + spliceWithPreAndPostHtlcs(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv CMD_SPLICE (splice-in + splice-out) with pending htlcs, resolved after splice locked", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ @@ -3791,24 +4310,5 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(finalState.commitments.latest.localCommit.spec.toLocal == 805_000_000.msat) assert(finalState.commitments.latest.localCommit.spec.toRemote == 695_000_000.msat) } -} - -// test taproot channels -class NormalSplicesStateWithTaprootChannelsSpec extends NormalSplicesStateSpec { - override val extraTags: Set[String] = Set(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee) -} - -class NormalSplicesStateWithLegacyTaprootChannelsSpec extends NormalSplicesStateSpec { - override val extraTags: Set[String] = Set(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy) -} - -// test migration from anchor outputs to taproot channels during splices -class NormalSplicesStateUpgradeToLegacyTaprootChannelsSpec extends NormalSplicesStateSpec { - override val extraTags: Set[String] = Set(ChannelStateTestsTags.AnchorOutputs) - override val spliceChannelType_opt: Option[ChannelType] = Some(ChannelTypes.SimpleTaprootChannelsStagingLegacy(scidAlias = false, zeroConf = false)) -} -class NormalSplicesStateUpgradeToTaprootChannelsSpec extends NormalSplicesStateSpec { - override val extraTags: Set[String] = Set(ChannelStateTestsTags.AnchorOutputs) - override val spliceChannelType_opt: Option[ChannelType] = Some(ChannelTypes.SimpleTaprootChannelsStaging(scidAlias = false, zeroConf = false)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 126560ae04..5abc3b77b5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -28,12 +28,13 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel._ import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.{NonceGenerator, Sphinx} import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.OutgoingPaymentPacket import fr.acinq.eclair.payment.relay.Relayer._ @@ -43,7 +44,7 @@ import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, CommitSigTlv, Error, FailureMessageCodecs, FailureReason, PermanentChannelFailure, RevokeAndAck, RevokeAndAckTlv, Shutdown, TemporaryNodeFailure, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelReestablishTlv, ChannelUpdate, ClosingSigned, CommitSig, CommitSigTlv, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, RevokeAndAckTlv, Shutdown, TemporaryNodeFailure, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} import org.scalatest.Inside.inside import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -1054,7 +1055,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.signed.size == 1) } - test("recv CommitSig (one htlc sent)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv CommitSig (one htlc sent)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1102,7 +1103,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 5) } - test("recv CommitSig (multiple htlcs in both directions) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + def testRecvCommitSigMultipleHtlcs(f: FixtureParam): Unit = { import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) @@ -1129,34 +1130,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 3) } - test("recv CommitSig (multiple htlcs in both directions) (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => - import f._ - - addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - addHtlc(1100000 msat, alice, bob, alice2bob, bob2alice) // a->b (trimmed to dust) - addHtlc(999999 msat, bob, alice, bob2alice, alice2bob) // b->a (dust) - addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) - addHtlc(999999 msat, alice, bob, alice2bob, bob2alice) // a->b (dust) - addHtlc(1100000 msat, bob, alice, bob2alice, alice2bob) // b->a (trimmed to dust) - - alice ! CMD_SIGN() - val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - assert(aliceCommitSig.htlcSignatures.length == 2) - alice2bob.forward(bob, aliceCommitSig) - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) - - // actual test begins - val bobCommitSig = bob2alice.expectMsgType[CommitSig] - assert(bobCommitSig.htlcSignatures.length == 3) - bob2alice.forward(alice, bobCommitSig) + test("recv CommitSig (multiple htlcs in both directions) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + testRecvCommitSigMultipleHtlcs(f) + } - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.index == 1) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 3) + test("recv CommitSig (multiple htlcs in both directions) (phoenix taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => + testRecvCommitSigMultipleHtlcs(f) } - test("recv CommitSig (multiple htlcs in both directions) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + def testRecvCommitSigMultipleHtlcZeroFees(f: FixtureParam): Unit = { import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) @@ -1183,6 +1165,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 5) } + test("recv CommitSig (multiple htlcs in both directions) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testRecvCommitSigMultipleHtlcZeroFees(f) + } + + test("recv CommitSig (multiple htlcs in both directions) (taproot zero fee htlc txs)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRecvCommitSigMultipleHtlcZeroFees(f) + } + test("recv CommitSig (multiple htlcs in both directions) (without fundingTxId tlv)") { f => import f._ @@ -1249,7 +1239,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val tx = bob.signCommitTx() // signature is invalid but it doesn't matter - bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) + bob ! CommitSig(ByteVector32.Zeroes, IndividualSignature(ByteVector64.Zeroes), Nil) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("cannot sign when there are no changes")) awaitCond(bob.stateName == CLOSING) @@ -1266,7 +1256,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val tx = bob.signCommitTx() // actual test begins - bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) + bob ! CommitSig(ByteVector32.Zeroes, IndividualSignature(ByteVector64.Zeroes), Nil) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid commitment signature")) awaitCond(bob.stateName == CLOSING) @@ -1275,7 +1265,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectWatchTxConfirmed(tx.txid) } - test("recv CommitSig (simple taproot channels, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv CommitSig (simple taproot channels, missing partial signature)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) val tx = bob.signCommitTx() @@ -1283,8 +1273,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins alice ! CMD_SIGN() val commitSig = alice2bob.expectMsgType[CommitSig] - val commitSigWithMissingNonce = commitSig.copy(tlvStream = commitSig.tlvStream.copy(records = commitSig.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.PartialSignatureWithNonceTlv]))) - bob ! commitSigWithMissingNonce + val commitSigMissingPartialSig = commitSig.copy(tlvStream = commitSig.tlvStream.copy(records = commitSig.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.PartialSignatureWithNonceTlv]))) + bob ! commitSigMissingPartialSig val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid commitment signature")) awaitCond(bob.stateName == CLOSING) @@ -1294,7 +1284,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectWatchTxConfirmed(tx.txid) } - test("recv CommitSig (simple taproot channels, invalid partial signature)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv CommitSig (simple taproot channels, invalid partial signature)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) val tx = bob.signCommitTx() @@ -1303,8 +1293,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_SIGN() val commitSig = alice2bob.expectMsgType[CommitSig] val Some(psig) = commitSig.partialSignature_opt - val invalidPsig = psig.copy(partialSig = psig.partialSig.reverse) - val commitSigWithInvalidPsig = commitSig.copy(tlvStream = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(invalidPsig))) + val invalidPsig = CommitSigTlv.PartialSignatureWithNonceTlv(psig.copy(partialSig = psig.partialSig.reverse)) + val commitSigWithInvalidPsig = commitSig.copy(tlvStream = commitSig.tlvStream.copy(records = commitSig.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.PartialSignatureWithNonceTlv]) + invalidPsig)) bob ! commitSigWithInvalidPsig val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid commitment signature")) @@ -1344,7 +1334,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val commitSig = alice2bob.expectMsgType[CommitSig] // actual test begins - val badCommitSig = commitSig.copy(htlcSignatures = commitSig.signature :: Nil) + val badCommitSig = commitSig.copy(htlcSignatures = commitSig.signature.sig :: Nil) bob ! badCommitSig val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid htlc signature")) @@ -1464,7 +1454,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectWatchTxConfirmed(tx.txid) } - test("recv RevokeAndAck (simple taproot channels, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => + test("recv RevokeAndAck (simple taproot channels, missing nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => import f._ val tx = alice.signCommitTx() addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1475,7 +1465,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val revokeAndAck = bob2alice.expectMsgType[RevokeAndAck] - val revokeAndAckWithMissingNonce = revokeAndAck.copy(tlvStream = revokeAndAck.tlvStream.copy(records = revokeAndAck.tlvStream.records.filterNot(tlv => tlv.isInstanceOf[RevokeAndAckTlv.NextLocalNoncesTlv] || tlv.isInstanceOf[RevokeAndAckTlv.NextLocalNonceTlv]))) + val revokeAndAckWithMissingNonce = revokeAndAck.copy(tlvStream = revokeAndAck.tlvStream.copy(records = revokeAndAck.tlvStream.records.filterNot(tlv => tlv.isInstanceOf[RevokeAndAckTlv.NextLocalNoncesTlv]))) alice ! revokeAndAckWithMissingNonce alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) @@ -1487,6 +1477,38 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectWatchTxConfirmed(tx.txid) } + test("recv RevokeAndAck (simple taproot channels, invalid nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + // Alice sends an HTLC to Bob. + val (r, add) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN() + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + // Bob responds with an invalid nonce for its *next* commitment. + val revokeAndAck = bob2alice.expectMsgType[RevokeAndAck] + val bobInvalidNonces = RevokeAndAckTlv.NextLocalNoncesTlv(revokeAndAck.nextCommitNonces.map { case (txId, _) => txId -> NonceGenerator.signingNonce(randomKey().publicKey, randomKey().publicKey, txId).publicNonce }.toSeq) + val revokeAndAckWithInvalidNonce = revokeAndAck.copy(tlvStream = revokeAndAck.tlvStream.copy(records = revokeAndAck.tlvStream.records.filterNot(tlv => tlv.isInstanceOf[RevokeAndAckTlv.NextLocalNoncesTlv]) + bobInvalidNonces)) + bob2alice.forward(alice, revokeAndAckWithInvalidNonce) + // This applies to the *next* commitment, there is no issue when finalizing the *current* commitment. + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + // Bob will force-close when receiving Alice's next commit_sig. + val commitTx = bob.signCommitTx() + fulfillHtlc(add.id, r, bob, alice, bob2alice, alice2bob) + bob ! CMD_SIGN() + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSING) + bob2blockchain.expectFinalTxPublished(commitTx.txid) + } + test("recv RevokeAndAck (over max dust htlc exposure)") { f => import f._ val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments @@ -1732,6 +1754,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testRevokeAndAckHtlcStaticRemoteKey _ } + test("recv RevokeAndAck (one htlc sent, option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { + testRevokeAndAckHtlcStaticRemoteKey _ + } + test("recv RevocationTimeout") { f => import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1776,6 +1802,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testReceiveCmdFulfillHtlc _ } + test("recv CMD_FULFILL_HTLC (option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { + testReceiveCmdFulfillHtlc _ + } + test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -1864,6 +1894,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testUpdateFulfillHtlc _ } + test("recv UpdateFulfillHtlc (option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { + testUpdateFulfillHtlc _ + } + test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => import f._ val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1945,6 +1979,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testCmdFailHtlc _ } + test("recv CMD_FAIL_HTLC (option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { + testCmdFailHtlc _ + } + test("recv CMD_FAIL_HTLC (with delay)") { f => import f._ val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -2075,6 +2113,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testUpdateFailHtlc _ } + test("recv UpdateFailHtlc (option_simple_taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { + testUpdateFailHtlc _ + } + test("recv UpdateFailMalformedHtlc") { f => import f._ @@ -2178,6 +2220,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testCmdUpdateFee _ } + test("recv CMD_UPDATE_FEE (simple taproot channel)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { + testCmdUpdateFee _ + } + test("recv CMD_UPDATE_FEE (over max dust htlc exposure)") { f => import f._ @@ -2581,6 +2627,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testCmdClose(f, None) } + test("recv CMD_CLOSE (no pending htlcs) (simple taproot channel)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testCmdClose(f, None) + } + test("recv CMD_CLOSE (with noSender)") { f => import f._ val sender = TestProbe() @@ -3596,6 +3646,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testErrorAnchorOutputsWithHtlcs(f) } + test("recv Error (simple taproot channel)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testErrorAnchorOutputsWithHtlcs(f) + } + test("recv Error (anchor outputs zero fee htlc txs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => // We should ignore the disable flag since there are htlcs in the commitment (funds at risk). testErrorAnchorOutputsWithHtlcs(f) @@ -3627,6 +3681,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = false) } + test("recv Error (simple taproot channel without htlcs)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = false) + } + test("recv Error (anchor outputs zero fee htlc txs without htlcs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = true) } @@ -3828,6 +3886,80 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == OFFLINE) } + test("recv INPUT_DISCONNECTED (with pending htlcs, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + alice2bob.ignoreMsg { case _: ChannelUpdate => true } + bob2alice.ignoreMsg { case _: ChannelUpdate => true } + + // Alice sends an HTLC to Bob. + val (ra1, htlcA1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) + // Bob sends an HTLC to Alice. + val (rb, htlcB) = addHtlc(25_000_000 msat, bob, alice, bob2alice, alice2bob) + bob ! CMD_SIGN() + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + val revA1 = alice2bob.expectMsgType[RevokeAndAck] // not received by Bob + alice2bob.expectMsgType[CommitSig] // not received by Bob + val (_, htlcA2) = addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) // not signed by either Alice or Bob + + alice ! INPUT_DISCONNECTED + val addSettledA = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] + assert(addSettledA.htlc == htlcA2) + assert(addSettledA.result.isInstanceOf[HtlcResult.DisconnectedBeforeSigned]) + alice2relayer.expectNoMessage(100 millis) + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + bob2relayer.expectNoMessage(100 millis) + awaitCond(bob.stateName == OFFLINE) + + // Alice and Bob finish signing the HTLCs on reconnection. + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + assert(alice2bob.expectMsgType[ChannelReestablish].nextCommitNonces == revA1.nextCommitNonces) + alice2bob.forward(bob) + bob2alice.expectMsgType[ChannelReestablish] + bob2alice.forward(alice) + assert(alice2bob.expectMsgType[RevokeAndAck].nextCommitNonces == revA1.nextCommitNonces) + alice2bob.forward(bob) + assert(alice2bob.expectMsgType[UpdateAddHtlc].paymentHash == htlcA1.paymentHash) + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + + // Alice and Bob fulfill the pending HTLCs. + fulfillHtlc(htlcA1.id, ra1, bob, alice, bob2alice, alice2bob) + fulfillHtlc(htlcB.id, rb, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + } + + test("recv INPUT_DISCONNECTED (missing nonces, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + val channelReestablish = alice2bob.expectMsgType[ChannelReestablish] + assert(channelReestablish.nextCommitNonces.size == 1) + bob2alice.expectMsgType[ChannelReestablish] + alice2bob.forward(bob, channelReestablish.copy(tlvStream = TlvStream(channelReestablish.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextLocalNoncesTlv])))) + bob2alice.expectMsgType[Error] + } + test("recv INPUT_DISCONNECTED (public channel)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip)) { f => import f._ bob2alice.expectMsgType[AnnouncementSignatures] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index d083981cd3..069feb2553 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -24,8 +24,9 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx} +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx, SetChannelId} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment._ @@ -34,8 +35,8 @@ import fr.acinq.eclair.payment.send.SpontaneousRecipient import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcTimeoutTx, ClaimRemoteAnchorTx} -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} +import fr.acinq.eclair.transactions.Transactions._ +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -156,11 +157,18 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] bob ! CMD_FULFILL_HTLC(0, r1, None) val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] - awaitCond(bob.stateData == initialState - .modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill) + awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill) ) } + test("recv CMD_FULFILL_HTLC (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] + bob ! CMD_FULFILL_HTLC(0, r1, None) + val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc] + awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill)) + } + test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -374,6 +382,22 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) } + test("recv CMD_SIGN (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + val sender = TestProbe() + bob ! CMD_FULFILL_HTLC(0, r1, None) + bob2alice.expectMsgType[UpdateFulfillHtlc] + bob2alice.forward(alice) + bob ! CMD_SIGN(replyTo_opt = Some(sender.ref)) + sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] + assert(bob2alice.expectMsgType[CommitSig].partialSignature_opt.nonEmpty) + bob2alice.forward(alice) + assert(alice2bob.expectMsgType[RevokeAndAck].nextCommitNonces.contains(bob.commitments.latest.fundingTxId)) + alice2bob.forward(bob) + assert(alice2bob.expectMsgType[CommitSig].partialSignature_opt.nonEmpty) + awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) + } + test("recv CMD_SIGN (no changes)") { f => import f._ val sender = TestProbe() @@ -414,7 +438,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val tx = bob.signCommitTx() // signature is invalid but it doesn't matter - bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) + bob ! CommitSig(ByteVector32.Zeroes, IndividualSignature(ByteVector64.Zeroes), Nil) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx @@ -425,7 +449,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CommitSig (invalid signature)") { f => import f._ val tx = bob.signCommitTx() - bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) + bob ! CommitSig(ByteVector32.Zeroes, IndividualSignature(ByteVector64.Zeroes), Nil) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx @@ -470,6 +494,19 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit awaitCond(alice.stateName == NEGOTIATING) } + test("recv RevokeAndAck (no more htlcs on either side, taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + // Bob fulfills the first HTLC. + fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + assert(alice.stateName == SHUTDOWN) + // Bob fulfills the second HTLC. + fulfillHtlc(1, r2, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + awaitCond(alice.stateName == NEGOTIATING_SIMPLE) + awaitCond(bob.stateName == NEGOTIATING_SIMPLE) + } + test("recv RevokeAndAck (invalid preimage)") { f => import f._ val tx = bob.signCommitTx() @@ -953,6 +990,61 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit assert(alice.stateName == SHUTDOWN) } + def testInputRestored(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { + import f._ + // Alice and Bob restart. + val aliceData = alice.underlyingActor.nodeParams.db.channels.getChannel(channelId(alice)).get + alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + alice ! INPUT_RESTORED(aliceData) + alice2blockchain.expectMsgType[SetChannelId] + val fundingTxId = alice2blockchain.expectMsgType[WatchFundingSpent].txId + awaitCond(alice.stateName == OFFLINE) + val bobData = bob.underlyingActor.nodeParams.db.channels.getChannel(channelId(bob)).get + bob.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + bob ! INPUT_RESTORED(bobData) + bob2blockchain.expectMsgType[SetChannelId] + bob2blockchain.expectMsgType[WatchFundingSpent] + awaitCond(bob.stateName == OFFLINE) + // They reconnect and provide nonces to resume HTLC settlement. + val aliceInit = Init(alice.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob.underlyingActor.nodeParams.features.initFeatures()) + alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit) + val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish] + val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish] + Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => commitmentFormat match { + case _: SegwitV0CommitmentFormat => + assert(channelReestablish.currentCommitNonce_opt.isEmpty) + assert(channelReestablish.nextCommitNonces.isEmpty) + case _: TaprootCommitmentFormat => + assert(channelReestablish.currentCommitNonce_opt.isEmpty) + assert(channelReestablish.nextCommitNonces.contains(fundingTxId)) + }) + alice2bob.forward(bob, channelReestablishAlice) + bob2alice.forward(alice, channelReestablishBob) + // They retransmit shutdown. + alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob) + bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice) + // They resume HTLC settlement. + fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + assert(alice.stateName == SHUTDOWN) + fulfillHtlc(1, r2, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + awaitCond(alice.stateName == NEGOTIATING_SIMPLE) + awaitCond(bob.stateName == NEGOTIATING_SIMPLE) + } + + test("recv INPUT_RESTORED", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testInputRestored(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_RESTORED (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testInputRestored(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv Error") { f => import f._ val aliceCommitTx = alice.signCommitTx() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index f6f6304357..0c3c2280ff 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -29,9 +29,9 @@ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsT import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.testutils.PimpTestProbe._ import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.ClosingSignedTlv.FeeRange -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingComplete, ClosingSig, ClosingSigned, ClosingTlv, Error, Shutdown, TlvStream, Warning} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingComplete, ClosingCompleteTlv, ClosingSig, ClosingSigTlv, ClosingSigned, ClosingTlv, Error, Shutdown, TlvStream, Warning} import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.Inside.inside import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -63,8 +63,10 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike alice ! CMD_CLOSE(sender.ref, None, feerates) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val aliceShutdown = alice2bob.expectMsgType[Shutdown] + if (alice.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat]) assert(aliceShutdown.closeeNonce_opt.nonEmpty) alice2bob.forward(bob, aliceShutdown) val bobShutdown = bob2alice.expectMsgType[Shutdown] + if (bob.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat]) assert(bobShutdown.closeeNonce_opt.nonEmpty) bob2alice.forward(alice, bobShutdown) if (alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.localChannelParams.initFeatures.hasFeature(Features.SimpleClose)) { awaitCond(alice.stateName == NEGOTIATING_SIMPLE) @@ -83,8 +85,10 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob ! CMD_CLOSE(sender.ref, None, feerates) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val bobShutdown = bob2alice.expectMsgType[Shutdown] + if (bob.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat]) assert(bobShutdown.closeeNonce_opt.nonEmpty) bob2alice.forward(alice, bobShutdown) val aliceShutdown = alice2bob.expectMsgType[Shutdown] + if (alice.commitments.latest.commitmentFormat.isInstanceOf[TaprootCommitmentFormat]) assert(aliceShutdown.closeeNonce_opt.nonEmpty) alice2bob.forward(bob, aliceShutdown) if (bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.localChannelParams.initFeatures.hasFeature(Features.SimpleClose)) { awaitCond(alice.stateName == NEGOTIATING_SIMPLE) @@ -484,25 +488,41 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2blockchain.expectMsgType[WatchTxConfirmed] } - def `recv ClosingComplete (both outputs)`(f: FixtureParam): Unit = { + def testReceiveClosingCompleteBothOutputs(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ aliceClose(f) val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete] assert(aliceClosingComplete.fees > 0.sat) - assert(aliceClosingComplete.closerAndCloseeOutputsSig_opt.orElse(aliceClosingComplete.closerAndCloseeOutputsPartialSig_opt).nonEmpty) - assert(aliceClosingComplete.closerOutputOnlySig_opt.orElse(aliceClosingComplete.closerOutputOnlyPartialSig_opt).nonEmpty) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => + assert(aliceClosingComplete.closerAndCloseeOutputsSig_opt.nonEmpty) + assert(aliceClosingComplete.closerOutputOnlySig_opt.nonEmpty) + case _: TaprootCommitmentFormat => + assert(aliceClosingComplete.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + assert(aliceClosingComplete.closerOutputOnlyPartialSig_opt.nonEmpty) + } assert(aliceClosingComplete.closeeOutputOnlySig_opt.orElse(aliceClosingComplete.closeeOutputOnlyPartialSig_opt).isEmpty) val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete] assert(bobClosingComplete.fees > 0.sat) - assert(bobClosingComplete.closerAndCloseeOutputsSig_opt.orElse(bobClosingComplete.closerAndCloseeOutputsPartialSig_opt).nonEmpty) - assert(bobClosingComplete.closerOutputOnlySig_opt.orElse(bobClosingComplete.closerOutputOnlyPartialSig_opt).nonEmpty) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => + assert(bobClosingComplete.closerAndCloseeOutputsSig_opt.nonEmpty) + assert(bobClosingComplete.closerOutputOnlySig_opt.nonEmpty) + case _: TaprootCommitmentFormat => + assert(bobClosingComplete.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + assert(bobClosingComplete.closerOutputOnlyPartialSig_opt.nonEmpty) + } assert(bobClosingComplete.closeeOutputOnlySig_opt.orElse(bobClosingComplete.closeeOutputOnlyPartialSig_opt).isEmpty) alice2bob.forward(bob, aliceClosingComplete) val bobClosingSig = bob2alice.expectMsgType[ClosingSig] assert(bobClosingSig.fees == aliceClosingComplete.fees) assert(bobClosingSig.lockTime == aliceClosingComplete.lockTime) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(bobClosingSig.closerAndCloseeOutputsSig_opt.nonEmpty) + case _: TaprootCommitmentFormat => assert(bobClosingSig.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + } bob2alice.forward(alice, bobClosingSig) val aliceTx = alice2blockchain.expectMsgType[PublishFinalTx] assert(aliceTx.desc == "closing-tx") @@ -519,6 +539,10 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceClosingSig = alice2bob.expectMsgType[ClosingSig] assert(aliceClosingSig.fees == bobClosingComplete.fees) assert(aliceClosingSig.lockTime == bobClosingComplete.lockTime) + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(aliceClosingSig.closerAndCloseeOutputsSig_opt.nonEmpty) + case _: TaprootCommitmentFormat => assert(aliceClosingSig.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + } alice2bob.forward(bob, aliceClosingSig) val bobTx = bob2blockchain.expectMsgType[PublishFinalTx] assert(bobTx.desc == "closing-tx") @@ -533,31 +557,36 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(bob.stateName == NEGOTIATING_SIMPLE) } - test("recv ClosingComplete (both outputs)", Tag(ChannelStateTestsTags.SimpleClose)) { f => - `recv ClosingComplete (both outputs)`(f) + test("recv ClosingComplete (both outputs)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testReceiveClosingCompleteBothOutputs(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv ClosingComplete (both outputs, simple taproot channels)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy)) { f => - `recv ClosingComplete (both outputs)`(f) + test("recv ClosingComplete (both outputs, simple taproot channels)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testReceiveClosingCompleteBothOutputs(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } - test("recv ClosingComplete (both outputs, simple taproot channels zero fee)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee)) { f => - `recv ClosingComplete (both outputs)`(f) - } - - test("recv ClosingComplete (single output)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f => + def testReceiveClosingCompleteSingleOutput(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ aliceClose(f) val closingComplete = alice2bob.expectMsgType[ClosingComplete] + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(closingComplete.closerOutputOnlySig_opt.nonEmpty) + case _: TaprootCommitmentFormat => assert(closingComplete.closerOutputOnlyPartialSig_opt.nonEmpty) + } assert(closingComplete.closerAndCloseeOutputsSig_opt.isEmpty) - assert(closingComplete.closerOutputOnlySig_opt.nonEmpty) + assert(closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty) assert(closingComplete.closeeOutputOnlySig_opt.isEmpty) + assert(closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty) // Bob has nothing at stake. bob2alice.expectNoMessage(100 millis) alice2bob.forward(bob, closingComplete) - bob2alice.expectMsgType[ClosingSig] - bob2alice.forward(alice) + val closingSig = bob2alice.expectMsgType[ClosingSig] + commitmentFormat match { + case _: SegwitV0CommitmentFormat => assert(closingSig.closerOutputOnlySig_opt.nonEmpty) + case _: TaprootCommitmentFormat => assert(closingSig.closerOutputOnlyPartialSig_opt.nonEmpty) + } + bob2alice.forward(alice, closingSig) val closingTx = alice2blockchain.expectMsgType[PublishFinalTx] assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == closingTx.tx.txid) alice2blockchain.expectWatchTxConfirmed(closingTx.tx.txid) @@ -566,6 +595,14 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(bob.stateName == NEGOTIATING_SIMPLE) } + test("recv ClosingComplete (single output)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.NoPushAmount)) { f => + testReceiveClosingCompleteSingleOutput(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv ClosingComplete (single output, taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot), Tag(ChannelStateTestsTags.NoPushAmount)) { f => + testReceiveClosingCompleteSingleOutput(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv ClosingComplete (single output, trimmed)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ val (r, htlc) = addHtlc(250_000 msat, alice, bob, alice2bob, bob2alice) @@ -594,24 +631,40 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(bob.stateName == NEGOTIATING_SIMPLE) } - test("recv ClosingComplete (missing closee output)", Tag(ChannelStateTestsTags.SimpleClose)) { f => + def testReceiveClosingCompleteMissingCloseeOutput(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ aliceClose(f) val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete] val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete] - alice2bob.forward(bob, aliceClosingComplete.copy(tlvStream = TlvStream(ClosingTlv.CloserOutputOnly(aliceClosingComplete.closerOutputOnlySig_opt.get)))) + val aliceClosingComplete1 = commitmentFormat match { + case _: SegwitV0CommitmentFormat => aliceClosingComplete.copy(tlvStream = TlvStream(ClosingTlv.CloserOutputOnly(aliceClosingComplete.closerOutputOnlySig_opt.get))) + case _: TaprootCommitmentFormat => aliceClosingComplete.copy(tlvStream = TlvStream(ClosingCompleteTlv.CloserOutputOnlyPartialSignature(aliceClosingComplete.closerOutputOnlyPartialSig_opt.get))) + } + alice2bob.forward(bob, aliceClosingComplete1) // Bob expects to receive a signature for a closing transaction containing his output, so he ignores Alice's // closing_complete instead of sending back his closing_sig. bob2alice.expectMsgType[Warning] bob2alice.expectNoMessage(100 millis) bob2alice.forward(alice, bobClosingComplete) val aliceClosingSig = alice2bob.expectMsgType[ClosingSig] - alice2bob.forward(bob, aliceClosingSig.copy(tlvStream = TlvStream(ClosingTlv.CloseeOutputOnly(aliceClosingSig.closerAndCloseeOutputsSig_opt.get)))) + val aliceClosingSig1 = commitmentFormat match { + case _: SegwitV0CommitmentFormat => aliceClosingSig.copy(tlvStream = TlvStream(ClosingTlv.CloseeOutputOnly(aliceClosingSig.closerAndCloseeOutputsSig_opt.get))) + case _: TaprootCommitmentFormat => aliceClosingSig.copy(tlvStream = TlvStream(ClosingSigTlv.CloseeOutputOnlyPartialSignature(aliceClosingSig.closerAndCloseeOutputsPartialSig_opt.get))) + } + alice2bob.forward(bob, aliceClosingSig1) bob2alice.expectMsgType[Warning] bob2alice.expectNoMessage(100 millis) bob2blockchain.expectNoMessage(100 millis) } + test("recv ClosingComplete (missing closee output)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testReceiveClosingCompleteMissingCloseeOutput(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv ClosingComplete (missing closee output, taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testReceiveClosingCompleteMissingCloseeOutput(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv ClosingComplete (with concurrent script update)", Tag(ChannelStateTestsTags.SimpleClose)) { f => import f._ aliceClose(f) @@ -894,6 +947,38 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike awaitCond(bob.stateName == CLOSING) } + test("recv CMD_CLOSE with RBF feerates (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + // Alice creates a first closing transaction. + aliceClose(f) + alice2bob.expectMsgType[ClosingComplete] + alice2bob.forward(bob) + bob2alice.expectMsgType[ClosingComplete] // ignored + val aliceTx1 = bob2blockchain.expectMsgType[PublishFinalTx] + bob2blockchain.expectWatchTxConfirmed(aliceTx1.tx.txid) + val closingSig1 = bob2alice.expectMsgType[ClosingSig] + assert(closingSig1.nextCloseeNonce_opt.nonEmpty) + bob2alice.forward(alice, closingSig1) + alice2blockchain.expectFinalTxPublished(aliceTx1.tx.txid) + alice2blockchain.expectWatchTxConfirmed(aliceTx1.tx.txid) + + // Alice sends another closing_complete, updating her fees. + val probe = TestProbe() + val aliceFeerate2 = alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].lastClosingFeerate * 1.25 + alice ! CMD_CLOSE(probe.ref, None, Some(ClosingFeerates(aliceFeerate2, aliceFeerate2, aliceFeerate2))) + probe.expectMsgType[RES_SUCCESS[CMD_CLOSE]] + assert(alice2bob.expectMsgType[ClosingComplete].fees > aliceTx1.fee) + alice2bob.forward(bob) + val aliceTx2 = bob2blockchain.expectMsgType[PublishFinalTx] + bob2blockchain.expectWatchTxConfirmed(aliceTx2.tx.txid) + val closingSig2 = bob2alice.expectMsgType[ClosingSig] + assert(closingSig2.nextCloseeNonce_opt.nonEmpty) + assert(closingSig2.nextCloseeNonce_opt != closingSig1.nextCloseeNonce_opt) + bob2alice.forward(alice, closingSig2) + alice2blockchain.expectFinalTxPublished(aliceTx2.tx.txid) + alice2blockchain.expectWatchTxConfirmed(aliceTx2.tx.txid) + } + test("recv CMD_CLOSE with RBF feerate too low", Tag(ChannelStateTestsTags.SimpleClose)) { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index c8ee7b5626..7aa37ed3e6 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 @@ -53,11 +53,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, alice2relayer: TestProbe, bob2relayer: TestProbe, channelUpdateListener: TestProbe, txListener: TestProbe, eventListener: TestProbe, bobCommitTxs: List[Transaction]) - val extraTags: Set[String] = Set(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy) - override def withFixture(test: OneArgTest): Outcome = { - val tags = test.tags ++ extraTags - val setup = init(tags = tags) + val setup = init() import setup._ // NOTE @@ -69,13 +66,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // and we want to be able to test the different scenarii. // Hence the WAIT_FOR_FUNDING_CONFIRMED->CLOSING or NORMAL->CLOSING transition will occur in the individual tests. - val unconfirmedFundingTx = tags.contains("funding_unconfirmed") + val unconfirmedFundingTx = test.tags.contains("funding_unconfirmed") val txListener = TestProbe() val eventListener = TestProbe() if (unconfirmedFundingTx) { within(30 seconds) { - val channelParams = computeChannelParams(setup, tags) + val channelParams = computeChannelParams(setup, test.tags) alice ! channelParams.initChannelAlice(TestConstants.fundingSatoshis, pushAmount_opt = Some(TestConstants.initiatorPushAmount)) alice2blockchain.expectMsgType[SetChannelId] bob ! channelParams.initChannelBob() @@ -104,7 +101,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } else { within(30 seconds) { - reachNormal(setup, tags) + reachNormal(setup, test.tags) if (test.tags.contains(ChannelStateTestsTags.ChannelsPublic) && test.tags.contains(ChannelStateTestsTags.DoNotInterceptGossip)) { alice2bob.expectMsgType[AnnouncementSignatures] alice2bob.forward(bob) @@ -145,7 +142,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) alice2blockchain.expectFinalTxPublished("commit-tx") - alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] alice2blockchain.expectFinalTxPublished("local-main-delayed") eventListener.expectMsgType[ChannelAborted] @@ -161,7 +157,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) alice2blockchain.expectFinalTxPublished("commit-tx") - alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] alice2blockchain.expectFinalTxPublished("local-main-delayed") eventListener.expectMsgType[ChannelAborted] @@ -179,11 +174,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) alice2bob.expectMsgType[Error] val commitTx = alice2blockchain.expectFinalTxPublished("commit-tx").tx - val claimLocalAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed").tx alice2blockchain.expectWatchTxConfirmed(commitTx.txid) alice2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) - alice2blockchain.expectWatchOutputSpent(claimLocalAnchor.tx.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -201,11 +194,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) alice2bob.expectMsgType[Error] val commitTx = alice2blockchain.expectFinalTxPublished("commit-tx").tx - val claimLocalAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed").tx alice2blockchain.expectWatchTxConfirmed(commitTx.txid) alice2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) - alice2blockchain.expectWatchOutputSpent(claimLocalAnchor.tx.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -224,11 +215,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == CLOSING) bob2alice.expectMsgType[Error] val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx - val claimLocalAnchor = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] val claimMain = bob2blockchain.expectFinalTxPublished("local-main-delayed").tx bob2blockchain.expectWatchTxConfirmed(commitTx.txid) bob2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) - bob2blockchain.expectWatchOutputSpent(claimLocalAnchor.tx.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -246,11 +235,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == CLOSING) bob2alice.expectMsgType[Error] val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx - val claimLocalAnchor = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] val claimMain = bob2blockchain.expectFinalTxPublished("local-main-delayed").tx bob2blockchain.expectWatchTxConfirmed(commitTx.txid) bob2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) - bob2blockchain.expectWatchOutputSpent(claimLocalAnchor.tx.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -268,11 +255,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == CLOSING) bob2alice.expectMsgType[Error] val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx - val claimLocalAnchor = bob2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] val claimMain = bob2blockchain.expectFinalTxPublished("local-main-delayed").tx bob2blockchain.expectWatchTxConfirmed(commitTx.txid) bob2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) - bob2blockchain.expectWatchOutputSpent(claimLocalAnchor.tx.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -310,7 +295,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testMutualCloseBeforeConverge(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ val sender = TestProbe() - //assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) bob.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(2500 sat)).copy(minimum = FeeratePerKw(250 sat), slow = FeeratePerKw(250 sat))) // alice initiates a closing with a low fee alice ! CMD_CLOSE(sender.ref, None, Some(ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(250 sat), FeeratePerKw(1000 sat)))) @@ -445,6 +430,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromClaimHtlcSuccess(f) } + test("recv WatchOutputSpentTriggered (extract preimage from Claim-HTLC-success tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + extractPreimageFromClaimHtlcSuccess(f) + } + private def extractPreimageFromHtlcSuccess(f: FixtureParam): Unit = { import f._ @@ -485,6 +474,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromHtlcSuccess(f) } + test("recv WatchOutputSpentTriggered (extract preimage from HTLC-success tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + extractPreimageFromHtlcSuccess(f) + } + private def extractPreimageFromRemovedHtlc(f: FixtureParam): Unit = { import f._ @@ -570,6 +563,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromRemovedHtlc(f) } + test("recv WatchOutputSpentTriggered (extract preimage for removed HTLC, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + extractPreimageFromRemovedHtlc(f) + } + private def extractPreimageFromNextHtlcs(f: FixtureParam): Unit = { import f._ @@ -663,6 +660,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromNextHtlcs(f) } + test("recv WatchOutputSpentTriggered (extract preimage for next batch of HTLCs, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + extractPreimageFromNextHtlcs(f) + } + test("recv CMD_BUMP_FORCE_CLOSE_FEE (local commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ @@ -684,7 +685,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testLocalCommitTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - // assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) val listener = TestProbe() systemA.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) @@ -745,6 +746,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testLocalCommitTxConfirmed(f, UnsafeLegacyAnchorOutputsCommitmentFormat) } + test("recv WatchTxConfirmedTriggered (local commit, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => + testLocalCommitTxConfirmed(f, PhoenixSimpleTaprootChannelCommitmentFormat) + } + test("recv WatchTxConfirmedTriggered (local commit with multiple htlcs for the same payment)") { f => import f._ @@ -894,7 +899,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(bob, alice, bob2alice, alice2bob) assert(alice2relayer.expectMsgType[RelayForward].add == htlc) val aliceCommitTx = alice.signCommitTx() - assert(aliceCommitTx.txOut.size == 5) // 2 main outputs + 2 anchor outputs + 1 htlc + assert(aliceCommitTx.txOut.size == 3) // 2 main outputs + 1 htlc // alice fulfills the HTLC but bob doesn't receive the signature alice ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true) @@ -930,7 +935,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // note that alice doesn't receive the last revocation // then we make alice unilaterally close the channel val (closingState, closingTxs) = localClose(alice, alice2blockchain) - assert(closingState.commitTx.txOut.length == 4) // htlc has been removed + assert(closingState.commitTx.txOut.length == 2) // htlc has been removed // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] @@ -1039,15 +1044,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // then we should re-publish unconfirmed transactions alice2blockchain.expectFinalTxPublished(closingState.commitTx.txid) // we increase the feerate of our main transaction, but cap it to our max-closing-feerate - val claimAnchor = alice2blockchain.expectReplaceableTxPublished[ClaimLocalAnchorTx] val mainTx2 = closingTxs.mainTx_opt.map(_ => alice2blockchain.expectFinalTxPublished("local-main-delayed")).get assert(mainTx2.tx.txOut.head.amount < closingTxs.mainTx_opt.get.txOut.head.amount) val mainFeerate = Transactions.fee2rate(mainTx2.fee, mainTx2.tx.weight()) assert(FeeratePerKw(14_500 sat) <= mainFeerate && mainFeerate <= FeeratePerKw(15_500 sat)) - assert(alice2blockchain.expectReplaceableTxPublished[HtlcTimeoutTx].input.outPoint == htlcTimeoutTx.txIn.head.outPoint) + assert(alice2blockchain.expectFinalTxPublished("htlc-timeout").input == htlcTimeoutTx.txIn.head.outPoint) alice2blockchain.expectWatchTxConfirmed(closingState.commitTx.txid) closingTxs.mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint)) - alice2blockchain.expectWatchOutputSpent(claimAnchor.tx.txIn.head.outPoint) alice2blockchain.expectWatchOutputSpent(htlcTimeoutTx.txIn.head.outPoint) // the htlc transaction confirms, so we publish a 3rd-stage transaction @@ -1069,7 +1072,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectFinalTxPublished("local-main-delayed") alice2blockchain.expectWatchOutputSpent(mainTx.txIn.head.outPoint) }) - alice2blockchain.expectWatchOutputSpent(claimAnchor.tx.txIn.head.outPoint) assert(alice2blockchain.expectFinalTxPublished("htlc-delayed").input == htlcDelayed.input) alice2blockchain.expectWatchOutputSpent(htlcDelayed.input) // the main transaction confirms @@ -1290,10 +1292,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.commitmentFormat == DefaultCommitmentFormat) { - // skipped because it sends directly to the node's bitcoin wallet - assert(closingState.localOutput_opt.isEmpty) - } + assert(closingState.localOutput_opt.isEmpty) assert(closingState.htlcOutputs.isEmpty) // when the commit tx is signed, alice knows that the htlc she sent right before the unilateral close will never reach the chain alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) @@ -1355,14 +1354,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last - assert(bobCommitTx.txOut.size == 4) // two main outputs + assert(bobCommitTx.txOut.size == 2) // two main outputs val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.commitmentFormat == DefaultCommitmentFormat) { - // skipped because it sends directly to the node's bitcoin wallet - assert(closingState.localOutput_opt.isEmpty) - assert(closingTxs.mainTx_opt.isEmpty) - } + assert(closingState.localOutput_opt.isEmpty) assert(closingState.htlcOutputs.isEmpty) + assert(closingTxs.mainTx_opt.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobCommitTx) @@ -1389,10 +1385,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobCommitTx = bobCommitTxs.last val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.htlcOutputs.isEmpty) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.commitmentFormat == DefaultCommitmentFormat) { - // skipped because it sends directly to the node's bitcoin wallet - assert(closingTxs.mainTx_opt.isEmpty) - } + assert(closingTxs.mainTx_opt.isEmpty) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit @@ -1439,12 +1432,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (remote commit, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.SimpleClose)) { f => import f._ - assume(f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == DefaultCommitmentFormat) mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - // assert(alice.commitments.latest.commitmentFormat == DefaultCommitmentFormat) + assert(alice.commitments.latest.commitmentFormat == DefaultCommitmentFormat) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last - assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchor outputs + assert(bobCommitTx.txOut.size == 2) // two main outputs alice ! WatchFundingSpentTriggered(bobCommitTx) // alice won't create a claimMainOutputTx because her main output is already spendable by the wallet @@ -1459,7 +1451,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - // assert(initialState.commitments.latest.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(initialState.commitments.latest.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors @@ -1478,7 +1470,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testRemoteCommitTxWithHtlcsConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - // assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) @@ -1491,7 +1483,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the latest commit tx. val bobCommitTx = bob.signCommitTx() - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat match { + commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } @@ -1528,6 +1520,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testRemoteCommitTxWithHtlcsConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } + test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRemoteCommitTxWithHtlcsConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv WatchTxConfirmedTriggered (remote commit) followed by htlc settlement", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ // Bob sends 2 HTLCs to Alice that will be settled during the force-close: one will be fulfilled, the other will be failed. @@ -1585,7 +1581,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv INPUT_RESTORED (remote commit)") { f => import f._ - assume(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == DefaultCommitmentFormat) + // Alice sends an htlc to Bob: Bob then force-closes. val (_, htlc) = addHtlc(50_000_000 msat, CltvExpiryDelta(24), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -1620,7 +1616,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with private def testNextRemoteCommitTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): (Transaction, PublishedForceCloseTxs, Set[UpdateAddHtlc]) = { import f._ - // assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) @@ -1639,7 +1635,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the next commit tx. val bobCommitTx = bob.signCommitTx() - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat match { + commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } @@ -1674,7 +1670,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (next remote commit, static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ - assume(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == DefaultCommitmentFormat) val (bobCommitTx, closingTxs, htlcs) = testNextRemoteCommitTxConfirmed(f, DefaultCommitmentFormat) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) assert(closingTxs.mainTx_opt.isEmpty) // with static_remotekey we don't claim out main output @@ -1711,6 +1706,25 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } + test("recv WatchTxConfirmedTriggered (next remote commit, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + val (bobCommitTx, closingTxs, htlcs) = testNextRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, tx)) + alice2relayer.expectNoMessage(100 millis) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, closingTxs.htlcTimeoutTxs(0)) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(100 millis) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(100 millis) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, closingTxs.htlcTimeoutTxs(2)) + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) + alice2relayer.expectNoMessage(100 millis) + awaitCond(alice.stateName == CLOSED) + } + test("recv WatchTxConfirmedTriggered (next remote commit) followed by htlc settlement", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ // Bob sends 2 HTLCs to Alice that will be settled during the force-close: one will be fulfilled, the other will be failed. @@ -1802,7 +1816,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with private def testFutureRemoteCommitTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Transaction = { import f._ val oldStateData = alice.stateData - // assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == commitmentFormat) + assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == commitmentFormat) // This HTLC will be fulfilled. val (ra1, htlca1) = addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) // These 2 HTLCs should timeout on-chain, but since alice lost data, she won't be able to claim them. @@ -1836,7 +1850,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) // bob is nice and publishes its commitment val bobCommitTx = bob.signCommitTx() - alice.stateData.asInstanceOf[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT].commitments.latest.commitmentFormat match { + commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 6) // two main outputs + two anchors + 2 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 4) // two main outputs + 2 HTLCs } @@ -1846,7 +1860,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (future remote commit)") { f => import f._ - assume(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == DefaultCommitmentFormat) val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, DefaultCommitmentFormat) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobCommitTx) @@ -1864,7 +1877,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (future remote commit, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ - assume(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == DefaultCommitmentFormat) val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, DefaultCommitmentFormat) // using option_static_remotekey alice doesn't need to sweep her output awaitCond(alice.stateName == CLOSING) @@ -1890,9 +1902,25 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } + test("recv WatchTxConfirmedTriggered (future remote commit, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + import f._ + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + // alice is able to claim its main output + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(mainTx.input) + alice2blockchain.expectNoMessage(100 millis) // alice ignores the htlc-timeout + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) + + // actual test starts here + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mainTx.tx) + awaitCond(alice.stateName == CLOSED) + } + test("recv INPUT_RESTORED (future remote commit)") { f => import f._ - assume(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == DefaultCommitmentFormat) val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, DefaultCommitmentFormat) // simulate a node restart @@ -1916,7 +1944,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob's first commit tx doesn't contain any htlc val bobCommit1 = RevokedCommit(bob.signCommitTx(), Nil) - alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat match { + commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit1.commitTx.txOut.size == 4) // 2 main outputs + 2 anchors case DefaultCommitmentFormat => assert(bobCommit1.commitTx.txOut.size == 2) // 2 main outputs } @@ -1932,7 +1960,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.signCommitTx().txOut.size == bobCommit2.commitTx.txOut.size) - alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat match { + commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit2.commitTx.txOut.size == 6) case DefaultCommitmentFormat => assert(bobCommit2.commitTx.txOut.size == 4) } @@ -1948,7 +1976,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.signCommitTx().txOut.size == bobCommit3.commitTx.txOut.size) - alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat match { + commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit3.commitTx.txOut.size == 8) case DefaultCommitmentFormat => assert(bobCommit3.commitTx.txOut.size == 6) } @@ -1962,7 +1990,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } assert(alice.signCommitTx().txOut.size == bobCommit4.commitTx.txOut.size) - alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat match { + commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit4.commitTx.txOut.size == 4) case DefaultCommitmentFormat => assert(bobCommit4.commitTx.txOut.size == 2) } @@ -1976,7 +2004,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val revokedCloseFixture = prepareRevokedClose(f, commitmentFormat) - // assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) // bob publishes one of his revoked txs val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTx @@ -2037,7 +2065,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("recv WatchFundingSpentTriggered (one revoked tx, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => - assume(f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == DefaultCommitmentFormat) testFundingSpentRevokedTx(f, DefaultCommitmentFormat) } @@ -2049,9 +2076,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testFundingSpentRevokedTx(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv WatchFundingSpentTriggered (multiple revoked tx)") { f => + test("recv WatchFundingSpentTriggered (one revoked tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testFundingSpentRevokedTx(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + test("recv WatchFundingSpentTriggered (multiple revoked tx)", Tag(ChannelStateTestsTags.OptionSimpleTaprootPhoenix)) { f => import f._ - val revokedCloseFixture = prepareRevokedClose(f, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat) + val revokedCloseFixture = prepareRevokedClose(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTx.txid).toSet.size == revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCloseTxs = { @@ -2061,15 +2092,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last.commitTx == revokedTx) // alice publishes penalty txs - val mainRemote = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") val htlcPenaltyTxs = (1 to htlcCount).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) - (mainPenalty.tx +: htlcPenaltyTxs.map(_.tx)).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + (mainTx.tx +: mainPenalty.tx +: htlcPenaltyTxs.map(_.tx)).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) - alice2blockchain.expectWatchOutputsSpent(mainPenalty.input +: (mainRemote.input +: htlcPenaltyTxs.map(_.input))) + alice2blockchain.expectWatchOutputsSpent(mainTx.input +: mainPenalty.input +: htlcPenaltyTxs.map(_.input)) alice2blockchain.expectNoMessage(100 millis) - RevokedCloseTxs(Some(mainRemote.tx), mainPenalty.tx, htlcPenaltyTxs.map(_.tx)) + RevokedCloseTxs(Some(mainTx.tx), mainPenalty.tx, htlcPenaltyTxs.map(_.tx)) } // bob publishes a first revoked tx (no htlc in that commitment) @@ -2082,9 +2113,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob's second revoked tx confirms: once all penalty txs are confirmed, alice can move to the closed state // NB: if multiple txs confirm in the same block, we may receive the events in any order alice ! WatchTxConfirmedTriggered(BlockHeight(100), 1, closingTxs.mainPenaltyTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(100), 2, closingTxs.mainTx_opt.get) alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, revokedCloseFixture.bobRevokedTxs(1).commitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, closingTxs.htlcPenaltyTxs(0)) - closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, tx)) assert(alice.stateName == CLOSING) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, closingTxs.htlcPenaltyTxs(1)) awaitCond(alice.stateName == CLOSED) @@ -2116,7 +2147,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("recv INPUT_RESTORED (one revoked tx)") { f => - assume(f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == DefaultCommitmentFormat) testInputRestoredRevokedTx(f, DefaultCommitmentFormat) } @@ -2128,10 +2158,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testInputRestoredRevokedTx(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } + test("recv INPUT_RESTORED (one revoked tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testInputRestoredRevokedTx(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + def testRevokedHtlcTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ - val revokedCloseFixture = prepareRevokedClose(f, f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat) - // assert(alice.commitments.latest.commitmentFormat == commitmentFormat) + val revokedCloseFixture = prepareRevokedClose(f, commitmentFormat) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) // bob publishes one of his revoked txs val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2) @@ -2223,7 +2257,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testRevokedHtlcTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } - test("recv WatchTxConfirmedTriggered (revoked aggregated htlc tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchTxConfirmedTriggered (revoked htlc-success tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRevokedHtlcTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def testRevokedAggregatedHtlcTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ // bob publishes one of his revoked txs @@ -2231,7 +2269,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2) alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) - // assert(alice.commitments.latest.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head assert(rvk.commitTx == bobRevokedCommit.commitTx) @@ -2290,7 +2328,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectNoMessage(100 millis) } - test("recv INPUT_RESTORED (revoked htlc transactions confirmed)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchTxConfirmedTriggered (revoked aggregated htlc tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testRevokedAggregatedHtlcTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv WatchTxConfirmedTriggered (revoked aggregated htlc tx, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRevokedAggregatedHtlcTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + + def testInputRestoredRevokedHtlcTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ // Bob publishes one of his revoked txs. @@ -2300,6 +2346,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val commitTx = bobRevokedCommit.commitTx alice ! WatchFundingSpentTriggered(commitTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) // Alice publishes the penalty txs and watches outputs. val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") @@ -2395,8 +2442,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } + test("recv INPUT_RESTORED (revoked htlc transactions confirmed)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testInputRestoredRevokedHtlcTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + } + + test("recv INPUT_RESTORED (revoked htlc transactions confirmed, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testInputRestoredRevokedHtlcTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + private def testRevokedTxConfirmed(f: FixtureParam, commitmentFormat: CommitmentFormat): Unit = { import f._ + assert(alice.commitments.latest.commitmentFormat == commitmentFormat) val initOutputCount = commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => 4 case DefaultCommitmentFormat => 2 @@ -2444,7 +2500,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("recv WatchTxConfirmedTriggered (revoked commit tx, pending htlcs)") { f => - assume(f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.commitmentFormat == DefaultCommitmentFormat) testRevokedTxConfirmed(f, DefaultCommitmentFormat) } @@ -2456,6 +2511,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testRevokedTxConfirmed(f, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) } + test("recv WatchTxConfirmedTriggered (revoked commit tx, pending htlcs, taproot)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f => + testRevokedTxConfirmed(f, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + } + test("recv ChannelReestablish") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) @@ -2485,11 +2544,3 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - -class ClosingStateWithTaprootChannelsSpec extends ClosingStateSpec { - override val extraTags: Set[String] = Set(ChannelStateTestsTags.OptionSimpleTaprootStagingZeroFee) -} - -class ClosingStateWithLegacyTaprootChannelsSpec extends ClosingStateSpec { - override val extraTags: Set[String] = Set(ChannelStateTestsTags.OptionSimpleTaprootStagingLegacy) -} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NonceGeneratorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NonceGeneratorSpec.scala new file mode 100644 index 0000000000..917d8aa998 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NonceGeneratorSpec.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2025 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.crypto + +import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.randomKey +import org.scalatest.funsuite.AnyFunSuite + +class NonceGeneratorSpec extends AnyFunSuite { + + test("generate deterministic commitment verification nonces") { + val fundingTxId1 = randomTxId() + val fundingKey1 = randomKey() + val remoteFundingKey1 = randomKey().publicKey + val fundingTxId2 = randomTxId() + val fundingKey2 = randomKey() + val remoteFundingKey2 = randomKey().publicKey + // The verification nonce changes for each commitment. + val nonces1 = (0 until 15).map(commitIndex => NonceGenerator.verificationNonce(fundingTxId1, fundingKey1, remoteFundingKey1, commitIndex)) + assert(nonces1.toSet.size == 15) + // We can re-compute verification nonces deterministically. + (0 until 15).foreach(i => assert(nonces1(i) == NonceGenerator.verificationNonce(fundingTxId1, fundingKey1, remoteFundingKey1, i))) + // Nonces for different splices are different. + val nonces2 = (0 until 15).map(commitIndex => NonceGenerator.verificationNonce(fundingTxId2, fundingKey2, remoteFundingKey2, commitIndex)) + assert((nonces1 ++ nonces2).toSet.size == 30) + // Changing any of the parameters changes the nonce value. + assert(!nonces1.contains(NonceGenerator.verificationNonce(fundingTxId2, fundingKey1, remoteFundingKey1, 3))) + assert(!nonces1.contains(NonceGenerator.verificationNonce(fundingTxId1, fundingKey2, remoteFundingKey1, 11))) + assert(!nonces1.contains(NonceGenerator.verificationNonce(fundingTxId1, fundingKey1, remoteFundingKey2, 7))) + } + + test("generate random signing nonces") { + val fundingTxId = randomTxId() + val localFundingKey = randomKey().publicKey + val remoteFundingKey = randomKey().publicKey + // Signing nonces are random and different every time, even if the parameters are the same. + val nonce1 = NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId) + val nonce2 = NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId) + assert(nonce1 != nonce2) + val nonce3 = NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, randomTxId()) + assert(nonce3 != nonce1) + assert(nonce3 != nonce2) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index ec0ad0586e..0350fead75 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -404,7 +404,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we prepare the revoked transactions F will publish val channelKeysF = nodes("F").nodeParams.channelKeyManager.channelKeys(commitmentsF.channelParams.channelConfig, commitmentsF.localChannelParams.fundingKeyPath) val commitmentKeysF = commitmentsF.latest.localKeys(channelKeysF) - val revokedCommitTx = commitmentsF.latest.fullySignedLocalCommitTx(channelKeysF).toTry.get + val revokedCommitTx = commitmentsF.latest.fullySignedLocalCommitTx(channelKeysF) // in this commitment, both parties should have a main output, there are four pending htlcs and anchor outputs if applicable commitmentFormat match { case Transactions.DefaultCommitmentFormat => assert(revokedCommitTx.txOut.size == 6) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index dcefce23aa..33801cdb8a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -24,6 +24,7 @@ import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.ConnectionDown import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} @@ -340,9 +341,9 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer) val channelId = randomBytes32() val commitSigs = Seq( - CommitSig(channelId, randomBytes64(), Nil), - CommitSig(channelId, randomBytes64(), Nil), - CommitSig(channelId, randomBytes64(), Nil), + CommitSig(channelId, IndividualSignature(randomBytes64()), Nil), + CommitSig(channelId, IndividualSignature(randomBytes64()), Nil), + CommitSig(channelId, IndividualSignature(randomBytes64()), Nil), ) probe.send(peerConnection, CommitSigBatch(commitSigs)) commitSigs.foreach(commitSig => transport.expectMsg(commitSig)) @@ -356,8 +357,8 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi // We receive a batch of commit_sig messages from a first channel. val channelId1 = randomBytes32() val commitSigs1 = Seq( - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), ) transport.send(peerConnection, commitSigs1.head) transport.expectMsg(TransportHandler.ReadAck(commitSigs1.head)) @@ -369,9 +370,9 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi // We receive a batch of commit_sig messages from a second channel. val channelId2 = randomBytes32() val commitSigs2 = Seq( - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(3))), - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(3))), - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(3))), + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 3), + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 3), + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 3), ) commitSigs2.dropRight(1).foreach(commitSig => { transport.send(peerConnection, commitSig) @@ -384,8 +385,8 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi // We receive another batch of commit_sig messages from the first channel, with unrelated messages in the batch. val commitSigs3 = Seq( - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), ) transport.send(peerConnection, commitSigs3.head) transport.expectMsg(TransportHandler.ReadAck(commitSigs3.head)) @@ -405,9 +406,9 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi // We start receiving a batch of commit_sig messages from the first channel, interleaved with a batch from the second // channel, which is not supported. val commitSigs4 = Seq( - CommitSig(channelId1, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), - CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(2))), + CommitSig(channelId1, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 2), + CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 2), ) transport.send(peerConnection, commitSigs4.head) transport.expectMsg(TransportHandler.ReadAck(commitSigs4.head)) @@ -420,7 +421,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi peer.expectMsg(CommitSigBatch(commitSigs4.tail)) // We receive a batch that exceeds our threshold: we process them individually. - val invalidCommitSigs = (0 until 30).map(_ => CommitSig(channelId2, randomBytes64(), Nil, TlvStream(CommitSigTlv.BatchTlv(30)))) + val invalidCommitSigs = (0 until 30).map(_ => CommitSig(channelId2, IndividualSignature(randomBytes64()), Nil, batchSize = 30)) invalidCommitSigs.foreach(commitSig => { transport.send(peerConnection, commitSig) transport.expectMsg(TransportHandler.ReadAck(commitSig)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index a8ae00261e..ed50512c83 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -297,12 +297,12 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTx = commitmentFormat match { case _: SimpleTaprootChannelCommitmentFormat => val Right(commitTx) = for { - localPartialSig <- txInfo.partialSign(localFundingPriv, remoteFundingPriv.publicKey, Map.empty, LocalNonce(secretLocalNonce, publicLocalNonce), publicNonces) - remotePartialSig <- txInfo.partialSign(remoteFundingPriv, localFundingPriv.publicKey, Map.empty, LocalNonce(secretRemoteNonce, publicRemoteNonce), publicNonces) + localPartialSig <- txInfo.partialSign(localFundingPriv, remoteFundingPriv.publicKey, LocalNonce(secretLocalNonce, publicLocalNonce), publicNonces) + remotePartialSig <- txInfo.partialSign(remoteFundingPriv, localFundingPriv.publicKey, LocalNonce(secretRemoteNonce, publicRemoteNonce), publicNonces) _ = assert(txInfo.checkRemotePartialSignature(localFundingPriv.publicKey, remoteFundingPriv.publicKey, remotePartialSig, publicLocalNonce)) invalidRemotePartialSig = ChannelSpendSignature.PartialSignatureWithNonce(randomBytes32(), remotePartialSig.nonce) _ = assert(!txInfo.checkRemotePartialSignature(localFundingPriv.publicKey, remoteFundingPriv.publicKey, invalidRemotePartialSig, publicLocalNonce)) - tx <- txInfo.aggregateSigs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localPartialSig, remotePartialSig, Map.empty) + tx <- txInfo.aggregateSigs(localFundingPriv.publicKey, remoteFundingPriv.publicKey, localPartialSig, remotePartialSig) } yield tx commitTx case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => @@ -548,11 +548,11 @@ class TransactionsSpec extends AnyFunSuite with Logging { } test("generate valid commitment and htlc transactions (simple taproot channels)") { - testCommitAndHtlcTxs(LegacySimpleTaprootChannelCommitmentFormat) + testCommitAndHtlcTxs(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) } - test("generate valid commitment and htlc transactions (zero fee simple taproot channels)") { - testCommitAndHtlcTxs(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) + test("generate valid commitment and htlc transactions (phoenix simple taproot channels)") { + testCommitAndHtlcTxs(PhoenixSimpleTaprootChannelCommitmentFormat) } test("generate taproot NUMS point") { 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 150b2c7aa4..6928e0bd93 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 @@ -17,6 +17,7 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, OutPoint, SatoshiLong, Script, ScriptWitness, Transaction, TxHash, TxId} import fr.acinq.eclair.FeatureSupport.Optional @@ -24,6 +25,7 @@ import fr.acinq.eclair.Features.DataLossProtect import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelSpendSignature.{IndividualSignature, PartialSignatureWithNonce} import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes} import fr.acinq.eclair.json.JsonSerializers import fr.acinq.eclair.reputation.Reputation @@ -140,10 +142,14 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("nonreg generic tlv") { val channelId = randomBytes32() + val partialSig = randomBytes32() val signature = randomBytes64() val key = randomKey() val point = randomKey().publicKey val txId = randomTxId() + val nextTxId = randomTxId() + val nonce = new IndividualNonce(randomBytes(66).toArray) + val nextNonce = new IndividualNonce(randomBytes(66).toArray) val randomData = randomBytes(42) val tlvTag = UInt64(hex"47010000") @@ -156,18 +162,24 @@ class LightningMessageCodecsSpec extends AnyFunSuite { hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"00 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFundingTlv(txId))), hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"01 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.YourLastFundingLockedTlv(txId))), hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"03 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.MyCurrentFundingLockedTlv(txId))), + hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"06 42" ++ ByteVector(nonce.toByteArray) -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.CurrentCommitNonceTlv(nonce))), + hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"04 c4" ++ txId.value.reverse ++ ByteVector(nonce.toByteArray) ++ nextTxId.value.reverse ++ ByteVector(nextNonce.toByteArray) -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextLocalNoncesTlv(Seq(txId -> nonce, nextTxId -> nextNonce)))), hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"fe47010000 00" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream[ChannelReestablishTlv](Set.empty[ChannelReestablishTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"fe47010000 07 bbbbbbbbbbbbbb" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream[ChannelReestablishTlv](Set.empty[ChannelReestablishTlv], Set(GenericTlv(tlvTag, hex"bbbbbbbbbbbbbb")))), - hex"0084" ++ channelId ++ signature ++ hex"0000" -> CommitSig(channelId, signature, Nil), - hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 00" -> CommitSig(channelId, signature, Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), - hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 07 cccccccccccccc" -> CommitSig(channelId, signature, Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), + hex"0084" ++ channelId ++ signature ++ hex"0000" -> CommitSig(channelId, IndividualSignature(signature), Nil), + hex"0084" ++ channelId ++ ByteVector64.Zeroes ++ hex"0000" ++ hex"02 62" ++ partialSig ++ ByteVector(nonce.toByteArray) -> CommitSig(channelId, PartialSignatureWithNonce(partialSig, nonce), Nil, batchSize = 1), + hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 00" -> CommitSig(channelId, IndividualSignature(signature), Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), + hex"0084" ++ channelId ++ signature ++ hex"0000 fe47010000 07 cccccccccccccc" -> CommitSig(channelId, IndividualSignature(signature), Nil, TlvStream[CommitSigTlv](Set.empty[CommitSigTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), hex"0085" ++ channelId ++ key.value ++ point.value -> RevokeAndAck(channelId, key, point), + hex"0085" ++ channelId ++ key.value ++ point.value ++ hex"06 62" ++ txId.value.reverse ++ ByteVector(nonce.toByteArray) -> RevokeAndAck(channelId, key, point, Seq(txId -> nonce)), + hex"0085" ++ channelId ++ key.value ++ point.value ++ hex"06 c4" ++ txId.value.reverse ++ ByteVector(nonce.toByteArray) ++ nextTxId.value.reverse ++ ByteVector(nextNonce.toByteArray) -> RevokeAndAck(channelId, key, point, Seq(txId -> nonce, nextTxId -> nextNonce)), hex"0085" ++ channelId ++ key.value ++ point.value ++ hex" fe47010000 00" -> RevokeAndAck(channelId, key, point, TlvStream[RevokeAndAckTlv](Set.empty[RevokeAndAckTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), hex"0085" ++ channelId ++ key.value ++ point.value ++ hex" fe47010000 07 cccccccccccccc" -> RevokeAndAck(channelId, key, point, TlvStream[RevokeAndAckTlv](Set.empty[RevokeAndAckTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), hex"0026" ++ channelId ++ hex"002a" ++ randomData -> Shutdown(channelId, randomData), + hex"0026" ++ channelId ++ hex"002a" ++ randomData ++ hex"08 42" ++ ByteVector(nonce.toByteArray) -> Shutdown(channelId, randomData, nonce), hex"0026" ++ channelId ++ hex"002a" ++ randomData ++ hex"fe47010000 00" -> Shutdown(channelId, randomData, TlvStream[ShutdownTlv](Set.empty[ShutdownTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))), hex"0026" ++ channelId ++ hex"002a" ++ randomData ++ hex"fe47010000 07 cccccccccccccc" -> Shutdown(channelId, randomData, TlvStream[ShutdownTlv](Set.empty[ShutdownTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))), @@ -195,12 +207,16 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val channelId1 = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") val channelId2 = ByteVector32(hex"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") val signature = ByteVector64(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + val partialSig = ByteVector32(hex"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") // This is a random mainnet transaction. val txBin1 = hex"020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000" val tx1 = Transaction.read(txBin1.toArray) // This is random, longer mainnet transaction. val txBin2 = hex"0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000" val tx2 = Transaction.read(txBin2.toArray) + val nonce = new IndividualNonce("2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b") + val nextNonce = new IndividualNonce("b218b34786408f0a1aee2b35a0e860aa234b8013d1c385d1fcb4583fc4472bedfdd69a53c71006ec9f8b33724b719a50aa137814f4d0c00caff4e1da0d9856a957e7") + val fundingNonce = new IndividualNonce("a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d") val fundingRate = LiquidityAds.FundingRate(25_000 sat, 250_000 sat, 750, 150, 50 sat, 500 sat) val testCases = Seq( TxAddInput(channelId1, UInt64(561), Some(tx1), 1, 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005", @@ -212,10 +228,13 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TxRemoveInput(channelId2, UInt64(561)) -> hex"0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231", TxRemoveOutput(channelId1, UInt64(1)) -> hex"0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001", TxComplete(channelId1) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + TxComplete(channelId1, nonce, nextNonce, None) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 04 84 2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b b218b34786408f0a1aee2b35a0e860aa234b8013d1c385d1fcb4583fc4472bedfdd69a53c71006ec9f8b33724b719a50aa137814f4d0c00caff4e1da0d9856a957e7", + TxComplete(channelId1, nonce, nextNonce, Some(fundingNonce)) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 04 c6 2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b b218b34786408f0a1aee2b35a0e860aa234b8013d1c385d1fcb4583fc4472bedfdd69a53c71006ec9f8b33724b719a50aa137814f4d0c00caff4e1da0d9856a957e7 a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d", 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, 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", + 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(IndividualSignature(signature))) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxSignatures(channelId2, tx1, Nil, Some(PartialSignatureWithNonce(partialSig, fundingNonce))) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 02 62 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d", TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 1_500_000 sat, requireConfirmedInputs = true, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 0 sat, requireConfirmedInputs = false, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000", @@ -240,6 +259,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("encode/decode open_channel") { val defaultOpen = OpenChannel(BlockHash(ByteVector32.Zeroes), ByteVector32.Zeroes, 1 sat, 1 msat, 1 sat, UInt64(1), 1 sat, 1 msat, FeeratePerKw(1 sat), CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(announceChannel = false)) + val nonce = new IndividualNonce("2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b") // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. // To allow extending all messages with TLV streams, the upfront_shutdown_script was moved to a TLV stream extension // in https://github.com/lightningnetwork/lightning-rfc/pull/714 and made mandatory when including a TLV stream. @@ -282,6 +302,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { defaultEncoded ++ hex"0000" ++ hex"0107 04400000001000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey(scidAlias = true, zeroConf = true)))), defaultEncoded ++ hex"0000" ++ hex"0107 04400000101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs(scidAlias = true, zeroConf = true)))), defaultEncoded ++ hex"0000" ++ hex"0107 04400000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)))), + // taproot channel type + nonce + defaultEncoded ++ hex"0000" ++ hex"01 17 1000000000000000000000000000000000400000000000" ++ hex"04 42 2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.SimpleTaprootChannelsStaging(scidAlias = true)), ChannelTlv.NextLocalNonceTlv(nonce))) ) for ((encoded, expected) <- testCases) { @@ -344,6 +366,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("encode/decode accept_channel") { val defaultAccept = AcceptChannel(ByteVector32.Zeroes, 1 sat, UInt64(1), 1 sat, 1 msat, 1, CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6)) + val nonce = new IndividualNonce("2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b") // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. // To allow extending all messages with TLV streams, the upfront_shutdown_script was moved to a TLV stream extension // in https://github.com/lightningnetwork/lightning-rfc/pull/714 and made mandatory when including a TLV stream. @@ -354,10 +377,11 @@ class LightningMessageCodecsSpec extends AnyFunSuite { defaultEncoded ++ hex"0000" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))), // empty upfront_shutdown_script defaultEncoded ++ hex"0000" ++ hex"0100" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard()))), // empty upfront_shutdown_script with channel type defaultEncoded ++ hex"0004 01abcdef" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"))), // non-empty upfront_shutdown_script + defaultEncoded ++ hex"0000" ++ hex"01 17 1000000000000000000000000000000000000000000000" ++ hex"04 42 2062534ccb3be5a8997843f3b6bc530a94cbc60eceb538674ceedd62d8be07f2dfa5df6acf3ded7444268d56925bb2c33afe71a55f4fa88f3985451a681415930f6b" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.SimpleTaprootChannelsStaging()), ChannelTlv.NextLocalNonceTlv(nonce))), // empty upfront_shutdown_script with taproot channel type and nonce defaultEncoded ++ hex"0004 01abcdef" ++ hex"01021000" -> defaultAccept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey()))), // non-empty upfront_shutdown_script with channel type defaultEncoded ++ hex"0000 0302002a 050102" -> defaultAccept.copy(tlvStream = TlvStream(Set[AcceptChannelTlv](ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)), Set(GenericTlv(UInt64(3), hex"002a"), GenericTlv(UInt64(5), hex"02")))), // empty upfront_shutdown_script + unknown odd tlv records defaultEncoded ++ hex"0002 1234 0303010203" -> defaultAccept.copy(tlvStream = TlvStream(Set[AcceptChannelTlv](ChannelTlv.UpfrontShutdownScriptTlv(hex"1234")), Set(GenericTlv(UInt64(3), hex"010203")))), // non-empty upfront_shutdown_script + unknown odd tlv records - defaultEncoded ++ hex"0303010203 05020123" -> defaultAccept.copy(tlvStream = TlvStream(Set.empty[AcceptChannelTlv], Set(GenericTlv(UInt64(3), hex"010203"), GenericTlv(UInt64(5), hex"0123")))) // no upfront_shutdown_script + unknown odd tlv records + defaultEncoded ++ hex"0303010203 05020123" -> defaultAccept.copy(tlvStream = TlvStream(Set.empty[AcceptChannelTlv], Set(GenericTlv(UInt64(3), hex"010203"), GenericTlv(UInt64(5), hex"0123")))), // no upfront_shutdown_script + unknown odd tlv records ) for ((encoded, expected) <- testCases) { @@ -525,8 +549,14 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("encode/decode closing messages") { val channelId = ByteVector32(hex"58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86") val sig1 = ByteVector64(hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") + val partialSig1 = ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101") + val nonce1 = new IndividualNonce("52682593fd0783ea60657ed2d118e8f958c4a7a198237749b6729eccf963be1bc559531ec4b83bcfc42009cd08f7e95747146cec2fd09571b3fa76656e3012a4c97a") val sig2 = ByteVector64(hex"02020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") + val partialSig2 = ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202") + val nonce2 = new IndividualNonce("585b2fe8ca7a969bbda11ee9cbc95386abfddcc901967f84da4011c2a7cb5ada1dae51bdcd93a8b2933fcec7b2cda5a3f43ea2d0a29eb126bd329d4735d5389fe703") val sig3 = ByteVector64(hex"03030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") + val partialSig3 = ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303") + val nonce3 = new IndividualNonce("19bed0825ceb5acf504cddea72e37a75505290a22850c183725963edfe2dfb9f26e27180b210c05635987b80b3de3b7d01732653565b9f25ec23f7aff26122e00bff") val closerScript = hex"deadbeef" val closeeScript = hex"d43db3ef1234" val testCases = Seq( @@ -535,11 +565,15 @@ class LightningMessageCodecsSpec extends AnyFunSuite { hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserAndCloseeOutputs(sig1))), hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserOutputOnly(sig1), ClosingTlv.CloserAndCloseeOutputs(sig2))), hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserOutputOnly(sig1), ClosingTlv.CloseeOutputOnly(sig2), ClosingTlv.CloserAndCloseeOutputs(sig3))), + hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 05620202020202020202020202020202020202020202020202020202020202020202585b2fe8ca7a969bbda11ee9cbc95386abfddcc901967f84da4011c2a7cb5ada1dae51bdcd93a8b2933fcec7b2cda5a3f43ea2d0a29eb126bd329d4735d5389fe703" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(PartialSignatureWithNonce(partialSig2, nonce2)))), + hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 0462010101010101010101010101010101010101010101010101010101010101010152682593fd0783ea60657ed2d118e8f958c4a7a198237749b6729eccf963be1bc559531ec4b83bcfc42009cd08f7e95747146cec2fd09571b3fa76656e3012a4c97a 0662030303030303030303030303030303030303030303030303030303030303030319bed0825ceb5acf504cddea72e37a75505290a22850c183725963edfe2dfb9f26e27180b210c05635987b80b3de3b7d01732653565b9f25ec23f7aff26122e00bff" -> ClosingComplete(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingCompleteTlv.CloserOutputOnlyPartialSignature(PartialSignatureWithNonce(partialSig1, nonce1)), ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature(PartialSignatureWithNonce(partialSig3, nonce3)))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloseeOutputOnly(sig1))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserAndCloseeOutputs(sig1))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserOutputOnly(sig1), ClosingTlv.CloserAndCloseeOutputs(sig2))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingTlv.CloserOutputOnly(sig1), ClosingTlv.CloseeOutputOnly(sig2), ClosingTlv.CloserAndCloseeOutputs(sig3))), + hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 04200101010101010101010101010101010101010101010101010101010101010101" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingSigTlv.CloserOutputOnlyPartialSignature(partialSig1))), + hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 05200202020202020202020202020202020202020202020202020202020202020202 06200303030303030303030303030303030303030303030303030303030303030303" -> ClosingSig(channelId, closerScript, closeeScript, 1105 sat, 0, TlvStream(ClosingSigTlv.CloseeOutputOnlyPartialSignature(partialSig2), ClosingSigTlv.CloserAndCloseeOutputsPartialSignature(partialSig3))), ) for ((encoded, expected) <- testCases) { val decoded = lightningMessageCodec.decode(encoded.bits).require.value @@ -557,7 +591,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { FundingCreated(randomBytes32(), TxId(ByteVector32.Zeroes), 3, randomBytes64()), FundingSigned(randomBytes32(), randomBytes64()), ChannelReady(randomBytes32(), point(2)), - ChannelReady(randomBytes32(), point(2), TlvStream(ChannelReadyTlv.ShortChannelIdTlv(Alias(123456)))), + ChannelReady(randomBytes32(), point(2), Alias(123456)), + ChannelReady(randomBytes32(), point(2), Alias(123456), new IndividualNonce(randomBytes(66).toArray)), UpdateFee(randomBytes32(), FeeratePerKw(2 sat)), Shutdown(randomBytes32(), bin(47, 0)), ClosingSigned(randomBytes32(), 2 sat, randomBytes64()), @@ -565,7 +600,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { UpdateFulfillHtlc(randomBytes32(), 2, bin32(0)), UpdateFailHtlc(randomBytes32(), 2, bin(154, 0)), UpdateFailMalformedHtlc(randomBytes32(), 2, randomBytes32(), 1111), - CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil), + CommitSig(randomBytes32(), IndividualSignature(randomBytes64()), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil), RevokeAndAck(randomBytes32(), scalar(0), point(1)), ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, RealShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey), NodeAnnouncement(randomBytes64(), Features(DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil),