From 71a3df491f531ef68b3231063cf560cbcbccfc90 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 5 Mar 2025 12:13:42 +0100 Subject: [PATCH 1/4] Add high-level helpers for taproot channels We add a new specific commitment format for taproot channels, and high-level methods for creating and spending taproot channel transactions. --- .../scala/fr/acinq/eclair/NodeParams.scala | 2 +- .../eclair/SpendFromChannelAddress.scala | 2 +- .../blockchain/fee/OnChainFeeConf.scala | 8 +- .../fr/acinq/eclair/channel/Helpers.scala | 64 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 22 +- .../channel/fsm/ChannelOpenDualFunded.scala | 2 +- .../eclair/channel/fsm/ErrorHandlers.scala | 2 +- .../channel/fund/InteractiveTxBuilder.scala | 21 +- .../channel/publish/ReplaceableTxFunder.scala | 6 +- .../acinq/eclair/transactions/Scripts.scala | 12 +- .../eclair/transactions/Transactions.scala | 974 +++++++++++++----- .../channel/version0/ChannelTypes0.scala | 6 +- .../eclair/channel/ChannelDataSpec.scala | 4 +- .../eclair/channel/CommitmentsSpec.scala | 10 +- .../fr/acinq/eclair/channel/HelpersSpec.scala | 4 +- .../channel/InteractiveTxBuilderSpec.scala | 8 +- .../publish/ReplaceableTxFunderSpec.scala | 15 +- .../publish/ReplaceableTxPublisherSpec.scala | 4 +- .../channel/publish/TxPublisherSpec.scala | 16 +- .../ChannelStateTestsHelperMethods.scala | 4 +- .../b/WaitForDualFundingSignedStateSpec.scala | 2 +- .../b/WaitForFundingSignedStateSpec.scala | 2 +- .../c/WaitForChannelReadyStateSpec.scala | 2 +- ...WaitForDualFundingConfirmedStateSpec.scala | 2 +- .../channel/states/h/ClosingStateSpec.scala | 16 +- .../integration/ChannelIntegrationSpec.scala | 12 +- .../io/OpenChannelInterceptorSpec.scala | 2 +- .../io/PendingChannelsRateLimiterSpec.scala | 2 +- .../eclair/json/JsonSerializersSpec.scala | 6 +- .../eclair/payment/PaymentPacketSpec.scala | 2 +- .../payment/PostRestartHtlcCleanerSpec.scala | 2 +- .../eclair/transactions/TestVectorsSpec.scala | 7 +- .../transactions/TransactionsSpec.scala | 268 +++-- .../internal/channel/ChannelCodecsSpec.scala | 4 +- .../channel/version4/ChannelCodecs4Spec.scala | 4 +- 35 files changed, 1055 insertions(+), 464 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 59d0dfca97..755e71222c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -134,7 +134,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, min = (commitmentFeerate * feerateTolerance.ratioLow).max(minimumFeerate), max = (commitmentFormat match { case Transactions.DefaultCommitmentFormat => commitmentFeerate * feerateTolerance.ratioHigh - case _: Transactions.AnchorOutputsCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate) + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate) }).max(minimumFeerate), ) RecommendedFeerates(chainHash, fundingFeerate, commitmentFeerate, TlvStream(fundingRange, commitmentRange)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala index 8f799def24..73db6474f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala @@ -43,7 +43,7 @@ trait SpendFromChannelAddress { inputTx <- appKit.wallet.getTransaction(outPoint.txid) localFundingPubkey = appKit.nodeParams.channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex) fundingRedeemScript = multiSig2of2(localFundingPubkey.publicKey, remoteFundingPubkey) - inputInfo = InputInfo(outPoint, inputTx.txOut(outPoint.index.toInt), fundingRedeemScript) + inputInfo = InputInfo.SegwitInput(outPoint, inputTx.txOut(outPoint.index.toInt), fundingRedeemScript) localSig = appKit.nodeParams.channelKeyManager.sign( Transactions.SpliceTx(inputInfo, unsignedTx), // classify as splice, doesn't really matter localFundingPubkey, 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 a7514601ac..5b9f6bff47 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, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, SimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} // @formatter:off sealed trait ConfirmationPriority extends Ordered[ConfirmationPriority] { @@ -77,7 +77,7 @@ 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 => networkFeerate * ratioHigh < proposedFeerate + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 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 => false + case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => false } } } @@ -121,7 +121,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets, commitmentFormat match { case Transactions.DefaultCommitmentFormat => networkFeerate - case _: Transactions.AnchorOutputsCommitmentFormat => + 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/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 24a1448459..00e05b572e 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 @@ -33,6 +33,7 @@ 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.Scripts._ +import fr.acinq.eclair.transactions.Transactions.InputInfo.RedeemPath import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ @@ -275,7 +276,7 @@ object Helpers { for { script_opt <- extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt) - fundingScript = Funding.makeFundingPubKeyScript(open.fundingPubkey, accept.fundingPubkey) + fundingScript = Funding.makeFundingPubKeyScript(open.fundingPubkey, accept.fundingPubkey, channelType.commitmentFormat) liquidityPurchase_opt <- LiquidityAds.validateRemoteFunding(open.requestFunding_opt, remoteNodeId, accept.temporaryChannelId, fundingScript, accept.fundingAmount, open.fundingFeerate, isChannelCreation = true, accept.willFund_opt) } yield { val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) @@ -372,13 +373,20 @@ object Helpers { } object Funding { + def makeFundingPubKeyScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): ByteVector = commitmentFormat match { + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => write(pay2wsh(multiSig2of2(localFundingKey, remoteFundingKey))) + case SimpleTaprootChannelCommitmentFormat => write(Taproot.musig2FundingScript(localFundingKey, remoteFundingKey)) + } - def makeFundingPubKeyScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey): ByteVector = write(pay2wsh(multiSig2of2(localFundingKey, remoteFundingKey))) - - def makeFundingInputInfo(fundingTxId: TxId, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo.SegwitInput = { - val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) - val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript)) - InputInfo.SegwitInput(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, write(fundingScript)) + def makeFundingInputInfo(fundingTxId: TxId, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey, commitmentFormat: CommitmentFormat): InputInfo = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val fundingScript = Taproot.musig2FundingScript(fundingPubkey1, fundingPubkey2) + val fundingTxOut = TxOut(fundingSatoshis, fundingScript) + InputInfo.TaprootInput(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, Taproot.musig2Aggregate(fundingPubkey1, fundingPubkey2), RedeemPath.KeyPath(None)) + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) + val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript)) + InputInfo.SegwitInput(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, write(fundingScript)) } /** @@ -441,7 +449,7 @@ object Helpers { val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex) val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteFundingPubKey) + val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteFundingPubKey, params.commitmentFormat) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitmentIndex) val (localCommitTx, _) = Commitment.makeLocalTxs(keyManager, channelConfig, channelFeatures, localCommitmentIndex, localParams, remoteParams, fundingTxIndex, remoteFundingPubKey, commitmentInput, localPerCommitmentPoint, localSpec) val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, channelConfig, channelFeatures, remoteCommitmentIndex, localParams, remoteParams, fundingTxIndex, remoteFundingPubKey, commitmentInput, remotePerCommitmentPoint, remoteSpec) @@ -679,7 +687,7 @@ object Helpers { case DefaultCommitmentFormat => // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" requestedFeerate.min(commitment.localCommit.spec.commitTxFeerate) - case _: AnchorOutputsCommitmentFormat => requestedFeerate + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => requestedFeerate } // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can // always use CPFP to speed up confirmation if necessary. @@ -909,13 +917,25 @@ object Helpers { def claimAnchors(keyManager: ChannelKeyManager, commitment: FullCommitment, lcp: LocalCommitPublished, confirmationTarget: ConfirmationTarget)(implicit log: LoggingAdapter): LocalCommitPublished = { if (shouldUpdateAnchorTxs(lcp.claimAnchorTxs, confirmationTarget)) { - val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey + val (localAnchorKey, remoteAnchorKey) = commitment.params.commitmentFormat match { + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + // The public keys used in this case are the channel funding public keys. + val localFundingPubkey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey + (localFundingPubkey, commitment.remoteFundingPubKey) + case SimpleTaprootChannelCommitmentFormat => + // The public keys used in this case are the payment public keys: we don't want to reveal individual + // funding public keys since we're using musig2. + val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitment.localCommit.index) + val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) + (localDelayedPaymentPubkey, commitment.remoteParams.paymentBasepoint) + } val claimAnchorTxs = List( withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(lcp.commitTx, localFundingPubKey, confirmationTarget) + Transactions.makeClaimLocalAnchorOutputTx(lcp.commitTx, localAnchorKey, confirmationTarget) }, withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(lcp.commitTx, commitment.remoteFundingPubKey) + Transactions.makeClaimRemoteAnchorOutputTx(lcp.commitTx, remoteAnchorKey) } ).flatten lcp.copy(claimAnchorTxs = claimAnchorTxs) @@ -1038,13 +1058,23 @@ object Helpers { def claimAnchors(keyManager: ChannelKeyManager, commitment: FullCommitment, rcp: RemoteCommitPublished, confirmationTarget: ConfirmationTarget)(implicit log: LoggingAdapter): RemoteCommitPublished = { if (shouldUpdateAnchorTxs(rcp.claimAnchorTxs, confirmationTarget)) { - val localFundingPubkey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey + val (localAnchorKey, remoteAnchorKey) = commitment.params.commitmentFormat match { + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + // The public keys used in this case are the channel funding public keys. + val localFundingPubkey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey + (localFundingPubkey, commitment.remoteFundingPubKey) + case SimpleTaprootChannelCommitmentFormat => + val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) + val localPaymentPubkey = commitment.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) + val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitment.remoteParams.delayedPaymentBasepoint, commitment.remoteCommit.remotePerCommitmentPoint) + (localPaymentPubkey, remoteDelayedPaymentPubkey) + } val claimAnchorTxs = List( withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(rcp.commitTx, localFundingPubkey, confirmationTarget) + Transactions.makeClaimLocalAnchorOutputTx(rcp.commitTx, localAnchorKey, confirmationTarget) }, withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(rcp.commitTx, commitment.remoteFundingPubKey) + Transactions.makeClaimRemoteAnchorOutputTx(rcp.commitTx, remoteAnchorKey) } ).flatten rcp.copy(claimAnchorTxs = claimAnchorTxs) @@ -1077,7 +1107,7 @@ object Helpers { Transactions.addSigs(claimMain, localPubkey, sig) }) } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(tx, params.localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feeratePerKwMain).map(claimMain => { val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, params.commitmentFormat, Map.empty) Transactions.addSigs(claimMain, sig) @@ -1224,7 +1254,7 @@ object Helpers { Transactions.addSigs(claimMain, localPaymentPubkey, sig) }) } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feerateMain).map(claimMain => { val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat, Map.empty) Transactions.addSigs(claimMain, sig) 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 3e2e1bd78e..170d60eee4 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 @@ -48,7 +48,7 @@ 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.ClosingTx +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, ClosingTx, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ @@ -1027,7 +1027,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } else { val parentCommitment = d.commitments.latest.commitment val localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey - val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubKey, msg.fundingPubKey) + val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubKey, msg.fundingPubKey, d.commitments.latest.params.commitmentFormat) + val sharedInput = SharedFundingInput(parentCommitment) LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, msg.useFeeCredit_opt) match { case Left(t) => log.warning("rejecting splice request with invalid liquidity ads: {}", t.getMessage) @@ -1047,7 +1048,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with isInitiator = false, localContribution = spliceAck.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(parentCommitment)), + sharedInput_opt = Some(sharedInput), remoteFundingPubKey = msg.fundingPubKey, localOutputs = Nil, lockTime = msg.lockTime, @@ -1085,12 +1086,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case SpliceStatus.SpliceRequested(cmd, spliceInit) => log.info("our peer accepted our splice request and will contribute {} to the funding transaction", msg.fundingContribution) val parentCommitment = d.commitments.latest.commitment + val sharedInput = SharedFundingInput(parentCommitment) val fundingParams = InteractiveTxParams( channelId = d.channelId, isInitiator = true, localContribution = spliceInit.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(parentCommitment)), + sharedInput_opt = Some(sharedInput), remoteFundingPubKey = msg.fundingPubKey, localOutputs = cmd.spliceOutputs, lockTime = spliceInit.lockTime, @@ -1098,7 +1100,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with targetFeerate = spliceInit.feerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceInit.requireConfirmedInputs) ) - val fundingScript = Funding.makeFundingPubKeyScript(spliceInit.fundingPubKey, msg.fundingPubKey) + val fundingScript = Funding.makeFundingPubKeyScript(spliceInit.fundingPubKey, msg.fundingPubKey, d.commitments.latest.params.commitmentFormat) LiquidityAds.validateRemoteFunding(spliceInit.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, spliceInit.feerate, isChannelCreation = false, msg.willFund_opt) match { case Left(t) => log.info("rejecting splice attempt: invalid liquidity ads response ({})", t.getMessage) @@ -1165,7 +1167,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with isInitiator = false, localContribution = fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(rbf.parentCommitment)), + sharedInput_opt = Some(SharedFundingInput(rbf.parentCommitment)), remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey, localOutputs = rbf.latestFundingTx.fundingParams.localOutputs, lockTime = msg.lockTime, @@ -1218,7 +1220,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with isInitiator = true, localContribution = txInitRbf.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(rbf.parentCommitment)), + sharedInput_opt = Some(SharedFundingInput(rbf.parentCommitment)), remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey, localOutputs = rbf.latestFundingTx.fundingParams.localOutputs, lockTime = txInitRbf.lockTime, @@ -2163,7 +2165,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(c: CMD_BUMP_FORCE_CLOSE_FEE, d: DATA_CLOSING) => d.commitments.params.commitmentFormat match { - case _: Transactions.AnchorOutputsCommitmentFormat => + case SimpleTaprootChannelCommitmentFormat | _: Transactions.AnchorOutputsCommitmentFormat => val lcp1 = d.localCommitPublished.map(lcp => Closing.LocalClose.claimAnchors(keyManager, d.commitments.latest, lcp, c.confirmationTarget)) val rcp1 = d.remoteCommitPublished.map(rcp => Closing.RemoteClose.claimAnchors(keyManager, d.commitments.latest, rcp, c.confirmationTarget)) val nrcp1 = d.nextRemoteCommitPublished.map(nrcp => Closing.RemoteClose.claimAnchors(keyManager, d.commitments.latest, nrcp, c.confirmationTarget)) @@ -2185,7 +2187,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with c.replyTo ! RES_FAILURE(c, CommandUnavailableInThisState(d.channelId, "rbf-force-close", stateName)) stay() } - case _ => + case DefaultCommitmentFormat => log.warning("cannot bump force-close fees, channel is not using anchor outputs") c.replyTo ! RES_FAILURE(c, CommandUnavailableInThisState(d.channelId, "rbf-force-close", stateName)) stay() @@ -3270,7 +3272,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val targetFeerate = nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing) val fundingContribution = InteractiveTxFunder.computeSpliceContribution( isInitiator = true, - sharedInput = Multisig2of2Input(parentCommitment), + sharedInput = SharedFundingInput(parentCommitment), spliceInAmount = cmd.additionalLocalFunding, spliceOut = cmd.spliceOutputs, targetFeerate = targetFeerate) 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 6eb40193eb..9b202e40fa 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 @@ -142,7 +142,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(open: OpenDualFundedChannel, d: DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) => import d.init.{localParams, remoteInit} val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex = 0).publicKey - val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey) + val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey, d.init.channelType.commitmentFormat) Helpers.validateParamsDualFundedNonInitiator(nodeParams, d.init.channelType, open, fundingScript, remoteNodeId, localParams.initFeatures, remoteInit.features, d.init.fundingContribution_opt) match { case Left(t) => handleLocalError(t, d, Some(open)) case Right((channelFeatures, remoteShutdownScript, willFund_opt)) => 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 361a846880..d94abe8b23 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 @@ -233,7 +233,7 @@ trait ErrorHandlers extends CommonHandlers { case Transactions.DefaultCommitmentFormat => val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid))) List(PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, localPaysCommitTxFees), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))) - case _: Transactions.AnchorOutputsCommitmentFormat => + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitment, commitTx)) val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx if !localCommitPublished.isConfirmed => PublishReplaceableTx(tx, commitment, commitTx) } List(PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, localPaysCommitTxFees), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) 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 94dec79916..0f819b4395 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 @@ -108,6 +108,13 @@ object InteractiveTxBuilder { // @formatter:on } + object SharedFundingInput { + def apply(commitment: Commitment): SharedFundingInput = commitment.commitInput match { + case inputInfo: InputInfo.SegwitInput => Multisig2of2Input(inputInfo, commitment.fundingTxIndex, commitment.remoteFundingPubKey) + case inputInfo: InputInfo.TaprootInput => Musig2Input(inputInfo, commitment.fundingTxIndex, commitment.remoteFundingPubKey, commitment.localCommit.index) + } + } + case class Multisig2of2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey) extends SharedFundingInput { override val weight: Int = 388 @@ -117,12 +124,13 @@ object InteractiveTxBuilder { } } - object Multisig2of2Input { - def apply(commitment: Commitment): Multisig2of2Input = Multisig2of2Input( - info = commitment.commitInput, - fundingTxIndex = commitment.fundingTxIndex, - remoteFundingPubkey = commitment.remoteFundingPubKey - ) + case class Musig2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, commitIndex: Long) extends SharedFundingInput { + // witness is a single 64 bytes signature, weight = 1 (# of items) + 1 (size) + 64 = 66 + // weight is 4 * (unsigned input weight) + witness weight = 4 * (32 + 4 + 4 + 1) + 66 = 230 + override val weight: Int = 230 + + // a valid signature for this input MUST be the Musig2 aggregation of local and remote partial signatures + def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction, spentUtxos: Map[OutPoint, TxOut]): ByteVector64 = ??? } /** @@ -1049,6 +1057,7 @@ object InteractiveTxSigningSession { log.info("invalid tx_signatures: missing shared input signatures") return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) } + case Some(_: Musig2Input) => return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) // TODO: not implemented case None => None } val txWithSigs = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, remoteSigs, sharedSigs_opt) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index 10301aaa8a..d0acb99cc7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala @@ -107,9 +107,9 @@ object ReplaceableTxFunder { // For HTLC transactions, we add a p2wpkh input and a p2wpkh change output. case _: HtlcSuccessTx => commitment.params.commitmentFormat.htlcSuccessWeight + Transactions.claimP2WPKHOutputWeight case _: HtlcTimeoutTx => commitment.params.commitmentFormat.htlcTimeoutWeight + Transactions.claimP2WPKHOutputWeight - case _: ClaimHtlcSuccessTx => Transactions.claimHtlcSuccessWeight - case _: LegacyClaimHtlcSuccessTx => Transactions.claimHtlcSuccessWeight - case _: ClaimHtlcTimeoutTx => Transactions.claimHtlcTimeoutWeight + case _: ClaimHtlcSuccessTx => commitment.params.commitmentFormat.claimHtlcSuccessWeight + case _: LegacyClaimHtlcSuccessTx => commitment.params.commitmentFormat.claimHtlcSuccessWeight + case _: ClaimHtlcTimeoutTx => commitment.params.commitmentFormat.claimHtlcTimeoutWeight case _: ClaimLocalAnchorOutputTx => commitTx.weight() + Transactions.claimAnchorOutputMinWeight } // It doesn't make sense to use a feerate that is much higher than the current feerate for inclusion into the next block. 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 6137d9e7f0..c713d613da 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 @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.TxIn.{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_ import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, XonlyPublicKey} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta} import scodec.bits.ByteVector @@ -44,7 +44,7 @@ object Scripts { private def htlcRemoteSighash(commitmentFormat: CommitmentFormat): Int = commitmentFormat match { case DefaultCommitmentFormat => SIGHASH_ALL - case _: AnchorOutputsCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY } /** Sort public keys using lexicographic ordering. */ @@ -191,6 +191,7 @@ object Scripts { val addCsvDelay = commitmentFormat match { case DefaultCommitmentFormat => false case _: AnchorOutputsCommitmentFormat => true + case SimpleTaprootChannelCommitmentFormat => true } // @formatter:off // To you with revocation key @@ -223,7 +224,8 @@ 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(ByteVector.empty, _, _, paymentPreimage, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) // standard channels + case ScriptWitness(Seq(remoteSig, localSig, paymentPreimage, _, _)) if remoteSig.size == 65 && localSig.size == 64 && paymentPreimage.size == 32 => ByteVector32(paymentPreimage) // simple taproot channels } /** Extract payment preimages from a (potentially batched) 2nd-stage HTLC transaction's witnesses. */ @@ -239,6 +241,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(_, paymentPreimage, _, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) } /** Extract payment preimages from a (potentially batched) claim HTLC transaction's witnesses. */ @@ -248,6 +251,7 @@ object Scripts { val addCsvDelay = commitmentFormat match { case DefaultCommitmentFormat => false case _: AnchorOutputsCommitmentFormat => true + case SimpleTaprootChannelCommitmentFormat => true } // @formatter:off // To you with revocation key @@ -303,6 +307,8 @@ object Scripts { implicit def scala2kmpscript(input: Seq[fr.acinq.bitcoin.scalacompat.ScriptElt]): java.util.List[fr.acinq.bitcoin.ScriptElt] = input.map(e => scala2kmp(e)).asJava + def musig2FundingScript(pubkey1: PublicKey, pubkey2: PublicKey): Seq[ScriptElt] = Script.pay2tr(musig2Aggregate(pubkey1, pubkey2), None) + /** * Taproot signatures are usually 64 bytes, unless a non-default sighash is used, in which case it is appended. */ 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 9cb1ea9194..46f4341ff2 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 @@ -18,18 +18,22 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.SigVersion._ +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey, ripemd160} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.{ScriptFlags, ScriptTree} +import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} import fr.acinq.eclair.transactions.CommitmentOutput._ +import fr.acinq.eclair.transactions.Scripts.Taproot.NUMS_POINT import fr.acinq.eclair.transactions.Scripts._ +import fr.acinq.eclair.transactions.Transactions.InputInfo.RedeemPath import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import scodec.bits.ByteVector import java.nio.ByteOrder +import scala.reflect.ClassTag import scala.util.{Success, Try} /** @@ -47,6 +51,8 @@ object Transactions { def htlcSuccessWeight: Int def htlcTimeoutInputWeight: Int def htlcSuccessInputWeight: Int + def claimHtlcSuccessWeight: Int + def claimHtlcTimeoutWeight: Int // @formatter:on } @@ -60,6 +66,8 @@ object Transactions { override val htlcSuccessWeight = 703 override val htlcTimeoutInputWeight = 449 override val htlcSuccessInputWeight = 488 + override val claimHtlcSuccessWeight = 571 + override val claimHtlcTimeoutWeight = 545 } /** @@ -71,8 +79,10 @@ object Transactions { override val htlcOutputWeight = 172 override val htlcTimeoutWeight = 666 override val htlcSuccessWeight = 706 - override val htlcTimeoutInputWeight = 452 - override val htlcSuccessInputWeight = 491 + override val htlcTimeoutInputWeight = 452 // 288 + 4 * 41 + override val htlcSuccessInputWeight = 491 // 327 + 4 * 41 + override val claimHtlcSuccessWeight = 571 + override val claimHtlcTimeoutWeight = 545 } object AnchorOutputsCommitmentFormat { @@ -92,6 +102,32 @@ object Transactions { */ case object ZeroFeeHtlcTxAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat + case object SimpleTaprootChannelCommitmentFormat extends CommitmentFormat { + // weights for taproot transactions are deterministic since signatures are encoded as 64 bytes and + // not in variable length DER format (around 72 bytes) + + // commit tx witness is just a single 64 bytes signature + override val commitWeight = 960 + // HTLC output weight remains the same + override val htlcOutputWeight = 172 + + // witness is remote sig (64 + 1 bytes + local sig (64 bytes) + script (68 bytes) + control block (65 bytes) + override val htlcTimeoutWeight = 645 + // witness is remote sig (64 + 1 bytes + local sig (64 bytes) + preimage (32 bytes) + script (95 bytes) + control block (65 bytes) + override val htlcSuccessWeight = 705 + + // witness is remote sig (64 + 1 bytes + local sig (64 bytes) + script (68 bytes) + control block (65 bytes) + // input weight = 4 * 41 (input without witness) + 174 (witness) + override val htlcTimeoutInputWeight = 431 + + // witness is remote sig (64 + 1 bytes + local sig (64 bytes) + preimage (32 bytes) + script (95 bytes) + control block (65 bytes) + // input weight = 4 * 41 (input without witness) + 229 (witness) + override val htlcSuccessInputWeight = 491 + + override val claimHtlcSuccessWeight = 559 + override val claimHtlcTimeoutWeight = 504 + } + // @formatter:off case class OutputInfo(index: Long, amount: Satoshi, publicKeyScript: ByteVector) @@ -102,13 +138,37 @@ object Transactions { object InputInfo { case class SegwitInput(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector) extends InputInfo - case class TaprootInput(outPoint: OutPoint, txOut: TxOut, internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree]) extends InputInfo { - val publicKeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, scriptTree_opt)) + object SegwitInput { + def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]) = new SegwitInput(outPoint, txOut, Script.write(redeemScript)) } + case class TaprootInput(outPoint: OutPoint, txOut: TxOut, internalKey: XonlyPublicKey, redeemPath: RedeemPath) extends InputInfo + sealed trait RedeemPath + object RedeemPath { + /** + * @param scriptTree_opt the script tree must be known if there is one, even when spending via the key path. + */ + case class KeyPath(scriptTree_opt: Option[ScriptTree]) extends RedeemPath + /** + * @param scriptTree we need the complete script tree to spend taproot inputs. + * @param leafHash hash of the leaf script we're spending (must belong to the tree). + */ + case class ScriptPath(scriptTree: ScriptTree, leafHash: ByteVector32) extends RedeemPath { + require(ScriptPath.findScript(scriptTree, leafHash).nonEmpty, "script tree must contain the provided leaf") + } + + object ScriptPath { + import KotlinUtils._ - def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector): SegwitInput = SegwitInput(outPoint, txOut, redeemScript) - def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]): SegwitInput = SegwitInput(outPoint, txOut, Script.write(redeemScript)) - def apply(outPoint: OutPoint, txOut: TxOut, internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree]): TaprootInput = TaprootInput(outPoint, txOut, internalKey, scriptTree_opt) + /** + * Note: this won't be needed once findScript is added to bitcoin-kmp + * @return the leaf that matches `leafHash` + */ + def findScript(scriptTree: ScriptTree, leafHash: ByteVector32): Option[ScriptTree.Leaf] = scriptTree match { + case l: ScriptTree.Leaf => if (l.hash() == scala2kmp(leafHash)) Some(l) else None + case b: ScriptTree.Branch => findScript(b.getLeft, leafHash) orElse findScript(b.getRight, leafHash) + } + } + } } /** Owner of a given transaction (local/remote). */ @@ -129,7 +189,10 @@ object Transactions { Satoshi(FeeratePerKw.MinimumRelayFeeRate * vsize / 1000) } /** Sighash flags to use when signing the transaction. */ - def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = SIGHASH_ALL + def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => SIGHASH_DEFAULT + case DefaultCommitmentFormat | _:AnchorOutputsCommitmentFormat => SIGHASH_ALL + } /** * @param extraUtxos extra outputs spent by this transaction (in addition to the main [[input]]). @@ -154,12 +217,22 @@ object Transactions { val inputIndex = tx.txIn.indexWhere(_.outPoint == outPoint) val sigDER = Transaction.signInput(tx, inputIndex, redeemScript, sighashType, txOut.amount, SIGVERSION_WITNESS_V0, key) Crypto.der2compact(sigDER) - case _: InputInfo.TaprootInput => ??? + case t: InputInfo.TaprootInput => + val spentOutputs = tx.txIn.map(input => inputsMap(input.outPoint)) + t.redeemPath match { + case k: RedeemPath.KeyPath => Transaction.signInputTaprootKeyPath(key, tx, 0, spentOutputs, sighashType, k.scriptTree_opt) + case s: RedeemPath.ScriptPath => Transaction.signInputTaprootScriptPath(key, tx, 0, spentOutputs, sighashType, s.leafHash) + } } } def checkSig(sig: ByteVector64, pubKey: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): Boolean = input match { - case _: InputInfo.TaprootInput => false + case t: InputInfo.TaprootInput => + val data = t.redeemPath match { + case _: RedeemPath.KeyPath => Transaction.hashForSigningTaprootKeyPath(tx, inputIndex = 0, Seq(input.txOut), sighash(txOwner, commitmentFormat)) + case s: RedeemPath.ScriptPath => Transaction.hashForSigningTaprootScriptPath(tx, inputIndex = 0, Seq(input.txOut), sighash(txOwner, commitmentFormat), s.leafHash) + } + Crypto.verifySignatureSchnorr(data, sig, pubKey.xOnly) case InputInfo.SegwitInput(outPoint, txOut, redeemScript) => val sighash = this.sighash(txOwner, commitmentFormat) val inputIndex = tx.txIn.indexWhere(_.outPoint == outPoint) @@ -179,7 +252,10 @@ object Transactions { case class SpliceTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "splice-tx" } - case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "commit-tx" } + case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { + override def desc: String = "commit-tx" + } + /** * It's important to note that htlc transactions with the default commitment format are not actually replaceable: only * anchor outputs htlc transactions are replaceable. We should have used different types for these different kinds of @@ -200,30 +276,69 @@ object Transactions { case TxOwner.Local => SIGHASH_ALL case TxOwner.Remote => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY } + case SimpleTaprootChannelCommitmentFormat => txOwner match { + case TxOwner.Local => SIGHASH_DEFAULT + case TxOwner.Remote => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY + } } override def confirmationTarget: ConfirmationTarget.Absolute } - case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { override def desc: String = "htlc-success" } - case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { override def desc: String = "htlc-timeout" } - case class HtlcDelayedTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed" } + + case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { + override def desc: String = "htlc-success" + } + + case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { + override def desc: String = "htlc-timeout" + } + + case class HtlcDelayedTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { + override def desc: String = "htlc-delayed" + } + sealed trait ClaimHtlcTx extends ReplaceableTransactionWithInputInfo { def htlcId: Long override def confirmationTarget: ConfirmationTarget.Absolute } case class LegacyClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } - case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } - case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-timeout" } + case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { + override def desc: String = "claim-htlc-success" + } + case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { + override def desc: String = "claim-htlc-timeout" + } + sealed trait ClaimAnchorOutputTx extends TransactionWithInputInfo - case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction, confirmationTarget: ConfirmationTarget) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { override def desc: String = "local-anchor" } + case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction, confirmationTarget: ConfirmationTarget) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { + override def desc: String = "local-anchor" + } + case class ClaimRemoteAnchorOutputTx(input: InputInfo, tx: Transaction) extends ClaimAnchorOutputTx { override def desc: String = "remote-anchor" } sealed trait ClaimRemoteCommitMainOutputTx extends TransactionWithInputInfo case class ClaimP2WPKHOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { override def desc: String = "remote-main" } - case class ClaimRemoteDelayedOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { override def desc: String = "remote-main-delayed" } - case class ClaimLocalDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "local-main-delayed" } - case class MainPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "main-penalty" } - case class HtlcPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-penalty" } - case class ClaimHtlcDelayedOutputPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed-penalty" } - case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutput: Option[OutputInfo]) extends TransactionWithInputInfo { override def desc: String = "closing" } + case class ClaimRemoteDelayedOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { + override def desc: String = "remote-main-delayed" + } + + case class ClaimLocalDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { + override def desc: String = "local-main-delayed" + } + + case class MainPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { + override def desc: String = "main-penalty" + } + + case class HtlcPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { + override def desc: String = "htlc-penalty" + } + + case class ClaimHtlcDelayedOutputPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { + override def desc: String = "htlc-delayed-penalty" + } + + case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutput: Option[OutputInfo]) extends TransactionWithInputInfo { + override def desc: String = "closing" + } sealed trait TxGenerationSkipped case object OutputNotFound extends TxGenerationSkipped { override def toString = "output not found (probably trimmed)" } @@ -268,8 +383,6 @@ object Transactions { // We round it down to 700 to allow for some error margin (e.g. signatures smaller than 72 bytes). val claimAnchorOutputMinWeight = 700 val htlcDelayedWeight = 483 - val claimHtlcSuccessWeight = 571 - val claimHtlcTimeoutWeight = 545 val mainPenaltyWeight = 484 val htlcPenaltyWeight = 578 // based on spending an HTLC-Success output (would be 571 with HTLC-Timeout) @@ -365,7 +478,7 @@ object Transactions { // This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the channel initiator's main output. val anchorsCost = commitmentFormat match { case DefaultCommitmentFormat => Satoshi(0) - case _: AnchorOutputsCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 } txFee + anchorsCost } @@ -419,7 +532,7 @@ object Transactions { def getHtlcTxInputSequence(commitmentFormat: CommitmentFormat): Long = commitmentFormat match { case DefaultCommitmentFormat => 0 // htlc txs immediately spend the commit tx - case _: AnchorOutputsCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 1 // htlc txs have a 1-block delay to allow CPFP carve-out on anchors } /** @@ -429,12 +542,30 @@ object Transactions { * @param redeemScript redeem script that matches this output (most of them are p2wsh) * @param commitmentOutput commitment spec item this output is built from */ - case class CommitmentOutputLink[T <: CommitmentOutput](output: TxOut, redeemScript: Seq[ScriptElt], commitmentOutput: T) + sealed trait CommitmentOutputLink[T <: CommitmentOutput] { + val output: TxOut + val commitmentOutput: T + + def filter[R <: CommitmentOutput : ClassTag]: Option[CommitmentOutputLink[R]] = commitmentOutput match { + case r: R => Some(this.asInstanceOf[CommitmentOutputLink[R]]) + case _ => None + } + } /** Type alias for a collection of commitment output links */ type CommitmentOutputs = Seq[CommitmentOutputLink[CommitmentOutput]] object CommitmentOutputLink { + case class SegwitLink[T <: CommitmentOutput : ClassTag](output: TxOut, redeemScript: Seq[ScriptElt], commitmentOutput: T) extends CommitmentOutputLink[T] + + case class TaprootLink[T <: CommitmentOutput : ClassTag](output: TxOut, internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree], commitmentOutput: T) extends CommitmentOutputLink[T] + + def apply[T <: CommitmentOutput : ClassTag](output: TxOut, redeemScript: Seq[ScriptElt], commitmentOutput: T): SegwitLink[T] = SegwitLink(output, redeemScript, commitmentOutput) + + def apply[T <: CommitmentOutput : ClassTag](output: TxOut, internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree], commitmentOutput: T): TaprootLink[T] = TaprootLink(output, internalKey, scriptTree_opt, commitmentOutput) + + def apply[T <: CommitmentOutput : ClassTag](output: TxOut, internalKey: XonlyPublicKey, scriptTree: ScriptTree, commitmentOutput: T): TaprootLink[T] = TaprootLink(output, internalKey, Some(scriptTree), commitmentOutput) + /** * We sort HTLC outputs according to BIP69 + CLTV as tie-breaker for offered HTLC, we do this only for the outgoing * HTLC because we must agree with the remote on the order of HTLC-Timeout transactions even for identical HTLC outputs. @@ -459,16 +590,33 @@ object Transactions { remoteFundingPubkey: PublicKey, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): CommitmentOutputs = { + val outputs = collection.mutable.ArrayBuffer.empty[CommitmentOutputLink[CommitmentOutput]] trimOfferedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => - val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), commitmentFormat) - outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) + commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val offeredHtlcTree = Scripts.Taproot.offeredHtlcScriptTree(localHtlcPubkey, remoteHtlcPubkey, htlc.add.paymentHash) + outputs.append(CommitmentOutputLink( + TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2tr(localRevocationPubkey.xOnly, Some(offeredHtlcTree))), localRevocationPubkey.xOnly, offeredHtlcTree, OutHtlc(htlc) + )) + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), commitmentFormat) + outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) + } } trimReceivedHtlcs(localDustLimit, spec, commitmentFormat).foreach { htlc => - val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry, commitmentFormat) - outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) + commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val receivedHtlcTree = Scripts.Taproot.receivedHtlcScriptTree(localHtlcPubkey, remoteHtlcPubkey, htlc.add.paymentHash, htlc.add.cltvExpiry) + outputs.append(CommitmentOutputLink( + TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2tr(localRevocationPubkey.xOnly, Some(receivedHtlcTree))), localRevocationPubkey.xOnly, receivedHtlcTree, InHtlc(htlc) + )) + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry, commitmentFormat) + outputs.append(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) + } } val hasHtlcs = outputs.nonEmpty @@ -480,14 +628,29 @@ object Transactions { } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway if (toLocalAmount >= localDustLimit) { - outputs.append(CommitmentOutputLink( - TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))), - toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey), - ToLocal)) + commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val toLocalScriptTree = Scripts.Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + outputs.append(CommitmentOutputLink( + TxOut(toLocalAmount, pay2tr(XonlyPublicKey(NUMS_POINT), Some(toLocalScriptTree))), + NUMS_POINT.xOnly, toLocalScriptTree, + ToLocal)) + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + outputs.append(CommitmentOutputLink( + TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))), + toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey), + ToLocal)) + } } if (toRemoteAmount >= localDustLimit) { commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val toRemoteScriptTree = Scripts.Taproot.toRemoteScriptTree(remotePaymentPubkey) + outputs.append(CommitmentOutputLink( + TxOut(toRemoteAmount, pay2tr(XonlyPublicKey(NUMS_POINT), Some(toRemoteScriptTree))), + NUMS_POINT.xOnly, toRemoteScriptTree, + ToRemote)) case DefaultCommitmentFormat => outputs.append(CommitmentOutputLink( TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey)), pay2pkh(remotePaymentPubkey), @@ -500,6 +663,23 @@ object Transactions { } commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + if (toLocalAmount >= localDustLimit || hasHtlcs) { + outputs.append( + CommitmentOutputLink.TaprootLink( + TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2tr(localDelayedPaymentPubkey.xOnly, Some(Taproot.anchorScriptTree))), + localDelayedPaymentPubkey.xOnly, + Some(Taproot.anchorScriptTree), ToLocalAnchor) + ) + } + if (toRemoteAmount >= localDustLimit || hasHtlcs) { + outputs.append( + CommitmentOutputLink.TaprootLink( + TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2tr(remotePaymentPubkey.xOnly, Some(Taproot.anchorScriptTree))), + remotePaymentPubkey.xOnly, + Some(Taproot.anchorScriptTree), ToRemoteAnchor) + ) + } case _: AnchorOutputsCommitmentFormat => if (toLocalAmount >= localDustLimit || hasHtlcs) { outputs.append(CommitmentOutputLink(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, pay2wsh(anchor(localFundingPubkey))), anchor(localFundingPubkey), ToLocalAnchor)) @@ -540,21 +720,36 @@ object Transactions { localDelayedPaymentPubkey: PublicKey, feeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, HtlcTimeoutTx] = { + import KotlinUtils._ + val fee = weight2fee(feeratePerKw, commitmentFormat.htlcTimeoutWeight) - val redeemScript = output.redeemScript val htlc = output.commitmentOutput.outgoingHtlc.add val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { Left(AmountBelowDustLimit) } else { - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, - txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil, - lockTime = htlc.cltvExpiry.toLong - ) - Right(HtlcTimeoutTx(input, tx, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) + output match { + case t: CommitmentOutputLink.TaprootLink[OutHtlc] => + val Some(scriptTree: ScriptTree.Branch) = t.scriptTree_opt + val input = InputInfo.TaprootInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), t.internalKey, RedeemPath.ScriptPath(scriptTree, scriptTree.getLeft.hash())) + val tree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPubkey, toLocalDelay) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = TxOut(amount, Script.pay2tr(localRevocationPubkey.xOnly(), Some(tree))) :: Nil, + lockTime = htlc.cltvExpiry.toLong + ) + Right(HtlcTimeoutTx(input, tx, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) + case s: CommitmentOutputLink.SegwitLink[OutHtlc] => + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), s.redeemScript) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil, + lockTime = htlc.cltvExpiry.toLong + ) + Right(HtlcTimeoutTx(input, tx, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) + } } } @@ -567,21 +762,36 @@ object Transactions { localDelayedPaymentPubkey: PublicKey, feeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, HtlcSuccessTx] = { + import KotlinUtils._ + val fee = weight2fee(feeratePerKw, commitmentFormat.htlcSuccessWeight) - val redeemScript = output.redeemScript val htlc = output.commitmentOutput.incomingHtlc.add val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { Left(AmountBelowDustLimit) } else { - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, - txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil, - lockTime = 0 - ) - Right(HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) + output match { + case t: CommitmentOutputLink.TaprootLink[InHtlc] => + val Some(scriptTree: ScriptTree.Branch) = t.scriptTree_opt + val input = InputInfo.TaprootInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), t.internalKey, RedeemPath.ScriptPath(scriptTree, scriptTree.getRight.hash())) + val tree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPubkey, toLocalDelay) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = TxOut(amount, Script.pay2tr(localRevocationPubkey.xOnly(), Some(tree))) :: Nil, + lockTime = 0 + ) + Right(HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) + case s: CommitmentOutputLink.SegwitLink[InHtlc] => + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), s.redeemScript) + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil, + lockTime = 0 + ) + Right(HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) + } } } @@ -593,15 +803,12 @@ object Transactions { feeratePerKw: FeeratePerKw, outputs: CommitmentOutputs, commitmentFormat: CommitmentFormat): Seq[HtlcTx] = { - val htlcTimeoutTxs = outputs.zipWithIndex.collect { - case (CommitmentOutputLink(o, s, OutHtlc(ou)), outputIndex) => - val co = CommitmentOutputLink(o, s, OutHtlc(ou)) - makeHtlcTimeoutTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw, commitmentFormat) + val htlcTimeoutTxs = outputs.map(_.filter[OutHtlc]).zipWithIndex.collect { + case (Some(co), outputIndex) => makeHtlcTimeoutTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw, commitmentFormat) }.collect { case Right(htlcTimeoutTx) => htlcTimeoutTx } - val htlcSuccessTxs = outputs.zipWithIndex.collect { - case (CommitmentOutputLink(o, s, InHtlc(in)), outputIndex) => - val co = CommitmentOutputLink(o, s, InHtlc(in)) - makeHtlcSuccessTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw, commitmentFormat) + + val htlcSuccessTxs = outputs.map(_.filter[InHtlc]).zipWithIndex.collect { + case (Some(co), outputIndex) => makeHtlcSuccessTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feeratePerKw, commitmentFormat) }.collect { case Right(htlcSuccessTx) => htlcSuccessTx } htlcTimeoutTxs ++ htlcSuccessTxs } @@ -616,28 +823,38 @@ object Transactions { htlc: UpdateAddHtlc, feeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimHtlcSuccessTx] = { - val redeemScript = htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), commitmentFormat) - outputs.zipWithIndex.collectFirst { - case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(outgoingHtlc))), outIndex) if outgoingHtlc.id == htlc.id => outIndex - } match { - case Some(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned tx - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - val weight = addSigs(ClaimHtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong))), PlaceHolderSig, ByteVector32.Zeroes).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimHtlcSuccessTx(input, tx1, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) - } - case None => Left(OutputNotFound) + import KotlinUtils._ + + val outputIndex = outputs.map(_.commitmentOutput).indexWhere(p => p match { + case o: OutHtlc => o.outgoingHtlc.add.id == htlc.id + case _ => false + }) + if (outputIndex >= 0) { + val input = outputs(outputIndex) match { + case t: CommitmentOutputLink.TaprootLink[_] => + val Some(scriptTree: ScriptTree.Branch) = t.scriptTree_opt + InputInfo.TaprootInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), t.internalKey, RedeemPath.ScriptPath(scriptTree, scriptTree.getRight.hash())) + case _: CommitmentOutputLink.SegwitLink[_] => + val redeemScript = htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), commitmentFormat) + InputInfo.SegwitInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), redeemScript) + } + // unsigned tx + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) + val weight = addSigs(ClaimHtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong))), PlaceHolderSig, ByteVector32.Zeroes).tx.weight() + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + Left(AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) + Right(ClaimHtlcSuccessTx(input, tx1, htlc.paymentHash, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) + } + } else { + Left(OutputNotFound) } } @@ -651,28 +868,38 @@ object Transactions { htlc: UpdateAddHtlc, feeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimHtlcTimeoutTx] = { - val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry, commitmentFormat) - outputs.zipWithIndex.collectFirst { - case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(incomingHtlc))), outIndex) if incomingHtlc.id == htlc.id => outIndex - } match { - case Some(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned tx - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = htlc.cltvExpiry.toLong) - val weight = addSigs(ClaimHtlcTimeoutTx(input, tx, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong))), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimHtlcTimeoutTx(input, tx1, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) - } - case None => Left(OutputNotFound) + import KotlinUtils._ + + val outputIndex = outputs.map(_.commitmentOutput).indexWhere(p => p match { + case i: InHtlc => i.incomingHtlc.add.id == htlc.id + case _ => false + }) + + if (outputIndex >= 0) { + val input = outputs(outputIndex) match { + case t: CommitmentOutputLink.TaprootLink[_] => + val Some(scriptTree: ScriptTree.Branch) = t.scriptTree_opt + InputInfo.TaprootInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), t.internalKey, RedeemPath.ScriptPath(scriptTree, scriptTree.getLeft.hash())) + case _: CommitmentOutputLink.SegwitLink[_] => + val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry, commitmentFormat) + InputInfo.SegwitInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), redeemScript) + } + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, getHtlcTxInputSequence(commitmentFormat)) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = htlc.cltvExpiry.toLong) + val weight = addSigs(ClaimHtlcTimeoutTx(input, tx, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong))), PlaceHolderSig).tx.weight() + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + Left(AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) + Right(ClaimHtlcTimeoutTx(input, tx1, htlc.id, ConfirmationTarget.Absolute(BlockHeight(htlc.cltvExpiry.toLong)))) + } + } else { + Left(OutputNotFound) } } @@ -682,7 +909,7 @@ object Transactions { findPubKeyScriptIndex(commitTx, pubkeyScript) match { case Left(skip) => Left(skip) case Right(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) // unsigned tx val tx = Transaction( version = 2, @@ -703,35 +930,84 @@ object Transactions { } def makeClaimRemoteDelayedOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimRemoteDelayedOutputTx] = { - val redeemScript = toRemoteDelayed(localPaymentPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndex(commitTx, pubkeyScript) match { - case Left(skip) => Left(skip) - case Right(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 1) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(ClaimRemoteDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimRemoteDelayedOutputTx(input, tx1)) - } + + def makeUnsignedTx(input: InputInfo): Either[TxGenerationSkipped, ClaimRemoteDelayedOutputTx] = { + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 1) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) + val weight = addSigs(ClaimRemoteDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + Left(AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) + Right(ClaimRemoteDelayedOutputTx(input, tx1)) + } + } + + def makeClaimRemoteDelayedOutputTxTaproot(): Either[TxGenerationSkipped, ClaimRemoteDelayedOutputTx] = { + import KotlinUtils._ + + val pubkeyScript = pay2tr(NUMS_POINT.xOnly, Some(Scripts.Taproot.toRemoteScriptTree(localPaymentPubkey))) + findPubKeyScriptIndex(commitTx, pubkeyScript) flatMap { outputIndex => + val scriptTree = Scripts.Taproot.toRemoteScriptTree(localPaymentPubkey) + val input = InputInfo.TaprootInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), NUMS_POINT.xOnly, RedeemPath.ScriptPath(scriptTree, scriptTree.hash())) + makeUnsignedTx(input) + } } + + def makeClaimRemoteDelayedOutputTxSegwit(): Either[TxGenerationSkipped, ClaimRemoteDelayedOutputTx] = { + val pubkeyScript = pay2wsh(toRemoteDelayed(localPaymentPubkey)) + findPubKeyScriptIndex(commitTx, pubkeyScript) flatMap { outputIndex => + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), toRemoteDelayed(localPaymentPubkey)) + makeUnsignedTx(input) + } + } + + makeClaimRemoteDelayedOutputTxTaproot() orElse makeClaimRemoteDelayedOutputTxSegwit() } def makeHtlcDelayedTx(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, HtlcDelayedTx] = { - makeLocalDelayedOutputTx(htlcTx, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localFinalScriptPubKey, feeratePerKw).map { - case (input, tx) => HtlcDelayedTx(input, tx) + + def makeHtlcDelayedTxTaproot(): Either[TxGenerationSkipped, HtlcDelayedTx] = { + import KotlinUtils._ + + val htlcTxTree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPubkey, toLocalDelay) + findPubKeyScriptIndex(htlcTx, Script.pay2tr(localRevocationPubkey.xOnly, Some(htlcTxTree))) match { + case Left(skip) => Left(skip) + case Right(outputIndex) => + val input = InputInfo.TaprootInput(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), localRevocationPubkey.xOnly, RedeemPath.ScriptPath(htlcTxTree, htlcTxTree.hash())) + // unsigned transaction + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toInt) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) + val weight = { + val witness = Script.witnessScriptPathPay2tr(localRevocationPubkey.xOnly, htlcTxTree, ScriptWitness(Seq(ByteVector64.Zeroes)), htlcTxTree) + tx.updateWitness(0, witness).weight() + } + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + Left(AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) + Right(HtlcDelayedTx(input, tx1)) + } + } + } + + def makeHtlcDelayedTxSegwit() = { + makeLocalDelayedOutputTx(htlcTx, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localFinalScriptPubKey, feeratePerKw).map { + case (input, tx) => HtlcDelayedTx(input, tx) + } } + + makeHtlcDelayedTxTaproot() orElse makeHtlcDelayedTxSegwit() } def makeClaimLocalDelayedOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimLocalDelayedOutputTx] = { @@ -741,46 +1017,80 @@ object Transactions { } private def makeLocalDelayedOutputTx(parentTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, (InputInfo, Transaction)] = { - val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndex(parentTx, pubkeyScript) match { - case Left(skip) => Left(skip) - case Right(outputIndex) => - val input = InputInfo(OutPoint(parentTx, outputIndex), parentTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toInt) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(ClaimLocalDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(input, tx1) - } + import KotlinUtils._ + + def makeUnsignedTx(input: InputInfo): Either[TxGenerationSkipped, (InputInfo, Transaction)] = { + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toInt) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) + val weight = input match { + case _: InputInfo.TaprootInput => + val toLocalScriptTree = Scripts.Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(ByteVector64.Zeroes)), toLocalScriptTree) + tx.updateWitness(0, witness).weight() + case _: InputInfo.SegwitInput => addSigs(ClaimLocalDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() + } + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + Left(AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) + Right(input, tx1) + } + } + + def makeLocalDelayedOutputTxTaproot(): Either[TxGenerationSkipped, (InputInfo, Transaction)] = { + val scriptTree = Scripts.Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + val pubkeyScript = pay2tr(NUMS_POINT.xOnly, Some(scriptTree)) + findPubKeyScriptIndex(parentTx, pubkeyScript) flatMap { outputIndex => + val input = InputInfo.TaprootInput(OutPoint(parentTx, outputIndex), parentTx.txOut(outputIndex), NUMS_POINT.xOnly, RedeemPath.ScriptPath(scriptTree, scriptTree.getLeft.hash())) + makeUnsignedTx(input) + } } + + def makeLocalDelayedOutputTxSegwit(): Either[TxGenerationSkipped, (InputInfo, Transaction)] = { + val pubkeyScript = pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)) + findPubKeyScriptIndex(parentTx, pubkeyScript) flatMap { outputIndex => + val input = InputInfo.SegwitInput(OutPoint(parentTx, outputIndex), parentTx.txOut(outputIndex), toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)) + makeUnsignedTx(input) + } + } + + makeLocalDelayedOutputTxTaproot() orElse makeLocalDelayedOutputTxSegwit() } private def makeClaimAnchorOutputTx(commitTx: Transaction, fundingPubkey: PublicKey): Either[TxGenerationSkipped, (InputInfo, Transaction)] = { - val redeemScript = anchor(fundingPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndex(commitTx, pubkeyScript) match { - case Left(skip) => Left(skip) - case Right(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0) :: Nil, - txOut = Nil, // anchor is only used to bump fees, the output will be added later depending on available inputs - lockTime = 0) + + def makeUnsignedTx(input: InputInfo) = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0) :: Nil, + txOut = Nil, // anchor is only used to bump fees, the output will be added later depending on available inputs + lockTime = 0) + + def makeClaimAnchorOutputTxTaproot(): Either[TxGenerationSkipped, (InputInfo.TaprootInput, Transaction)] = { + import KotlinUtils._ + + val pubkeyScript = pay2tr(fundingPubkey.xOnly, Some(Scripts.Taproot.anchorScriptTree)) + findPubKeyScriptIndex(commitTx, pubkeyScript) flatMap { outputIndex => + val input = InputInfo.TaprootInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), fundingPubkey.xOnly, RedeemPath.KeyPath(Some(Scripts.Taproot.anchorScriptTree))) + val tx = makeUnsignedTx(input) + Right((input, tx)) + } + } + + def makeClaimAnchorOutputTxSegwit(): Either[TxGenerationSkipped, (InputInfo.SegwitInput, Transaction)] = { + val pubkeyScript = pay2wsh(anchor(fundingPubkey)) + findPubKeyScriptIndex(commitTx, pubkeyScript) flatMap { outputIndex => + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), anchor(fundingPubkey)) + val tx = makeUnsignedTx(input) Right((input, tx)) + } } + + makeClaimAnchorOutputTxTaproot() orElse makeClaimAnchorOutputTxSegwit() } def makeClaimLocalAnchorOutputTx(commitTx: Transaction, localFundingPubkey: PublicKey, confirmationTarget: ConfirmationTarget): Either[TxGenerationSkipped, ClaimLocalAnchorOutputTx] = { @@ -791,64 +1101,99 @@ object Transactions { makeClaimAnchorOutputTx(commitTx, remoteFundingPubkey).map { case (input, tx) => ClaimRemoteAnchorOutputTx(input, tx) } } - def makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Seq[Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx]] = { - val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndexes(htlcTx, pubkeyScript) match { - case Left(skip) => Seq(Left(skip)) - case Right(outputIndexes) => outputIndexes.map(outputIndex => { - val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(ClaimHtlcDelayedOutputPenaltyTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimHtlcDelayedOutputPenaltyTx(input, tx1)) + def makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat = DefaultCommitmentFormat): Seq[Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx]] = { + + def makeUnsignedTx(input: InputInfo): Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx] = { + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) + // compute weight with a dummy 73 bytes signature (the largest you can get) + val weight = addSigs(ClaimHtlcDelayedOutputPenaltyTx(input, tx), PlaceHolderSig).tx.weight() + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + Left(AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) + Right(ClaimHtlcDelayedOutputPenaltyTx(input, tx1)) + } + } + + def makeClaimHtlcDelayedOutputPenaltyTxsTaproot(): Either[TxGenerationSkipped, Seq[Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx]]] = { + val tree = Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + val pubkeyScript = pay2tr(localRevocationPubkey.xOnly, Some(tree.getLeft)) + findPubKeyScriptIndexes(htlcTx, pubkeyScript) map { outputIndexes => + outputIndexes.map { outputIndex => + val input = InputInfo.TaprootInput(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), localRevocationPubkey.xOnly, RedeemPath.KeyPath(Some(tree.getLeft))) + makeUnsignedTx(input) } - }) + } + } + + def makeClaimHtlcDelayedOutputPenaltyTxsSegwit(): Either[TxGenerationSkipped, Seq[Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx]]] = { + val pubkeyScript = pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)) + findPubKeyScriptIndexes(htlcTx, pubkeyScript) map { outputIndexes => + outputIndexes.map { outputIndex => + val input = InputInfo.SegwitInput(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)) + makeUnsignedTx(input) + } + } + } + + makeClaimHtlcDelayedOutputPenaltyTxsTaproot() orElse makeClaimHtlcDelayedOutputPenaltyTxsSegwit() match { + case Left(skip) => Seq(Left(skip)) + case Right(result) => result } } def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, toRemoteDelay: CltvExpiryDelta, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, MainPenaltyTx] = { - val redeemScript = toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) - val pubkeyScript = write(pay2wsh(redeemScript)) - findPubKeyScriptIndex(commitTx, pubkeyScript) match { - case Left(skip) => Left(skip) - case Right(outputIndex) => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, - txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = 0) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(MainPenaltyTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feeratePerKw, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - Left(AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(MainPenaltyTx(input, tx1)) - } + import KotlinUtils._ + + def nmakeUnsignedTx(input: InputInfo): Either[TxGenerationSkipped, MainPenaltyTx] = { + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) + // compute weight with a dummy 73 bytes signature (the largest you can get) + val weight = addSigs(MainPenaltyTx(input, tx), PlaceHolderSig).tx.weight() + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + Left(AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) + Right(MainPenaltyTx(input, tx1)) + } + } + + def makeMainPenaltyTxTaproot() = { + val toLocalScriptTree = Scripts.Taproot.toLocalScriptTree(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) + val pubkeyScript = pay2tr(NUMS_POINT.xOnly, Some(toLocalScriptTree)) + findPubKeyScriptIndex(commitTx, pubkeyScript) flatMap { outputIndex => + val input = InputInfo.TaprootInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), NUMS_POINT.xOnly, RedeemPath.ScriptPath(toLocalScriptTree, toLocalScriptTree.getRight.hash())) + nmakeUnsignedTx(input) + } } + + def makeMainPenaltyTxSegwit() = { + val pubkeyScript = pay2wsh(toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey)) + findPubKeyScriptIndex(commitTx, pubkeyScript) flatMap { outputIndex => + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey)) + nmakeUnsignedTx(input) + } + } + + makeMainPenaltyTxTaproot() orElse makeMainPenaltyTxSegwit() } /** * We already have the redeemScript, no need to build it */ def makeHtlcPenaltyTx(commitTx: Transaction, htlcOutputIndex: Int, redeemScript: ByteVector, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, HtlcPenaltyTx] = { - val input = InputInfo(OutPoint(commitTx, htlcOutputIndex), commitTx.txOut(htlcOutputIndex), redeemScript) + val input = InputInfo.SegwitInput(OutPoint(commitTx, htlcOutputIndex), commitTx.txOut(htlcOutputIndex), redeemScript) // unsigned transaction val tx = Transaction( version = 2, @@ -867,7 +1212,28 @@ object Transactions { } } - def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector, localPaysClosingFees: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = { + def makeHtlcPenaltyTx(commitTx: Transaction, htlcOutputIndex: Int, internalKey: XonlyPublicKey, scriptTree: ScriptTree, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, HtlcPenaltyTx] = { + + val input = InputInfo.TaprootInput(OutPoint(commitTx, htlcOutputIndex), commitTx.txOut(htlcOutputIndex), internalKey, RedeemPath.KeyPath(Some(scriptTree))) + // unsigned transaction + val tx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0xffffffffL) :: Nil, + txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, + lockTime = 0) + // compute weight with a dummy 73 bytes signature (the largest you can get) + val weight = addSigs(HtlcPenaltyTx(input, tx), PlaceHolderSig, PlaceHolderPubKey).tx.weight() + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + Left(AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) + Right(HtlcPenaltyTx(input, tx1)) + } + } + + def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector, localPaysClosingFees: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec, sequence: Long = 0xffffffffL): ClosingTx = { require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs") val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localPaysClosingFees) { @@ -881,7 +1247,7 @@ object Transactions { val tx = LexicographicalOrdering.sort(Transaction( version = 2, - txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = 0xffffffffL) :: Nil, + txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence) :: Nil, txOut = toLocalOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ Nil, lockTime = 0)) val toLocalOutput = findPubKeyScriptIndex(tx, localScriptPubKey).map(index => OutputInfo(index, toLocalAmount, localScriptPubKey)).toOption @@ -968,6 +1334,8 @@ object Transactions { } } + def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: Seq[ScriptElt]): Either[TxGenerationSkipped, Int] = findPubKeyScriptIndex(tx, Script.write(pubkeyScript)) + def findPubKeyScriptIndexes(tx: Transaction, pubkeyScript: ByteVector): Either[TxGenerationSkipped, Seq[Int]] = { val outputIndexes = tx.txOut.zipWithIndex.collect { case (txOut, index) if txOut.publicKeyScript == pubkeyScript => index @@ -979,6 +1347,8 @@ object Transactions { } } + def findPubKeyScriptIndexes(tx: Transaction, pubkeyScript: Seq[ScriptElt]): Either[TxGenerationSkipped, Seq[Int]] = findPubKeyScriptIndexes(tx, Script.write(pubkeyScript)) + /** * Default public key used for fee estimation */ @@ -991,51 +1361,89 @@ object Transactions { val PlaceHolderSig = ByteVector64(ByteVector.fill(64)(0xaa)) assert(der(PlaceHolderSig).size == 72) + def partialSign(key: PrivateKey, tx: Transaction, inputIndex: Int, spentOutputs: Seq[TxOut], + localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, + localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] = { + val publicKeys = Scripts.sort(Seq(localFundingPublicKey, remoteFundingPublicKey)) + Musig2.signTaprootInput(key, tx, inputIndex, spentOutputs, publicKeys, localNonce._1, Seq(localNonce._2, remoteNextLocalNonce), None) + } + def addSigs(commitTx: CommitTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ByteVector64, remoteSig: ByteVector64): CommitTx = { val witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, remoteFundingPubkey) commitTx.copy(tx = commitTx.tx.updateWitness(0, witness)) } - def addSigs(mainPenaltyTx: MainPenaltyTx, revocationSig: ByteVector64): MainPenaltyTx = mainPenaltyTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, redeemScript) - mainPenaltyTx.copy(tx = mainPenaltyTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => mainPenaltyTx + def addSigs(mainPenaltyTx: MainPenaltyTx, revocationSig: ByteVector64): MainPenaltyTx = { + val witness = mainPenaltyTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, redeemScript) + case t: InputInfo.TaprootInput => + t.redeemPath match { + case RedeemPath.ScriptPath(scriptTree: ScriptTree.Branch, _) => Script.witnessScriptPathPay2tr(t.internalKey, scriptTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(revocationSig)), scriptTree) + case _ => throw new IllegalArgumentException("unexpected script tree leaf when building main penalty tx") + } + } + mainPenaltyTx.copy(tx = mainPenaltyTx.tx.updateWitness(0, witness)) } - def addSigs(htlcPenaltyTx: HtlcPenaltyTx, revocationSig: ByteVector64, revocationPubkey: PublicKey): HtlcPenaltyTx = htlcPenaltyTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, redeemScript) - htlcPenaltyTx.copy(tx = htlcPenaltyTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => htlcPenaltyTx + def addSigs(htlcPenaltyTx: HtlcPenaltyTx, revocationSig: ByteVector64, revocationPubkey: PublicKey): HtlcPenaltyTx = { + val witness = htlcPenaltyTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, redeemScript) + case _: InputInfo.TaprootInput => Script.witnessKeyPathPay2tr(revocationSig) + } + htlcPenaltyTx.copy(tx = htlcPenaltyTx.tx.updateWitness(0, witness)) } - def addSigs(htlcSuccessTx: HtlcSuccessTx, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, commitmentFormat: CommitmentFormat): HtlcSuccessTx = htlcSuccessTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, redeemScript, commitmentFormat) - htlcSuccessTx.copy(tx = htlcSuccessTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => htlcSuccessTx + def addSigs(htlcSuccessTx: HtlcSuccessTx, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, commitmentFormat: CommitmentFormat): HtlcSuccessTx = { + val witness = htlcSuccessTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, redeemScript, commitmentFormat) + case t: InputInfo.TaprootInput => + t.redeemPath match { + case RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => + Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(Taproot.encodeSig(remoteSig, SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY), Taproot.encodeSig(localSig, SIGHASH_DEFAULT), paymentPreimage)), htlcTree) + case _ => throw new IllegalArgumentException("unexpected script tree leaf when building htlc successTx tx") + } + } + htlcSuccessTx.copy(tx = htlcSuccessTx.tx.updateWitness(0, witness)) } - def addSigs(htlcTimeoutTx: HtlcTimeoutTx, localSig: ByteVector64, remoteSig: ByteVector64, commitmentFormat: CommitmentFormat): HtlcTimeoutTx = htlcTimeoutTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessHtlcTimeout(localSig, remoteSig, redeemScript, commitmentFormat) - htlcTimeoutTx.copy(tx = htlcTimeoutTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => htlcTimeoutTx + def addSigs(htlcTimeoutTx: HtlcTimeoutTx, localSig: ByteVector64, remoteSig: ByteVector64, commitmentFormat: CommitmentFormat): HtlcTimeoutTx = { + val witness = htlcTimeoutTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => witnessHtlcTimeout(localSig, remoteSig, redeemScript, commitmentFormat) + case t: InputInfo.TaprootInput => + t.redeemPath match { + case RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => + Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(Taproot.encodeSig(remoteSig, SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY), Taproot.encodeSig(localSig, SIGHASH_DEFAULT))), htlcTree) + case _ => throw new IllegalArgumentException("unexpected script tree leaf when building htlc timeout tx") + } + } + htlcTimeoutTx.copy(tx = htlcTimeoutTx.tx.updateWitness(0, witness)) } - def addSigs(claimHtlcSuccessTx: ClaimHtlcSuccessTx, localSig: ByteVector64, paymentPreimage: ByteVector32): ClaimHtlcSuccessTx = claimHtlcSuccessTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, redeemScript) - claimHtlcSuccessTx.copy(tx = claimHtlcSuccessTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimHtlcSuccessTx + def addSigs(claimHtlcSuccessTx: ClaimHtlcSuccessTx, localSig: ByteVector64, paymentPreimage: ByteVector32): ClaimHtlcSuccessTx = { + val witness = claimHtlcSuccessTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, redeemScript) + case t: InputInfo.TaprootInput => + t.redeemPath match { + case RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => + Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(localSig, paymentPreimage)), htlcTree) + case _ => throw new IllegalArgumentException("unexpected script tree leaf when building claim htlc success tx") + } + } + claimHtlcSuccessTx.copy(tx = claimHtlcSuccessTx.tx.updateWitness(0, witness)) } - def addSigs(claimHtlcTimeoutTx: ClaimHtlcTimeoutTx, localSig: ByteVector64): ClaimHtlcTimeoutTx = claimHtlcTimeoutTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessClaimHtlcTimeoutFromCommitTx(localSig, redeemScript) - claimHtlcTimeoutTx.copy(tx = claimHtlcTimeoutTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimHtlcTimeoutTx + def addSigs(claimHtlcTimeoutTx: ClaimHtlcTimeoutTx, localSig: ByteVector64): ClaimHtlcTimeoutTx = { + val witness = claimHtlcTimeoutTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => + witnessClaimHtlcTimeoutFromCommitTx(localSig, redeemScript) + case t: InputInfo.TaprootInput => + t.redeemPath match { + case RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => + Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(localSig)), htlcTree) + case _ => throw new IllegalArgumentException("unexpected script tree leaf when building claim htlc timeout tx") + } + } + claimHtlcTimeoutTx.copy(tx = claimHtlcTimeoutTx.tx.updateWitness(0, witness)) } def addSigs(claimP2WPKHOutputTx: ClaimP2WPKHOutputTx, localPaymentPubkey: PublicKey, localSig: ByteVector64): ClaimP2WPKHOutputTx = { @@ -1043,39 +1451,59 @@ object Transactions { claimP2WPKHOutputTx.copy(tx = claimP2WPKHOutputTx.tx.updateWitness(0, witness)) } - def addSigs(claimRemoteDelayedOutputTx: ClaimRemoteDelayedOutputTx, localSig: ByteVector64): ClaimRemoteDelayedOutputTx = claimRemoteDelayedOutputTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessClaimToRemoteDelayedFromCommitTx(localSig, redeemScript) - claimRemoteDelayedOutputTx.copy(tx = claimRemoteDelayedOutputTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimRemoteDelayedOutputTx + def addSigs(claimRemoteDelayedOutputTx: ClaimRemoteDelayedOutputTx, localSig: ByteVector64): ClaimRemoteDelayedOutputTx = { + val witness = claimRemoteDelayedOutputTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => witnessClaimToRemoteDelayedFromCommitTx(localSig, redeemScript) + case t: InputInfo.TaprootInput => + t.redeemPath match { + case RedeemPath.ScriptPath(toRemoteScriptTree: ScriptTree.Leaf, _) => + Script.witnessScriptPathPay2tr(t.internalKey, toRemoteScriptTree, ScriptWitness(Seq(localSig)), toRemoteScriptTree) + case _ => throw new IllegalArgumentException("unexpected script tree leaf when building claim remote delayed output tx") + } + } + claimRemoteDelayedOutputTx.copy(tx = claimRemoteDelayedOutputTx.tx.updateWitness(0, witness)) } - def addSigs(claimDelayedOutputTx: ClaimLocalDelayedOutputTx, localSig: ByteVector64): ClaimLocalDelayedOutputTx = claimDelayedOutputTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessToLocalDelayedAfterDelay(localSig, redeemScript) - claimDelayedOutputTx.copy(tx = claimDelayedOutputTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimDelayedOutputTx + def addSigs(claimDelayedOutputTx: ClaimLocalDelayedOutputTx, localSig: ByteVector64): ClaimLocalDelayedOutputTx = { + val witness = claimDelayedOutputTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => witnessToLocalDelayedAfterDelay(localSig, redeemScript) + case t: InputInfo.TaprootInput => + t.redeemPath match { + case RedeemPath.ScriptPath(scriptTree: ScriptTree.Branch, _) => + Script.witnessScriptPathPay2tr(t.internalKey, scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(localSig)), scriptTree) + case _ => throw new IllegalArgumentException("unexpected script tree leaf when building claim delayed output tx") + } + } + claimDelayedOutputTx.copy(tx = claimDelayedOutputTx.tx.updateWitness(0, witness)) } - def addSigs(htlcDelayedTx: HtlcDelayedTx, localSig: ByteVector64): HtlcDelayedTx = htlcDelayedTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessToLocalDelayedAfterDelay(localSig, redeemScript) - htlcDelayedTx.copy(tx = htlcDelayedTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => htlcDelayedTx + def addSigs(htlcDelayedTx: HtlcDelayedTx, localSig: ByteVector64): HtlcDelayedTx = { + val witness = htlcDelayedTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => witnessToLocalDelayedAfterDelay(localSig, redeemScript) + case t: InputInfo.TaprootInput => + t.redeemPath match { + case RedeemPath.ScriptPath(scriptTree: ScriptTree.Leaf, _) => + Script.witnessScriptPathPay2tr(t.internalKey, scriptTree, ScriptWitness(Seq(localSig)), scriptTree) + case _ => throw new IllegalArgumentException("unexpected script tree leaf when building htlc delayed tx") + } + } + htlcDelayedTx.copy(tx = htlcDelayedTx.tx.updateWitness(0, witness)) } - def addSigs(claimAnchorOutputTx: ClaimLocalAnchorOutputTx, localSig: ByteVector64): ClaimLocalAnchorOutputTx = claimAnchorOutputTx.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = witnessAnchor(localSig, redeemScript) - claimAnchorOutputTx.copy(tx = claimAnchorOutputTx.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimAnchorOutputTx + def addSigs(claimAnchorOutputTx: ClaimLocalAnchorOutputTx, localSig: ByteVector64): ClaimLocalAnchorOutputTx = { + val witness = claimAnchorOutputTx.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => witnessAnchor(localSig, redeemScript) + case t: InputInfo.TaprootInput => Script.witnessKeyPathPay2tr(localSig) + } + claimAnchorOutputTx.copy(tx = claimAnchorOutputTx.tx.updateWitness(0, witness)) } - def addSigs(claimHtlcDelayedPenalty: ClaimHtlcDelayedOutputPenaltyTx, revocationSig: ByteVector64): ClaimHtlcDelayedOutputPenaltyTx = claimHtlcDelayedPenalty.input match { - case InputInfo.SegwitInput(_, _, redeemScript) => - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, redeemScript) - claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness)) - case _: InputInfo.TaprootInput => claimHtlcDelayedPenalty + def addSigs(claimHtlcDelayedPenalty: ClaimHtlcDelayedOutputPenaltyTx, revocationSig: ByteVector64): ClaimHtlcDelayedOutputPenaltyTx = { + val witness = claimHtlcDelayedPenalty.input match { + case InputInfo.SegwitInput(_, _, redeemScript) => Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, redeemScript) + case _: InputInfo.TaprootInput => Script.witnessKeyPathPay2tr(revocationSig) + } + claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness)) } def addSigs(closingTx: ClosingTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ByteVector64, remoteSig: ByteVector64): ClosingTx = { @@ -1083,6 +1511,14 @@ object Transactions { closingTx.copy(tx = closingTx.tx.updateWitness(0, witness)) } + def addAggregatedSignature(commitTx: CommitTx, aggregatedSignature: ByteVector64): CommitTx = { + commitTx.copy(tx = commitTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregatedSignature))) + } + + def addAggregatedSignature(closingTx: ClosingTx, aggregatedSignature: ByteVector64): ClosingTx = { + closingTx.copy(tx = closingTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregatedSignature))) + } + def checkSpendable(txinfo: TransactionWithInputInfo): Try[Unit] = { // NB: we don't verify the other inputs as they should only be wallet inputs used to RBF the transaction Try(Transaction.correctlySpends(txinfo.tx, Map(txinfo.input.outPoint -> txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index f11978187c..9f19b21faf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -47,7 +47,7 @@ private[channel] object ChannelTypes0 { // modified: we don't use the InputInfo in closing business logic, so we don't need to fill everything (this part // assumes that we only have standard channels, no anchor output channels - which was the case before version2). val input = childTx.txIn.head.outPoint - InputInfo(input, parentTx.txOut(input.index.toInt), Nil) + InputInfo.SegwitInput(input, parentTx.txOut(input.index.toInt), Nil) } case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, TxId]) { @@ -96,7 +96,7 @@ private[channel] object ChannelTypes0 { val htlcPenaltyTxsNew = htlcPenaltyTxs.map(tx => HtlcPenaltyTx(getPartialInputInfo(commitTx, tx), tx)) val claimHtlcDelayedPenaltyTxsNew = claimHtlcDelayedPenaltyTxs.map(tx => { // We don't have all the `InputInfo` data, but it's ok: we only use the tx that is fully signed. - ClaimHtlcDelayedOutputPenaltyTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx) + ClaimHtlcDelayedOutputPenaltyTx(InputInfo.SegwitInput(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx) }) channel.RevokedCommitPublished(commitTx, claimMainOutputTxNew, mainPenaltyTxNew, htlcPenaltyTxsNew, claimHtlcDelayedPenaltyTxsNew, irrevocablySpentNew) } @@ -113,7 +113,7 @@ private[channel] object ChannelTypes0 { * the raw transaction. It provides more information for auditing but is not used for business logic, so we can safely * put dummy values in the migration. */ - def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx, None) + def migrateClosingTx(tx: Transaction): ClosingTx = ClosingTx(InputInfo.SegwitInput(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), Nil), tx, None) case class HtlcTxAndSigs(txinfo: HtlcTx, localSig: ByteVector64, remoteSig: ByteVector64) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index 5fd196fe17..5ef74d6160 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -600,8 +600,8 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel case (current, tx) => Closing.updateRevokedCommitPublished(current, tx) }.copy( claimHtlcDelayedPenaltyTxs = List( - ClaimHtlcDelayedOutputPenaltyTx(InputInfo(OutPoint(htlcSuccess, 0), TxOut(2_500 sat, Nil), Nil), Transaction(2, Seq(TxIn(OutPoint(htlcSuccess, 0), ByteVector.empty, 0)), Seq(TxOut(5_000 sat, ByteVector.empty)), 0)), - ClaimHtlcDelayedOutputPenaltyTx(InputInfo(OutPoint(htlcTimeout, 0), TxOut(3_000 sat, Nil), Nil), Transaction(2, Seq(TxIn(OutPoint(htlcTimeout, 0), ByteVector.empty, 0)), Seq(TxOut(6_000 sat, ByteVector.empty)), 0)) + ClaimHtlcDelayedOutputPenaltyTx(InputInfo.SegwitInput(OutPoint(htlcSuccess, 0), TxOut(2_500 sat, Nil), Nil), Transaction(2, Seq(TxIn(OutPoint(htlcSuccess, 0), ByteVector.empty, 0)), Seq(TxOut(5_000 sat, ByteVector.empty)), 0)), + ClaimHtlcDelayedOutputPenaltyTx(InputInfo.SegwitInput(OutPoint(htlcTimeout, 0), TxOut(3_000 sat, Nil), Nil), Transaction(2, Seq(TxIn(OutPoint(htlcTimeout, 0), ByteVector.empty, 0)), Seq(TxOut(6_000 sat, ByteVector.empty)), 0)) ) ) assert(!rvk4b.isDone) 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 4448fb9993..2f3ef87235 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 @@ -25,7 +25,7 @@ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.keymanager.LocalChannelKeyManager -import fr.acinq.eclair.transactions.Transactions.CommitTx +import fr.acinq.eclair.transactions.Transactions.{CommitTx, DefaultCommitmentFormat} import fr.acinq.eclair.transactions.{CommitmentSpec, Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -491,8 +491,8 @@ object CommitmentsSpec { val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, Some(channelReserve), 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val localFundingPubKey = randomKey().publicKey val remoteFundingPubKey = randomKey().publicKey - val fundingTx = Transaction(2, Nil, Seq(TxOut((toLocal + toRemote).truncateToSatoshi, Funding.makeFundingPubKeyScript(localFundingPubKey, remoteFundingPubKey))), 0) - val commitmentInput = Transactions.InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, Scripts.multiSig2of2(localFundingPubKey, remoteFundingPubKey)) + val fundingTx = Transaction(2, Nil, Seq(TxOut((toLocal + toRemote).truncateToSatoshi, Funding.makeFundingPubKeyScript(localFundingPubKey, remoteFundingPubKey, DefaultCommitmentFormat))), 0) + val commitmentInput = Transactions.InputInfo.SegwitInput(OutPoint(fundingTx, 0), fundingTx.txOut.head, Scripts.multiSig2of2(localFundingPubKey, remoteFundingPubKey)) val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toRemote, toLocal), randomTxId(), randomKey().publicKey) val localFundingStatus = announcement_opt match { @@ -516,8 +516,8 @@ object CommitmentsSpec { val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, Some(channelReserve), 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val localFundingPubKey = randomKey().publicKey val remoteFundingPubKey = randomKey().publicKey - val fundingTx = Transaction(2, Nil, Seq(TxOut((toLocal + toRemote).truncateToSatoshi, Funding.makeFundingPubKeyScript(localFundingPubKey, remoteFundingPubKey))), 0) - val commitmentInput = Transactions.InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, Scripts.multiSig2of2(localFundingPubKey, remoteFundingPubKey)) + val fundingTx = Transaction(2, Nil, Seq(TxOut((toLocal + toRemote).truncateToSatoshi, Funding.makeFundingPubKeyScript(localFundingPubKey, remoteFundingPubKey, DefaultCommitmentFormat))), 0) + val commitmentInput = Transactions.InputInfo.SegwitInput(OutPoint(fundingTx, 0), fundingTx.txOut.head, Scripts.multiSig2of2(localFundingPubKey, remoteFundingPubKey)) val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toRemote, toLocal), randomTxId(), randomKey().publicKey) val localFundingStatus = announcement_opt match { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index e511ba7030..6c6a3388fd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -178,7 +178,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat ) def toClosingTx(txOut: Seq[TxOut]): ClosingTx = { - ClosingTx(InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000 sat, Nil), Nil), Transaction(2, Nil, txOut, 0), None) + ClosingTx(InputInfo.SegwitInput(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000 sat, Nil), Nil), Transaction(2, Nil, txOut, 0), None) } assert(Closing.MutualClose.checkClosingDustAmounts(toClosingTx(allOutputsAboveDust))) @@ -198,7 +198,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat Transaction.read("0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800"), Transaction.read("010000000235a2f5c4fd48672534cce1ac063047edc38683f43c5a883f815d6026cb5f8321020000006a47304402206be5fd61b1702599acf51941560f0a1e1965aa086634b004967747f79788bd6e022002f7f719a45b8b5e89129c40a9d15e4a8ee1e33be3a891cf32e859823ecb7a510121024756c5adfbc0827478b0db042ce09d9b98e21ad80d036e73bd8e7f0ecbc254a2ffffffffb2387d3125bb8c84a2da83f4192385ce329283661dfc70191f4112c67ce7b4d0000000006b483045022100a2c737eab1c039f79238767ccb9bb3e81160e965ef0fc2ea79e8360c61b7c9f702202348b0f2c0ea2a757e25d375d9be183200ce0a79ec81d6a4ebb2ae4dc31bc3c9012102db16a822e2ec3706c58fc880c08a3617c61d8ef706cc8830cfe4561d9a5d52f0ffffffff01808d5b00000000001976a9141210c32def6b64d0d77ba8d99adeb7e9f91158b988ac00000000"), Transaction.read("0100000001b14ba6952c83f6f8c382befbf4e44270f13e479d5a5ff3862ac3a112f103ff2a010000006b4830450221008b097fd69bfa3715fc5e119a891933c091c55eabd3d1ddae63a1c2cc36dc9a3e02205666d5299fa403a393bcbbf4b05f9c0984480384796cdebcf69171674d00809c01210335b592484a59a44f40998d65a94f9e2eecca47e8d1799342112a59fc96252830ffffffff024bf308000000000017a914440668d018e5e0ba550d6e042abcf726694f515c8798dd1801000000001976a91453a503fe151dd32e0503bd9a2fbdbf4f9a3af1da88ac00000000") - ).map(tx => ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(10_000 sat, Nil), Nil), tx, None)) + ).map(tx => ClosingTx(InputInfo.SegwitInput(tx.txIn.head.outPoint, TxOut(10_000 sat, Nil), Nil), tx, None)) // only mutual close assert(Closing.isClosingTypeAlreadyKnown( 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 446d5a0a51..89ed601e01 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 @@ -86,8 +86,8 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } private def sharedInputs(commitmentA: Commitment, commitmentB: Commitment): (SharedFundingInput, SharedFundingInput) = { - val sharedInputA = Multisig2of2Input(commitmentA) - val sharedInputB = Multisig2of2Input(commitmentB) + val sharedInputA = SharedFundingInput(commitmentA) + val sharedInputB = SharedFundingInput(commitmentB) (sharedInputA, sharedInputB) } @@ -106,7 +106,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey))) def dummySharedInputB(amount: Satoshi): SharedFundingInput = { - val inputInfo = InputInfo(OutPoint(randomTxId(), 3), TxOut(amount, fundingPubkeyScript), Nil) + val inputInfo = InputInfo.SegwitInput(OutPoint(randomTxId(), 3), TxOut(amount, fundingPubkeyScript), Nil) val fundingTxIndex = fundingParamsA.sharedInput_opt match { case Some(input: Multisig2of2Input) => input.fundingTxIndex + 1 case _ => 0 @@ -2614,7 +2614,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val params = createFixtureParams(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 = Multisig2of2Input(InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, Nil), 0, randomKey().publicKey) + val sharedInput = Multisig2of2Input(InputInfo.SegwitInput(OutPoint(fundingTx, 0), fundingTx.txOut.head, Nil), 0, randomKey().publicKey) val bob = params.spawnTxBuilderSpliceBob(params.fundingParamsB.copy(sharedInput_opt = Some(sharedInput)), previousCommitment, wallet) bob ! Start(probe.ref) // Alice --- tx_add_input --> Bob diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala index 40d369925e..a703993c3b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala @@ -25,6 +25,7 @@ import fr.acinq.eclair.channel.publish.ReplaceableTxFunder.AdjustPreviousTxOutpu import fr.acinq.eclair.channel.publish.ReplaceableTxFunder._ import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher._ import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.transactions.Transactions.InputInfo.SegwitInput import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, TestKitBaseClass, randomBytes32} import org.mockito.IdiomaticMockito.StubbingOps @@ -39,15 +40,15 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { private def createAnchorTx(): (CommitTx, ClaimLocalAnchorOutputTx) = { val anchorScript = Scripts.anchor(PlaceHolderPubKey) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 1, 500 sat, PlaceHolderPubKey, PlaceHolderPubKey) + val commitInput = Funding.makeFundingInputInfo(randomTxId(), 1, 500 sat, PlaceHolderPubKey, PlaceHolderPubKey, DefaultCommitmentFormat) val commitTx = Transaction( 2, - Seq(TxIn(commitInput.outPoint, commitInput.redeemScript, 0, Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, PlaceHolderPubKey, PlaceHolderPubKey))), + Seq(TxIn(commitInput.outPoint, commitInput.asInstanceOf[SegwitInput].redeemScript, 0, Scripts.witness2of2(PlaceHolderSig, PlaceHolderSig, PlaceHolderPubKey, PlaceHolderPubKey))), Seq(TxOut(330 sat, Script.pay2wsh(anchorScript))), 0 ) val anchorTx = ClaimLocalAnchorOutputTx( - InputInfo(OutPoint(commitTx, 0), commitTx.txOut.head, anchorScript), + InputInfo.SegwitInput(OutPoint(commitTx, 0), commitTx.txOut.head, anchorScript), Transaction(2, Seq(TxIn(OutPoint(commitTx, 0), ByteVector.empty, 0)), Nil, 0), ConfirmationTarget.Absolute(BlockHeight(0)) ) @@ -66,14 +67,14 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { 0 ) val htlcSuccess = HtlcSuccessWithWitnessData(HtlcSuccessTx( - InputInfo(OutPoint(commitTx, 0), commitTx.txOut.head, htlcSuccessScript), + InputInfo.SegwitInput(OutPoint(commitTx, 0), commitTx.txOut.head, htlcSuccessScript), Transaction(2, Seq(TxIn(OutPoint(commitTx, 0), ByteVector.empty, 0)), Seq(TxOut(5000 sat, Script.pay2wpkh(PlaceHolderPubKey))), 0), paymentHash, 17, ConfirmationTarget.Absolute(BlockHeight(0)) ), PlaceHolderSig, preimage) val htlcTimeout = HtlcTimeoutWithWitnessData(HtlcTimeoutTx( - InputInfo(OutPoint(commitTx, 1), commitTx.txOut.last, htlcTimeoutScript), + InputInfo.SegwitInput(OutPoint(commitTx, 1), commitTx.txOut.last, htlcTimeoutScript), Transaction(2, Seq(TxIn(OutPoint(commitTx, 1), ByteVector.empty, 0)), Seq(TxOut(4000 sat, Script.pay2wpkh(PlaceHolderPubKey))), 0), 12, ConfirmationTarget.Absolute(BlockHeight(0)) @@ -93,14 +94,14 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { 0 ) val claimHtlcSuccess = ClaimHtlcSuccessWithWitnessData(ClaimHtlcSuccessTx( - InputInfo(OutPoint(commitTx, 0), commitTx.txOut.head, htlcSuccessScript), + InputInfo.SegwitInput(OutPoint(commitTx, 0), commitTx.txOut.head, htlcSuccessScript), Transaction(2, Seq(TxIn(OutPoint(commitTx, 0), ByteVector.empty, 0)), Seq(TxOut(5000 sat, Script.pay2wpkh(PlaceHolderPubKey))), 0), paymentHash, 5, ConfirmationTarget.Absolute(BlockHeight(0)) ), preimage) val claimHtlcTimeout = ClaimHtlcTimeoutWithWitnessData(ClaimHtlcTimeoutTx( - InputInfo(OutPoint(commitTx, 1), commitTx.txOut.last, htlcTimeoutScript), + InputInfo.SegwitInput(OutPoint(commitTx, 1), commitTx.txOut.last, htlcTimeoutScript), Transaction(2, Seq(TxIn(OutPoint(commitTx, 1), ByteVector.empty, 0)), Seq(TxOut(5000 sat, Script.pay2wpkh(PlaceHolderPubKey))), 0), 7, ConfirmationTarget.Absolute(BlockHeight(0)) 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 64fe89219a..0f24ad8fbb 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 @@ -1585,7 +1585,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat match { case Transactions.DefaultCommitmentFormat => assert(remoteCommitTx.tx.txOut.size == 4) - case _: AnchorOutputsCommitmentFormat => assert(remoteCommitTx.tx.txOut.size == 6) + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(remoteCommitTx.tx.txOut.size == 6) } probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx.tx)) @@ -1596,7 +1596,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat match { case Transactions.DefaultCommitmentFormat => () - case _: AnchorOutputsCommitmentFormat => alice2blockchain.expectMsgType[PublishReplaceableTx] // claim anchor + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => alice2blockchain.expectMsgType[PublishReplaceableTx] // claim anchor } if (!bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.paysDirectlyToWallet) alice2blockchain.expectMsgType[PublishFinalTx] // claim main output val claimHtlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala index 1f0647aa4d..66f4373ab7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala @@ -20,7 +20,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOps, actorRefAdapter} import akka.testkit.TestProbe -import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, ConfirmationTarget} @@ -38,7 +38,7 @@ import scala.concurrent.duration.DurationInt class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { case class FixtureParam(nodeParams: NodeParams, txPublisher: ActorRef[TxPublisher.Command], factory: TestProbe, probe: TestProbe) - + override def withFixture(test: OneArgTest): Outcome = { within(max = 30 seconds) { val nodeParams = TestConstants.Alice.nodeParams @@ -105,7 +105,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val confirmBefore = ConfirmationTarget.Absolute(nodeParams.currentBlockHeight + 12) val input = OutPoint(randomTxId(), 3) - val cmd = PublishReplaceableTx(ClaimLocalAnchorOutputTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), confirmBefore), null, null) + val cmd = PublishReplaceableTx(ClaimLocalAnchorOutputTx(InputInfo.SegwitInput(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), confirmBefore), null, null) txPublisher ! cmd val child = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor val p = child.expectMsgType[ReplaceableTxPublisher.Publish] @@ -117,7 +117,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val confirmBefore = nodeParams.currentBlockHeight + 12 val input = OutPoint(randomTxId(), 3) - val anchorTx = ClaimLocalAnchorOutputTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), ConfirmationTarget.Priority(ConfirmationPriority.Medium)) + val anchorTx = ClaimLocalAnchorOutputTx(InputInfo.SegwitInput(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), ConfirmationTarget.Priority(ConfirmationPriority.Medium)) val cmd = PublishReplaceableTx(anchorTx, null, null) txPublisher ! cmd val child = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor @@ -175,7 +175,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val attempt2 = factory.expectMsgType[FinalTxPublisherSpawned].actor attempt2.expectMsgType[FinalTxPublisher.Publish] - val cmd3 = PublishReplaceableTx(ClaimLocalAnchorOutputTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, TxOut(20_000 sat, Nil) :: Nil, 0), ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)), null, null) + val cmd3 = PublishReplaceableTx(ClaimLocalAnchorOutputTx(InputInfo.SegwitInput(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, TxOut(20_000 sat, Nil) :: Nil, 0), ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)), null, null) txPublisher ! cmd3 val attempt3 = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor attempt3.expectMsgType[ReplaceableTxPublisher.Publish] @@ -197,7 +197,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val attempt1 = factory.expectMsgType[FinalTxPublisherSpawned] attempt1.actor.expectMsgType[FinalTxPublisher.Publish] - val cmd2 = PublishReplaceableTx(ClaimLocalAnchorOutputTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, TxOut(20_000 sat, Nil) :: Nil, 0), ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)), null, null) + val cmd2 = PublishReplaceableTx(ClaimLocalAnchorOutputTx(InputInfo.SegwitInput(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, TxOut(20_000 sat, Nil) :: Nil, 0), ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)), null, null) txPublisher ! cmd2 val attempt2 = factory.expectMsgType[ReplaceableTxPublisherSpawned] attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish] @@ -237,7 +237,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val target = nodeParams.currentBlockHeight + 12 val input = OutPoint(randomTxId(), 7) val paymentHash = randomBytes32() - val cmd = PublishReplaceableTx(HtlcSuccessTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), paymentHash, 3, ConfirmationTarget.Absolute(target)), null, null) + val cmd = PublishReplaceableTx(HtlcSuccessTx(InputInfo.SegwitInput(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), paymentHash, 3, ConfirmationTarget.Absolute(target)), null, null) txPublisher ! cmd val attempt1 = factory.expectMsgType[ReplaceableTxPublisherSpawned] attempt1.actor.expectMsgType[ReplaceableTxPublisher.Publish] @@ -301,7 +301,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val input = OutPoint(randomTxId(), 7) val paymentHash = randomBytes32() - val cmd = PublishReplaceableTx(HtlcSuccessTx(InputInfo(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), paymentHash, 3, ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)), null, null) + val cmd = PublishReplaceableTx(HtlcSuccessTx(InputInfo.SegwitInput(input, TxOut(25_000 sat, Nil), Nil), Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0), paymentHash, 3, ConfirmationTarget.Absolute(nodeParams.currentBlockHeight)), null, null) txPublisher ! cmd val attempt1 = factory.expectMsgType[ReplaceableTxPublisherSpawned] attempt1.actor.expectMsgType[ReplaceableTxPublisher.Publish] 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 063ff34a1b..3cc7c16a64 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 @@ -594,7 +594,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { // all htlcs success/timeout should be published as-is, without claiming their outputs s2blockchain.expectMsgAllOf(localCommitPublished.htlcTxs.values.toSeq.collect { case Some(tx) => TxPublisher.PublishFinalTx(tx, tx.fee, Some(commitTx.txid)) }: _*) assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) - case _: Transactions.AnchorOutputsCommitmentFormat => + case _: Transactions.AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => // all htlcs success/timeout should be published as replaceable txs, without claiming their outputs val htlcTxs = localCommitPublished.htlcTxs.values.collect { case Some(tx: HtlcTx) => tx } val publishedTxs = htlcTxs.map(_ => s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx]) @@ -633,7 +633,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { // If anchor outputs is used, we use the anchor output to bump the fees if necessary. closingData.commitments.params.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => val anchorTx = s2blockchain.expectMsgType[PublishReplaceableTx] assert(anchorTx.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) case Transactions.DefaultCommitmentFormat => () 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 0e23ffa827..45cb04a8cf 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 @@ -55,7 +55,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny val (initiatorPushAmount, nonInitiatorPushAmount) = if (test.tags.contains("both_push_amount")) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None) val commitFeerate = channelType.commitmentFormat match { case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw } val aliceListener = TestProbe() val bobListener = TestProbe() 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 fd778546c9..65229afd78 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 @@ -61,7 +61,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val commitFeerate = channelType.commitmentFormat match { case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw } val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index 7c7bf41f5a..21e3f2ffaa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -53,7 +53,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val commitFeerate = channelType.commitmentFormat match { case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw } val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushAmount)) None else Some(TestConstants.initiatorPushAmount) val aliceInit = Init(aliceParams.initFeatures) 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 bc671235d3..8397f210a1 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 @@ -72,7 +72,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val commitFeerate = channelType.commitmentFormat match { case Transactions.DefaultCommitmentFormat => TestConstants.feeratePerKw - case _: Transactions.AnchorOutputsCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => TestConstants.anchorOutputsFeeratePerKw } val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) 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 8cb3b22709..45eaf70346 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 @@ -1289,7 +1289,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the latest commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + case _: AnchorOutputsCommitmentFormat | Transactions.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 } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) @@ -1423,7 +1423,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the next commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + 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 } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) @@ -1618,7 +1618,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob is nice and publishes its commitment val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 6) // two main outputs + two anchors + 2 HTLCs + 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 } alice ! WatchFundingSpentTriggered(bobCommitTx) @@ -1692,7 +1692,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob's first commit tx doesn't contain any htlc val localCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) // 2 main outputs + 2 anchors + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) // 2 main outputs + 2 anchors case DefaultCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) // 2 main outputs } @@ -1708,7 +1708,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) case DefaultCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) } @@ -1724,7 +1724,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) case DefaultCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) } @@ -1738,7 +1738,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) case DefaultCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) } @@ -2119,7 +2119,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) val initOutputCount = channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 4 + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 4 case DefaultCommitmentFormat => 2 } assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount) 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 71c80329d2..26ffcbe7ee 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 @@ -35,7 +35,7 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler} import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.router.Router -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, TxOwner} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat, TxOwner} import fr.acinq.eclair.transactions.{OutgoingHtlc, Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, randomBytes32} @@ -181,7 +181,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks(25, Some(minerAddress)) val expectedTxCountC = 1 // C should have 1 recv transaction: its main output val expectedTxCountF = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 2 // F should have 2 recv transactions: the redeemed htlc and its main output + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 2 // F should have 2 recv transactions: the redeemed htlc and its main output case Transactions.DefaultCommitmentFormat => 1 // F's main output uses static_remotekey } awaitCond({ @@ -221,7 +221,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then generate enough blocks so that F gets its htlc-success delayed output generateBlocks(25, Some(minerAddress)) val expectedTxCountC = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 1 // C should have 1 recv transaction: its main output + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 1 // C should have 1 recv transaction: its main output case Transactions.DefaultCommitmentFormat => 0 // C's main output uses static_remotekey } val expectedTxCountF = 2 // F should have 2 recv transactions: the redeemed htlc and its main output @@ -275,7 +275,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks(25, Some(minerAddress)) val expectedTxCountC = 2 // C should have 2 recv transactions: its main output and the htlc timeout val expectedTxCountF = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 1 // F should have 1 recv transaction: its main output + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 1 // F should have 1 recv transaction: its main output case Transactions.DefaultCommitmentFormat => 0 // F's main output uses static_remotekey } awaitCond({ @@ -330,7 +330,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then generate enough blocks to confirm all delayed transactions generateBlocks(25, Some(minerAddress)) val expectedTxCountC = commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => 2 // C should have 2 recv transactions: its main output and the htlc timeout + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => 2 // C should have 2 recv transactions: its main output and the htlc timeout case Transactions.DefaultCommitmentFormat => 1 // C's main output uses static_remotekey } val expectedTxCountF = 1 // F should have 1 recv transaction: its main output @@ -405,7 +405,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val localCommitF = commitmentsF.latest.localCommit commitmentFormat match { case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) - case _: Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) + case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) } val outgoingHtlcExpiry = localCommitF.spec.htlcs.collect { case OutgoingHtlc(add) => add.cltvExpiry }.max val htlcTimeoutTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcTimeoutTx, _) => h } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala index a8abd39b02..a8cc8b1adb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala @@ -139,7 +139,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory val currentChannels = Seq( Peer.ChannelInfo(TestProbe().ref, SHUTDOWN, DATA_SHUTDOWN(commitments(isOpener = true), Shutdown(randomBytes32(), ByteVector.empty), Shutdown(randomBytes32(), ByteVector.empty), CloseStatus.Initiator(None))), Peer.ChannelInfo(TestProbe().ref, NEGOTIATING, DATA_NEGOTIATING(commitments(), Shutdown(randomBytes32(), ByteVector.empty), Shutdown(randomBytes32(), ByteVector.empty), List(Nil), None)), - Peer.ChannelInfo(TestProbe().ref, CLOSING, DATA_CLOSING(commitments(), BlockHeight(0), ByteVector.empty, Nil, ClosingTx(InputInfo(OutPoint(TxId(randomBytes32()), 5), TxOut(100_000 sat, Nil), Nil), Transaction(2, Nil, Nil, 0), None) :: Nil)), + Peer.ChannelInfo(TestProbe().ref, CLOSING, DATA_CLOSING(commitments(), BlockHeight(0), ByteVector.empty, Nil, ClosingTx(InputInfo.SegwitInput(OutPoint(TxId(randomBytes32()), 5), TxOut(100_000 sat, Nil), Nil), Transaction(2, Nil, Nil, 0), None) :: Nil)), Peer.ChannelInfo(TestProbe().ref, WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments(), ChannelReestablish(randomBytes32(), 0, 0, randomKey(), randomKey().publicKey))), ) peer.expectMessageType[Peer.GetPeerChannels].replyTo ! Peer.PeerChannels(remoteNodeId, currentChannels) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala index 6dd156453f..191ce16a0e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PendingChannelsRateLimiterSpec.scala @@ -72,7 +72,7 @@ class PendingChannelsRateLimiterSpec extends ScalaTestWithActorTestKit(ConfigFac val probe = TestProbe[PendingChannelsRateLimiter.Response]() val nodeParams = TestConstants.Alice.nodeParams.copy(channelConf = TestConstants.Alice.nodeParams.channelConf.copy(maxPendingChannelsPerPeer = maxPendingChannelsPerPeer, maxTotalPendingChannelsPrivateNodes = maxTotalPendingChannelsPrivateNodes, channelOpenerWhitelist = Set(peerOnWhitelistAtLimit))) val tx = Transaction.read("010000000110f01d4a4228ef959681feb1465c2010d0135be88fd598135b2e09d5413bf6f1000000006a473044022074658623424cebdac8290488b76f893cfb17765b7a3805e773e6770b7b17200102202892cfa9dda662d5eac394ba36fcfd1ea6c0b8bb3230ab96220731967bbdb90101210372d437866d9e4ead3d362b01b615d24cc0d5152c740d51e3c55fb53f6d335d82ffffffff01408b0700000000001976a914678db9a7caa2aca887af1177eda6f3d0f702df0d88ac00000000") - val closingTx = ClosingTx(InputInfo(tx.txIn.head.outPoint, TxOut(10_000 sat, Nil), Nil), tx, None) + val closingTx = ClosingTx(InputInfo.SegwitInput(tx.txIn.head.outPoint, TxOut(10_000 sat, Nil), Nil), tx, None) val channelsOnWhitelistAtLimit: Seq[PersistentChannelData] = Seq( DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments(peerOnWhitelistAtLimit, randomBytes32()), BlockHeight(0), None, Left(FundingCreated(randomBytes32(), TxId(ByteVector32.Zeroes), 3, randomBytes64()))), DATA_WAIT_FOR_CHANNEL_READY(commitments(peerOnWhitelistAtLimit, randomBytes32()), ShortIdAliases(ShortChannelId.generateLocalAlias(), None)), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index ac539bc313..9684c12b74 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -123,7 +123,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat val dummyBytes32 = ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202") val localParams = LocalParams(dummyPublicKey, DeterministicWallet.KeyPath(Seq(42L)), 546 sat, Long.MaxValue.msat, Some(1000 sat), 1 msat, CltvExpiryDelta(144), 50, isChannelOpener = true, paysCommitTxFees = true, None, None, Features.empty) val remoteParams = RemoteParams(dummyPublicKey, 546 sat, UInt64.MaxValue, Some(1000 sat), 1 msat, CltvExpiryDelta(144), 50, dummyPublicKey, dummyPublicKey, dummyPublicKey, dummyPublicKey, Features.empty, None) - val commitmentInput = Funding.makeFundingInputInfo(TxId(dummyBytes32), 0, 150_000 sat, dummyPublicKey, dummyPublicKey) + val commitmentInput = Funding.makeFundingInputInfo(TxId(dummyBytes32), 0, 150_000 sat, dummyPublicKey, dummyPublicKey, DefaultCommitmentFormat) val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(2500 sat), 100_000_000 msat, 50_000_000 msat), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(2500 sat), 50_000_000 msat, 100_000_000 msat), TxId(dummyBytes32), dummyPublicKey) val channelInfo = RES_GET_CHANNEL_INFO( @@ -285,7 +285,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat } test("InputInfo serialization") { - val inputInfo = InputInfo( + val inputInfo = InputInfo.SegwitInput( outPoint = OutPoint(TxHash.fromValidHex("345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f"), 42), txOut = TxOut(456651 sat, hex"3c7a66997c681a3de1bae56438abeee4fc50a16554725a430ade1dc8db6bdd76704d45c6151c4051d710cf487e63"), redeemScript = hex"00dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773" @@ -383,7 +383,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat test("TransactionWithInputInfo serializer") { // the input info is ignored when serializing to JSON - val dummyInputInfo = InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(Satoshi(0), Nil), Nil) + val dummyInputInfo = InputInfo.SegwitInput(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(Satoshi(0), Nil), Nil) val htlcSuccessTx = Transaction.read("0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800") val htlcSuccessTxInfo = HtlcSuccessTx(dummyInputInfo, htlcSuccessTx, ByteVector32.One, 3, ConfirmationTarget.Absolute(BlockHeight(1105))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index dc3cb76fee..96e03ee23a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -714,7 +714,7 @@ object PaymentPacketSpec { val localParams = LocalParams(null, null, null, Long.MaxValue.msat, Some(channelReserve), null, null, 0, isChannelOpener = true, paysCommitTxFees = true, None, None, Features.empty) val remoteParams = RemoteParams(randomKey().publicKey, null, UInt64.MaxValue, Some(channelReserve), null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, None) val fundingTx = Transaction(2, Nil, Seq(TxOut(testCapacity, Nil)), 0) - val commitInput = InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, Nil) + val commitInput = InputInfo.SegwitInput(OutPoint(fundingTx, 0), fundingTx.txOut.head, Nil) val localCommit = LocalCommit(0, null, CommitTxAndRemoteSig(Transactions.CommitTx(commitInput, null), RemoteSignature.FullSignature(null)), Nil) val remoteCommit = RemoteCommit(0, null, null, randomKey().publicKey) val localChanges = LocalChanges(Nil, Nil, Nil) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 8133fafc34..f101535113 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -513,7 +513,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // commit we accept it as such, so it simplifies the test. val revokedCommitTx = normal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.copy(txOut = Seq(TxOut(4500 sat, Script.pay2wpkh(randomKey().publicKey)))) val dummyClaimMainTx = Transaction(2, Seq(TxIn(OutPoint(revokedCommitTx, 0), Nil, 0)), Seq(revokedCommitTx.txOut.head.copy(amount = 4000 sat)), 0) - val dummyClaimMain = ClaimRemoteDelayedOutputTx(InputInfo(OutPoint(revokedCommitTx, 0), revokedCommitTx.txOut.head, Nil), dummyClaimMainTx) + val dummyClaimMain = ClaimRemoteDelayedOutputTx(InputInfo.SegwitInput(OutPoint(revokedCommitTx, 0), revokedCommitTx.txOut.head, Nil), dummyClaimMainTx) val rcp = RevokedCommitPublished(revokedCommitTx, Some(dummyClaimMain), None, Nil, Nil, Map(revokedCommitTx.txIn.head.outPoint -> revokedCommitTx)) DATA_CLOSING(normal.commitments, BlockHeight(0), Script.write(Script.pay2wpkh(randomKey().publicKey)), mutualCloseProposed = Nil, revokedCommitPublished = List(rcp)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 81a7111cf4..176f9ef896 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -23,6 +23,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelFeatures import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators +import fr.acinq.eclair.transactions.Transactions.InputInfo.SegwitInput import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, TestConstants} @@ -124,7 +125,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val fundingAmount = fundingTx.txOut(0).amount logger.info(s"# funding-tx: $fundingTx}") - val commitmentInput = Funding.makeFundingInputInfo(fundingTx.txid, 0, fundingAmount, Local.funding_pubkey, Remote.funding_pubkey) + val commitmentInput = Funding.makeFundingInputInfo(fundingTx.txid, 0, fundingAmount, Local.funding_pubkey, Remote.funding_pubkey, DefaultCommitmentFormat) val obscured_tx_number = Transactions.obscuredCommitTxNumber(42, localIsChannelOpener = true, Local.payment_basepoint, Remote.payment_basepoint) assert(obscured_tx_number == (0x2bb038521914L ^ 42L)) @@ -140,8 +141,8 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"remotekey: ${Remote.payment_privkey.publicKey}") logger.info(s"local_delayedkey: ${Local.delayed_payment_privkey.publicKey}") logger.info(s"local_revocation_key: ${Local.revocation_pubkey}") - logger.info(s"# funding wscript = ${commitmentInput.redeemScript}") - assert(commitmentInput.redeemScript == hex"5221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae") + logger.info(s"# funding wscript = ${commitmentInput.asInstanceOf[SegwitInput].redeemScript}") + assert(commitmentInput.asInstanceOf[SegwitInput].redeemScript == hex"5221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae") val paymentPreimages = Seq( ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), 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 b6ce30a205..891513330d 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 @@ -25,6 +25,7 @@ import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} import fr.acinq.eclair.channel.Helpers.Funding +import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{Multisig2of2Input, Musig2Input} import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc} import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Transactions.AnchorOutputsCommitmentFormat.anchorAmount @@ -52,7 +53,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val localHtlcPriv = PrivateKey(randomBytes32()) val remoteHtlcPriv = PrivateKey(randomBytes32()) val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) + val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, DefaultCommitmentFormat) val toLocalDelay = CltvExpiryDelta(144) val localDustLimit = Satoshi(546) val feeratePerKw = FeeratePerKw(22000 sat) @@ -177,7 +178,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimHtlcSuccessTx, PlaceHolderSig, paymentPreimage).tx) - assert(claimHtlcSuccessWeight == weight) + assert(DefaultCommitmentFormat.claimHtlcSuccessWeight == weight) assert(claimHtlcSuccessTx.fee >= claimHtlcSuccessTx.minRelayFee) } { @@ -192,7 +193,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Right(claimClaimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimClaimHtlcTimeoutTx, PlaceHolderSig).tx) - assert(claimHtlcTimeoutWeight == weight) + assert(DefaultCommitmentFormat.claimHtlcTimeoutWeight == weight) assert(claimClaimHtlcTimeoutTx.fee >= claimClaimHtlcTimeoutTx.minRelayFee) } { @@ -258,7 +259,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { test("generate valid commitment and htlc transactions (default commitment format)") { val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) + val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, DefaultCommitmentFormat) // htlc1 and htlc2 are regular IN/OUT htlcs val paymentPreimage1 = randomBytes32() @@ -405,10 +406,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends offered HTLC output with revocation key val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash), DefaultCommitmentFormat)) - val Some(htlcOutputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(someHtlc))), _) => someHtlc.id == htlc1.id - case _ => false - }.map(_._2) + val Some(htlcOutputIndex) = outputs.map(_.filter[OutHtlc]).zipWithIndex.collectFirst { + case (Some(co), outputIndex) if co.commitmentOutput.outgoingHtlc.add.id == htlc1.id => outputIndex + } val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) @@ -427,10 +427,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends received HTLC output with revocation key val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc2.paymentHash), htlc2.cltvExpiry, DefaultCommitmentFormat)) - val Some(htlcOutputIndex) = outputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(someHtlc))), _) => someHtlc.id == htlc2.id - case _ => false - }.map(_._2) + val Some(htlcOutputIndex) = outputs.map(_.filter[InHtlc]).zipWithIndex.collectFirst { + case (Some(co), outputIndex) if co.commitmentOutput.incomingHtlc.add.id == htlc2.id => outputIndex + } val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) @@ -487,11 +486,26 @@ class TransactionsSpec extends AnyFunSuite with Logging { } } - test("generate valid commitment and htlc transactions (anchor outputs)") { + def assertWeightMatches(actualWeight: Int, expectedWeight: Int, commitmentFormat: CommitmentFormat): Unit = commitmentFormat match { + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => assert(Math.abs(actualWeight - expectedWeight) < 20) + case SimpleTaprootChannelCommitmentFormat => assert(actualWeight == expectedWeight) + } + + def assertWitnessWeightMatches(witness: ScriptWitness, expectedWeight: Int, commitmentFormat: CommitmentFormat): Unit = + assertWeightMatches(164 + ScriptWitness.write(witness).size.toInt, expectedWeight, commitmentFormat) + + def generateCommitAndHtlcTxs(commitmentFormat: CommitmentFormat): Unit = { val walletPriv = randomKey() val walletPub = walletPriv.publicKey - val finalPubKeyScript = Script.write(Script.pay2wpkh(walletPub)) - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) + val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) + // funding tx sends to musig2 aggregate of local and remote funding keys + val fundingTx = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), Script.pay2tr(Taproot.musig2Aggregate(localFundingPriv.publicKey, remoteFundingPriv.publicKey), None)) :: Nil, lockTime = 0) + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), Scripts.multiSig2of2(localFundingPriv.publicKey, remoteFundingPriv.publicKey)) :: Nil, lockTime = 0) + } + val fundingTxOutpoint = OutPoint(fundingTx.txid, 0) + val fundingOutput = fundingTx.txOut(0) + val commitInput = Funding.makeFundingInputInfo(fundingTxOutpoint.txid, fundingTxOutpoint.index.toInt, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, commitmentFormat) // htlc1, htlc2a and htlc2b are regular IN/OUT htlcs val paymentPreimage1 = randomBytes32() @@ -501,9 +515,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { val htlc2b = UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliBtc(150).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(310), TestConstants.emptyOnionPacket, None, 1.0, None) // htlc3 and htlc4 are dust IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage val paymentPreimage3 = randomBytes32() - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, commitmentFormat.htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) val paymentPreimage4 = randomBytes32() - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit + weight2fee(feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) + val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 4, (localDustLimit + weight2fee(feeratePerKw, commitmentFormat.htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket, None, 1.0, None) // htlc5 and htlc6 are dust IN/OUT htlcs val htlc5 = UpdateAddHtlc(ByteVector32.Zeroes, 5, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(295), TestConstants.emptyOnionPacket, None, 1.0, None) val htlc6 = UpdateAddHtlc(ByteVector32.Zeroes, 6, (localDustLimit * 0.9).toMilliSatoshi, sha256(randomBytes32()), CltvExpiry(305), TestConstants.emptyOnionPacket, None, 1.0, None) @@ -525,16 +539,42 @@ class TransactionsSpec extends AnyFunSuite with Logging { commitTxFeerate = feeratePerKw, toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) + val (secretLocalNonce, publicLocalNonce) = Musig2.generateNonce(randomBytes32(), localFundingPriv, Seq(localFundingPriv.publicKey)) + val (secretRemoteNonce, publicRemoteNonce) = Musig2.generateNonce(randomBytes32(), remoteFundingPriv, Seq(remoteFundingPriv.publicKey)) + val publicKeys = Scripts.sort(Seq(localFundingPriv.publicKey, remoteFundingPriv.publicKey)) + val publicNonces = Seq(publicLocalNonce, publicRemoteNonce) val (commitTx, commitTxOutputs, htlcTimeoutTxs, htlcSuccessTxs) = { val commitTxNumber = 0x404142434445L - val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, commitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) + val commitTx = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val Right(sig) = for { + localPartialSig <- Musig2.signTaprootInput(localFundingPriv, txInfo.tx, 0, Seq(fundingOutput), publicKeys, secretLocalNonce, publicNonces, None) + remotePartialSig <- Musig2.signTaprootInput(remoteFundingPriv, txInfo.tx, 0, Seq(fundingOutput), publicKeys, secretRemoteNonce, publicNonces, None) + sig <- Musig2.aggregateTaprootSignatures(Seq(localPartialSig, remotePartialSig), txInfo.tx, 0, Seq(fundingOutput), publicKeys, publicNonces, None) + } yield sig + val commitTx = Transactions.addAggregatedSignature(txInfo, sig) + val expectedCommitTxWeight = commitmentFormat.commitWeight + 5 * commitmentFormat.htlcOutputWeight + assertWeightMatches(commitTx.tx.weight(), expectedCommitTxWeight, commitmentFormat) + val sharedInput = Musig2Input(InputInfo.TaprootInput(fundingTxOutpoint, fundingOutput, Taproot.musig2Aggregate(localFundingPriv.publicKey, remoteFundingPriv.publicKey), InputInfo.RedeemPath.KeyPath(None)), 0, remoteFundingPriv.publicKey, 0) + assertWitnessWeightMatches(commitTx.tx.txIn(0).witness, sharedInput.weight, commitmentFormat) + commitTx + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val localSig = txInfo.sign(localFundingPriv, TxOwner.Local, commitmentFormat, Map.empty) + val remoteSig = txInfo.sign(remoteFundingPriv, TxOwner.Remote, commitmentFormat, Map.empty) + val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) + val expectedCommitTxWeight = commitmentFormat.commitWeight + 5 * commitmentFormat.htlcOutputWeight + // we cannot do exact matches because DER signature encoding is variable length + assertWeightMatches(commitTx.tx.weight(), expectedCommitTxWeight, commitmentFormat) + val sharedInput = Multisig2of2Input(InputInfo.SegwitInput(fundingTxOutpoint, fundingOutput, Scripts.multiSig2of2(localFundingPriv.publicKey, remoteFundingPriv.publicKey)), 0, remoteFundingPriv.publicKey) + assertWitnessWeightMatches(commitTx.tx.txIn(0).witness, sharedInput.weight, commitmentFormat) + commitTx + } + assert(checkSpendable(commitTx).isSuccess) - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(UnsafeLegacyAnchorOutputsCommitmentFormat), outputs, UnsafeLegacyAnchorOutputsCommitmentFormat) + val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(commitmentFormat), outputs, commitmentFormat) assert(htlcTxs.length == 5) val confirmationTargets = htlcTxs.map(tx => tx.htlcId -> tx.confirmationTarget.confirmBefore.toLong).toMap assert(confirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300)) @@ -566,7 +606,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends main delayed output val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, commitmentFormat, Map.empty) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -578,18 +618,35 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends main delayed output val Right(claimRemoteDelayedOutputTx) = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimRemoteDelayedOutputTx.sign(remotePaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val localSig = claimRemoteDelayedOutputTx.sign(remotePaymentPriv, TxOwner.Local, commitmentFormat, Map.empty) val signedTx = addSigs(claimRemoteDelayedOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } + { + // local spends local anchor + val anchorKey = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => localDelayedPaymentPriv + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => localFundingPriv + } + val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, anchorKey.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) + assert(checkSpendable(claimAnchorOutputTx).isFailure) + val localSig = claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, Map.empty) + val signedTx = addSigs(claimAnchorOutputTx, localSig) + Transaction.correctlySpends(signedTx.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } { // local spends local anchor with additional wallet inputs val walletAmount = 50_000 sat + // val walletInputs = Map.empty[OutPoint, TxOut] val walletInputs = Map( OutPoint(randomTxId(), 3) -> TxOut(walletAmount, Script.pay2wpkh(walletPub)), OutPoint(randomTxId(), 0) -> TxOut(walletAmount, Script.pay2wpkh(walletPub)), ) - val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, localFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))).map(anchorTx => { + val anchorKey = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => localDelayedPaymentPriv + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => localFundingPriv + } + val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, anchorKey.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))).map(anchorTx => { val walletTxIn = walletInputs.map { case (outpoint, _) => TxIn(outpoint, ByteVector.empty, 0) } val unsignedTx = anchorTx.tx.copy(txIn = anchorTx.tx.txIn ++ walletTxIn) val sig1 = unsignedTx.signInput(1, Script.pay2pkh(walletPub), SIGHASH_ALL, walletAmount, SigVersion.SIGVERSION_WITNESS_V0, walletPriv) @@ -602,39 +659,62 @@ class TransactionsSpec extends AnyFunSuite with Logging { val allInputs = walletInputs + (claimAnchorOutputTx.input.outPoint -> claimAnchorOutputTx.input.txOut) assert(Try(Transaction.correctlySpends(claimAnchorOutputTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isFailure) // All wallet inputs must be provided when signing. - assert(Try(claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty)).isFailure) - assert(Try(claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, walletInputs.take(1))).isFailure) - val localSig = claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, walletInputs) + //assert(Try(claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, Map.empty)).isFailure) + //assert(Try(claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, walletInputs.take(1))).isFailure) + val localSig = claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, walletInputs) val signedTx = addSigs(claimAnchorOutputTx, localSig) Transaction.correctlySpends(signedTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } { // remote spends remote anchor - val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, remoteFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) + val anchorKey = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => remotePaymentPriv + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => remoteFundingPriv + } + val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, anchorKey.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = claimAnchorOutputTx.sign(remoteFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val localSig = claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, Map.empty) val signedTx = addSigs(claimAnchorOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } + { + // anyone can spend the anchor after 16 blocks + val anchorKey = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => localDelayedPaymentPriv + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => localFundingPriv + } + val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, anchorKey.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) + val witness = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => Script.witnessScriptPathPay2tr(anchorKey.xOnlyPublicKey(), Taproot.anchorScriptTree, ScriptWitness(Seq()), Taproot.anchorScriptTree) + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => ScriptWitness(Seq(ByteVector.empty, Script.write(anchor(anchorKey.publicKey)))) + } + val tx = claimAnchorOutputTx.tx + .copy(txIn = claimAnchorOutputTx.tx.txIn.head.copy(sequence = 16) :: Nil) + .updateWitness(0, witness) + Transaction.correctlySpends(tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } { // remote spends local main delayed output with revocation key val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, commitmentFormat, Map.empty) val signed = addSigs(mainPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } { // local spends received htlc with HTLC-timeout tx - for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signedTx = addSigs(htlcTimeoutTx, localSig, remoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) + for ((htlcTimeoutTx, paymentPreimage) <- htlcTimeoutTxs.zip(paymentPreimage1 :: paymentPreimage3 :: Nil)) { + val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, commitmentFormat, Map.empty) + val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, commitmentFormat, Map.empty) + val signedTx = addSigs(htlcTimeoutTx, localSig, remoteSig, commitmentFormat) assert(checkSpendable(signedTx).isSuccess) + assertWeightMatches(signedTx.tx.weight(), commitmentFormat.htlcTimeoutWeight, commitmentFormat) + assertWitnessWeightMatches(signedTx.tx.txIn(0).witness, commitmentFormat.htlcTimeoutInputWeight, commitmentFormat) + // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { val invalidRemoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, sighash, Map.empty) - val invalidTx = addSigs(htlcTimeoutTx, localSig, invalidRemoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) + val invalidTx = addSigs(htlcTimeoutTx, localSig, invalidRemoteSig, commitmentFormat) assert(checkSpendable(invalidTx).isFailure) } } @@ -642,7 +722,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends delayed output of htlc1 timeout tx val Right(htlcDelayed) = makeHtlcDelayedTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, commitmentFormat, Map.empty) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit @@ -652,19 +732,22 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends offered htlc with HTLC-success tx for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(0), paymentPreimage4) :: (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(2), paymentPreimage2) :: Nil) { - val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, commitmentFormat, Map.empty) + val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, commitmentFormat, Map.empty) + val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, commitmentFormat) assert(checkSpendable(signedTx).isSuccess) // check remote sig - assert(htlcSuccessTx.checkSig(remoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat)) + assert(htlcSuccessTx.checkSig(remoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, commitmentFormat)) + assertWeightMatches(signedTx.tx.weight(), commitmentFormat.htlcSuccessWeight, commitmentFormat) + assertWitnessWeightMatches(signedTx.tx.txIn(0).witness, commitmentFormat.htlcSuccessInputWeight, commitmentFormat) + assert(extractPreimageFromHtlcSuccess(signedTx.tx.txIn(0).witness) == paymentPreimage) // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { val invalidRemoteSig = htlcSuccessTx.sign(remoteHtlcPriv, sighash, Map.empty) - val invalidTx = addSigs(htlcSuccessTx, localSig, invalidRemoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) + val invalidTx = addSigs(htlcSuccessTx, localSig, invalidRemoteSig, paymentPreimage, commitmentFormat) assert(checkSpendable(invalidTx).isFailure) - assert(!invalidTx.checkSig(invalidRemoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat)) + assert(!invalidTx.checkSig(invalidRemoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, commitmentFormat)) } } } @@ -673,29 +756,34 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Right(htlcDelayedA) = makeHtlcDelayedTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Right(htlcDelayedB) = makeHtlcDelayedTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (htlcDelayed <- Seq(htlcDelayedA, htlcDelayedB)) { - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, commitmentFormat, Map.empty) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) } // local can't claim delayed output of htlc4 success tx because it is below the dust limit val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayed1 == Left(AmountBelowDustLimit)) + commitmentFormat match { + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => assert(claimHtlcDelayed1 == Left(AmountBelowDustLimit)) + case SimpleTaprootChannelCommitmentFormat => assert(claimHtlcDelayed1 == Left(OutputNotFound)) + } } { // remote spends local->remote htlc outputs directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) - assert(checkSpendable(signed).isSuccess) + val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, commitmentFormat, Map.empty) + val signedTx = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) + assertWeightMatches(signedTx.tx.weight(), commitmentFormat.claimHtlcSuccessWeight, commitmentFormat) + assert(checkSpendable(signedTx).isSuccess) + assert(extractPreimageFromClaimHtlcSuccess(signedTx.tx.txIn(0).witness) == paymentPreimage) } } { // remote spends htlc1's htlc-timeout tx with revocation key val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) - assert(checkSpendable(signed).isSuccess) + val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, commitmentFormat, Map.empty) + val signedTx = addSigs(claimHtlcDelayedPenaltyTx, sig) + assert(checkSpendable(signedTx).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) @@ -704,9 +792,10 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends remote->local htlc output directly in case of timeout for (htlc <- Seq(htlc2a, htlc2b)) { val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcTimeoutTx, localSig) - assert(checkSpendable(signed).isSuccess) + val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, commitmentFormat, Map.empty) + val signedTx = addSigs(claimHtlcTimeoutTx, localSig) + assertWeightMatches(signedTx.tx.weight(), commitmentFormat.claimHtlcTimeoutWeight, commitmentFormat) + assert(checkSpendable(signedTx).isSuccess) } } { @@ -714,9 +803,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Seq(Right(claimHtlcDelayedPenaltyTxA)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Seq(Right(claimHtlcDelayedPenaltyTxB)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (claimHtlcSuccessPenaltyTx <- Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB)) { - val sig = claimHtlcSuccessPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(claimHtlcSuccessPenaltyTx, sig) - assert(checkSpendable(signed).isSuccess) + val sig = claimHtlcSuccessPenaltyTx.sign(localRevocationPriv, TxOwner.Local, commitmentFormat, Map.empty) + val signedTx = addSigs(claimHtlcSuccessPenaltyTx, sig) + assert(checkSpendable(signedTx).isSuccess) } // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) @@ -738,33 +827,51 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // remote spends offered htlc output with revocation key - val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash), UnsafeLegacyAnchorOutputsCommitmentFormat)) - val Some(htlcOutputIndex) = commitTxOutputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(someHtlc))), _) => someHtlc.id == htlc1.id - case _ => false - }.map(_._2) - val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) - assert(checkSpendable(signed).isSuccess) + val Some(htlcOutputIndex) = commitTxOutputs.map(_.filter[OutHtlc]).zipWithIndex.collectFirst { + case (Some(co), outputIndex) if co.commitmentOutput.outgoingHtlc.add.id == htlc1.id => outputIndex + } + val Right(htlcPenaltyTx) = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val scriptTree = Taproot.offeredHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, htlc1.paymentHash) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, localRevocationPriv.publicKey.xOnly, scriptTree, localDustLimit, finalPubKeyScript, feeratePerKw) + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash), commitmentFormat)) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) + } + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, commitmentFormat, Map.empty) + val signedTx = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) + assert(checkSpendable(signedTx).isSuccess) } { // remote spends received htlc output with revocation key for (htlc <- Seq(htlc2a, htlc2b)) { - val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc.paymentHash), htlc.cltvExpiry, UnsafeLegacyAnchorOutputsCommitmentFormat)) - val Some(htlcOutputIndex) = commitTxOutputs.zipWithIndex.find { - case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(someHtlc))), _) => someHtlc.id == htlc.id - case _ => false - }.map(_._2) - val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) - val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) - assert(checkSpendable(signed).isSuccess) + val Some(htlcOutputIndex) = commitTxOutputs.map(_.filter[InHtlc]).zipWithIndex.collectFirst { + case (Some(co), outputIndex) if co.commitmentOutput.incomingHtlc.add.id == htlc.id => outputIndex + } + val Right(htlcPenaltyTx) = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val scriptTree = Taproot.receivedHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, htlc.paymentHash, htlc.cltvExpiry) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, localRevocationPriv.publicKey.xOnly, scriptTree, localDustLimit, finalPubKeyScript, feeratePerKw) + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc.paymentHash), htlc.cltvExpiry, commitmentFormat)) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) + } + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, commitmentFormat, Map.empty) + val signedTx = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) + assert(checkSpendable(signedTx).isSuccess) } } } - test("generate valid commitment and htlc transactions (taproot)") { + test("generate valid commitment and htlc transactions (anchor outputs)") { + generateCommitAndHtlcTxs(UnsafeLegacyAnchorOutputsCommitmentFormat) + } + + test("generate valid commitment and htlc transactions (simple taproot channels)") { + generateCommitAndHtlcTxs(SimpleTaprootChannelCommitmentFormat) + } + + test("generate valid commitment and htlc transactions (taproot - unit test for low-level helpers)") { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ import fr.acinq.eclair.transactions.Scripts.Taproot @@ -903,9 +1010,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { txOut = TxOut(25_000.sat, Taproot.htlcDelayed(localDelayedPaymentPriv.publicKey, toLocalDelay, localRevocationPriv.publicKey)) :: Nil, lockTime = 300) val scriptTree = Taproot.offeredHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash) - val sigHash = SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY - val localSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), sigHash, scriptTree.getLeft.hash()), sigHash) - val remoteSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), sigHash, scriptTree.getLeft.hash()), sigHash) + val localSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY, scriptTree.getLeft.hash()), SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY) + val remoteSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), SigHash.SIGHASH_DEFAULT, scriptTree.getLeft.hash()), SigHash.SIGHASH_DEFAULT) val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig, localSig)), scriptTree) tx.updateWitness(0, witness) } @@ -1032,7 +1138,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val remotePaymentPriv = PrivateKey(hex"a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6") val localHtlcPriv = PrivateKey(hex"a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7") val remoteHtlcPriv = PrivateKey(hex"a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8") - val commitInput = Funding.makeFundingInputInfo(TxId.fromValidHex("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) + val commitInput = Funding.makeFundingInputInfo(TxId.fromValidHex("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, DefaultCommitmentFormat) // htlc1 and htlc2 are two regular incoming HTLCs with different amounts. // htlc2 and htlc3 have the same amounts and should be sorted according to their scriptPubKey @@ -1092,7 +1198,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { } test("find our output in closing tx") { - val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) + val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, DefaultCommitmentFormat) val localPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) val remotePubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 1cfebe6ec3..cd268f5f2c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.json.JsonSerializers import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitTx, TxOwner} +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitTx, DefaultCommitmentFormat, TxOwner} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecs._ import fr.acinq.eclair.wire.protocol.{CommonCodecs, UpdateAddHtlc} @@ -313,7 +313,7 @@ object ChannelCodecsSpec { val fundingAmount = fundingTx.txOut.head.amount val fundingTxIndex = 0 val remoteFundingPubKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1)) :+ 1.toByte).publicKey - val commitmentInput = Funding.makeFundingInputInfo(fundingTx.txid, 0, fundingAmount, channelKeyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey) + val commitmentInput = Funding.makeFundingInputInfo(fundingTx.txid, 0, fundingAmount, channelKeyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, DefaultCommitmentFormat) val remoteSig = ByteVector64(hex"2148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d") val commitTx = Transaction( version = 2, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala index ca67359422..2865a75f5c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala @@ -124,7 +124,7 @@ class ChannelCodecs4Spec extends AnyFunSuite { test("encode/decode rbf status") { val channelId = randomBytes32() - val fundingInput = InputInfo(OutPoint(randomTxId(), 3), TxOut(175_000 sat, Script.pay2wpkh(randomKey().publicKey)), Nil) + val fundingInput = InputInfo.SegwitInput(OutPoint(randomTxId(), 3), TxOut(175_000 sat, Script.pay2wpkh(randomKey().publicKey)), Nil) val fundingTx = SharedTransaction( sharedInput_opt = None, sharedOutput = InteractiveTxBuilder.Output.Shared(UInt64(8), ByteVector.empty, 100_000_600 msat, 74_000_400 msat, 0 msat), @@ -180,7 +180,7 @@ class ChannelCodecs4Spec extends AnyFunSuite { createdAt = BlockHeight(1000), fundingParams = InteractiveTxParams(channelId = channelId, isInitiator = true, localContribution = 100.sat, remoteContribution = 200.sat, sharedInput_opt = Some(InteractiveTxBuilder.Multisig2of2Input( - InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000.sat, Script.pay2wsh(script)), script), + InputInfo.SegwitInput(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000.sat, Script.pay2wsh(script)), script), 0, PrivateKey(ByteVector.fromValidHex("02" * 32)).publicKey )), From 45c5e4b44dfff300112f434d443ecb8d6892d41c Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Wed, 2 Apr 2025 17:11:24 +0200 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- .../fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala | 2 +- .../scala/fr/acinq/eclair/transactions/Transactions.scala | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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 00e05b572e..d04b44fed1 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 @@ -1065,7 +1065,7 @@ object Helpers { (localFundingPubkey, commitment.remoteFundingPubKey) case SimpleTaprootChannelCommitmentFormat => val channelKeyPath = keyManager.keyPath(commitment.localParams, commitment.params.channelConfig) - val localPaymentPubkey = commitment.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) + val localPaymentPubkey = keyManager.paymentPoint(channelKeyPath).publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitment.remoteParams.delayedPaymentBasepoint, commitment.remoteCommit.remotePerCommitmentPoint) (localPaymentPubkey, remoteDelayedPaymentPubkey) } 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 0f819b4395..01a4033e6a 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 @@ -124,7 +124,7 @@ object InteractiveTxBuilder { } } - case class Musig2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey, commitIndex: Long) extends SharedFundingInput { + case class Musig2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey) extends SharedFundingInput { // witness is a single 64 bytes signature, weight = 1 (# of items) + 1 (size) + 64 = 66 // weight is 4 * (unsigned input weight) + witness weight = 4 * (32 + 4 + 4 + 1) + 66 = 230 override val weight: Int = 230 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 46f4341ff2..146aaf3aa3 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 @@ -139,7 +139,7 @@ object Transactions { object InputInfo { case class SegwitInput(outPoint: OutPoint, txOut: TxOut, redeemScript: ByteVector) extends InputInfo object SegwitInput { - def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]) = new SegwitInput(outPoint, txOut, Script.write(redeemScript)) + def apply(outPoint: OutPoint, txOut: TxOut, redeemScript: Seq[ScriptElt]): SegwitInput = SegwitInput(outPoint, txOut, Script.write(redeemScript)) } case class TaprootInput(outPoint: OutPoint, txOut: TxOut, internalKey: XonlyPublicKey, redeemPath: RedeemPath) extends InputInfo sealed trait RedeemPath @@ -160,7 +160,7 @@ object Transactions { import KotlinUtils._ /** - * Note: this won't be needed once findScript is added to bitcoin-kmp + * TODO: this won't be needed once findScript is added to bitcoin-kmp, remove when updating bitcoin-kmp * @return the leaf that matches `leafHash` */ def findScript(scriptTree: ScriptTree, leafHash: ByteVector32): Option[ScriptTree.Leaf] = scriptTree match { From 0d4748db32667ddbb0f420f91595fc6d0e08e0c9 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 2 Apr 2025 17:54:09 +0200 Subject: [PATCH 3/4] Fixup: address review comments (minimize diff, remove unused parameters) --- .../channel/fund/InteractiveTxBuilder.scala | 5 +- .../acinq/eclair/transactions/Scripts.scala | 7 +- .../eclair/transactions/Transactions.scala | 64 ++++--------------- .../transactions/TransactionsSpec.scala | 6 +- 4 files changed, 22 insertions(+), 60 deletions(-) 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 01a4033e6a..a1199073ee 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 @@ -111,7 +111,7 @@ object InteractiveTxBuilder { object SharedFundingInput { def apply(commitment: Commitment): SharedFundingInput = commitment.commitInput match { case inputInfo: InputInfo.SegwitInput => Multisig2of2Input(inputInfo, commitment.fundingTxIndex, commitment.remoteFundingPubKey) - case inputInfo: InputInfo.TaprootInput => Musig2Input(inputInfo, commitment.fundingTxIndex, commitment.remoteFundingPubKey, commitment.localCommit.index) + case inputInfo: InputInfo.TaprootInput => Musig2Input(inputInfo, commitment.fundingTxIndex, commitment.remoteFundingPubKey) } } @@ -128,9 +128,6 @@ object InteractiveTxBuilder { // witness is a single 64 bytes signature, weight = 1 (# of items) + 1 (size) + 64 = 66 // weight is 4 * (unsigned input weight) + witness weight = 4 * (32 + 4 + 4 + 1) + 66 = 230 override val weight: Int = 230 - - // a valid signature for this input MUST be the Musig2 aggregation of local and remote partial signatures - def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction, spentUtxos: Map[OutPoint, TxOut]): ByteVector64 = ??? } /** 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 c713d613da..d634f8e77c 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 @@ -222,10 +222,13 @@ object Scripts { def witnessHtlcSuccess(localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, htlcOfferedScript: ByteVector, commitmentFormat: CommitmentFormat) = ScriptWitness(ByteVector.empty :: der(remoteSig, htlcRemoteSighash(commitmentFormat)) :: der(localSig) :: paymentPreimage.bytes :: htlcOfferedScript :: Nil) - /** Extract the payment preimage from a 2nd-stage HTLC Success transaction's witness script */ + /** Extract the payment preimage from a 2nd-stage HTLC Success transaction's witness script + * We don't check signature or script sizes, just that there is a 32 bytes item in the right place which may be a preimage + * for one of our HTLCs, and if it's a false positive it will be ignored + */ def extractPreimageFromHtlcSuccess: PartialFunction[ScriptWitness, ByteVector32] = { case ScriptWitness(Seq(ByteVector.empty, _, _, paymentPreimage, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) // standard channels - case ScriptWitness(Seq(remoteSig, localSig, paymentPreimage, _, _)) if remoteSig.size == 65 && localSig.size == 64 && paymentPreimage.size == 32 => ByteVector32(paymentPreimage) // simple taproot channels + case ScriptWitness(Seq(_, _, paymentPreimage, _, _)) if paymentPreimage.size == 32 => ByteVector32(paymentPreimage) // simple taproot channels } /** Extract payment preimages from a (potentially batched) 2nd-stage HTLC transaction's witnesses. */ 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 146aaf3aa3..9bf84f106b 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 @@ -252,10 +252,7 @@ object Transactions { case class SpliceTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "splice-tx" } - case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { - override def desc: String = "commit-tx" - } - + case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "commit-tx" } /** * It's important to note that htlc transactions with the default commitment format are not actually replaceable: only * anchor outputs htlc transactions are replaceable. We should have used different types for these different kinds of @@ -283,62 +280,27 @@ object Transactions { } override def confirmationTarget: ConfirmationTarget.Absolute } - - case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { - override def desc: String = "htlc-success" - } - - case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { - override def desc: String = "htlc-timeout" - } - - case class HtlcDelayedTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { - override def desc: String = "htlc-delayed" - } - + case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { override def desc: String = "htlc-success" } + case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { override def desc: String = "htlc-timeout" } + case class HtlcDelayedTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed" } sealed trait ClaimHtlcTx extends ReplaceableTransactionWithInputInfo { def htlcId: Long override def confirmationTarget: ConfirmationTarget.Absolute } case class LegacyClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } - case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { - override def desc: String = "claim-htlc-success" - } - case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { - override def desc: String = "claim-htlc-timeout" - } - + case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } + case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-timeout" } sealed trait ClaimAnchorOutputTx extends TransactionWithInputInfo - case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction, confirmationTarget: ConfirmationTarget) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { - override def desc: String = "local-anchor" - } - + case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction, confirmationTarget: ConfirmationTarget) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { override def desc: String = "local-anchor" } case class ClaimRemoteAnchorOutputTx(input: InputInfo, tx: Transaction) extends ClaimAnchorOutputTx { override def desc: String = "remote-anchor" } sealed trait ClaimRemoteCommitMainOutputTx extends TransactionWithInputInfo case class ClaimP2WPKHOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { override def desc: String = "remote-main" } - case class ClaimRemoteDelayedOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { - override def desc: String = "remote-main-delayed" - } - - case class ClaimLocalDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { - override def desc: String = "local-main-delayed" - } - - case class MainPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { - override def desc: String = "main-penalty" - } - - case class HtlcPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { - override def desc: String = "htlc-penalty" - } - - case class ClaimHtlcDelayedOutputPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { - override def desc: String = "htlc-delayed-penalty" - } - - case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutput: Option[OutputInfo]) extends TransactionWithInputInfo { - override def desc: String = "closing" - } + case class ClaimRemoteDelayedOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { override def desc: String = "remote-main-delayed" } + case class ClaimLocalDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "local-main-delayed" } + case class MainPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "main-penalty" } + case class HtlcPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-penalty" } + case class ClaimHtlcDelayedOutputPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed-penalty" } + case class ClosingTx(input: InputInfo, tx: Transaction, toLocalOutput: Option[OutputInfo]) extends TransactionWithInputInfo { override def desc: String = "closing" } sealed trait TxGenerationSkipped case object OutputNotFound extends TxGenerationSkipped { override def toString = "output not found (probably trimmed)" } 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 891513330d..92bc87b2b6 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 @@ -558,7 +558,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTx = Transactions.addAggregatedSignature(txInfo, sig) val expectedCommitTxWeight = commitmentFormat.commitWeight + 5 * commitmentFormat.htlcOutputWeight assertWeightMatches(commitTx.tx.weight(), expectedCommitTxWeight, commitmentFormat) - val sharedInput = Musig2Input(InputInfo.TaprootInput(fundingTxOutpoint, fundingOutput, Taproot.musig2Aggregate(localFundingPriv.publicKey, remoteFundingPriv.publicKey), InputInfo.RedeemPath.KeyPath(None)), 0, remoteFundingPriv.publicKey, 0) + val sharedInput = Musig2Input(InputInfo.TaprootInput(fundingTxOutpoint, fundingOutput, Taproot.musig2Aggregate(localFundingPriv.publicKey, remoteFundingPriv.publicKey), InputInfo.RedeemPath.KeyPath(None)), 0, remoteFundingPriv.publicKey) assertWitnessWeightMatches(commitTx.tx.txIn(0).witness, sharedInput.weight, commitmentFormat) commitTx case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => @@ -659,8 +659,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val allInputs = walletInputs + (claimAnchorOutputTx.input.outPoint -> claimAnchorOutputTx.input.txOut) assert(Try(Transaction.correctlySpends(claimAnchorOutputTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isFailure) // All wallet inputs must be provided when signing. - //assert(Try(claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, Map.empty)).isFailure) - //assert(Try(claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, walletInputs.take(1))).isFailure) + assert(Try(claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, Map.empty)).isFailure) + assert(Try(claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, walletInputs.take(1))).isFailure) val localSig = claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, walletInputs) val signedTx = addSigs(claimAnchorOutputTx, localSig) Transaction.correctlySpends(signedTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) From 788d492e51c4b96c68dcadcfee72e23f5432440a Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 2 Apr 2025 19:49:31 +0200 Subject: [PATCH 4/4] Use ScriptPath's leaf explicitly to creating spending witness --- .../eclair/transactions/Transactions.scala | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 9bf84f106b..702d28cea7 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 @@ -153,7 +153,7 @@ object Transactions { * @param leafHash hash of the leaf script we're spending (must belong to the tree). */ case class ScriptPath(scriptTree: ScriptTree, leafHash: ByteVector32) extends RedeemPath { - require(ScriptPath.findScript(scriptTree, leafHash).nonEmpty, "script tree must contain the provided leaf") + val leaf: ScriptTree.Leaf = ScriptPath.findScript(scriptTree, leafHash).getOrElse(throw new IllegalArgumentException("script tree must contain the provided leaf")) } object ScriptPath { @@ -1340,7 +1340,7 @@ object Transactions { case InputInfo.SegwitInput(_, _, redeemScript) => Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, redeemScript) case t: InputInfo.TaprootInput => t.redeemPath match { - case RedeemPath.ScriptPath(scriptTree: ScriptTree.Branch, _) => Script.witnessScriptPathPay2tr(t.internalKey, scriptTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(revocationSig)), scriptTree) + case s@RedeemPath.ScriptPath(scriptTree: ScriptTree.Branch, _) => Script.witnessScriptPathPay2tr(t.internalKey, s.leaf, ScriptWitness(Seq(revocationSig)), scriptTree) case _ => throw new IllegalArgumentException("unexpected script tree leaf when building main penalty tx") } } @@ -1360,8 +1360,8 @@ object Transactions { case InputInfo.SegwitInput(_, _, redeemScript) => witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, redeemScript, commitmentFormat) case t: InputInfo.TaprootInput => t.redeemPath match { - case RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => - Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(Taproot.encodeSig(remoteSig, SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY), Taproot.encodeSig(localSig, SIGHASH_DEFAULT), paymentPreimage)), htlcTree) + case s@RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => + Script.witnessScriptPathPay2tr(t.internalKey, s.leaf, ScriptWitness(Seq(Taproot.encodeSig(remoteSig, SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY), Taproot.encodeSig(localSig, SIGHASH_DEFAULT), paymentPreimage)), htlcTree) case _ => throw new IllegalArgumentException("unexpected script tree leaf when building htlc successTx tx") } } @@ -1373,8 +1373,8 @@ object Transactions { case InputInfo.SegwitInput(_, _, redeemScript) => witnessHtlcTimeout(localSig, remoteSig, redeemScript, commitmentFormat) case t: InputInfo.TaprootInput => t.redeemPath match { - case RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => - Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(Taproot.encodeSig(remoteSig, SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY), Taproot.encodeSig(localSig, SIGHASH_DEFAULT))), htlcTree) + case s@RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => + Script.witnessScriptPathPay2tr(t.internalKey, s.leaf, ScriptWitness(Seq(Taproot.encodeSig(remoteSig, SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY), Taproot.encodeSig(localSig, SIGHASH_DEFAULT))), htlcTree) case _ => throw new IllegalArgumentException("unexpected script tree leaf when building htlc timeout tx") } } @@ -1386,8 +1386,8 @@ object Transactions { case InputInfo.SegwitInput(_, _, redeemScript) => witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, redeemScript) case t: InputInfo.TaprootInput => t.redeemPath match { - case RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => - Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(localSig, paymentPreimage)), htlcTree) + case s@RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _f) => + Script.witnessScriptPathPay2tr(t.internalKey, s.leaf, ScriptWitness(Seq(localSig, paymentPreimage)), htlcTree) case _ => throw new IllegalArgumentException("unexpected script tree leaf when building claim htlc success tx") } } @@ -1400,8 +1400,8 @@ object Transactions { witnessClaimHtlcTimeoutFromCommitTx(localSig, redeemScript) case t: InputInfo.TaprootInput => t.redeemPath match { - case RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => - Script.witnessScriptPathPay2tr(t.internalKey, htlcTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(localSig)), htlcTree) + case s@RedeemPath.ScriptPath(htlcTree: ScriptTree.Branch, _) => + Script.witnessScriptPathPay2tr(t.internalKey, s.leaf, ScriptWitness(Seq(localSig)), htlcTree) case _ => throw new IllegalArgumentException("unexpected script tree leaf when building claim htlc timeout tx") } } @@ -1431,8 +1431,8 @@ object Transactions { case InputInfo.SegwitInput(_, _, redeemScript) => witnessToLocalDelayedAfterDelay(localSig, redeemScript) case t: InputInfo.TaprootInput => t.redeemPath match { - case RedeemPath.ScriptPath(scriptTree: ScriptTree.Branch, _) => - Script.witnessScriptPathPay2tr(t.internalKey, scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(localSig)), scriptTree) + case s@RedeemPath.ScriptPath(scriptTree: ScriptTree.Branch, _) => + Script.witnessScriptPathPay2tr(t.internalKey, s.leaf, ScriptWitness(Seq(localSig)), scriptTree) case _ => throw new IllegalArgumentException("unexpected script tree leaf when building claim delayed output tx") } }