From 71a3df491f531ef68b3231063cf560cbcbccfc90 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 5 Mar 2025 12:13:42 +0100 Subject: [PATCH 1/2] 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 4df5e9772be28b021d3cf743d173628e085143d3 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 21 Oct 2024 11:58:52 +0200 Subject: [PATCH 2/2] Implement simple taproot channels This commit implements: - feature bits for simple taproot channels - TLV extensions for funding/closing wire messages - modifications to how we handle channel funding, splicing and mutual closing - changes to the commitment structures The v1 channel establishment protocol is modified to include nonces for creating and signing taproot transactions. This is bascially the original simple taproot proposal, which does not cover dual-funding, splices and rbf. We assume that simple taproot channels depends on the simple close protocol, which we extend to include musig2 nonces. Dual-funding, splices and rbf are supported by extending the interactive tx session protocol to include musig2 nonce, which are attached to the `tx_complete` message. There are 2 types of nonces: - "funding nonces", which are used to sign a new funding tx that spends the current funding tx (splice, rbf). - "commit nonces", which are used to sign the commit tx that is one of the outputs of the interactive session. "funding nonces" can be randomly generated on-the-fly: either the interactive session will fail, and they can be forgotten, or it will succeed and we'll get a new, fully signed funding tx. "commit nonces" can be deterministically generated. This make nonce exchange simpler to reason about: - when we send `tx_complete`, we know exactly what the funding tx and commit tx will be (so the funding tx id can be mixed in the nonce generation process). - dual funding, splice and rbf message do not need to be modified Channel re-establishment becomes a bit more complex, as one node could still be waiting for signatures while the other has completed the splice workflow, but it can be mitigated by storing the last sent commit_sig and re-sending it again if needed. --- .../main/scala/fr/acinq/eclair/Features.scala | 14 + .../bitcoind/rpc/BitcoinCoreClient.scala | 2 +- .../fr/acinq/eclair/channel/ChannelData.scala | 6 +- .../eclair/channel/ChannelExceptions.scala | 1 + .../eclair/channel/ChannelFeatures.scala | 35 +- .../fr/acinq/eclair/channel/Commitments.scala | 166 +++- .../fr/acinq/eclair/channel/Helpers.scala | 236 +++-- .../fr/acinq/eclair/channel/fsm/Channel.scala | 167 +++- .../channel/fsm/ChannelOpenDualFunded.scala | 11 +- .../channel/fsm/ChannelOpenSingleFunded.scala | 176 +++- .../channel/fsm/CommonFundingHandlers.scala | 18 +- .../eclair/channel/fsm/CommonHandlers.scala | 5 +- .../eclair/channel/fsm/ErrorHandlers.scala | 2 +- .../channel/fund/InteractiveTxBuilder.scala | 190 +++- .../channel/publish/ReplaceableTxFunder.scala | 21 +- .../crypto/keymanager/ChannelKeyManager.scala | 35 +- .../keymanager/LocalChannelKeyManager.scala | 44 +- .../eclair/transactions/Transactions.scala | 39 +- .../wire/internal/channel/ChannelCodecs.scala | 4 +- .../channel/version0/ChannelCodecs0.scala | 3 +- .../channel/version1/ChannelCodecs1.scala | 3 +- .../channel/version2/ChannelCodecs2.scala | 3 +- .../channel/version3/ChannelCodecs3.scala | 5 +- .../channel/version4/ChannelCodecs4.scala | 5 +- .../channel/version5/ChannelCodecs5.scala | 918 ++++++++++++++++++ .../channel/version5/ChannelTypes5.scala | 87 ++ .../eclair/wire/protocol/ChannelTlv.scala | 56 +- .../eclair/wire/protocol/CommonCodecs.scala | 9 + .../acinq/eclair/wire/protocol/HtlcTlv.scala | 20 +- .../wire/protocol/InteractiveTxTlv.scala | 27 +- .../wire/protocol/LightningMessageTypes.scala | 77 +- .../channel/InteractiveTxBuilderSpec.scala | 48 +- .../publish/ReplaceableTxPublisherSpec.scala | 115 ++- .../ChannelStateTestsHelperMethods.scala | 12 +- .../b/WaitForDualFundingSignedStateSpec.scala | 32 + ...WaitForDualFundingConfirmedStateSpec.scala | 18 +- .../states/e/NormalSplicesStateSpec.scala | 70 +- .../channel/states/e/OfflineStateSpec.scala | 10 +- .../states/g/NegotiatingStateSpec.scala | 48 + .../channel/states/h/ClosingStateSpec.scala | 142 ++- .../internal/channel/ChannelCodecsSpec.scala | 6 +- .../protocol/LightningMessageCodecsSpec.scala | 11 +- .../acinq/eclair/api/handlers/Channel.scala | 6 +- 43 files changed, 2547 insertions(+), 356 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelTypes5.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index b7b60e287c..c5818878a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -310,6 +310,11 @@ object Features { val mandatory = 60 } + case object SimpleTaproot extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_simple_taproot" + val mandatory = 80 + } + /** This feature bit indicates that the node is a mobile wallet that can be woken up via push notifications. */ case object WakeUpNotificationClient extends Feature with InitFeature { val rfcName = "wake_up_notification_client" @@ -339,6 +344,11 @@ object Features { val mandatory = 154 } + case object SimpleTaprootStaging extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_simple_taproot_staging" + val mandatory = 182 + } + /** * Activate this feature to provide on-the-fly funding to remote nodes, as specified in bLIP 36: https://github.com/lightning/blips/blob/master/blip-0036.md. * TODO: add NodeFeature once bLIP is merged. @@ -381,6 +391,8 @@ object Features { ZeroConf, KeySend, SimpleClose, + SimpleTaproot, + SimpleTaprootStaging, WakeUpNotificationClient, TrampolinePaymentPrototype, AsyncPaymentPrototype, @@ -400,6 +412,8 @@ object Features { TrampolinePaymentPrototype -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil), SimpleClose -> (ShutdownAnySegwit :: Nil), + SimpleTaproot -> (ChannelType :: SimpleClose :: Nil), + SimpleTaprootStaging -> (ChannelType :: SimpleClose :: Nil), AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil), OnTheFlyFunding -> (SplicePrototype :: Nil), FundingFeeCredit -> (OnTheFlyFunding :: Nil) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index 5dfecb60ce..91b6bcff23 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -305,7 +305,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool }) } - private def utxoUpdatePsbt(psbt: Psbt)(implicit ec: ExecutionContext): Future[Psbt] = { + def utxoUpdatePsbt(psbt: Psbt)(implicit ec: ExecutionContext): Future[Psbt] = { val encoded = Base64.getEncoder.encodeToString(Psbt.write(psbt).toByteArray) rpcClient.invoke("utxoupdatepsbt", encoded).map(json => { val JString(base64) = json diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 2e1a9c887f..0ab37f41d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, PossiblyHarmful, typed} +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxId, TxOut} import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} @@ -559,7 +560,7 @@ sealed trait ChannelDataWithCommitments extends PersistentChannelData { final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData { val channelId: ByteVector32 = initFundee.temporaryChannelId } -final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel) extends TransientChannelData { +final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel, nextLocalNonce: Option[kotlin.Pair[SecretNonce, IndividualNonce]] = None) extends TransientChannelData { val channelId: ByteVector32 = initFunder.temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_INTERNAL(params: ChannelParams, @@ -576,7 +577,8 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(params: ChannelParams, pushAmount: MilliSatoshi, commitTxFeerate: FeeratePerKw, remoteFundingPubKey: PublicKey, - remoteFirstPerCommitmentPoint: PublicKey) extends TransientChannelData { + remoteFirstPerCommitmentPoint: PublicKey, + remoteNextLocalNonce: Option[IndividualNonce]) extends TransientChannelData { val channelId: ByteVector32 = params.channelId } final case class DATA_WAIT_FOR_FUNDING_SIGNED(params: ChannelParams, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index eb37925c1d..f6c38a9a50 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -150,4 +150,5 @@ case class CommandUnavailableInThisState (override val channelId: Byte case class ForbiddenDuringSplice (override val channelId: ByteVector32, command: String) extends ChannelException(channelId, s"cannot process $command while splicing") case class ForbiddenDuringQuiescence (override val channelId: ByteVector32, command: String) extends ChannelException(channelId, s"cannot process $command while quiescent") case class ConcurrentRemoteSplice (override val channelId: ByteVector32) extends ChannelException(channelId, "splice attempt canceled, remote initiated splice before us") +case class MissingNextLocalNonce (override val channelId: ByteVector32) extends ChannelException(channelId, "next local nonce tlv is missing") // @formatter:on \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 3ab4c20b0e..a03051ea92 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature} /** @@ -31,9 +31,11 @@ import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeatur case class ChannelFeatures(features: Set[PermanentChannelFeature]) { /** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */ - val paysDirectlyToWallet: Boolean = hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs) && !hasFeature(Features.AnchorOutputsZeroFeeHtlcTx) + val paysDirectlyToWallet: Boolean = hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs) && !hasFeature(Features.AnchorOutputsZeroFeeHtlcTx) && !hasFeature((Features.SimpleTaprootStaging)) /** Legacy option_anchor_outputs is used for Phoenix, because Phoenix doesn't have an on-chain wallet to pay for fees. */ - val commitmentFormat: CommitmentFormat = if (hasFeature(Features.AnchorOutputs)) { + val commitmentFormat: CommitmentFormat = if (hasFeature(Features.SimpleTaprootStaging)) { + SimpleTaprootChannelCommitmentFormat + } else if (hasFeature(Features.AnchorOutputs)) { UnsafeLegacyAnchorOutputsCommitmentFormat } else if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { ZeroFeeHtlcTxAnchorOutputsCommitmentFormat @@ -129,6 +131,18 @@ object ChannelTypes { override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" } + case class SimpleTaprootChannelsStaging(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType { + /** Known channel-type features */ + override def features: Set[ChannelTypeFeature] = Set( + if (scidAlias) Some(Features.ScidAlias) else None, + if (zeroConf) Some(Features.ZeroConf) else None, + Some(Features.SimpleTaprootStaging), + ).flatten + override def paysDirectlyToWallet: Boolean = false + override def commitmentFormat: CommitmentFormat = SimpleTaprootChannelCommitmentFormat + override def toString: String = s"simple_taproot_channel_staging${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" + } + case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { override def features: Set[InitFeature] = featureBits.activated.keySet override def toString: String = s"0x${featureBits.toByteVector.toHex}" @@ -151,12 +165,19 @@ object ChannelTypes { AnchorOutputsZeroFeeHtlcTx(), AnchorOutputsZeroFeeHtlcTx(zeroConf = true), AnchorOutputsZeroFeeHtlcTx(scidAlias = true), - AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)) + AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), + SimpleTaprootChannelsStaging(), + SimpleTaprootChannelsStaging(zeroConf = true), + SimpleTaprootChannelsStaging(scidAlias = true), + SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true), + ) .map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) .toMap // NB: Bolt 2: features must exactly match in order to identify a channel type. - def fromFeatures(features: Features[InitFeature]): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) + def fromFeatures(features: Features[InitFeature]): ChannelType = { + features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) + } /** Pick the channel type based on local and remote feature bits, as defined by the spec. */ def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): SupportedChannelType = { @@ -164,7 +185,9 @@ object ChannelTypes { val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel val zeroConf = canUse(Features.ZeroConf) - if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) { + if (canUse(Features.SimpleTaprootStaging)) { + SimpleTaprootChannelsStaging(scidAlias, zeroConf) + } else if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) { AnchorOutputsZeroFeeHtlcTx(scidAlias, zeroConf) } else if (canUse(Features.AnchorOutputs)) { AnchorOutputs(scidAlias, zeroConf) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 2bf8cc5e34..127e48b0dd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -1,17 +1,20 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Script, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Musig2, Satoshi, SatoshiLong, Script, Transaction, TxId} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} +import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.ChannelConf import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.crypto.{Generators, ShaChain} import fr.acinq.eclair.payment.OutgoingPaymentPacket import fr.acinq.eclair.router.Announcements +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._ @@ -185,6 +188,11 @@ case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: RemoteSignature) object CommitTxAndRemoteSig { def apply(commitTx: CommitTx, remoteSig: ByteVector64): CommitTxAndRemoteSig = CommitTxAndRemoteSig(commitTx, RemoteSignature(remoteSig)) + + def apply(commitTx: CommitTx, sigOrPartialSig: Either[ByteVector64, RemoteSignature.PartialSignatureWithNonce]): CommitTxAndRemoteSig = sigOrPartialSig match { + case Left(sig) => CommitTxAndRemoteSig(commitTx, RemoteSignature.FullSignature(sig)) + case Right(psig) => CommitTxAndRemoteSig(commitTx, RemoteSignature.PartialSignatureWithNonce(psig.partialSig, psig.nonce)) + } } /** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */ @@ -193,10 +201,20 @@ case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: object LocalCommit { def fromCommitSig(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxId: TxId, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, - commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey): Either[ChannelException, LocalCommit] = { + commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey)(implicit log: LoggingAdapter): Either[ChannelException, LocalCommit] = { val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(keyManager, params.channelConfig, params.channelFeatures, localCommitIndex, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, localPerCommitmentPoint, spec) - if (!localCommitTx.checkSig(commit.signature, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) { - return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx)) + commit.sigOrPartialSig match { + case Left(sig) => + if (!localCommitTx.checkSig(sig, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) { + return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx)) + } + case Right(psig) => + val fundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey + val localNonce = keyManager.verificationNonce(localCommitTx.input.outPoint.txid, fundingPubkey, localCommitIndex) + if (!localCommitTx.checkPartialSignature(psig, fundingPubkey, localNonce._2, remoteFundingPubKey)) { + log.debug(s"fromCommitSig: invalid partial signature $psig fundingPubkey = $fundingPubkey, fundingTxIndex = $fundingTxIndex localCommitIndex = $localCommitIndex localNonce = $localNonce remoteFundingPubKey = $remoteFundingPubKey") + return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx)) + } } val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) if (commit.htlcSignatures.size != sortedHtlcTxs.size) { @@ -210,19 +228,35 @@ object LocalCommit { } HtlcTxAndRemoteSig(htlcTx, remoteSig) } - Right(LocalCommit(localCommitIndex, spec, CommitTxAndRemoteSig(localCommitTx, RemoteSignature.FullSignature(commit.signature)), htlcTxsAndRemoteSigs)) + val remoteSig = commit.sigOrPartialSig match { + case Left(sig) => RemoteSignature.FullSignature(sig) + case Right(psig) => psig + } + Right(LocalCommit(localCommitIndex, spec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs)) } } /** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */ -case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePerCommitmentPoint: PublicKey) { - def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo): CommitSig = { +case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePerCommitmentPoint: PublicKey, localSig_opt: Option[CommitSig] = None) { + def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo, remoteNonce_opt: Option[IndividualNonce])(implicit log: LoggingAdapter): CommitSig = { val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, index, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remotePerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Map.empty) + val localFundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) + val (sig, tlvStream) = commitInput match { + case _: InputInfo.TaprootInput => + val localNonce = keyManager.signingNonce(localFundingPubKey.publicKey) + val Some(remoteNonce) = remoteNonce_opt + val Right(localPartialSigOfRemoteTx) = keyManager.partialSign(remoteCommitTx, localFundingPubKey, remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce) + log.debug(s"RemoteCommit.sign localPartialSigOfRemoteTx = $localPartialSigOfRemoteTx fundingTxIndex = $fundingTxIndex remote commit index = $index remote nonce = $remoteNonce") + val tlvStream: TlvStream[CommitSigTlv] = TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(localPartialSigOfRemoteTx, localNonce._2))) + (ByteVector64.Zeroes, tlvStream) + case _ => + val sig = keyManager.sign(remoteCommitTx, localFundingPubKey, TxOwner.Remote, params.commitmentFormat, Map.empty) + (sig, TlvStream[CommitSigTlv]()) + } val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Map.empty)) - CommitSig(params.channelId, sig, htlcSigs.toList) + CommitSig(params.channelId, sig, htlcSigs.toList, tlvStream) } } @@ -618,12 +652,27 @@ case class Commitment(fundingTxIndex: Long, Right(()) } - def sendCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int)(implicit log: LoggingAdapter): (Commitment, CommitSig) = { + def sendCommit(keyManager: ChannelKeyManager, params: ChannelParams, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, nextRemoteNonce_opt: Option[IndividualNonce])(implicit log: LoggingAdapter): (Commitment, CommitSig) = { // remote commitment will include all local proposed changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, remoteCommit.index + 1, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remoteNextPerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Map.empty) - + val localFundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) + val sig = remoteCommitTx.input match { + case _: InputInfo.TaprootInput => + ByteVector64.Zeroes + case _ => + keyManager.sign(remoteCommitTx, localFundingPubKey, TxOwner.Remote, params.commitmentFormat, Map.empty) + } + val partialSig: Set[CommitSigTlv] = remoteCommitTx.input match { + case _: InputInfo.TaprootInput => + val localNonce = keyManager.signingNonce(localFundingPubKey.publicKey) + val Some(remoteNonce) = nextRemoteNonce_opt + val Right(psig) = keyManager.partialSign(remoteCommitTx, localFundingPubKey, remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce) + log.debug(s"sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with fundingTxIndex = $fundingTxIndex remoteCommit.index (should add +1) = ${remoteCommit.index} remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint") + Set(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2))) + case _ => + Set.empty + } val sortedHtlcTxs: Seq[TransactionWithInputInfo] = htlcTxs.sortBy(_.input.outPoint.index) val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Map.empty)) @@ -634,8 +683,9 @@ case class Commitment(fundingTxIndex: Long, val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList, TlvStream(Set( if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None - ).flatten[CommitSigTlv])) - val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)) + ).flatten[CommitSigTlv] ++ partialSig)) + log.debug(s"sendCommit: setting remoteNextPerCommitmentPoint to $remoteNextPerCommitmentPoint") + val nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint, Some(commitSig))) (copy(nextRemoteCommit_opt = Some(nextRemoteCommit)), commitSig) } @@ -659,9 +709,31 @@ case class Commitment(fundingTxIndex: Long, /** Return a fully signed commit tx, that can be published as-is. */ def fullySignedLocalCommitTx(params: ChannelParams, keyManager: ChannelKeyManager): CommitTx = { val unsignedCommitTx = localCommit.commitTxAndRemoteSig.commitTx - val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Local, params.commitmentFormat, Map.empty) - val RemoteSignature.FullSignature(remoteSig) = localCommit.commitTxAndRemoteSig.remoteSig - val commitTx = addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig) + val commitTx = localCommit.commitTxAndRemoteSig.remoteSig match { + case RemoteSignature.FullSignature(remoteSig) => + val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Local, params.commitmentFormat, Map.empty) + addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig) + case RemoteSignature.PartialSignatureWithNonce(remotePsig, remoteNonce) => + val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) + val fundingTxId = if (!params.channelFeatures.hasFeature(Features.DualFunding) && this.fundingTxIndex == 0 && localCommit.index == 0) { + TxId(ByteVector32.One) // special case because for channel establishment v1 we exchange the first nonce before the funding tx id is known + } else { + unsignedCommitTx.input.outPoint.txid + } + val localNonce = keyManager.verificationNonce(fundingTxId, fundingPubKey.publicKey, localCommit.index) + + (for { + partialSig <- keyManager.partialSign(unsignedCommitTx, fundingPubKey, remoteFundingPubKey, TxOwner.Local, localNonce, remoteNonce) + inputIndex = unsignedCommitTx.tx.txIn.indexWhere(_.outPoint == unsignedCommitTx.input.outPoint) + aggSig <- Musig2.aggregateTaprootSignatures( + Seq(partialSig, remotePsig), + unsignedCommitTx.tx, inputIndex, Seq(unsignedCommitTx.input.txOut), + Scripts.sort(Seq(fundingPubKey.publicKey, remoteFundingPubKey)), + Seq(localNonce._2, remoteNonce), + None) + signedCommitTx = unsignedCommitTx.copy(tx = unsignedCommitTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggSig))) + } yield signedCommitTx).fold(t => throw new IllegalArgumentException("commit signatures are invalid"), identity) + } // We verify the remote signature when receiving their commit_sig, so this check should always pass. require(checkSpendable(commitTx).isSuccess, "commit signatures are invalid") commitTx @@ -685,7 +757,7 @@ object Commitment { val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex).publicKey val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - val remotePaymentPubkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) { + val remotePaymentPubkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey) || channelFeatures.hasFeature(Features.SimpleTaproot) || channelFeatures.hasFeature(Features.SimpleTaprootStaging)) { remoteParams.paymentBasepoint } else { Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) @@ -713,7 +785,7 @@ object Commitment { val channelKeyPath = keyManager.keyPath(localParams, channelConfig) val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath, fundingTxIndex).publicKey val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - val localPaymentPubkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey)) { + val localPaymentPubkey = if (channelFeatures.hasFeature(Features.StaticRemoteKey) || channelFeatures.hasFeature(Features.SimpleTaprootStaging)) { localPaymentBasepoint } else { Generators.derivePubKey(localPaymentBasepoint, remotePerCommitmentPoint) @@ -1006,11 +1078,22 @@ case class Commitments(params: ChannelParams, } } - def sendCommit(keyManager: ChannelKeyManager)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = { + def sendCommit(keyManager: ChannelKeyManager, nextRemoteNonces: List[IndividualNonce] = List.empty)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = { remoteNextCommitInfo match { case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId)) case Right(remoteNextPerCommitmentPoint) => - val (active1, sigs) = active.map(_.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size)).unzip + var nonceIndex = 0 + val (active1, sigs) = active.map { c => + val remoteNonce_opt = c.commitInput match { + case _: InputInfo.TaprootInput => + val n = nextRemoteNonces(nonceIndex) + nonceIndex = nonceIndex + 1 + Some(n) + case _ => None + } + c.sendCommit(keyManager, params, changes, remoteNextPerCommitmentPoint, active.size, remoteNonce_opt) + } unzip + val commitments1 = copy( changes = changes.copy( localChanges = changes.localChanges.copy(proposed = Nil, signed = changes.localChanges.proposed), @@ -1033,6 +1116,10 @@ case class Commitments(params: ChannelParams, } val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 1) + + val fundingIndexes = active.map(_.fundingTxIndex).toSet + if (fundingIndexes.size > 1) log.warning(s"more than 1 funding tx index") + // Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments. val active1 = active.zip(commits).map { case (commitment, commit) => commitment.receiveCommit(keyManager, params, changes, localPerCommitmentPoint, commit) match { @@ -1043,10 +1130,20 @@ case class Commitments(params: ChannelParams, // we will send our revocation preimage + our next revocation hash val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, localCommitIndex) val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, localCommitIndex + 2) + val tlvStream: TlvStream[RevokeAndAckTlv] = { + val nonces = this.active.filter(_.commitInput.isInstanceOf[InputInfo.TaprootInput]).map(c => { + val fundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, c.fundingTxIndex).publicKey + val n = keyManager.verificationNonce(c.fundingTxId, fundingPubkey, localCommitIndex + 2) + log.debug(s"revokeandack: creating verification nonce ${n._2} fundingIndex = ${c.fundingTxIndex} commit index = ${localCommitIndex + 2}") + n + }) + if (nonces.isEmpty) TlvStream.empty else TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList)) + } val revocation = RevokeAndAck( channelId = channelId, perCommitmentSecret = localPerCommitmentSecret, - nextPerCommitmentPoint = localNextPerCommitmentPoint + nextPerCommitmentPoint = localNextPerCommitmentPoint, + tlvStream ) val commitments1 = copy( changes = changes.copy( @@ -1063,6 +1160,7 @@ case class Commitments(params: ChannelParams, remoteNextCommitInfo match { case Right(_) => Left(UnexpectedRevocation(channelId)) case Left(_) if revocation.perCommitmentSecret.publicKey != active.head.remoteCommit.remotePerCommitmentPoint => Left(InvalidRevocation(channelId)) + case Left(_) if this.active.exists(_.commitInput.isInstanceOf[InputInfo.TaprootInput]) && revocation.nexLocalNonces.isEmpty => Left(MissingNextLocalNonce(channelId)) case Left(_) => // Since htlcs are shared across all commitments, we generate the actions only once based on the first commitment. val receivedHtlcs = changes.remoteChanges.signed.collect { @@ -1154,18 +1252,28 @@ case class Commitments(params: ChannelParams, active.forall { commitment => val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey val remoteFundingKey = commitment.remoteFundingPubKey - val fundingScript = Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey)) commitment.commitInput match { - case InputInfo.SegwitInput(_, _, redeemScript) => redeemScript == fundingScript - case _: InputInfo.TaprootInput => false + case s: InputInfo.SegwitInput => s.redeemScript == Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey)) + case t: InputInfo.TaprootInput => t.internalKey == Scripts.Taproot.musig2Aggregate(localFundingKey, remoteFundingKey) && t.redeemPath.isInstanceOf[RedeemPath.KeyPath] } } } /** This function should be used to ignore a commit_sig that we've already received. */ - def ignoreRetransmittedCommitSig(commitSig: CommitSig): Boolean = { - val RemoteSignature.FullSignature(latestRemoteSig) = latest.localCommit.commitTxAndRemoteSig.remoteSig - params.channelFeatures.hasFeature(Features.DualFunding) && commitSig.batchSize == 1 && latestRemoteSig == commitSig.signature + def ignoreRetransmittedCommitSig(commitSig: CommitSig, keyManager: ChannelKeyManager): Boolean = commitSig.sigOrPartialSig match { + case _ if !params.channelFeatures.hasFeature(Features.DualFunding) => false + case _ if commitSig.batchSize != 1 => false + case Left(sig) => + latest.localCommit.commitTxAndRemoteSig.remoteSig match { + case f: RemoteSignature.FullSignature => f.sig == sig + case _: RemoteSignature.PartialSignatureWithNonce => false + } + case Right(psig) => + // we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed) + // => instead we simply check that the provided partial signature is valid for our latest commit tx + val localFundingKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, latest.fundingTxIndex).publicKey + val (_, localNonce) = keyManager.verificationNonce(latest.fundingTxId, localFundingKey, latest.localCommit.index) + latest.localCommit.commitTxAndRemoteSig.commitTx.checkPartialSignature(psig, localFundingKey, localNonce, latest.remoteFundingPubKey) } def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = { 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..18df8d52f8 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 @@ -18,6 +18,7 @@ package fr.acinq.eclair.channel import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ @@ -135,6 +136,7 @@ object Helpers { } val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) + if ((channelFeatures.hasFeature(Features.SimpleTaproot) || channelFeatures.hasFeature(Features.SimpleTaprootStaging)) && open.nexLocalNonce_opt.isEmpty) return Left(MissingNextLocalNonce(open.temporaryChannelId)) // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large. val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingSatoshis) @@ -242,6 +244,7 @@ object Helpers { if (reserveToFundingRatio > nodeParams.channelConf.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.channelConf.maxReserveToFundingRatio)) val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) + if ((channelFeatures.hasFeature(Features.SimpleTaproot) || channelFeatures.hasFeature(Features.SimpleTaprootStaging)) && accept.nexLocalNonce_opt.isEmpty) return Left(MissingNextLocalNonce(open.temporaryChannelId)) extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } @@ -539,10 +542,20 @@ object Helpers { val channelKeyPath = keyManager.keyPath(commitments.params.localParams, commitments.params.channelConfig) val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, commitments.localCommitIndex - 1) val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommitIndex + 1) + val nonces = commitments.active.filter(_.commitInput.isInstanceOf[InputInfo.TaprootInput]).map { c => + val fundingPubkey = keyManager.fundingPublicKey(commitments.params.localParams.fundingKeyPath, c.fundingTxIndex).publicKey + keyManager.verificationNonce(c.fundingTxId, fundingPubkey, commitments.localCommitIndex + 1) + } + val tlvStream: TlvStream[RevokeAndAckTlv] = if (nonces.isEmpty) { + TlvStream.empty + } else { + TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map(_._2).toList)) + } val revocation = RevokeAndAck( channelId = commitments.channelId, perCommitmentSecret = localPerCommitmentSecret, - nextPerCommitmentPoint = localNextPerCommitmentPoint + nextPerCommitmentPoint = localNextPerCommitmentPoint, + tlvStream ) checkRemoteCommit(remoteChannelReestablish, retransmitRevocation_opt = Some(revocation)) } else if (commitments.localCommitIndex > remoteChannelReestablish.nextRemoteRevocationNumber + 1) { @@ -730,13 +743,18 @@ object Helpers { } /** We are the closer: we sign closing transactions for which we pay the fees. */ - def makeSimpleClosingTx(currentBlockHeight: BlockHeight, keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = { + def makeSimpleClosingTx(currentBlockHeight: BlockHeight, keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw, localClosingNonce_opt: Option[(SecretNonce, IndividualNonce)] = None, remoteClosingNonce_opt: Option[IndividualNonce] = None): Either[ChannelException, (ClosingTxs, ClosingComplete)] = { // We must convert the feerate to a fee: we must build dummy transactions to compute their weight. val closingFee = { val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) dummyClosingTxs.preferred_opt match { case Some(dummyTx) => - val dummySignedTx = Transactions.addSigs(dummyTx, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig) + val dummySignedTx = commitment.commitInput match { + case _: InputInfo.TaprootInput => + Transactions.addAggregatedSignature(dummyTx, Transactions.PlaceHolderSig) + case _: InputInfo.SegwitInput => + Transactions.addSigs(dummyTx, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig) + } SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.tx.weight())) case None => return Left(CannotGenerateClosingTx(commitment.channelId)) } @@ -748,11 +766,27 @@ object Helpers { case _ => return Left(CannotGenerateClosingTx(commitment.channelId)) } val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, currentBlockHeight.toLong, TlvStream(Set( - closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), - closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), - closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), - ).flatten[ClosingTlv])) + val tlvs = commitment.commitInput match { + case _: InputInfo.TaprootInput => + def partialSign(tx: ClosingTx) = { + val Right(psig) = keyManager.partialSign(tx, localFundingPubKey, commitment.remoteFundingPubKey, TxOwner.Local, localClosingNonce_opt.get, remoteClosingNonce_opt.get) + psig + } + + TlvStream(Set( + closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputsPartialSignature(partialSign(tx))), + closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnlyPartialSignature(partialSign(tx))), + closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnlyPartialSignature(partialSign(tx))), + ).flatten[ClosingTlv]) + case _: InputInfo.SegwitInput => + TlvStream(Set( + closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), + closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), + closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), + ).flatten[ClosingTlv]) + } + + val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, currentBlockHeight.toLong, tlvs) Right(closingTxs, closingComplete) } @@ -762,33 +796,67 @@ object Helpers { * Callers should ignore failures: since the protocol is fully asynchronous, failures here simply mean that they * are not using our latest script (race condition between our closing_complete and theirs). */ - def signSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = { + def signSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete, localClosingNonce_opt: Option[(SecretNonce, IndividualNonce)] = None, remoteClosingNonce_opt: Option[IndividualNonce] = None): Either[ChannelException, (ClosingTx, ClosingSig)] = { val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees) val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) // If our output isn't dust, they must provide a signature for a transaction that includes it. // Note that we're the closee, so we look for signatures including the closee output. - (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { - case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) - case (Some(_), None) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) - case (None, Some(_)) if closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) - case _ => () + commitment.commitInput match { + case _: InputInfo.TaprootInput => + (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { + case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty && closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (Some(_), None) if closingComplete.closerAndCloseeOutputsPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (None, Some(_)) if closingComplete.closeeOutputOnlyPartialSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case _ => () + } + case _: InputInfo.SegwitInput => + (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { + case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (Some(_), None) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case (None, Some(_)) if closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId)) + case _ => () + } } - // We choose the closing signature that matches our preferred closing transaction. - val closingTxsWithSigs = Seq( - closingComplete.closerAndCloseeOutputsSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndCloseeOutputs(localSig)))), - closingComplete.closeeOutputOnlySig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloseeOutputOnly(localSig)))), - closingComplete.closerOutputOnlySig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserOutputOnly(localSig)))), - ).flatten - closingTxsWithSigs.headOption match { - case Some((closingTx, remoteSig, sigToTlv)) => - val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) - Transactions.checkSpendable(signedClosingTx) match { - case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) - case Success(_) => Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig)))) + + commitment.commitInput match { + case _: InputInfo.TaprootInput => + // We choose the closing signature that matches our preferred closing transaction. + val closingTxsWithSigs = Seq( + closingComplete.closerAndCloseeOutputsPartialSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndCloseeOutputsPartialSignature(localSig)))), + closingComplete.closeeOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloseeOutputOnlyPartialSignature(localSig)))), + closingComplete.closerOutputOnlyPartialSig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserOutputOnlyPartialSignature(localSig)))), + ).flatten + closingTxsWithSigs.headOption match { + case Some((closingTx, remoteSig, sigToTlv)) => + val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) + (for { + localSig <- keyManager.partialSign(closingTx, localFundingPubKey, commitment.remoteFundingPubKey, TxOwner.Local, localClosingNonce_opt.get, remoteClosingNonce_opt.get) + aggregatedSignature <- Transactions.aggregatePartialSignatures(closingTx, localSig, remoteSig, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localClosingNonce_opt.get._2, remoteClosingNonce_opt.get) + signedClosingTx = Transactions.addAggregatedSignature(closingTx, aggregatedSignature) + } yield (signedClosingTx, localSig)) match { + case Right((signedClosingTx, localSig)) if Transactions.checkSpendable(signedClosingTx).isSuccess => Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig)))) + case _ => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + } + case None => Left(MissingCloseSignature(commitment.channelId)) + } + case _: InputInfo.SegwitInput => + // We choose the closing signature that matches our preferred closing transaction. + val closingTxsWithSigs = Seq( + closingComplete.closerAndCloseeOutputsSig_opt.flatMap(remoteSig => closingTxs.localAndRemote_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserAndCloseeOutputs(localSig)))), + closingComplete.closeeOutputOnlySig_opt.flatMap(remoteSig => closingTxs.localOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloseeOutputOnly(localSig)))), + closingComplete.closerOutputOnlySig_opt.flatMap(remoteSig => closingTxs.remoteOnly_opt.map(tx => (tx, remoteSig, localSig => ClosingTlv.CloserOutputOnly(localSig)))), + ).flatten + closingTxsWithSigs.headOption match { + case Some((closingTx, remoteSig, sigToTlv)) => + val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) + val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) + val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) + Transactions.checkSpendable(signedClosingTx) match { + case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + case Success(_) => Right(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig)))) + } + case None => Left(MissingCloseSignature(commitment.channelId)) } - case None => Left(MissingCloseSignature(commitment.channelId)) } } @@ -799,22 +867,44 @@ object Helpers { * sent another closing_complete before receiving their closing_sig, which is now obsolete: we ignore it and wait * for their next closing_sig that will match our latest closing_complete. */ - def receiveSimpleClosingSig(keyManager: ChannelKeyManager, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig): Either[ChannelException, ClosingTx] = { - val closingTxsWithSig = Seq( - closingSig.closerAndCloseeOutputsSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))), - closingSig.closerOutputOnlySig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))), - closingSig.closeeOutputOnlySig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))), - ).flatten - closingTxsWithSig.headOption match { - case Some((closingTx, remoteSig)) => - val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) - val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) - Transactions.checkSpendable(signedClosingTx) match { - case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) - case Success(_) => Right(signedClosingTx) + def receiveSimpleClosingSig(keyManager: ChannelKeyManager, commitment: FullCommitment, closingTxs: ClosingTxs, closingSig: ClosingSig, localNonce: Option[(SecretNonce, IndividualNonce)] = None, remoteNonce: Option[IndividualNonce] = None): Either[ChannelException, ClosingTx] = { + commitment.commitInput match { + case _: InputInfo.TaprootInput => + val closingTxsWithSig = Seq( + closingSig.closerAndCloseeOutputsPartialSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))), + closingSig.closerOutputOnlyPartialSig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))), + closingSig.closeeOutputOnlyPartialSig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))), + ).flatten + closingTxsWithSig.headOption match { + case Some((closingTx, remoteSig)) => + val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) + (for { + localSig <- keyManager.partialSign(closingTx, localFundingPubKey, commitment.remoteFundingPubKey, TxOwner.Local, localNonce.get, remoteNonce.get) + aggregatedSig <- Transactions.aggregatePartialSignatures(closingTx, localSig, remoteSig, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localNonce.get._2, remoteNonce.get) + signedClosingTx = Transactions.addAggregatedSignature(closingTx, aggregatedSig) + } yield signedClosingTx) match { + case Right(signedClosingTx) if Transactions.checkSpendable(signedClosingTx).isSuccess => Right(signedClosingTx) + case _ => Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + } + case None => Left(MissingCloseSignature(commitment.channelId)) + } + case _: InputInfo.SegwitInput => + val closingTxsWithSig = Seq( + closingSig.closerAndCloseeOutputsSig_opt.flatMap(sig => closingTxs.localAndRemote_opt.map(tx => (tx, sig))), + closingSig.closerOutputOnlySig_opt.flatMap(sig => closingTxs.localOnly_opt.map(tx => (tx, sig))), + closingSig.closeeOutputOnlySig_opt.flatMap(sig => closingTxs.remoteOnly_opt.map(tx => (tx, sig))), + ).flatten + closingTxsWithSig.headOption match { + case Some((closingTx, remoteSig)) => + val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) + val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) + val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) + Transactions.checkSpendable(signedClosingTx) match { + case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + case Success(_) => Right(signedClosingTx) + } + case None => Left(MissingCloseSignature(commitment.channelId)) } - case None => Left(MissingCloseSignature(commitment.channelId)) } } @@ -1070,10 +1160,10 @@ object Helpers { (localPaymentPubkey, remoteDelayedPaymentPubkey) } val claimAnchorTxs = List( - withTxGenerationLog("local-anchor") { + withTxGenerationLog("local-anchor-from-remote-commit-tx") { Transactions.makeClaimLocalAnchorOutputTx(rcp.commitTx, localAnchorKey, confirmationTarget) }, - withTxGenerationLog("remote-anchor") { + withTxGenerationLog("remote-anchor-from-remote-commit-tx") { Transactions.makeClaimRemoteAnchorOutputTx(rcp.commitTx, remoteAnchorKey) } ).flatten @@ -1274,23 +1364,43 @@ object Helpers { // we retrieve the information needed to rebuild htlc scripts val htlcInfos = db.listHtlcInfos(channelId, commitmentNumber) log.info("got {} htlcs for commitmentNumber={}", htlcInfos.size, commitmentNumber) - val htlcsRedeemScripts = ( - htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry, commitmentFormat) } ++ - htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), commitmentFormat) } - ) - .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) - .toMap - - // and finally we steal the htlc outputs - val htlcPenaltyTxs = commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => - val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) - withTxGenerationLog("htlc-penalty") { - Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, finalScriptPubKey, feeratePenalty).map(htlcPenalty => { - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Map.empty) - Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) - }) - } - }.toList.flatten + + val htlcPenaltyTxs = commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + val scriptTrees = ( + htlcInfos.map { case (paymentHash, cltvExpiry) => Taproot.receivedHtlcScriptTree(remoteHtlcPubkey, localHtlcPubkey, paymentHash, cltvExpiry) } ++ + htlcInfos.map { case (paymentHash, _) => Taproot.offeredHtlcScriptTree(remoteHtlcPubkey, localHtlcPubkey, paymentHash) }) + .map(scriptTree => Script.write(Script.pay2tr(remoteRevocationPubkey.xOnly, Some(scriptTree))) -> scriptTree) + .toMap + + commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if scriptTrees.contains(txOut.publicKeyScript) => + val scriptTree = scriptTrees(txOut.publicKeyScript) + withTxGenerationLog("htlc-penalty") { + Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, remoteRevocationPubkey.xOnly, scriptTree, localParams.dustLimit, finalScriptPubKey, feeratePenalty).map(htlcPenalty => { + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Map.empty) + Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) + }) + } + }.toList.flatten + case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => + val htlcsRedeemScripts = ( + htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry, commitmentFormat) } ++ + htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), commitmentFormat) } + ) + .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) + .toMap + + // and finally we steal the htlc outputs + commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => + val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) + withTxGenerationLog("htlc-penalty") { + Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, finalScriptPubKey, feeratePenalty).map(htlcPenalty => { + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Map.empty) + Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) + }) + } + }.toList.flatten + } RevokedCommitPublished( commitTx = commitTx, 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 170d60eee4..558e78e49b 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 @@ -20,6 +20,7 @@ import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter} import akka.actor.{Actor, ActorContext, ActorRef, FSM, OneForOneStrategy, PossiblyHarmful, Props, SupervisorStrategy, typed} import akka.event.Logging.MDC +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction, TxId} import fr.acinq.eclair.Logs.LogCategory @@ -49,7 +50,9 @@ import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, ClosingTx, DefaultCommitmentFormat, SimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{ClosingTx, InputInfo} import fr.acinq.eclair.transactions._ +import fr.acinq.eclair.wire.protocol.ChannelTlv.NextLocalNoncesTlv import fr.acinq.eclair.wire.protocol._ import scala.collection.immutable.Queue @@ -201,6 +204,20 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val keyManager: ChannelKeyManager = nodeParams.channelKeyManager + // remote nonces, one for each active commitment, with the same ordering + var remoteNextLocalNonces: List[IndividualNonce] = List.empty + + // // will be added to remoteNextLocalNonces once a splice has been completed + var pendingRemoteNextLocalNonce: Option[IndividualNonce] = None + + def setRemoteNextLocalNonces(info: String, n: List[IndividualNonce]): Unit = { + this.remoteNextLocalNonces = n + log.debug("{} set remoteNextLocalNonces to {}", info, remoteNextLocalNonces) + } + + var localClosingNonce: Option[(SecretNonce, IndividualNonce)] = None // used to sign closing txs + var remoteClosingNonce: Option[IndividualNonce] = None + // we pass these to helpers classes so that they have the logging context implicit def implicitLog: akka.event.DiagnosticLoggingAdapter = diagLog @@ -558,7 +575,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("ignoring CMD_SIGN (nothing to sign)") stay() case Right(_) => - d.commitments.sendCommit(keyManager) match { + d.commitments.sendCommit(keyManager, this.remoteNextLocalNonces) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String) val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit @@ -615,7 +632,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with stay() using d1 storing() sending signingSession1.localSigs calling endQuiescence(d1) } } - case _ if d.commitments.ignoreRetransmittedCommitSig(commit) => + case _ if d.commitments.ignoreRetransmittedCommitSig(commit, keyManager) => // We haven't received our peer's tx_signatures for the latest funding transaction and asked them to resend it on reconnection. // They also resend their corresponding commit_sig, but we have already received it so we should ignore it. // Note that the funding transaction may have confirmed while we were reconnecting. @@ -662,6 +679,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with d.commitments.receiveRevocation(revocation, nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).dustTolerance.maxExposure) match { case Right((commitments1, actions)) => cancelTimer(RevocationTimeout.toString) + setRemoteNextLocalNonces("received RevokeAndAck", revocation.nexLocalNonces) log.debug("received a new rev, spec:\n{}", commitments1.latest.specs2String) actions.foreach { case PostRevocationAction.RelayHtlc(add) => @@ -681,8 +699,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with if (d.remoteShutdown.isDefined && !commitments1.changes.localHasUnsignedOutgoingHtlcs) { // we were waiting for our pending htlcs to be signed before replying with our local shutdown val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) - val localShutdown = Shutdown(d.channelId, finalScriptPubKey) - // this should always be defined, we provide a fallback for backward compat with older channels + val tlvStream: TlvStream[ShutdownTlv] = d.commitments.latest.commitInput match { + case _: InputInfo.TaprootInput => + val localFundingPubKey = keyManager.fundingPublicKey(d.commitments.latest.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex).publicKey + log.info("generating closing nonce {} with fundingKeyPath = {} fundingTxIndex = {}", localClosingNonce, d.commitments.latest.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex) + localClosingNonce = Some(keyManager.signingNonce(localFundingPubKey)) + TlvStream(ShutdownTlv.ShutdownNonce(localClosingNonce.get._2)) + case _: InputInfo.SegwitInput => + TlvStream.empty + } + val localShutdown = Shutdown(d.channelId, finalScriptPubKey, tlvStream) + // this should always be defined, we provide a fallback for backward compat with older channels val closeStatus = d.closeStatus_opt.getOrElse(CloseStatus.NonInitiator(None)) // note: it means that we had pending htlcs to sign, therefore we go to SHUTDOWN, not to NEGOTIATING require(commitments1.latest.remoteCommit.spec.htlcs.nonEmpty, "we must have just signed new htlcs, otherwise we would have sent our Shutdown earlier") @@ -708,7 +735,16 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with d.commitments.params.validateLocalShutdownScript(localScriptPubKey) match { case Left(e) => handleCommandError(e, c) case Right(localShutdownScript) => - val shutdown = Shutdown(d.channelId, localShutdownScript) + val tlvStream: TlvStream[ShutdownTlv] = d.commitments.latest.commitInput match { + case _: InputInfo.TaprootInput => + val localFundingPubKey = keyManager.fundingPublicKey(d.commitments.latest.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex).publicKey + log.info("generating closing nonce {} with fundingKeyPath = {} fundingTxIndex = {}", localClosingNonce, d.commitments.latest.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex) + localClosingNonce = Some(keyManager.signingNonce(localFundingPubKey)) + TlvStream(ShutdownTlv.ShutdownNonce(localClosingNonce.get._2)) + case _: InputInfo.SegwitInput => + TlvStream.empty + } + val shutdown = Shutdown(d.channelId, localShutdownScript, tlvStream) handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown), closeStatus_opt = Some(CloseStatus.Initiator(c.feerates)))) storing() sending shutdown } } @@ -752,12 +788,26 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // in the meantime we won't send new changes stay() using d.copy(remoteShutdown = Some(remoteShutdown), closeStatus_opt = Some(CloseStatus.NonInitiator(None))) } else { + if (d.commitments.latest.commitInput.isInstanceOf[InputInfo.TaprootInput]) { + require(remoteShutdown.shutdownNonce_opt.isDefined, "missing shutdown nonce") + } + remoteClosingNonce = remoteShutdown.shutdownNonce_opt + // so we don't have any unsigned outgoing changes val (localShutdown, sendList) = d.localShutdown match { case Some(localShutdown) => (localShutdown, Nil) case None => - val localShutdown = Shutdown(d.channelId, getOrGenerateFinalScriptPubKey(d)) + val tlvStream: TlvStream[ShutdownTlv] = d.commitments.latest.commitInput match { + case _: InputInfo.TaprootInput => + val localFundingPubKey = keyManager.fundingPublicKey(d.commitments.latest.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex).publicKey + log.info("generating closing nonce {} with fundingKeyPath = {} fundingTxIndex = {}", localClosingNonce, d.commitments.latest.localParams.fundingKeyPath, d.commitments.latest.fundingTxIndex) + localClosingNonce = Some(keyManager.signingNonce(localFundingPubKey)) + TlvStream(ShutdownTlv.ShutdownNonce(localClosingNonce.get._2)) + case _: InputInfo.SegwitInput => + TlvStream.empty + } + val localShutdown = Shutdown(d.channelId, getOrGenerateFinalScriptPubKey(d), tlvStream) // we need to send our shutdown if we didn't previously (localShutdown, localShutdown :: Nil) } @@ -771,7 +821,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) { // there are no pending signed changes, let's directly negotiate a closing transaction if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) { - val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus) + val (d1, closingComplete_opt) = startSimpleClose(d.commitments, localShutdown, remoteShutdown, closeStatus, localClosingNonce) goto(NEGOTIATING_SIMPLE) using d1 storing() sending sendList ++ closingComplete_opt.toSeq } else if (d.commitments.params.localParams.paysClosingFees) { // we pay the closing fees, so we initiate the negotiation by sending the first closing_signed @@ -1057,6 +1107,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceAck.requireConfirmedInputs) ) val sessionId = randomBytes32() + log.debug("spawning InteractiveTxBuilder with remoteNextLocalNonces {}", remoteNextLocalNonces) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( sessionId, nodeParams, fundingParams, @@ -1297,8 +1348,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info("ignoring outgoing interactive-tx message {} from previous session", msg.getClass.getSimpleName) stay() } - case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt) => + case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteNonce_opt) => log.info(s"splice tx created with fundingTxIndex=${signingSession.fundingTxIndex} fundingTxId=${signingSession.fundingTx.txId}") + this.pendingRemoteNextLocalNonce = nextRemoteNonce_opt cmd_opt.foreach(cmd => cmd.replyTo ! RES_SPLICE(fundingTxIndex = signingSession.fundingTxIndex, signingSession.fundingTx.txId, signingSession.fundingParams.fundingAmount, signingSession.localCommit.fold(_.spec, _.spec).toLocal)) remoteCommitSig_opt.foreach(self ! _) liquidityPurchase_opt.collect { @@ -1333,6 +1385,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Right((commitments1, _)) => log.info("publishing funding tx for channelId={} fundingTxId={}", d.channelId, fundingTx.signedTx.txid) Metrics.recordSplice(dfu.fundingParams, fundingTx.tx) + // README: splice has been completed, update remote nonces with the one sent in splice_init/splice_ack + setRemoteNextLocalNonces("received TxSignatures", this.pendingRemoteNextLocalNonce.toList ++ this.remoteNextLocalNonces) stay() using d.copy(commitments = commitments1) storing() calling publishFundingTx(dfu1) case Left(_) => stay() @@ -1353,6 +1407,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice) log.info("publishing funding tx for channelId={} fundingTxId={}", d.channelId, signingSession1.fundingTx.sharedTx.txId) Metrics.recordSplice(signingSession1.fundingTx.fundingParams, signingSession1.fundingTx.sharedTx.tx) + setRemoteNextLocalNonces("end of quiescence", this.pendingRemoteNextLocalNonce.toList ++ this.remoteNextLocalNonces) stay() using d1 storing() sending signingSession1.localSigs calling publishFundingTx(signingSession1.fundingTx) calling endQuiescence(d1) } case _ => @@ -1550,7 +1605,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("ignoring CMD_SIGN (nothing to sign)") stay() case Right(_) => - d.commitments.sendCommit(keyManager) match { + d.commitments.sendCommit(keyManager, this.remoteNextLocalNonces) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", commitments1.latest.specs2String) val nextRemoteCommit = commitments1.latest.nextRemoteCommit_opt.get.commit @@ -1748,6 +1803,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val localClosingFees = MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf) val nextPreferredFee = MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee) MutualClose.makeClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) + Closing.MutualClose.makeClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) } val closingTxProposed1 = (d.closingTxProposed: @unchecked) match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) @@ -1824,7 +1880,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // No need to persist their latest script, they will re-sent it on reconnection. stay() using d.copy(remoteScriptPubKey = closingComplete.closerScriptPubKey) sending Warning(d.channelId, InvalidCloseeScript(d.channelId, closingComplete.closeeScriptPubKey, d.localScriptPubKey).getMessage) } else { - MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, closingComplete.closeeScriptPubKey, closingComplete.closerScriptPubKey, closingComplete) match { + MutualClose.signSimpleClosingTx(keyManager, d.commitments.latest, closingComplete.closeeScriptPubKey, closingComplete.closerScriptPubKey, closingComplete, localClosingNonce, remoteClosingNonce) match { case Left(f) => log.warning("invalid closing_complete: {}", f.getMessage) stay() sending Warning(d.channelId, f.getMessage) @@ -1839,7 +1895,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // Note that if we sent two closing_complete in a row, without waiting for their closing_sig for the first one, // this will fail because we only care about our latest closing_complete. This is fine, we should receive their // closing_sig for the last closing_complete afterwards. - MutualClose.receiveSimpleClosingSig(keyManager, d.commitments.latest, d.proposedClosingTxs.last, closingSig) match { + MutualClose.receiveSimpleClosingSig(keyManager, d.commitments.latest, d.proposedClosingTxs.last, closingSig, localClosingNonce, remoteClosingNonce) match { case Left(f) => log.warning("invalid closing_sig: {}", f.getMessage) stay() sending Warning(d.channelId, f.getMessage) @@ -2261,13 +2317,21 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val channelKeyPath = keyManager.keyPath(d.channelParams.localParams, d.channelParams.channelConfig) val myFirstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0) val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTx.txId)) + val myNextLocalNonce = d.signingSession.commitInput match { + case _: InputInfo.TaprootInput => + val localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.channelParams.localParams.fundingKeyPath, 0).publicKey + val (_, publicNonce) = keyManager.verificationNonce(d.signingSession.fundingTx.txId, localFundingPubKey, 1) + Set(NextLocalNoncesTlv(List(publicNonce))) + case _: InputInfo.SegwitInput => + Set.empty + } val channelReestablish = ChannelReestablish( channelId = d.channelId, nextLocalCommitmentNumber = d.signingSession.nextLocalCommitmentNumber, nextRemoteRevocationNumber = 0, yourLastPerCommitmentSecret = PrivateKey(ByteVector32.Zeroes), myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint, - TlvStream(nextFundingTlv), + TlvStream(nextFundingTlv ++ myNextLocalNonce), ) val d1 = Helpers.updateFeatures(d, localInit, remoteInit) goto(SYNCING) using d1 sending channelReestablish @@ -2313,13 +2377,32 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.YourLastFundingLockedTlv(c.fundingTxId)).toSet } else Set.empty + + val nonces = d.commitments.active.filter(_.commitInput.isInstanceOf[InputInfo.TaprootInput]).map(c => { + val localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, c.fundingTxIndex).publicKey + keyManager.verificationNonce(c.fundingTxId, localFundingPubKey, d.commitments.localCommitIndex + 1)._2 + }) + val nonces1 = d match { + case d: DATA_NORMAL => d.spliceStatus match { + case w: SpliceStatus.SpliceWaitingForSigs if w.signingSession.commitInput.isInstanceOf[InputInfo.TaprootInput] => + val localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, w.signingSession.fundingTxIndex).publicKey + val nonce = keyManager.verificationNonce(w.signingSession.fundingTx.txId, localFundingPubKey, w.signingSession.localCommitIndex + 1)._2 + nonce +: nonces + case _ => nonces + } + case _ => nonces + } + val myNextLocalNonces = if (nonces1.isEmpty) Set.empty else { + Set(NextLocalNoncesTlv(nonces1.toList)) + } + val channelReestablish = ChannelReestablish( channelId = d.channelId, nextLocalCommitmentNumber = nextLocalCommitmentNumber, nextRemoteRevocationNumber = d.commitments.remoteCommitIndex, yourLastPerCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret), myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint, - tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlvs) + tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlvs ++ myNextLocalNonces) ) // we update local/remote connection-local global/local features, we don't persist it right now val d1 = Helpers.updateFeatures(d, localInit, remoteInit) @@ -2351,20 +2434,29 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with }) when(SYNCING)(handleExceptions { - case Event(_: ChannelReestablish, _: DATA_WAIT_FOR_FUNDING_CONFIRMED) => + case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => + require(channelReestablish.nextLocalNonces.size == d.commitments.active.count(_.commitInput.isInstanceOf[InputInfo.TaprootInput]), "missing next local nonce") + setRemoteNextLocalNonces("received channelReestablish", channelReestablish.nextLocalNonces) goto(WAIT_FOR_FUNDING_CONFIRMED) case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => + if (d.signingSession.commitInput.isInstanceOf[InputInfo.TaprootInput]) { + require(channelReestablish.nextLocalNonces.size == 1, "missing next local nonce") + } + setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) channelReestablish.nextFundingTxId_opt match { case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 => // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received // their commit_sig or their tx_signatures (depending on who must send tx_signatures first). - val commitSig = d.signingSession.remoteCommit.sign(keyManager, d.channelParams, d.signingSession.fundingTxIndex, d.signingSession.fundingParams.remoteFundingPubKey, d.signingSession.commitInput) + val commitSig = d.signingSession.remoteCommit.sign(keyManager, d.channelParams, d.signingSession.fundingTxIndex, d.signingSession.fundingParams.remoteFundingPubKey, d.signingSession.commitInput, remoteNextLocalNonces.headOption) goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED) } case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + require(channelReestablish.nextLocalNonces.size == d.commitments.active.count(_.commitInput.isInstanceOf[InputInfo.TaprootInput]), "missing next local nonce") + setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) + channelReestablish.nextFundingTxId_opt match { case Some(fundingTxId) => d.status match { @@ -2372,7 +2464,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with if (channelReestablish.nextLocalCommitmentNumber == 0) { // They haven't received our commit_sig: we retransmit it. // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. - val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) + val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput, remoteNextLocalNonces.headOption) goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig } else { // They have already received our commit_sig, but we were waiting for them to send either commit_sig or @@ -2383,7 +2475,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures // and our commit_sig if they haven't received it already. if (channelReestablish.nextLocalCommitmentNumber == 0) { - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) + val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput, remoteNextLocalNonces.headOption) goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs) } else { goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs @@ -2396,14 +2488,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case None => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) } - case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => + case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => + require(channelReestablish.nextLocalNonces.size == d.commitments.active.count(_.commitInput.isInstanceOf[InputInfo.TaprootInput]), "missing next local nonce") + setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) log.debug("re-sending channel_ready") - val channelReady = createChannelReady(d.aliases, d.commitments.params) + val channelReady = createChannelReady(d.aliases, d.commitments) goto(WAIT_FOR_CHANNEL_READY) sending channelReady case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => log.debug("re-sending channel_ready") - val channelReady = createChannelReady(d.aliases, d.commitments.params) + require(channelReestablish.nextLocalNonces.size == d.commitments.active.count(_.commitInput.isInstanceOf[InputInfo.TaprootInput]), "missing next local nonce") + setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) + val channelReady = createChannelReady(d.aliases, d.commitments) // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures // and our commit_sig if they haven't received it already. channelReestablish.nextFundingTxId_opt match { @@ -2411,7 +2507,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with d.commitments.latest.localFundingStatus.localSigs_opt match { case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 => log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) + val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput, remoteNextLocalNonces.headOption) goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady) case Some(txSigs) => log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) @@ -2424,6 +2520,19 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => + log.debug(s"received $channelReestablish") + d.spliceStatus match { + case _: SpliceStatus.SpliceWaitingForSigs if channelReestablish.nextLocalNonces.size == d.commitments.active.count(_.commitInput.isInstanceOf[InputInfo.TaprootInput]) + 1 => + this.pendingRemoteNextLocalNonce = channelReestablish.nextLocalNonces.headOption + setRemoteNextLocalNonces(s"received ChannelReestablish (waiting for sigs)", channelReestablish.nextLocalNonces.tail) + case _ if channelReestablish.nextLocalNonces.size == d.commitments.active.count(_.commitInput.isInstanceOf[InputInfo.TaprootInput]) - 1 => + () + case _ => + require(channelReestablish.nextLocalNonces.size >= d.commitments.active.count(_.commitInput.isInstanceOf[InputInfo.TaprootInput]), "missing next local nonce") + setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) + this.pendingRemoteNextLocalNonce = None + } + Syncing.checkSync(keyManager, d.commitments, channelReestablish) match { case syncFailure: SyncResult.Failure => handleSyncFailure(channelReestablish, syncFailure, d) @@ -2450,7 +2559,15 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("re-sending channel_ready") val channelKeyPath = keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - sendQueue = sendQueue :+ ChannelReady(d.commitments.channelId, nextPerCommitmentPoint) + val tlvStream: TlvStream[ChannelReadyTlv] = d.commitments.latest.commitInput match { + case _: InputInfo.TaprootInput => + val localFundingPubkey = keyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, 0).publicKey + val (_, nextLocalNonce) = keyManager.verificationNonce(d.commitments.latest.fundingTxId, localFundingPubkey, 1) // README: check!! + TlvStream(ChannelTlv.NextLocalNonceTlv(nextLocalNonce)) + case _: InputInfo.SegwitInput => + TlvStream() + } + sendQueue = sendQueue :+ ChannelReady(d.commitments.channelId, nextPerCommitmentPoint, tlvStream) } if (notAnnouncedYet) { // The funding transaction is confirmed, so we've already sent our announcement_signatures. @@ -2473,7 +2590,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // They haven't received our commit_sig: we retransmit it. // We're also waiting for signatures from them, and will send our tx_signatures once we receive them. log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId) - val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) + val commitSig = signingSession.remoteCommit.localSig_opt.get sendQueue = sendQueue :+ commitSig } d.spliceStatus @@ -2484,7 +2601,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // tx_signatures and our commit_sig if they haven't received it already. if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) + val commitSig = d.commitments.latest.remoteCommit.localSig_opt.get sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs } else { log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) @@ -2601,6 +2718,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => handleUpdateRelayFeeDisconnected(c, d) case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) => + require(channelReestablish.nextLocalNonces.size == d.commitments.active.count(_.commitInput.isInstanceOf[InputInfo.TaprootInput]), "missing next local nonce") + setRemoteNextLocalNonces("received ChannelReestablish", channelReestablish.nextLocalNonces) Syncing.checkSync(keyManager, d.commitments, channelReestablish) match { case syncFailure: SyncResult.Failure => handleSyncFailure(channelReestablish, syncFailure, d) 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 9b202e40fa..0090bfcd09 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 @@ -178,6 +178,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { open.useFeeCredit_opt.map(c => ChannelTlv.FeeCreditUsedTlv(c)), d.init.pushAmount_opt.map(amount => ChannelTlv.PushAmountTlv(amount)), ).flatten + log.debug("sending AcceptDualFundedChannel with {}", tlvs) val accept = AcceptDualFundedChannel( temporaryChannelId = open.temporaryChannelId, fundingAmount = localAmount, @@ -335,7 +336,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(msg: InteractiveTxBuilder.Response, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => msg match { case InteractiveTxBuilder.SendMessage(_, msg) => stay() sending msg - case InteractiveTxBuilder.Succeeded(status, commitSig, liquidityPurchase_opt) => + case InteractiveTxBuilder.Succeeded(status, commitSig, liquidityPurchase_opt, nextRemoteNonce_opt) => + this.pendingRemoteNextLocalNonce = nextRemoteNonce_opt d.deferred.foreach(self ! _) d.replyTo_opt.foreach(_ ! OpenChannelResponse.Created(d.channelId, status.fundingTx.txId, status.fundingTx.tx.localFees.truncateToSatoshi)) liquidityPurchase_opt.collect { @@ -698,7 +700,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case DualFundingStatus.RbfInProgress(cmd_opt, _, remoteCommitSig_opt) => msg match { case InteractiveTxBuilder.SendMessage(_, msg) => stay() sending msg - case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt) => + case InteractiveTxBuilder.Succeeded(signingSession, commitSig, liquidityPurchase_opt, nextRemoteNonce_opt) => + this.pendingRemoteNextLocalNonce = nextRemoteNonce_opt cmd_opt.foreach(cmd => cmd.replyTo ! RES_BUMP_FUNDING_FEE(rbfIndex = d.previousFundingTxs.length, signingSession.fundingTx.txId, signingSession.fundingTx.tx.localFees.truncateToSatoshi)) remoteCommitSig_opt.foreach(self ! _) liquidityPurchase_opt.collect { @@ -725,7 +728,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { // We still watch the funding tx for confirmation even if we can use the zero-conf channel right away. watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepth), delay_opt = None) val shortIds = createShortIdAliases(d.channelId) - val channelReady = createChannelReady(shortIds, d.commitments.params) + val channelReady = createChannelReady(shortIds, d.commitments) d.deferred.foreach(self ! _) goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, shortIds) storing() sending channelReady case Left(_) => stay() @@ -735,7 +738,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { acceptFundingTxConfirmed(w, d) match { case Right((commitments1, _)) => val shortIds = createShortIdAliases(d.channelId) - val channelReady = createChannelReady(shortIds, d.commitments.params) + val channelReady = createChannelReady(shortIds, d.commitments) reportRbfFailure(d.status, InvalidRbfTxConfirmed(d.channelId)) val toSend = d.status match { case DualFundingStatus.WaitingForConfirmations | DualFundingStatus.RbfAborted => Seq(channelReady) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 934b9d6d7b..0f8a7ea039 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -19,23 +19,24 @@ package fr.acinq.eclair.channel.fsm import akka.actor.Status import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.pattern.pipe -import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Musig2, SatoshiLong, Script, TxId} import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx +import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel._ import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.io.Peer.OpenChannelResponse -import fr.acinq.eclair.transactions.Transactions.TxOwner +import fr.acinq.eclair.transactions.Transactions.{InputInfo, SimpleTaprootChannelCommitmentFormat, TxOwner} import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, AcceptChannelTlv, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, OpenChannelTlv, PartialSignatureWithNonceTlv, TlvStream} import fr.acinq.eclair.{Features, MilliSatoshiLong, UInt64, randomKey, toLongId} import scodec.bits.ByteVector -import scala.util.{Failure, Success} +import scala.util.{Failure, Success, Try} /** * Created by t-bast on 28/03/2022. @@ -78,6 +79,21 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used // See https://github.com/lightningnetwork/lightning-rfc/pull/714. val localShutdownScript = input.localParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) + val tlvStream: TlvStream[OpenChannelTlv] = input.channelType.commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + // include our verification nonce at funding_index = 0, commit_index = 0 + val (_, localNonce) = keyManager.verificationNonce(TxId(ByteVector32.One), fundingPubKey, 0) + TlvStream( + ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), + ChannelTlv.ChannelTypeTlv(input.channelType), + ChannelTlv.NextLocalNonceTlv(localNonce) + ) + case _ => + TlvStream( + ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), + ChannelTlv.ChannelTypeTlv(input.channelType) + ) + } val open = OpenChannel( chainHash = nodeParams.chainHash, temporaryChannelId = input.temporaryChannelId, @@ -97,10 +113,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), channelFlags = input.channelFlags, - tlvStream = TlvStream( - ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), - ChannelTlv.ChannelTypeTlv(input.channelType) - )) + tlvStream = tlvStream) goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(input, open) sending open }) @@ -133,6 +146,20 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. // See https://github.com/lightningnetwork/lightning-rfc/pull/714. val localShutdownScript = d.initFundee.localParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty) + val tlvStream: TlvStream[AcceptChannelTlv] = if (channelFeatures.hasFeature(Features.SimpleTaproot) || channelFeatures.hasFeature(Features.SimpleTaprootStaging)) { + // include our verification nonce at funding_index = 0, commit_index = 0 + val localNonce = keyManager.verificationNonce(TxId(ByteVector32.One), fundingPubkey, 0) + TlvStream( + ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), + ChannelTlv.ChannelTypeTlv(d.initFundee.channelType), + ChannelTlv.NextLocalNonceTlv(localNonce._2) + ) + } else { + TlvStream( + ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), + ChannelTlv.ChannelTypeTlv(d.initFundee.channelType) + ) + } val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, dustLimitSatoshis = d.initFundee.localParams.dustLimit, maxHtlcValueInFlightMsat = UInt64(d.initFundee.localParams.maxHtlcValueInFlightMsat.toLong), @@ -147,11 +174,8 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), - tlvStream = TlvStream( - ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), - ChannelTlv.ChannelTypeTlv(d.initFundee.channelType) - )) - goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(params, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.fundingPubkey, open.firstPerCommitmentPoint) sending accept + tlvStream = tlvStream) + goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(params, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.fundingPubkey, open.firstPerCommitmentPoint, open.nexLocalNonce_opt) sending accept } case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) @@ -162,7 +186,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { }) when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(init, open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(init, open, _)) => Helpers.validateParamsSingleFundedFunder(nodeParams, init.channelType, init.localParams.initFeatures, init.remoteInit.features, open, accept) match { case Left(t) => d.initFunder.replyTo ! OpenChannelResponse.Rejected(t.getMessage) @@ -185,9 +209,14 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { log.debug("remote params: {}", remoteParams) log.info("remote will use fundingMinDepth={}", accept.minimumDepth) val localFundingPubkey = keyManager.fundingPublicKey(init.localParams.fundingKeyPath, fundingTxIndex = 0) - val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, accept.fundingPubkey))) + val fundingPubkeyScript = if (channelFeatures.hasFeature(Features.SimpleTaproot) || (channelFeatures.hasFeature(Features.SimpleTaprootStaging))) { + Script.write(Scripts.Taproot.musig2FundingScript(localFundingPubkey.publicKey, accept.fundingPubkey)) + } else { + Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, accept.fundingPubkey))) + } wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate, init.fundingTxFeeBudget_opt).pipeTo(self) val params = ChannelParams(init.temporaryChannelId, init.channelConfig, channelFeatures, init.localParams, remoteParams, open.channelFlags) + setRemoteNextLocalNonces("received AcceptChannel", accept.nexLocalNonce_opt.toList) goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(params, init.fundingAmount, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo) } @@ -216,21 +245,38 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Left(ex) => handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), TxOwner.Remote, params.commitmentFormat, Map.empty) + val fundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0) // signature of their initial commitment tx that pays remote pushMsat - val fundingCreated = FundingCreated( - temporaryChannelId = temporaryChannelId, - fundingTxId = fundingTx.txid, - fundingOutputIndex = fundingTxOutputIndex, - signature = localSigOfRemoteTx - ) + val fundingCreated = localCommitTx.input match { + case _: Transactions.InputInfo.TaprootInput => + val localNonce = keyManager.verificationNonce(TxId(ByteVector32.One), fundingPubkey.publicKey, 0) + val Right(sig) = keyManager.partialSign(remoteCommitTx, + fundingPubkey, remoteFundingPubKey, TxOwner.Remote, + localNonce, remoteNextLocalNonces.head + ) + FundingCreated( + temporaryChannelId = temporaryChannelId, + fundingTxId = fundingTx.txid, + fundingOutputIndex = fundingTxOutputIndex, + signature = ByteVector64.Zeroes, + tlvStream = TlvStream(PartialSignatureWithNonceTlv(PartialSignatureWithNonce(sig, localNonce._2))) + ) + case _ => + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubkey, TxOwner.Remote, params.commitmentFormat, Map.empty) + FundingCreated( + temporaryChannelId = temporaryChannelId, + fundingTxId = fundingTx.txid, + fundingOutputIndex = fundingTxOutputIndex, + signature = localSigOfRemoteTx, + ) + } val channelId = toLongId(fundingTx.txid, fundingTxOutputIndex) val params1 = params.copy(channelId = channelId) peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages txPublisher ! SetChannelId(remoteNodeId, channelId) context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) // NB: we don't send a ChannelSignatureSent for the first commit - goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(params1, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), fundingCreated, replyTo) sending fundingCreated + goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(params1, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint, None), fundingCreated, replyTo) sending fundingCreated } case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => @@ -256,7 +302,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(FundingCreated(_, fundingTxId, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(params, fundingAmount, pushMsat, commitTxFeerate, remoteFundingPubKey, remoteFirstPerCommitmentPoint)) => + case Event(fc@FundingCreated(_, fundingTxId, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(params, fundingAmount, pushMsat, commitTxFeerate, remoteFundingPubKey, remoteFirstPerCommitmentPoint, remoteNextLocalNonce)) => val temporaryChannelId = params.channelId // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) Funding.makeFirstCommitTxs(keyManager, params, localFundingAmount = 0 sat, remoteFundingAmount = fundingAmount, localPushAmount = 0 msat, remotePushAmount = pushMsat, commitTxFeerate, fundingTxId, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remoteFirstPerCommitmentPoint = remoteFirstPerCommitmentPoint) match { @@ -264,25 +310,54 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => // check remote signature validity val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Map.empty) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) + + val signedLocalCommitTx = localCommitTx.input match { + case _: InputInfo.TaprootInput => + val localNonce = keyManager.verificationNonce(TxId(ByteVector32.One), fundingPubKey.publicKey, 0) + val Right(signedLocalCommitTx) = for { + localPartialSigOfLocalTx <- keyManager.partialSign(localCommitTx, fundingPubKey, remoteFundingPubKey, TxOwner.Local, localNonce, remoteNextLocalNonce.get) + remoteSigOfLocalTx <- fc.sigOrPartialSig + aggSig <- Musig2.aggregateTaprootSignatures( + Seq(localPartialSigOfLocalTx, remoteSigOfLocalTx.partialSig), + localCommitTx.tx, localCommitTx.tx.txIn.indexWhere(_.outPoint == localCommitTx.input.outPoint), Seq(localCommitTx.input.txOut), + Scripts.sort(Seq(fundingPubKey.publicKey, remoteFundingPubKey)), + Seq(localNonce._2, remoteNextLocalNonce.get), + None) + } yield localCommitTx.copy(tx = localCommitTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggSig))) + signedLocalCommitTx + case _ => + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Map.empty) + Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) + } + Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, fundingTxId, fundingTxIndex = 0, localCommitTx.tx), d, None) case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, params.commitmentFormat, Map.empty) val channelId = toLongId(fundingTxId, fundingTxOutputIndex) - val fundingSigned = FundingSigned( - channelId = channelId, - signature = localSigOfRemoteTx - ) + val fundingSigned = signedLocalCommitTx.input match { + case _: InputInfo.TaprootInput => + val localNonce = keyManager.verificationNonce(TxId(ByteVector32.One), fundingPubKey.publicKey, 0) + val Right(localPartialSigOfRemoteTx) = keyManager.partialSign(remoteCommitTx, fundingPubKey, remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNextLocalNonce.get) + FundingSigned( + channelId = channelId, + signature = ByteVector64.Zeroes, + TlvStream(PartialSignatureWithNonceTlv(PartialSignatureWithNonce(localPartialSigOfRemoteTx, localNonce._2))) + ) + case _ => + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, params.commitmentFormat, Map.empty) + FundingSigned( + channelId = channelId, + signature = localSigOfRemoteTx + ) + } val commitment = Commitment( fundingTxIndex = 0, firstRemoteCommitIndex = 0, remoteFundingPubKey = remoteFundingPubKey, localFundingStatus = SingleFundedUnconfirmedFundingTx(None), remoteFundingStatus = RemoteFundingStatus.NotLocked, - localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), - remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), + localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, fc.sigOrPartialSig), htlcTxsAndRemoteSigs = Nil), + remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint, None), nextRemoteCommit_opt = None) val commitments = Commitments( params = params.copy(channelId = channelId), @@ -310,12 +385,30 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { - case Event(msg@FundingSigned(_, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_SIGNED(params, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, _)) => + case Event(msg@FundingSigned(_, _, _), d@DATA_WAIT_FOR_FUNDING_SIGNED(params, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, _)) => // we make sure that their sig checks out and that our first commit tx is spendable val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Map.empty) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) - Transactions.checkSpendable(signedLocalCommitTx) match { + val signedLocalCommitTx = Try(localCommitTx.input match { + case _: InputInfo.TaprootInput => + require(msg.sigOrPartialSig.isRight, "missing partial signature and nonce") + val localNonce = keyManager.verificationNonce(TxId(ByteVector32.One), fundingPubKey.publicKey, 0) + val Right(remotePartialSigWithNonce) = msg.sigOrPartialSig + val Right(signedCommitTx) = for { + partialSig <- keyManager.partialSign(localCommitTx, fundingPubKey, remoteFundingPubKey, TxOwner.Local, localNonce, remotePartialSigWithNonce.nonce) + aggSig <- Transactions.aggregatePartialSignatures( + localCommitTx, + partialSig, remotePartialSigWithNonce.partialSig, + fundingPubKey.publicKey, remoteFundingPubKey, + localNonce._2, remotePartialSigWithNonce.nonce) + signedTx = localCommitTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggSig)) + } yield localCommitTx.copy(tx = signedTx) + signedCommitTx + case _: InputInfo.SegwitInput => + val Left(remoteSig) = msg.sigOrPartialSig + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Map.empty) + Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) + }) + signedLocalCommitTx.flatMap(commitTx => Transactions.checkSpendable(commitTx)) match { case Failure(cause) => // we rollback the funding tx, it will never be published wallet.rollback(fundingTx) @@ -328,7 +421,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { remoteFundingPubKey = remoteFundingPubKey, localFundingStatus = SingleFundedUnconfirmedFundingTx(Some(fundingTx)), remoteFundingStatus = RemoteFundingStatus.NotLocked, - localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), + localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, msg.sigOrPartialSig), htlcTxsAndRemoteSigs = Nil), remoteCommit = remoteCommit, nextRemoteCommit_opt = None ) @@ -375,6 +468,9 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions { case Event(remoteChannelReady: ChannelReady, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => + if (d.commitments.latest.commitInput.isInstanceOf[InputInfo.TaprootInput]) { + require(remoteChannelReady.nexLocalNonce_opt.isDefined, "missing next local nonce") + } // We are here if: // - we're using zero-conf, but our peer was very fast and we received their channel_ready before our watcher // notification that the funding tx has been successfully published: in that case we don't put a duplicate watch @@ -398,7 +494,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { // We still watch the funding tx for confirmation even if we can use the zero-conf channel right away. watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepth), delay_opt = None) val shortIds = createShortIdAliases(d.channelId) - val channelReady = createChannelReady(shortIds, d.commitments.params) + val channelReady = createChannelReady(shortIds, d.commitments) d.deferred.foreach(self ! _) goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds) storing() sending channelReady case Left(_) => stay() @@ -408,7 +504,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { acceptFundingTxConfirmed(w, d) match { case Right((commitments1, _)) => val shortIds = createShortIdAliases(d.channelId) - val channelReady = createChannelReady(shortIds, d.commitments.params) + val channelReady = createChannelReady(shortIds, d.commitments) d.deferred.foreach(self ! _) goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments1, shortIds) storing() sending channelReady case Left(_) => stay() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala index 6c93742af4..efe1d0f23c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala @@ -26,8 +26,9 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh, REFRESH_CHANNEL_UPDATE_INTERVAL} import fr.acinq.eclair.db.RevokedHtlcInfoCleaner import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReady, ChannelReadyTlv, TlvStream} +import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.{RealShortChannelId, ShortChannelId} +import fr.acinq.eclair.wire.protocol._ import scala.concurrent.duration.{DurationInt, FiniteDuration} import scala.util.{Failure, Success, Try} @@ -122,11 +123,21 @@ trait CommonFundingHandlers extends CommonHandlers { aliases } - def createChannelReady(aliases: ShortIdAliases, params: ChannelParams): ChannelReady = { + def createChannelReady(aliases: ShortIdAliases, commitments: Commitments): ChannelReady = { + val params = commitments.params + val fundingTxId = commitments.latest.fundingTxId val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) + val tlvStream: TlvStream[ChannelReadyTlv] = commitments.latest.commitInput match { + case _: InputInfo.TaprootInput => + val localFundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0).publicKey + val (_, nextLocalNonce) = keyManager.verificationNonce(fundingTxId, localFundingPubkey, 1) + TlvStream(ChannelReadyTlv.ShortChannelIdTlv(aliases.localAlias), ChannelTlv.NextLocalNonceTlv(nextLocalNonce)) + case _: InputInfo.SegwitInput => + TlvStream(ChannelReadyTlv.ShortChannelIdTlv(aliases.localAlias)) + } // we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway - ChannelReady(params.channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(aliases.localAlias))) + ChannelReady(params.channelId, nextPerCommitmentPoint, tlvStream) } def receiveChannelReady(aliases: ShortIdAliases, channelReady: ChannelReady, commitments: Commitments): DATA_NORMAL = { @@ -150,6 +161,7 @@ trait CommonFundingHandlers extends CommonHandlers { }, remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint) ) + setRemoteNextLocalNonces("received ChannelReady", channelReady.nexLocalNonce_opt.toList) // TODO: this is wrong, there should be a different nonce for each commitment peer ! ChannelReadyForPayments(self, remoteNodeId, commitments.channelId, fundingTxIndex = 0) DATA_NORMAL(commitments1, aliases1, None, initialChannelUpdate, None, None, None, SpliceStatus.NoSplice) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala index 64493d3251..2a1899edfa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.channel.fsm import akka.actor.FSM import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.{Features, MilliSatoshiLong} +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb @@ -132,11 +133,11 @@ trait CommonHandlers { finalScriptPubkey } - def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closeStatus: CloseStatus): (DATA_NEGOTIATING_SIMPLE, Option[ClosingComplete]) = { + def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closeStatus: CloseStatus, localNonce_opt: Option[(SecretNonce, IndividualNonce)] = None): (DATA_NEGOTIATING_SIMPLE, Option[ClosingComplete]) = { val localScript = localShutdown.scriptPubKey val remoteScript = remoteShutdown.scriptPubKey val closingFeerate = closeStatus.feerates_opt.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentBitcoinCoreFeerates)) - MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate) match { + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, commitments.latest, localScript, remoteScript, closingFeerate, localNonce_opt, remoteShutdown.shutdownNonce_opt) match { case Left(f) => log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) val d = DATA_NEGOTIATING_SIMPLE(commitments, closingFeerate, localScript, remoteScript, Nil, Nil) 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 d94abe8b23..e4fb8338d6 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 @@ -27,7 +27,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.UnhandledExceptionStrategy import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.ClosingTx +import fr.acinq.eclair.transactions.Transactions.{ClosingTx, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel, Warning} import java.sql.SQLException 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..7a9f4ea772 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 @@ -22,19 +22,21 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.{ActorRef, Behavior} import akka.event.LoggingAdapter import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, LexicographicalOrdering, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, LexicographicalOrdering, Musig2, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut} import fr.acinq.eclair.blockchain.OnChainChannelFunder import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel.Helpers.Funding +import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Output.Local import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Purpose import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager -import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, TxOwner} +import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, SimpleTaprootChannelCommitmentFormat, TxOwner} import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ToMilliSatoshiConversion, UInt64} @@ -92,7 +94,7 @@ object InteractiveTxBuilder { sealed trait Response case class SendMessage(sessionId: ByteVector32, msg: LightningMessage) extends Response - case class Succeeded(signingSession: InteractiveTxSigningSession.WaitingForSigs, commitSig: CommitSig, liquidityPurchase_opt: Option[LiquidityAds.Purchase]) extends Response + case class Succeeded(signingSession: InteractiveTxSigningSession.WaitingForSigs, commitSig: CommitSig, liquidityPurchase_opt: Option[LiquidityAds.Purchase], nextRemoteNonce_opt: Option[IndividualNonce]) extends Response sealed trait Failed extends Response { def cause: ChannelException } case class LocalFailure(cause: ChannelException) extends Failed case class RemoteFailure(cause: ChannelException) extends Failed @@ -105,6 +107,9 @@ object InteractiveTxBuilder { // @formatter:off def info: InputInfo def weight: Int + // we don't need to provide extra inputs here, as this method will only be called for multisig-2-of-2 inputs which only require the output that is spent for signing + // for taproot channels, we'll use a musig2 input that is not signed like this: instead, the signature will be the Musig2 aggregation if a local and remote partial signature + def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction, spentUtxos: Map[OutPoint, TxOut]): ByteVector64 // @formatter:on } @@ -324,11 +329,12 @@ object InteractiveTxBuilder { remoteInputs: Seq[IncomingInput] = Nil, localOutputs: Seq[OutgoingOutput] = Nil, remoteOutputs: Seq[IncomingOutput] = Nil, - txCompleteSent: Boolean = false, - txCompleteReceived: Boolean = false, + txCompleteSent: Option[TxComplete] = None, + txCompleteReceived: Option[TxComplete] = None, inputsReceivedCount: Int = 0, - outputsReceivedCount: Int = 0) { - val isComplete: Boolean = txCompleteSent && txCompleteReceived + outputsReceivedCount: Int = 0, + secretNonces: Map[UInt64, (SecretNonce, IndividualNonce)] = Map.empty) { + val isComplete: Boolean = txCompleteSent.isDefined && txCompleteReceived.isDefined } /** Unsigned transaction created collaboratively. */ @@ -349,6 +355,9 @@ object InteractiveTxBuilder { def localOnlyNonChangeOutputs: List[Output.Local.NonChange] = localOutputs.collect { case o: Local.NonChange => o } + // outputs spent by this tx + val spentOutputs: Seq[TxOut] = (sharedInput_opt.toSeq ++ localInputs ++ remoteInputs).sortBy(_.serialId).map(_.txOut) + def buildUnsignedTx(): Transaction = { val sharedTxIn = sharedInput_opt.map(i => (i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence))).toSeq val localTxIn = localInputs.map(i => (i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence))) @@ -465,15 +474,28 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon private val log = context.log private val keyManager = nodeParams.channelKeyManager private val localFundingPubKey: PublicKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex).publicKey - private val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubKey, fundingParams.remoteFundingPubKey))) + private val fundingPubkeyScript: ByteVector = channelParams.commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + Script.write(Scripts.Taproot.musig2FundingScript(localFundingPubKey, fundingParams.remoteFundingPubKey)) + case _ => + Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubKey, fundingParams.remoteFundingPubKey))) + } private val remoteNodeId = channelParams.remoteParams.nodeId private val previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction] = purpose match { case rbf: FundingTxRbf => rbf.previousTransactions case rbf: SpliceTxRbf => rbf.previousTransactions case _ => Nil } + private val localNonce = fundingParams.sharedInput_opt.collect { + case s: Musig2Input => { + val localFundingPubKey1 = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, s.fundingTxIndex).publicKey + keyManager.signingNonce(localFundingPubKey1) + } + } + log.debug("creating local nonce {} for fundingTxIndex {}", localNonce, purpose.fundingTxIndex) def start(): Behavior[Command] = { + log.info(s"starting funder with $fundingPubkeyScript") val txFunder = context.spawnAnonymous(InteractiveTxFunder(remoteNodeId, fundingParams, fundingPubkeyScript, purpose, wallet)) txFunder ! InteractiveTxFunder.FundTransaction(context.messageAdapter[InteractiveTxFunder.Response](r => FundTransactionResult(r))) Behaviors.receiveMessagePartial { @@ -525,17 +547,42 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon TxAddInput(fundingParams.channelId, i.serialId, Some(i.previousTx), i.previousTxOutput, i.sequence) case i: Input.Shared => TxAddInput(fundingParams.channelId, i.serialId, i.outPoint, i.sequence) } + val nextSecretNonces = addInput match { + case i: Input.Shared if localNonce.isDefined => + session.secretNonces + (i.serialId -> localNonce.get) + case _ => session.secretNonces + } replyTo ! SendMessage(sessionId, message) - val next = session.copy(toSend = tail, localInputs = session.localInputs :+ addInput, txCompleteSent = false) + val next = session.copy(toSend = tail, localInputs = session.localInputs :+ addInput, txCompleteSent = None, secretNonces = nextSecretNonces) receive(next) case (addOutput: Output) +: tail => val message = TxAddOutput(fundingParams.channelId, addOutput.serialId, addOutput.amount, addOutput.pubkeyScript) replyTo ! SendMessage(sessionId, message) - val next = session.copy(toSend = tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = false) + val next = session.copy(toSend = tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = None) receive(next) case Nil => - replyTo ! SendMessage(sessionId, TxComplete(fundingParams.channelId)) - val next = session.copy(txCompleteSent = true) + val fundingNonces = channelParams.commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + (session.remoteInputs ++ session.localInputs).sortBy(_.serialId).collect { + case i: Input.Shared => session.secretNonces.get(i.serialId).map(_._2).getOrElse(throw new RuntimeException("missing secret nonce")) + } + case _ => Nil + } + val commitNonces = channelParams.commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + validateTx(session).map(_.buildUnsignedTx().txid) match { + case Right(fundingTxId) => + List( + keyManager.verificationNonce(fundingTxId, this.localFundingPubKey, this.purpose.localCommitIndex)._2, + keyManager.verificationNonce(fundingTxId, this.localFundingPubKey, this.purpose.localCommitIndex + 1)._2, + ) + case _ => Nil + } + case _ => Nil + } + val txComplete = TxComplete(fundingParams.channelId, fundingNonces.toList, commitNonces) + replyTo ! SendMessage(sessionId, txComplete) + val next = session.copy(txCompleteSent = Some(txComplete)) if (next.isComplete) { validateAndSign(next) } else { @@ -544,7 +591,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon } } - private def receiveInput(session: InteractiveTxSession, addInput: TxAddInput): Either[ChannelException, IncomingInput] = { + private def receiveInput(session: InteractiveTxSession, addInput: TxAddInput): Either[ChannelException, InteractiveTxSession] = { if (session.inputsReceivedCount + 1 >= MAX_INPUTS_OUTPUTS_RECEIVED) { return Left(TooManyInteractiveTxRounds(fundingParams.channelId)) } @@ -575,7 +622,17 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon if (input.sequence > 0xfffffffdL) { return Left(NonReplaceableInput(fundingParams.channelId, addInput.serialId, input.outPoint.txid, input.outPoint.index, addInput.sequence)) } - Right(input) + val session1 = session.copy( + remoteInputs = session.remoteInputs :+ input, + inputsReceivedCount = session.inputsReceivedCount + 1, + txCompleteReceived = None, + ) + val session2 = input match { + case i: Input.Shared if this.localNonce.isDefined => + session1.copy(secretNonces = session1.secretNonces + (i.serialId -> localNonce.get)) + case _ => session1 + } + Right(session2) } private def receiveOutput(session: InteractiveTxSession, addOutput: TxAddOutput): Either[ChannelException, IncomingOutput] = { @@ -607,12 +664,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Left(f) => replyTo ! RemoteFailure(f) unlockAndStop(session) - case Right(input) => - val next = session.copy( - remoteInputs = session.remoteInputs :+ input, - inputsReceivedCount = session.inputsReceivedCount + 1, - txCompleteReceived = false, - ) + case Right(next) => send(next) } case addOutput: TxAddOutput => @@ -624,7 +676,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val next = session.copy( remoteOutputs = session.remoteOutputs :+ output, outputsReceivedCount = session.outputsReceivedCount + 1, - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) } @@ -633,7 +685,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Some(_) => val next = session.copy( remoteInputs = session.remoteInputs.filterNot(_.serialId == removeInput.serialId), - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) case None => @@ -645,15 +697,15 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Some(_) => val next = session.copy( remoteOutputs = session.remoteOutputs.filterNot(_.serialId == removeOutput.serialId), - txCompleteReceived = false, + txCompleteReceived = None, ) send(next) case None => replyTo ! RemoteFailure(UnknownSerialId(fundingParams.channelId, removeOutput.serialId)) unlockAndStop(session) } - case _: TxComplete => - val next = session.copy(txCompleteReceived = true) + case txComplete: TxComplete => + val next = session.copy(txCompleteReceived = Some(txComplete)) if (next.isComplete) { validateAndSign(next) } else { @@ -683,7 +735,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon replyTo ! RemoteFailure(cause) unlockAndStop(session) case Right(completeTx) => - signCommitTx(completeTx) + signCommitTx(session, completeTx) } case _: WalletFailure => replyTo ! RemoteFailure(UnconfirmedInteractiveTxInputs(fundingParams.channelId)) @@ -836,7 +888,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon Right(sharedTx) } - private def signCommitTx(completeTx: SharedTransaction): Behavior[Command] = { + private def signCommitTx(session: InteractiveTxSession, completeTx: SharedTransaction): Behavior[Command] = { val fundingTx = completeTx.buildUnsignedTx() val fundingOutputIndex = fundingTx.txOut.indexWhere(_.publicKeyScript == fundingPubkeyScript) val liquidityFee = fundingParams.liquidityFees(liquidityPurchase_opt) @@ -856,18 +908,35 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)) => require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!") val fundingPubKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex) - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat, Map.empty) + val localSigOfRemoteTx = localCommitTx.input match { + case _: InputInfo.TaprootInput => + ByteVector64.Zeroes + case _ => + keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat, Map.empty) + } + val tlvStream: TlvStream[CommitSigTlv] = remoteCommitTx.input match { + case _: InputInfo.TaprootInput => + val localFundingPubKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex).publicKey + val localNonce = keyManager.signingNonce(localFundingPubKey) + val Some(remoteNonce) = session.txCompleteReceived.flatMap(_.commitNonces.headOption) + val Right(psig) = keyManager.partialSign(remoteCommitTx, fundingPubKey, fundingParams.remoteFundingPubKey, TxOwner.Remote, localNonce, remoteNonce) + log.debug(s"signCommitTx: creating partial signature $psig for commit tx ${remoteCommitTx.tx.txid} with local nonce ${localNonce._2} remote nonce $remoteNonce") + TlvStream(CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce._2))) + case _ => + TlvStream.empty + } val localPerCommitmentPoint = keyManager.htlcPoint(keyManager.keyPath(channelParams.localParams, channelParams.channelConfig)) val htlcSignatures = sortedHtlcTxs.map(keyManager.sign(_, localPerCommitmentPoint, purpose.remotePerCommitmentPoint, TxOwner.Remote, channelParams.commitmentFormat, Map.empty)).toList - val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures) + val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures, tlvStream) val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil) - val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) - signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit) + log.debug(s"signCommitTx: setting remotePerCommitmentPoint to ${purpose.remotePerCommitmentPoint}") + val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint, Some(localCommitSig)) + signFundingTx(session, completeTx, localCommitSig, localCommit, remoteCommit) } } - private def signFundingTx(completeTx: SharedTransaction, commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { - signTx(completeTx) + private def signFundingTx(session: InteractiveTxSession, completeTx: SharedTransaction, commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { + signFundingTx(session, completeTx) Behaviors.receiveMessagePartial { case SignTransactionResult(signedTx) => log.info(s"interactive-tx txid=${signedTx.txId} partially signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", signedTx.tx.localInputs.length, signedTx.tx.remoteInputs.length, signedTx.tx.localOutputs.length, signedTx.tx.remoteOutputs.length) @@ -902,7 +971,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon remoteCommit, liquidityPurchase_opt.map(_.basicInfo(isBuyer = fundingParams.isInitiator)) ) - replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt) + replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt, session.txCompleteReceived.flatMap(_.commitNonces.lastOption)) Behaviors.stopped case WalletFailure(t) => log.error("could not sign funding transaction: ", t) @@ -917,15 +986,34 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon } } - private def signTx(unsignedTx: SharedTransaction): Unit = { + private def signFundingTx(session: InteractiveTxSession, unsignedTx: SharedTransaction): Unit = { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ val tx = unsignedTx.buildUnsignedTx() val sharedSig_opt = fundingParams.sharedInput_opt.collect { - case i: Multisig2of2Input => i.sign(keyManager, channelParams, tx, unsignedTx.inputDetails) + case m: Multisig2of2Input => m.sign(keyManager, channelParams, tx, unsignedTx.inputDetails) + } + val sharedPartialSig_opt = fundingParams.sharedInput_opt.collect { + case m: Musig2Input => + val sharedInputs = (session.localInputs ++ session.remoteInputs).collect { case i: Input.Shared => i } + // there should be a single shared input + val serialId = sharedInputs.head.serialId + val localNonce = session.secretNonces(serialId) + val fundingKey = keyManager.fundingPublicKey(this.channelParams.localParams.fundingKeyPath, m.fundingTxIndex) + val inputIndex = tx.txIn.indexWhere(_.outPoint == m.info.outPoint) + // there should be one remote nonce for each shared input ordered by serial id + val remoteNonces = sharedInputs.sortBy(_.serialId).zip(session.txCompleteReceived.get.fundingNonces).map { case (i, n) => i.serialId -> n }.toMap + val remoteNonce = remoteNonces(serialId) + val Right(psig) = keyManager.partialSign(tx, inputIndex, unsignedTx.spentOutputs, fundingKey, m.remoteFundingPubkey, TxOwner.Local, localNonce, remoteNonce) + log.debug(s"signFundingTx: creating partial sig $psig for ${tx.txid} inputIndex=$inputIndex") + log.debug(s"fundingKey = ${fundingKey.publicKey} fundingTxIndex = ${m.fundingTxIndex}") + log.debug(s"remoteFundingPubkey = ${m.remoteFundingPubkey}") + log.debug(s"local nonce = ${localNonce._2} fundingTxIndex = ${m.fundingTxIndex} commitIndex = ${m.commitIndex}") + log.debug(s"remote nonce = ${remoteNonce}") + PartialSignatureWithNonce(psig, localNonce._2) } if (unsignedTx.localInputs.isEmpty) { - context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt))) + context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt, sharedPartialSig_opt))) } else { // We track our wallet inputs and outputs, so we can verify them when we sign the transaction: if Eclair is managing bitcoin core wallet keys, it will // only sign our wallet inputs, and check that it can re-compute private keys for our wallet outputs. @@ -958,7 +1046,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon }.sum require(actualLocalAmountOut == expectedLocalAmountOut, s"local output amount $actualLocalAmountOut does not match what we expect ($expectedLocalAmountOut): bitcoin core may be malicious") val sigs = partiallySignedTx.txIn.filter(txIn => localOutpoints.contains(txIn.outPoint)).map(_.witness) - PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, partiallySignedTx, sigs, sharedSig_opt)) + PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, partiallySignedTx, sigs, sharedSig_opt, sharedPartialSig_opt)) }) { case Failure(t) => WalletFailure(t) case Success(signedTx) => SignTransactionResult(signedTx) @@ -1057,7 +1145,31 @@ 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 Some(sharedInput: Musig2Input) => + (partiallySignedTx.localSigs.previousFundingTxPartialSig_opt, remoteSigs.previousFundingTxPartialSig_opt) match { + case (Some(localPartialSig), Some(remotePartialSig)) => + val localFundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, sharedInput.fundingTxIndex).publicKey + val unsignedTx = partiallySignedTx.tx.buildUnsignedTx() + log.debug(s"adding remote sigs for ${unsignedTx.txid}") + log.debug("local partial sig is using nonce {}", localPartialSig.nonce) + log.debug("remote partial sig is using nonce {}", remotePartialSig.nonce) + log.debug(s"local funding key = ${localFundingPubkey}") + log.debug(s"remote funding key = ${sharedInput.remoteFundingPubkey}") + log.debug(s"spent outputs = ${partiallySignedTx.tx.spentOutputs}") + val inputIndex = unsignedTx.txIn.indexWhere(_.outPoint == sharedInput.info.outPoint) + val Right(aggSig) = Musig2.aggregateTaprootSignatures( + Seq(localPartialSig.partialSig, remotePartialSig.partialSig), + unsignedTx, + inputIndex, + partiallySignedTx.tx.spentOutputs, + Scripts.sort(Seq(localFundingPubkey, sharedInput.remoteFundingPubkey)), + Seq(localPartialSig.nonce, remotePartialSig.nonce), + None) + Some(Script.witnessKeyPathPay2tr(aggSig)) + case _ => + log.info("invalid tx_signatures: missing shared input partial signatures") + return Left(InvalidFundingSignature(fundingParams.channelId, Some(partiallySignedTx.txId))) + } case None => None } val txWithSigs = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, remoteSigs, sharedSigs_opt) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index d0acb99cc7..e39ba1c70f 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 @@ -20,14 +20,14 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.psbt.Psbt -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Script, Transaction, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.channel.FullCommitment import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher._ import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext -import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{NodeParams, NotificationsLogger} @@ -231,7 +231,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, private val log = context.log def fund(txWithWitnessData: ReplaceableTxWithWitnessData, targetFeerate: FeeratePerKw): Behavior[Command] = { - log.info("funding {} tx (targetFeerate={})", txWithWitnessData.txInfo.desc, targetFeerate) + log.info("funding {} tx (targetFeerate={}) txId={}", txWithWitnessData.txInfo.desc, targetFeerate, txWithWitnessData.txInfo.tx.txid) txWithWitnessData match { case claimLocalAnchor: ClaimLocalAnchorWithWitnessData => val commitFeerate = cmd.commitment.localCommit.spec.commitTxFeerate @@ -313,7 +313,20 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, val channelKeyPath = keyManager.keyPath(cmd.commitment.localParams, cmd.commitment.params.channelConfig) fundedTx match { case claimAnchorTx: ClaimLocalAnchorWithWitnessData => - val localSig = keyManager.sign(claimAnchorTx.txInfo, keyManager.fundingPublicKey(cmd.commitment.localParams.fundingKeyPath, cmd.commitment.fundingTxIndex), TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) + log.info(s"signing ${claimAnchorTx.txInfo.tx} with walletUtxos $walletUtxos") + val localSig = fundedTx.txInfo.input match { + case _: InputInfo.SegwitInput => + keyManager.sign(claimAnchorTx.txInfo, keyManager.fundingPublicKey(cmd.commitment.localParams.fundingKeyPath, cmd.commitment.fundingTxIndex), TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) + case _: InputInfo.TaprootInput => + // for simple taproot channels, our anchor output in our commit tx and our remote peer's commit tx used different keys + val spendFromRemote = claimAnchorTx.txInfo.input.txOut.publicKeyScript == Script.write(Script.pay2tr(keyManager.paymentPoint(channelKeyPath).publicKey.xOnly, Some(Scripts.Taproot.anchorScriptTree))) + if (spendFromRemote) { + keyManager.sign(claimAnchorTx.txInfo, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) + } else { + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, cmd.commitment.localCommit.index) + keyManager.sign(claimAnchorTx.txInfo, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) + } + } val signedTx = claimAnchorTx.copy(txInfo = addSigs(claimAnchorTx.txInfo, localSig)) signWalletInputs(signedTx, txFeerate, amountIn, walletUtxos) case htlcTx: HtlcWithWitnessData => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index 78a2b981bb..4f837040b3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -16,9 +16,10 @@ package fr.acinq.eclair.crypto.keymanager +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, DeterministicWallet, OutPoint, Protocol, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Protocol, Transaction, TxId, TxOut} import fr.acinq.eclair.channel.{ChannelConfig, LocalParams} import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} import scodec.bits.ByteVector @@ -41,6 +42,27 @@ trait ChannelKeyManager { def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PublicKey + /** + * Create a deterministic verification nonce for a specific funding public key and commit tx index. The public nonce will be sent to our peer to create a partial signature + * of our commit tx, the private nonce is never shared (and never serialized or stored) and is used to create our local partial signature to be combined with our peer's. + * + * @param fundingTxId funding transaction id + * @param fundingPubKey funding public key + * @param commitIndex commit tx index + * @return a verification nonce that is used to create a partial musig2 signature for our commit tx. + */ + def verificationNonce(fundingTxId: TxId, fundingPubKey: PublicKey, commitIndex: Long): (SecretNonce, IndividualNonce) + + /** + * Create a new, randomized singing nonce for a specific funding public key. These nonces are used to create a partial musig2 signature for our peer's commit tx and are sent + * alongside the partial signature. They are created on the fly, and never stored. + * + * @param fundingPubKey funding public key + * @return a signing nonce that can be used to create a musig2 signature with the funding private key that matches the provided key. + * Each call to this methode will return a different, randomized signing nonce. + */ + def signingNonce(fundingPubKey: PublicKey): (SecretNonce, IndividualNonce) + def keyPath(localParams: LocalParams, channelConfig: ChannelConfig): DeterministicWallet.KeyPath = { if (channelConfig.hasOption(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) { // deterministic mode: use the funding pubkey to compute the channel key path @@ -64,11 +86,16 @@ trait ChannelKeyManager { * @param publicKey extended public key * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with the private key that matches the input extended public key */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 + def partialSign(tx: TransactionWithInputInfo, localPublicKey: ExtendedPublicKey, remotePublicKey: PublicKey, txOwner: TxOwner, localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] = { + partialSign(tx.tx, tx.tx.txIn.indexWhere(_.outPoint == tx.input.outPoint), Seq(tx.input.txOut), localPublicKey, remotePublicKey, txOwner, localNonce, remoteNextLocalNonce) + } + + def partialSign(tx: Transaction, inputIndex: Int, spentOutputs: Seq[TxOut], localPublicKey: ExtendedPublicKey, remotePublicKey: PublicKey, txOwner: TxOwner, localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] /** * This method is used to spend funds sent to htlc keys/delayed keys @@ -78,7 +105,7 @@ trait ChannelKeyManager { * @param remotePoint remote point * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 @@ -91,7 +118,7 @@ trait ChannelKeyManager { * @param remoteSecret remote secret * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 01be5a95c4..5179e329e6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -17,14 +17,16 @@ package fr.acinq.eclair.crypto.keymanager import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} +import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, KeyAggCache, SecretNonce} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet._ -import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Transaction, TxId, TxOut} import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} -import fr.acinq.eclair.{KamonExt, randomLong} +import fr.acinq.eclair.{KamonExt, randomBytes32, randomLong} import grizzled.slf4j.Logging import kamon.tag.TagSet import scodec.bits.ByteVector @@ -95,16 +97,38 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha private def shaSeed(channelKeyPath: DeterministicWallet.KeyPath): ByteVector32 = Crypto.sha256(privateKeys.get(internalKeyPath(channelKeyPath, hardened(5))).privateKey.value :+ 1.toByte) + private def nonceSeed(channelKeyPath: DeterministicWallet.KeyPath): ByteVector32 = Crypto.sha256(shaSeed(channelKeyPath)) + override def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): PrivateKey = Generators.perCommitSecret(shaSeed(channelKeyPath), index) override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): PublicKey = Generators.perCommitPoint(shaSeed(channelKeyPath), index) + private def generateNonce(sessionId: ByteVector32, publicKey: PublicKey, extraInput: Option[ByteVector32] = None): (SecretNonce, IndividualNonce) = { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + val keyAggCache = KeyAggCache.create(java.util.List.of(publicKey)).getSecond + val nonces = fr.acinq.bitcoin.crypto.musig2.SecretNonce.generate(sessionId, null, publicKey, null, keyAggCache, extraInput.map(scala2kmp).orNull) + nonces.getFirst -> nonces.getSecond + } + + override def verificationNonce(fundingTxId: TxId, fundingPubKey: PublicKey, index: Long): (SecretNonce, IndividualNonce) = { + val keyPath = ChannelKeyManager.keyPath(fundingPubKey) + val sessionId = Generators.perCommitSecret(nonceSeed(keyPath), index).value + val nonce = generateNonce(sessionId, fundingPubKey, Some(fundingTxId.value)) + nonce + } + + override def signingNonce(fundingPubKey: PublicKey): (SecretNonce, IndividualNonce) = { + val sessionId = randomBytes32() + val nonce = generateNonce(sessionId, fundingPubKey) + nonce + } + /** * @param tx input transaction * @param publicKey extended public key * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with the private key that matches the input extended public key */ override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { @@ -117,6 +141,16 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha } } + override def partialSign(tx: Transaction, inputIndex: Int, spentOutputs: Seq[TxOut], localPublicKey: ExtendedPublicKey, remotePublicKey: PublicKey, txOwner: TxOwner, localNonce: (SecretNonce, IndividualNonce), remoteNextLocalNonce: IndividualNonce): Either[Throwable, ByteVector32] = { + val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.CommitTx) + Metrics.SignTxCount.withTags(tags).increment() + KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { + val privateKey = privateKeys.get(localPublicKey.path).privateKey + val psig = Transactions.partialSign(privateKey, tx, inputIndex, spentOutputs, localPublicKey.publicKey, remotePublicKey, localNonce, remoteNextLocalNonce) + psig + } + } + /** * This method is used to spend funds sent to htlc keys/delayed keys * @@ -125,7 +159,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param remotePoint remote point * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { @@ -147,7 +181,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param remoteSecret remote secret * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. */ override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { 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..bb0faf27a4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -25,6 +25,7 @@ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} +import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce import fr.acinq.eclair.transactions.CommitmentOutput._ import fr.acinq.eclair.transactions.Scripts.Taproot.NUMS_POINT import fr.acinq.eclair.transactions.Scripts._ @@ -33,6 +34,7 @@ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import scodec.bits.ByteVector import java.nio.ByteOrder +import scala.jdk.CollectionConverters.SeqHasAsJava import scala.reflect.ClassTag import scala.util.{Success, Try} @@ -217,12 +219,12 @@ 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 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) - } + 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) + } } } @@ -254,6 +256,19 @@ object Transactions { case class CommitTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "commit-tx" + + def checkPartialSignature(psig: PartialSignatureWithNonce, localPubKey: PublicKey, localNonce: IndividualNonce, remotePubKey: PublicKey): Boolean = { + import KotlinUtils._ + val session = fr.acinq.bitcoin.crypto.musig2.Musig2.taprootSession( + this.tx, + 0, + java.util.List.of(this.input.txOut), + Scripts.sort(Seq(localPubKey, remotePubKey)).map(scala2kmp).asJava, + java.util.List.of(localNonce, psig.nonce), + null + ).getRight + session.verify(psig.partialSig, psig.nonce, remotePubKey) + } } /** @@ -1368,6 +1383,18 @@ object Transactions { Musig2.signTaprootInput(key, tx, inputIndex, spentOutputs, publicKeys, localNonce._1, Seq(localNonce._2, remoteNextLocalNonce), None) } + def aggregatePartialSignatures(txinfo: TransactionWithInputInfo, + localSig: ByteVector32, remoteSig: ByteVector32, + localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, + localNonce: IndividualNonce, remoteNonce: IndividualNonce): Either[Throwable, ByteVector64] = { + Musig2.aggregateTaprootSignatures( + Seq(localSig, remoteSig), txinfo.tx, txinfo.tx.txIn.indexWhere(_.outPoint == txinfo.input.outPoint), + Seq(txinfo.input.txOut), + Scripts.sort(Seq(localFundingPublicKey, remoteFundingPublicKey)), + Seq(localNonce, remoteNonce), + 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)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala index 779490fdab..b8f7b207aa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala @@ -22,6 +22,7 @@ import fr.acinq.eclair.wire.internal.channel.version1.ChannelCodecs1 import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2 import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3 import fr.acinq.eclair.wire.internal.channel.version4.ChannelCodecs4 +import fr.acinq.eclair.wire.internal.channel.version5.ChannelCodecs5 import grizzled.slf4j.Logging import scodec.Codec import scodec.codecs.{byte, discriminated} @@ -67,7 +68,8 @@ object ChannelCodecs extends Logging { * More info here: https://github.com/scodec/scodec/issues/122 */ val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(byte) - .typecase(4, ChannelCodecs4.channelDataCodec) + .typecase(5, ChannelCodecs5.channelDataCodec) + .typecase(4, ChannelCodecs4.channelDataCodec.decodeOnly) .typecase(3, ChannelCodecs3.channelDataCodec.decodeOnly) .typecase(2, ChannelCodecs2.channelDataCodec.decodeOnly) .typecase(1, ChannelCodecs1.channelDataCodec.decodeOnly) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index 281e3d7d8b..8c2028cfef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -175,7 +175,8 @@ private[channel] object ChannelCodecs0 { ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit].decodeOnly + ("remotePerCommitmentPoint" | publicKey) :: + ("localCommitSig_opt" | provide[Option[CommitSig]](None))).as[RemoteCommit].decodeOnly val updateFulfillHtlcCodec: Codec[UpdateFulfillHtlc] = ( ("channelId" | bytes32) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 323774a41f..1e18ea69be 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -135,7 +135,8 @@ private[channel] object ChannelCodecs1 { ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit] + ("remotePerCommitmentPoint" | publicKey) :: + ("localCommitSig_opt" | provide[Option[CommitSig]](None))).as[RemoteCommit] val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 93d405f7cf..ef12020e7c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -162,7 +162,8 @@ private[channel] object ChannelCodecs2 { ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit] + ("remotePerCommitmentPoint" | publicKey) :: + ("localCommitSig_opt" | provide[Option[CommitSig]](None))).as[RemoteCommit] val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index e9aa69650e..7333510278 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ -import fr.acinq.eclair.wire.protocol.UpdateMessage +import fr.acinq.eclair.wire.protocol.{CommitSig, UpdateMessage} import fr.acinq.eclair.{Alias, BlockHeight, FeatureSupport, Features, MilliSatoshiLong, PermanentChannelFeature} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -215,7 +215,8 @@ private[channel] object ChannelCodecs3 { ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit] + ("remotePerCommitmentPoint" | publicKey) :: + ("localCommitSig_opt" | provide[Option[CommitSig]](None))).as[RemoteCommit] val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala index 2169e640b8..dbd034233e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala @@ -183,7 +183,7 @@ private[channel] object ChannelCodecs4 { val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = ( ("txinfo" | htlcTxCodec) :: ("remoteSig" | bytes64)).as[HtlcTxAndRemoteSig] - + val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = ( ("commitTx" | commitTxCodec) :: ("remoteSig" | bytes64.as[RemoteSignature.FullSignature].upcast[RemoteSignature])).as[CommitTxAndRemoteSig] @@ -466,7 +466,8 @@ private[channel] object ChannelCodecs4 { ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("txid" | txId) :: - ("remotePerCommitmentPoint" | publicKey)).as[RemoteCommit] + ("remotePerCommitmentPoint" | publicKey) :: + ("localCommitSig_opt" | provide[Option[CommitSig]](None))).as[RemoteCommit] private def nextRemoteCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[NextRemoteCommit] = ( ("sig" | lengthDelimited(commitSigCodec)) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala new file mode 100644 index 0000000000..62ed78bc95 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala @@ -0,0 +1,918 @@ +package fr.acinq.eclair.wire.internal.channel.version5 + +import fr.acinq.bitcoin.ScriptTree +import fr.acinq.bitcoin.io.ByteArrayInput +import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, XonlyPublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, ScriptWitness, Transaction, TxOut} +import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, ConfirmationTarget} +import fr.acinq.eclair.channel.LocalFundingStatus._ +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, PartiallySignedSharedTransaction} +import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit +import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} +import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.transactions.Transactions.InputInfo.RedeemPath +import fr.acinq.eclair.transactions.Transactions._ +import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} +import fr.acinq.eclair.wire.protocol.CommonCodecs._ +import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ +import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{Alias, BlockHeight, FeatureSupport, Features, MilliSatoshiLong, PermanentChannelFeature, RealShortChannelId, channel} +import scodec.bits.{BitVector, ByteVector} +import scodec.codecs._ +import scodec.{Attempt, Codec} + +private[channel] object ChannelCodecs5 { + + private[version5] object Codecs { + + val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] + + val channelConfigCodec: Codec[ChannelConfig] = lengthDelimited(bytes).xmap(b => { + val activated: Set[ChannelConfigOption] = b.bits.toIndexedSeq.reverse.zipWithIndex.collect { + case (true, 0) => ChannelConfig.FundingPubKeyBasedChannelKeyPath + }.toSet + ChannelConfig(activated) + }, cfg => { + val indices = cfg.options.map(_.supportBit) + if (indices.isEmpty) { + ByteVector.empty + } else { + // NB: when converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits. + var buffer = BitVector.fill(indices.max + 1)(high = false).bytes.bits + indices.foreach(i => buffer = buffer.set(i)) + buffer.reverse.bytes + } + }) + + /** We use the same encoding as init features, even if we don't need the distinction between mandatory and optional */ + val channelFeaturesCodec: Codec[ChannelFeatures] = lengthDelimited(bytes).xmap( + (b: ByteVector) => ChannelFeatures(Features(b).activated.keySet.collect { case f: PermanentChannelFeature => f }), // we make no difference between mandatory/optional, both are considered activated + (cf: ChannelFeatures) => Features(cf.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector // we encode features as mandatory, by convention + ) + + def localParamsCodec(channelFeatures: ChannelFeatures): Codec[LocalParams] = ( + ("nodeId" | publicKey) :: + ("channelPath" | keyPathCodec) :: + ("dustLimit" | satoshi) :: + ("maxHtlcValueInFlightMsat" | millisatoshi) :: + ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: + ("htlcMinimum" | millisatoshi) :: + ("toSelfDelay" | cltvExpiryDelta) :: + ("maxAcceptedHtlcs" | uint16) :: + // We pad to keep codecs byte-aligned. + ("isChannelOpener" | bool) :: ("paysCommitTxFees" | bool) :: ignore(6) :: + ("upfrontShutdownScript_opt" | optional(bool8, lengthDelimited(bytes))) :: + ("walletStaticPaymentBasepoint" | optional(provide(channelFeatures.paysDirectlyToWallet), publicKey)) :: + ("features" | combinedFeaturesCodec)).as[LocalParams] + + def remoteParamsCodec(channelFeatures: ChannelFeatures): Codec[RemoteParams] = ( + ("nodeId" | publicKey) :: + ("dustLimit" | satoshi) :: + ("maxHtlcValueInFlightMsat" | uint64) :: + ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: + ("htlcMinimum" | millisatoshi) :: + ("toSelfDelay" | cltvExpiryDelta) :: + ("maxAcceptedHtlcs" | uint16) :: + ("revocationBasepoint" | publicKey) :: + ("paymentBasepoint" | publicKey) :: + ("delayedPaymentBasepoint" | publicKey) :: + ("htlcBasepoint" | publicKey) :: + ("features" | combinedFeaturesCodec) :: + ("shutdownScript" | optional(bool8, lengthDelimited(bytes)))).as[RemoteParams] + + def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList) + + val htlcCodec: Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool8) + .typecase(true, lengthDelimited(updateAddHtlcCodec).as[IncomingHtlc]) + .typecase(false, lengthDelimited(updateAddHtlcCodec).as[OutgoingHtlc]) + + def minimalHtlcCodec(htlcs: Set[UpdateAddHtlc]): Codec[UpdateAddHtlc] = uint64overflow.xmap[UpdateAddHtlc](id => htlcs.find(_.id == id).get, _.id) + + def minimalDirectedHtlcCodec(htlcs: Set[DirectedHtlc]): Codec[DirectedHtlc] = discriminated[DirectedHtlc].by(bool8) + .typecase(true, minimalHtlcCodec(htlcs.collect(DirectedHtlc.incoming)).as[IncomingHtlc]) + .typecase(false, minimalHtlcCodec(htlcs.collect(DirectedHtlc.outgoing)).as[OutgoingHtlc]) + + private def baseCommitmentSpecCodec(directedHtlcCodec: Codec[DirectedHtlc]): Codec[CommitmentSpec] = ( + ("htlcs" | setCodec(directedHtlcCodec)) :: + ("feeratePerKw" | feeratePerKw) :: + ("toLocal" | millisatoshi) :: + ("toRemote" | millisatoshi)).as[CommitmentSpec] + + /** HTLCs are stored separately to avoid duplicating data. */ + def minimalCommitmentSpecCodec(htlcs: Set[DirectedHtlc]): Codec[CommitmentSpec] = baseCommitmentSpecCodec(minimalDirectedHtlcCodec(htlcs)) + + /** HTLCs are stored in full, the codec is stateless but creates duplication between local/remote commitment, and across commitments. */ + val commitmentSpecCodec: Codec[CommitmentSpec] = baseCommitmentSpecCodec(htlcCodec) + + val outPointCodec: Codec[OutPoint] = lengthDelimited(bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d))) + + val txOutCodec: Codec[TxOut] = lengthDelimited(bytes.xmap(d => TxOut.read(d.toArray), d => TxOut.write(d))) + + val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))) + + val scriptTreeCodec: Codec[ScriptTree] = lengthDelimited(bytes.xmap(d => ScriptTree.read(new ByteArrayInput(d.toArray)), d => ByteVector.view(d.write()))) + + val xonlyPublicKey: Codec[XonlyPublicKey] = publicKey.xmap(p => p.xOnly, x => x.publicKey) + + private val segwitInputCodec: Codec[InputInfo.SegwitInput] = { + (outPointCodec :: txOutCodec :: lengthDelimited(bytes)).as[InputInfo.SegwitInput] + } + + private val redeemPathKeyPathCodec: Codec[RedeemPath.KeyPath] = (optional(bool8, scriptTreeCodec)).as[RedeemPath.KeyPath] + private val redeemPathScriptPathCodec: Codec[RedeemPath.ScriptPath] = (scriptTreeCodec :: bytes32).as[RedeemPath.ScriptPath] + private val redeemPathCodec: Codec[RedeemPath] = discriminated[RedeemPath].by(uint8) + .typecase(0x01, redeemPathKeyPathCodec) + .typecase(0x02, redeemPathScriptPathCodec) + + private val taprootInputCodec: Codec[InputInfo.TaprootInput] = { + (outPointCodec :: txOutCodec :: xonlyPublicKey :: redeemPathCodec).as[InputInfo.TaprootInput] + } + + val inputInfoCodec: Codec[InputInfo] = discriminated[InputInfo].by(uint8) + .typecase(0x01, segwitInputCodec) + .typecase(0x02, taprootInputCodec) + + val outputInfoCodec: Codec[OutputInfo] = ( + ("index" | uint32) :: + ("amount" | satoshi) :: + ("scriptPubKey" | lengthDelimited(bytes))).as[OutputInfo] + + private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0))) + private val blockHeightConfirmationTarget: Codec[ConfirmationTarget.Absolute] = blockHeight.xmap(ConfirmationTarget.Absolute, _.confirmBefore) + private val confirmationPriority: Codec[ConfirmationPriority] = discriminated[ConfirmationPriority].by(uint8) + .typecase(0x01, provide(ConfirmationPriority.Slow)) + .typecase(0x02, provide(ConfirmationPriority.Medium)) + .typecase(0x03, provide(ConfirmationPriority.Fast)) + private val priorityConfirmationTarget: Codec[ConfirmationTarget.Priority] = confirmationPriority.xmap(ConfirmationTarget.Priority, _.priority) + private val confirmationTarget: Codec[ConfirmationTarget] = discriminated[ConfirmationTarget].by(uint8) + .typecase(0x00, blockHeightConfirmationTarget) + .typecase(0x01, priorityConfirmationTarget) + + val commitTxCodec: Codec[CommitTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx] + val htlcSuccessTxCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[HtlcSuccessTx] + val htlcTimeoutTxCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[HtlcTimeoutTx] + private val htlcSuccessTxNoConfirmCodec: Codec[HtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[HtlcSuccessTx] + private val htlcTimeoutTxNoConfirmCodec: Codec[HtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[HtlcTimeoutTx] + val htlcDelayedTxCodec: Codec[HtlcDelayedTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcDelayedTx] + private val legacyClaimHtlcSuccessTxCodec: Codec[LegacyClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[LegacyClaimHtlcSuccessTx] + val claimHtlcSuccessTxCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[ClaimHtlcSuccessTx] + val claimHtlcTimeoutTxCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | blockHeightConfirmationTarget)).as[ClaimHtlcTimeoutTx] + private val claimHtlcSuccessTxNoConfirmCodec: Codec[ClaimHtlcSuccessTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | bytes32) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[ClaimHtlcSuccessTx] + private val claimHtlcTimeoutTxNoConfirmCodec: Codec[ClaimHtlcTimeoutTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("htlcId" | uint64overflow) :: ("confirmationTarget" | defaultConfirmationTarget)).as[ClaimHtlcTimeoutTx] + val claimLocalDelayedOutputTxCodec: Codec[ClaimLocalDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx] + val claimP2WPKHOutputTxCodec: Codec[ClaimP2WPKHOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx] + val claimRemoteDelayedOutputTxCodec: Codec[ClaimRemoteDelayedOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteDelayedOutputTx] + val mainPenaltyTxCodec: Codec[MainPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx] + val htlcPenaltyTxCodec: Codec[HtlcPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx] + val claimHtlcDelayedOutputPenaltyTxCodec: Codec[ClaimHtlcDelayedOutputPenaltyTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcDelayedOutputPenaltyTx] + val claimLocalAnchorOutputTxCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("confirmationTarget" | confirmationTarget)).as[ClaimLocalAnchorOutputTx] + private val claimLocalAnchorOutputTxBlockHeightConfirmCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("confirmationTarget" | blockHeightConfirmationTarget).upcast[ConfirmationTarget]).as[ClaimLocalAnchorOutputTx] + private val claimLocalAnchorOutputTxNoConfirmCodec: Codec[ClaimLocalAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("confirmationTarget" | defaultConfirmationTarget).upcast[ConfirmationTarget]).as[ClaimLocalAnchorOutputTx] + private val claimRemoteAnchorOutputTxCodec: Codec[ClaimRemoteAnchorOutputTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimRemoteAnchorOutputTx] + val closingTxCodec: Codec[ClosingTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("outputIndex" | optional(bool8, outputInfoCodec))).as[ClosingTx] + + val claimRemoteCommitMainOutputTxCodec: Codec[ClaimRemoteCommitMainOutputTx] = discriminated[ClaimRemoteCommitMainOutputTx].by(uint8) + .typecase(0x01, claimP2WPKHOutputTxCodec) + .typecase(0x02, claimRemoteDelayedOutputTxCodec) + + val claimAnchorOutputTxCodec: Codec[ClaimAnchorOutputTx] = discriminated[ClaimAnchorOutputTx].by(uint8) + // Important: order matters! + .typecase(0x12, claimLocalAnchorOutputTxCodec) + .typecase(0x11, claimLocalAnchorOutputTxBlockHeightConfirmCodec) + .typecase(0x01, claimLocalAnchorOutputTxNoConfirmCodec) + .typecase(0x02, claimRemoteAnchorOutputTxCodec) + + val htlcTxCodec: Codec[HtlcTx] = discriminated[HtlcTx].by(uint8) + // Important: order matters! + .typecase(0x11, htlcSuccessTxCodec) + .typecase(0x12, htlcTimeoutTxCodec) + .typecase(0x01, htlcSuccessTxNoConfirmCodec) + .typecase(0x02, htlcTimeoutTxNoConfirmCodec) + + val claimHtlcTxCodec: Codec[ClaimHtlcTx] = discriminated[ClaimHtlcTx].by(uint8) + // Important: order matters! + .typecase(0x22, claimHtlcTimeoutTxCodec) + .typecase(0x23, claimHtlcSuccessTxCodec) + .typecase(0x01, legacyClaimHtlcSuccessTxCodec) + .typecase(0x02, claimHtlcTimeoutTxNoConfirmCodec) + .typecase(0x03, claimHtlcSuccessTxNoConfirmCodec) + + val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = ( + ("txinfo" | htlcTxCodec) :: + ("remoteSig" | bytes64)).as[HtlcTxAndRemoteSig] + + // remoteSig is now either a signature or a partial signature with nonce. To retain compatibility with the previous codec, we use remoteSig as a left/right indicator, + // a value of all zeroes meaning right (a valid signature cannot be all zeroes) + val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = ( + commitTxCodec :: bytes64.consume { + sig => if (sig == ByteVector64.Zeroes) + partialSignatureWithNonce.as[RemoteSignature.PartialSignatureWithNonce].upcast[RemoteSignature] + else + provide(RemoteSignature.FullSignature(sig)).upcast[RemoteSignature] + } { + case RemoteSignature.FullSignature(sig) => sig + case _: RemoteSignature.PartialSignatureWithNonce => ByteVector64.Zeroes + } + ).as[CommitTxAndRemoteSig] + + val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) + + val localChangesCodec: Codec[LocalChanges] = ( + ("proposed" | listOfN(uint16, updateMessageCodec)) :: + ("signed" | listOfN(uint16, updateMessageCodec)) :: + ("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges] + + val remoteChangesCodec: Codec[RemoteChanges] = ( + ("proposed" | listOfN(uint16, updateMessageCodec)) :: + ("acked" | listOfN(uint16, updateMessageCodec)) :: + ("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges] + + val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local] + + val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( + ("originChannelId" | bytes32) :: + ("originHtlcId" | int64) :: + ("amountIn" | millisatoshi)).as[Upstream.Cold.Channel] + + val legacyUpstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( + ("originChannelId" | bytes32) :: + ("originHtlcId" | int64) :: + ("amountIn" | millisatoshi) :: + ("amountOut" | ignore(64))).as[Upstream.Cold.Channel] + + val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = ( + ("originChannelId" | bytes32) :: + ("originHtlcId" | int64) :: + ("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel] + + val legacyUpstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline] + + val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelCodec).as[Upstream.Cold.Trampoline] + + val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16) + // NB: order matters! + .typecase(0x06, upstreamChannelCodec) + .typecase(0x05, upstreamTrampolineCodec) + .typecase(0x04, legacyUpstreamTrampolineCodec) + .typecase(0x03, upstreamLocalCodec) + .typecase(0x02, legacyUpstreamChannelCodec) + + val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin]( + upstream => Origin.Cold(upstream), + { + case Origin.Hot(_, upstream) => Upstream.Cold(upstream) + case Origin.Cold(upstream) => upstream + } + ) + + def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList) + + val originsMapCodec: Codec[Map[Long, Origin]] = mapCodec(int64, originCodec) + + val spentMapCodec: Codec[Map[OutPoint, Transaction]] = mapCodec(outPointCodec, txCodec) + + private val multisig2of2InputCodec: Codec[InteractiveTxBuilder.Multisig2of2Input] = ( + ("info" | inputInfoCodec) :: + ("fundingTxIndex" | uint32) :: + ("remoteFundingPubkey" | publicKey)).as[InteractiveTxBuilder.Multisig2of2Input] + + private val musig2of2InputCodec: Codec[InteractiveTxBuilder.Musig2Input] = ( + ("info" | inputInfoCodec) :: + ("fundingTxIndex" | uint32) :: + ("remoteFundingPubkey" | publicKey) :: + ("commitIndex" | uint32)).as[InteractiveTxBuilder.Musig2Input] + + private val sharedFundingInputCodec: Codec[InteractiveTxBuilder.SharedFundingInput] = discriminated[InteractiveTxBuilder.SharedFundingInput].by(uint16) + .typecase(0x01, multisig2of2InputCodec) + .typecase(0x02, musig2of2InputCodec) + + private val requireConfirmedInputsCodec: Codec[InteractiveTxBuilder.RequireConfirmedInputs] = (("forLocal" | bool8) :: ("forRemote" | bool8)).as[InteractiveTxBuilder.RequireConfirmedInputs] + + private val fundingParamsCodec: Codec[InteractiveTxBuilder.InteractiveTxParams] = ( + ("channelId" | bytes32) :: + ("isInitiator" | bool8) :: + ("localContribution" | satoshiSigned) :: + ("remoteContribution" | satoshiSigned) :: + ("sharedInput_opt" | optional(bool8, sharedFundingInputCodec)) :: + ("remoteFundingPubKey" | publicKey) :: + ("localOutputs" | listOfN(uint16, txOutCodec)) :: + ("lockTime" | uint32) :: + ("dustLimit" | satoshi) :: + ("targetFeerate" | feeratePerKw) :: + ("requireConfirmedInputs" | requireConfirmedInputsCodec)).as[InteractiveTxBuilder.InteractiveTxParams] + + // This codec was used by a first prototype version of splicing that only worked without HTLCs. + private val sharedInteractiveTxInputWithoutHtlcsCodec: Codec[InteractiveTxBuilder.Input.Shared] = ( + ("serialId" | uint64) :: + ("outPoint" | outPointCodec) :: + ("publicKeyScript" | provide(ByteVector.empty)) :: + ("sequence" | uint32) :: + ("localAmount" | millisatoshi) :: + ("remoteAmount" | millisatoshi) :: + ("htlcAmount" | provide(0 msat))).as[InteractiveTxBuilder.Input.Shared] + + private val sharedInteractiveTxInputWithHtlcsCodec: Codec[InteractiveTxBuilder.Input.Shared] = ( + ("serialId" | uint64) :: + ("outPoint" | outPointCodec) :: + ("publicKeyScript" | provide(ByteVector.empty)) :: + ("sequence" | uint32) :: + ("localAmount" | millisatoshi) :: + ("remoteAmount" | millisatoshi) :: + ("htlcAmount" | millisatoshi)).as[InteractiveTxBuilder.Input.Shared] + + private val sharedInteractiveTxInputWithHtlcsAndPubkeyScriptCodec: Codec[InteractiveTxBuilder.Input.Shared] = ( + ("serialId" | uint64) :: + ("outPoint" | outPointCodec) :: + ("publicKeyScript" | lengthDelimited(bytes)) :: + ("sequence" | uint32) :: + ("localAmount" | millisatoshi) :: + ("remoteAmount" | millisatoshi) :: + ("htlcAmount" | millisatoshi)).as[InteractiveTxBuilder.Input.Shared] + + private val sharedInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Shared] = discriminated[InteractiveTxBuilder.Input.Shared].by(byte) + .typecase(0x03, sharedInteractiveTxInputWithHtlcsAndPubkeyScriptCodec) + .typecase(0x02, sharedInteractiveTxInputWithHtlcsCodec) + .typecase(0x01, sharedInteractiveTxInputWithoutHtlcsCodec) + + private val sharedInteractiveTxOutputWithoutHtlcsCodec: Codec[InteractiveTxBuilder.Output.Shared] = ( + ("serialId" | uint64) :: + ("scriptPubKey" | lengthDelimited(bytes)) :: + ("localAmount" | millisatoshi) :: + ("remoteAmount" | millisatoshi) :: + ("htlcAmount" | provide(0 msat))).as[InteractiveTxBuilder.Output.Shared] + + private val sharedInteractiveTxOutputWithHtlcsCodec: Codec[InteractiveTxBuilder.Output.Shared] = ( + ("serialId" | uint64) :: + ("scriptPubKey" | lengthDelimited(bytes)) :: + ("localAmount" | millisatoshi) :: + ("remoteAmount" | millisatoshi) :: + ("htlcAmount" | millisatoshi)).as[InteractiveTxBuilder.Output.Shared] + + private val sharedInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Shared] = discriminated[InteractiveTxBuilder.Output.Shared].by(byte) + .typecase(0x02, sharedInteractiveTxOutputWithHtlcsCodec) + .typecase(0x01, sharedInteractiveTxOutputWithoutHtlcsCodec) + + private val localOnlyInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Local] = ( + ("serialId" | uint64) :: + ("previousTx" | txCodec) :: + ("previousTxOutput" | uint32) :: + ("sequence" | uint32)).as[InteractiveTxBuilder.Input.Local] + + private val localInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Local] = discriminated[InteractiveTxBuilder.Input.Local].by(byte) + .typecase(0x01, localOnlyInteractiveTxInputCodec) + + private val remoteOnlyInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Remote] = ( + ("serialId" | uint64) :: + ("outPoint" | outPointCodec) :: + ("txOut" | txOutCodec) :: + ("sequence" | uint32)).as[InteractiveTxBuilder.Input.Remote] + + private val remoteInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Remote] = discriminated[InteractiveTxBuilder.Input.Remote].by(byte) + .typecase(0x01, remoteOnlyInteractiveTxInputCodec) + + private val localInteractiveTxChangeOutputCodec: Codec[InteractiveTxBuilder.Output.Local.Change] = ( + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | lengthDelimited(bytes))).as[InteractiveTxBuilder.Output.Local.Change] + + private val localInteractiveTxNonChangeOutputCodec: Codec[InteractiveTxBuilder.Output.Local.NonChange] = ( + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | lengthDelimited(bytes))).as[InteractiveTxBuilder.Output.Local.NonChange] + + private val localInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Local] = discriminated[InteractiveTxBuilder.Output.Local].by(byte) + .typecase(0x01, localInteractiveTxChangeOutputCodec) + .typecase(0x02, localInteractiveTxNonChangeOutputCodec) + + private val remoteStandardInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Remote] = ( + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | lengthDelimited(bytes))).as[InteractiveTxBuilder.Output.Remote] + + private val remoteInteractiveTxOutputCodec: Codec[InteractiveTxBuilder.Output.Remote] = discriminated[InteractiveTxBuilder.Output.Remote].by(byte) + .typecase(0x01, remoteStandardInteractiveTxOutputCodec) + + private val sharedTransactionCodec: Codec[InteractiveTxBuilder.SharedTransaction] = ( + ("sharedInput" | optional(bool8, sharedInteractiveTxInputCodec)) :: + ("sharedOutput" | sharedInteractiveTxOutputCodec) :: + ("localInputs" | listOfN(uint16, localInteractiveTxInputCodec)) :: + ("remoteInputs" | listOfN(uint16, remoteInteractiveTxInputCodec)) :: + ("localOutputs" | listOfN(uint16, localInteractiveTxOutputCodec)) :: + ("remoteOutputs" | listOfN(uint16, remoteInteractiveTxOutputCodec)) :: + ("lockTime" | uint32)).as[InteractiveTxBuilder.SharedTransaction] + + private val partiallySignedSharedTransactionCodec: Codec[InteractiveTxBuilder.PartiallySignedSharedTransaction] = ( + ("sharedTx" | sharedTransactionCodec) :: + ("localSigs" | lengthDelimited(txSignaturesCodec))).as[InteractiveTxBuilder.PartiallySignedSharedTransaction] + + private val scriptWitnessCodec: Codec[ScriptWitness] = listOfN(uint16, lengthDelimited(bytes)).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) + + private val fullySignedSharedTransactionCodec: Codec[InteractiveTxBuilder.FullySignedSharedTransaction] = ( + ("sharedTx" | sharedTransactionCodec) :: + ("localSigs" | lengthDelimited(txSignaturesCodec)) :: + ("remoteSigs" | lengthDelimited(txSignaturesCodec)) :: + ("sharedSigs_opt" | optional(bool8, scriptWitnessCodec))).as[InteractiveTxBuilder.FullySignedSharedTransaction] + + private val signedSharedTransactionCodec: Codec[InteractiveTxBuilder.SignedSharedTransaction] = discriminated[InteractiveTxBuilder.SignedSharedTransaction].by(uint16) + .typecase(0x01, partiallySignedSharedTransactionCodec) + .typecase(0x02, fullySignedSharedTransactionCodec) + + private val liquidityFeesCodec: Codec[LiquidityAds.Fees] = (("miningFees" | satoshi) :: ("serviceFees" | satoshi)).as[LiquidityAds.Fees] + + private val liquidityPurchaseCodec: Codec[LiquidityAds.PurchaseBasicInfo] = ( + ("isBuyer" | bool8) :: + ("amount" | satoshi) :: + ("fees" | liquidityFeesCodec)).as[LiquidityAds.PurchaseBasicInfo] + + private val dualFundedUnconfirmedFundingTxWithoutLiquidityPurchaseCodec: Codec[DualFundedUnconfirmedFundingTx] = ( + ("sharedTx" | signedSharedTransactionCodec) :: + ("createdAt" | blockHeight) :: + ("fundingParams" | fundingParamsCodec) :: + ("liquidityPurchase" | provide(Option.empty[LiquidityAds.PurchaseBasicInfo]))).as[DualFundedUnconfirmedFundingTx].xmap( + dfu => fillSharedInputScript(dfu), + dfu => dfu + ) + + private val dualFundedUnconfirmedFundingTxCodec: Codec[DualFundedUnconfirmedFundingTx] = ( + ("sharedTx" | signedSharedTransactionCodec) :: + ("createdAt" | blockHeight) :: + ("fundingParams" | fundingParamsCodec) :: + ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[DualFundedUnconfirmedFundingTx].xmap( + dfu => fillSharedInputScript(dfu), + dfu => dfu + ) + + // When decoding interactive-tx from older codecs, we fill the shared input publicKeyScript if necessary. + private def fillSharedInputScript(dfu: DualFundedUnconfirmedFundingTx): DualFundedUnconfirmedFundingTx = { + (dfu.sharedTx.tx.sharedInput_opt, dfu.fundingParams.sharedInput_opt) match { + case (Some(sharedTxInput), Some(sharedFundingParamsInput)) if sharedTxInput.publicKeyScript.isEmpty => + val sharedTxInput1 = sharedTxInput.copy(publicKeyScript = sharedFundingParamsInput.info.txOut.publicKeyScript) + val sharedTx1 = dfu.sharedTx.tx.copy(sharedInput_opt = Some(sharedTxInput1)) + val dfu1 = dfu.sharedTx match { + case pt: PartiallySignedSharedTransaction => dfu.copy(sharedTx = pt.copy(tx = sharedTx1)) + case ft: FullySignedSharedTransaction => dfu.copy(sharedTx = ft.copy(tx = sharedTx1)) + } + dfu1 + case _ => dfu + } + } + + val fundingTxStatusCodec: Codec[LocalFundingStatus] = discriminated[LocalFundingStatus].by(uint8) + .typecase(0x0a, (txCodec :: realshortchannelid :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[ConfirmedFundingTx]) + .typecase(0x01, optional(bool8, txCodec).as[SingleFundedUnconfirmedFundingTx]) + .typecase(0x07, dualFundedUnconfirmedFundingTxCodec) + .typecase(0x08, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[ZeroconfPublishedFundingTx]) + .typecase(0x09, (txCodec :: provide(RealShortChannelId(0)) :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: optional(bool8, liquidityPurchaseCodec)).as[ConfirmedFundingTx]) + .typecase(0x02, dualFundedUnconfirmedFundingTxWithoutLiquidityPurchaseCodec) + .typecase(0x05, (txCodec :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ZeroconfPublishedFundingTx]) + .typecase(0x06, (txCodec :: provide(RealShortChannelId(0)) :: optional(bool8, lengthDelimited(txSignaturesCodec)) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ConfirmedFundingTx]) + .typecase(0x03, (txCodec :: provide(Option.empty[TxSignatures]) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ZeroconfPublishedFundingTx]) + .typecase(0x04, (txCodec :: provide(RealShortChannelId(0)) :: provide(Option.empty[TxSignatures]) :: provide(Option.empty[LiquidityAds.PurchaseBasicInfo])).as[ConfirmedFundingTx]) + + val remoteFundingStatusCodec: Codec[RemoteFundingStatus] = discriminated[RemoteFundingStatus].by(uint8) + .typecase(0x01, provide(RemoteFundingStatus.NotLocked)) + .typecase(0x02, provide(RemoteFundingStatus.Locked)) + + val paramsCodec: Codec[ChannelParams] = ( + ("channelId" | bytes32) :: + ("channelConfig" | channelConfigCodec) :: + (("channelFeatures" | channelFeaturesCodec) >>:~ { channelFeatures => + ("localParams" | localParamsCodec(channelFeatures)) :: + ("remoteParams" | remoteParamsCodec(channelFeatures)) :: + ("channelFlags" | channelflags) + })).as[ChannelParams] + + val waitForRevCodec: Codec[WaitForRev] = ("sentAfterLocalCommitIndex" | uint64overflow).as[WaitForRev] + + val changesCodec: Codec[CommitmentChanges] = ( + ("localChanges" | localChangesCodec) :: + ("remoteChanges" | remoteChangesCodec) :: + ("localNextHtlcId" | uint64overflow) :: + ("remoteNextHtlcId" | uint64overflow)).as[CommitmentChanges] + + private def localCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[LocalCommit] = ( + ("index" | uint64overflow) :: + ("spec" | commitmentSpecCodec) :: + ("commitTxAndRemoteSig" | commitTxAndRemoteSigCodec) :: + ("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[LocalCommit] + + private def remoteCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[RemoteCommit] = ( + ("index" | uint64overflow) :: + ("spec" | commitmentSpecCodec) :: + ("txid" | txId) :: + ("remotePerCommitmentPoint" | publicKey) :: + ("localCommitSig_opt" | optional(bool8, lengthDelimited(commitSigCodec)))).as[RemoteCommit] + + private def nextRemoteCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[NextRemoteCommit] = ( + ("sig" | lengthDelimited(commitSigCodec)) :: + ("commit" | remoteCommitCodec(commitmentSpecCodec))).as[NextRemoteCommit] + + private def commitmentCodecWithoutFirstRemoteCommitIndex(htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( + ("fundingTxIndex" | uint32) :: + ("firstRemoteCommitIndex" | provide(0L)) :: + ("fundingPubKey" | publicKey) :: + ("fundingTxStatus" | fundingTxStatusCodec) :: + ("remoteFundingStatus" | remoteFundingStatusCodec) :: + ("localCommit" | localCommitCodec(minimalCommitmentSpecCodec(htlcs))) :: + ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: + ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] + + private def commitmentCodec(htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( + ("fundingTxIndex" | uint32) :: + ("firstRemoteCommitIndex" | uint64overflow) :: + ("fundingPubKey" | publicKey) :: + ("fundingTxStatus" | fundingTxStatusCodec) :: + ("remoteFundingStatus" | remoteFundingStatusCodec) :: + ("localCommit" | localCommitCodec(minimalCommitmentSpecCodec(htlcs))) :: + ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: + ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] + + /** + * When multiple commitments are active, htlcs are shared between all of these commitments. + * There may be up to 2 * 483 = 966 htlcs, and every htlc uses at least 1452 bytes and at most 65536 bytes. + * The resulting htlc set size is thus between 1,4 MB and 64 MB, which can be pretty large. + * To avoid writing that htlc set multiple times to disk, we encode it separately. + */ + case class EncodedCommitments(params: ChannelParams, + changes: CommitmentChanges, + // The direction we use is from our local point of view. + htlcs: Set[DirectedHtlc], + active: List[Commitment], + inactive: List[Commitment], + remoteNextCommitInfo: Either[WaitForRev, PublicKey], + remotePerCommitmentSecrets: ShaChain, + originChannels: Map[Long, Origin], + remoteChannelData_opt: Option[ByteVector]) { + def toCommitments: Commitments = { + Commitments( + params = params, + changes = changes, + active = active, + inactive = inactive, + remoteNextCommitInfo, + remotePerCommitmentSecrets, + originChannels, + remoteChannelData_opt + ) + } + } + + object EncodedCommitments { + def apply(commitments: Commitments): EncodedCommitments = { + // The direction we use is from our local point of view: we use sets, which deduplicates htlcs that are in both + // local and remote commitments. + // All active commitments have the same htlc set, but each inactive commitment may have a distinct htlc set + val commitmentsSet = (commitments.active.head +: commitments.inactive).toSet + val htlcs = commitmentsSet.flatMap(_.localCommit.spec.htlcs) ++ + commitmentsSet.flatMap(_.remoteCommit.spec.htlcs.map(_.opposite)) ++ + commitmentsSet.flatMap(_.nextRemoteCommit_opt.toList.flatMap(_.commit.spec.htlcs.map(_.opposite))) + EncodedCommitments( + params = commitments.params, + changes = commitments.changes, + htlcs = htlcs, + active = commitments.active.toList, + inactive = commitments.inactive.toList, + remoteNextCommitInfo = commitments.remoteNextCommitInfo, + remotePerCommitmentSecrets = commitments.remotePerCommitmentSecrets, + originChannels = commitments.originChannels, + remoteChannelData_opt = commitments.remoteChannelData_opt + ) + } + } + + val commitmentsCodecWithoutFirstRemoteCommitIndex: Codec[Commitments] = ( + ("params" | paramsCodec) :: + ("changes" | changesCodec) :: + (("htlcs" | setCodec(htlcCodec)) >>:~ { htlcs => + ("active" | listOfN(uint16, commitmentCodecWithoutFirstRemoteCommitIndex(htlcs))) :: + ("inactive" | listOfN(uint16, commitmentCodecWithoutFirstRemoteCommitIndex(htlcs))) :: + ("remoteNextCommitInfo" | either(bool8, waitForRevCodec, publicKey)) :: + ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: + ("originChannels" | originsMapCodec) :: + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) + })).as[EncodedCommitments].xmap( + encoded => encoded.toCommitments, + commitments => EncodedCommitments(commitments) + ) + + val commitmentsCodec: Codec[Commitments] = ( + ("params" | paramsCodec) :: + ("changes" | changesCodec) :: + (("htlcs" | setCodec(htlcCodec)) >>:~ { htlcs => + ("active" | listOfN(uint16, commitmentCodec(htlcs))) :: + ("inactive" | listOfN(uint16, commitmentCodec(htlcs))) :: + ("remoteNextCommitInfo" | either(bool8, waitForRevCodec, publicKey)) :: + ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: + ("originChannels" | originsMapCodec) :: + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) + })).as[EncodedCommitments].xmap( + encoded => encoded.toCommitments, + commitments => EncodedCommitments(commitments) + ) + + val versionedCommitmentsCodec: Codec[Commitments] = discriminated[Commitments].by(uint8) + .typecase(0x01, commitmentsCodec) + + val closingFeeratesCodec: Codec[ClosingFeerates] = ( + ("preferred" | feeratePerKw) :: + ("min" | feeratePerKw) :: + ("max" | feeratePerKw)).as[ClosingFeerates] + + val closeStatusCodec: Codec[CloseStatus] = discriminated[CloseStatus].by(uint8) + .typecase(0x01, optional(bool8, closingFeeratesCodec).as[CloseStatus.Initiator]) + .typecase(0x02, optional(bool8, closingFeeratesCodec).as[CloseStatus.NonInitiator]) + + val closingTxProposedCodec: Codec[ClosingTxProposed] = ( + ("unsignedTx" | closingTxCodec) :: + ("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed] + + val localCommitPublishedCodec: Codec[LocalCommitPublished] = ( + ("commitTx" | txCodec) :: + ("claimMainDelayedOutputTx" | optional(bool8, claimLocalDelayedOutputTxCodec)) :: + ("htlcTxs" | mapCodec(outPointCodec, optional(bool8, htlcTxCodec))) :: + ("claimHtlcDelayedTx" | listOfN(uint16, htlcDelayedTxCodec)) :: + ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: + ("spent" | spentMapCodec)).as[LocalCommitPublished] + + val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( + ("commitTx" | txCodec) :: + ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: + ("claimHtlcTxs" | mapCodec(outPointCodec, optional(bool8, claimHtlcTxCodec))) :: + ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: + ("spent" | spentMapCodec)).as[RemoteCommitPublished] + + val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( + ("commitTx" | txCodec) :: + ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: + ("mainPenaltyTx" | optional(bool8, mainPenaltyTxCodec)) :: + ("htlcPenaltyTxs" | listOfN(uint16, htlcPenaltyTxCodec)) :: + ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: + ("spent" | spentMapCodec)).as[RevokedCommitPublished] + + // We don't bother removing the duplication across HTLCs: this is a short-lived state during which the channel + // cannot be used for payments. + private val (interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec, interactiveTxWaitingForSigsCodec): (Codec[InteractiveTxSigningSession.WaitingForSigs], Codec[InteractiveTxSigningSession.WaitingForSigs]) = { + val unsignedLocalCommitCodec: Codec[UnsignedLocalCommit] = ( + ("index" | uint64overflow) :: + ("spec" | commitmentSpecCodec) :: + ("commitTx" | commitTxCodec) :: + ("htlcTxs" | listOfN(uint16, htlcTxCodec))).as[UnsignedLocalCommit] + + val waitingForSigsWithoutLiquidityPurchaseCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( + ("fundingParams" | fundingParamsCodec) :: + ("fundingTxIndex" | uint32) :: + ("fundingTx" | partiallySignedSharedTransactionCodec) :: + ("localCommit" | either(bool8, unsignedLocalCommitCodec, localCommitCodec(commitmentSpecCodec))) :: + ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: + ("liquidityPurchase" | provide(Option.empty[LiquidityAds.PurchaseBasicInfo]))).as[InteractiveTxSigningSession.WaitingForSigs] + + val waitingForSigsCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( + ("fundingParams" | fundingParamsCodec) :: + ("fundingTxIndex" | uint32) :: + ("fundingTx" | partiallySignedSharedTransactionCodec) :: + ("localCommit" | either(bool8, unsignedLocalCommitCodec, localCommitCodec(commitmentSpecCodec))) :: + ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: + ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[InteractiveTxSigningSession.WaitingForSigs] + + (waitingForSigsWithoutLiquidityPurchaseCodec, waitingForSigsCodec) + } + + val dualFundingStatusCodec: Codec[DualFundingStatus] = discriminated[DualFundingStatus].by(uint8) + .\(0x01) { case status: DualFundingStatus if !status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs] => DualFundingStatus.WaitingForConfirmations }(provide(DualFundingStatus.WaitingForConfirmations)) + .\(0x03) { case status: DualFundingStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[DualFundingStatus.RbfWaitingForSigs]) + .\(0x02) { case status: DualFundingStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec.as[DualFundingStatus.RbfWaitingForSigs]) + + val spliceStatusCodec: Codec[SpliceStatus] = discriminated[SpliceStatus].by(uint8) + .\(0x01) { case status: SpliceStatus if !status.isInstanceOf[SpliceStatus.SpliceWaitingForSigs] => SpliceStatus.NoSplice }(provide(SpliceStatus.NoSplice)) + .\(0x03) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) + .\(0x02) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) + + private val shortids: Codec[ChannelTypes5.ShortIds] = ( + ("real_opt" | optional(bool8, realshortchannelid)) :: + ("localAlias" | discriminated[Alias].by(uint16).typecase(1, alias)) :: + ("remoteAlias_opt" | optional(bool8, alias)) + ).as[ChannelTypes5.ShortIds].decodeOnly + + val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: + ("waitingSince" | blockHeight) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: + ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] + + val DATA_WAIT_FOR_FUNDING_CONFIRMED_0a_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("waitingSince" | blockHeight) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: + ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] + + val DATA_WAIT_FOR_CHANNEL_READY_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: + ("shortIds" | shortids)).as[ChannelTypes5.DATA_WAIT_FOR_CHANNEL_READY_0b].map(_.migrate()).decodeOnly + + val DATA_WAIT_FOR_CHANNEL_READY_0b_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("shortIds" | shortids)).as[ChannelTypes5.DATA_WAIT_FOR_CHANNEL_READY_0b].map(_.migrate()).decodeOnly + + val DATA_WAIT_FOR_CHANNEL_READY_15_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("aliases" | aliases)).as[DATA_WAIT_FOR_CHANNEL_READY] + + val DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_09_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] = ( + ("channelParams" | paramsCodec) :: + ("secondRemotePerCommitmentPoint" | publicKey) :: + ("localPushAmount" | millisatoshi) :: + ("remotePushAmount" | millisatoshi) :: + ("status" | interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec) :: + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] + + val DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_13_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] = ( + ("channelParams" | paramsCodec) :: + ("secondRemotePerCommitmentPoint" | publicKey) :: + ("localPushAmount" | millisatoshi) :: + ("remotePushAmount" | millisatoshi) :: + ("status" | interactiveTxWaitingForSigsCodec) :: + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] + + val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_02_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( + ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: + ("localPushAmount" | millisatoshi) :: + ("remotePushAmount" | millisatoshi) :: + ("waitingSince" | blockHeight) :: + ("lastChecked" | blockHeight) :: + ("status" | dualFundingStatusCodec) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec)))).as[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + + val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_0c_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("localPushAmount" | millisatoshi) :: + ("remotePushAmount" | millisatoshi) :: + ("waitingSince" | blockHeight) :: + ("lastChecked" | blockHeight) :: + ("status" | dualFundingStatusCodec) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec)))).as[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + + val DATA_WAIT_FOR_DUAL_FUNDING_READY_03_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( + ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: + ("shortIds" | shortids)).as[ChannelTypes5.DATA_WAIT_FOR_DUAL_FUNDING_READY_0d].map(_.migrate()).decodeOnly + + val DATA_WAIT_FOR_DUAL_FUNDING_READY_0d_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("shortIds" | shortids)).as[ChannelTypes5.DATA_WAIT_FOR_DUAL_FUNDING_READY_0d].map(_.migrate()).decodeOnly + + val DATA_WAIT_FOR_DUAL_FUNDING_READY_16_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("aliases" | aliases)).as[DATA_WAIT_FOR_DUAL_FUNDING_READY] + + val DATA_NORMAL_04_Codec: Codec[DATA_NORMAL] = ( + ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: + ("shortids" | shortids) :: + ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: + ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: + ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closingFeerates" | optional(bool8, closingFeeratesCodec)) :: + ("spliceStatus" | spliceStatusCodec)).as[ChannelTypes5.DATA_NORMAL_0e].map(_.migrate()).decodeOnly + + val DATA_NORMAL_0e_Codec: Codec[DATA_NORMAL] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("shortids" | shortids) :: + ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: + ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: + ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closingFeerates" | optional(bool8, closingFeeratesCodec)) :: + ("spliceStatus" | spliceStatusCodec)).as[ChannelTypes5.DATA_NORMAL_0e].map(_.migrate()).decodeOnly + + val DATA_NORMAL_14_Codec: Codec[DATA_NORMAL] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("aliases" | aliases) :: + ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: + ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: + ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + // If there are closing fees defined we consider ourselves to be the closing initiator. + ("closingFeerates" | optional(bool8, closingFeeratesCodec).map[Option[CloseStatus]](feerates_opt => Some(CloseStatus.Initiator(feerates_opt))).decodeOnly) :: + ("spliceStatus" | spliceStatusCodec)).as[DATA_NORMAL] + + val DATA_NORMAL_18_Codec: Codec[DATA_NORMAL] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("aliases" | aliases) :: + ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: + ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: + ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closeStatus" | optional(bool8, closeStatusCodec)) :: + ("spliceStatus" | spliceStatusCodec)).as[DATA_NORMAL] + + val DATA_SHUTDOWN_05_Codec: Codec[DATA_SHUTDOWN] = ( + ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: + ("localShutdown" | lengthDelimited(shutdownCodec)) :: + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + // If there are closing fees defined we consider ourselves to be the closing initiator. + ("closingFeerates" | optional(bool8, closingFeeratesCodec).map[CloseStatus](feerates_opt => CloseStatus.Initiator(feerates_opt)).decodeOnly)).as[DATA_SHUTDOWN] + + val DATA_SHUTDOWN_0f_Codec: Codec[DATA_SHUTDOWN] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("localShutdown" | lengthDelimited(shutdownCodec)) :: + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + // If there are closing fees defined we consider ourselves to be the closing initiator. + ("closingFeerates" | optional(bool8, closingFeeratesCodec).map[CloseStatus](feerates_opt => CloseStatus.Initiator(feerates_opt)).decodeOnly)).as[DATA_SHUTDOWN] + + val DATA_SHUTDOWN_19_Codec: Codec[DATA_SHUTDOWN] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("localShutdown" | lengthDelimited(shutdownCodec)) :: + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + ("closeStatus" | closeStatusCodec)).as[DATA_SHUTDOWN] + + val DATA_NEGOTIATING_06_Codec: Codec[DATA_NEGOTIATING] = ( + ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: + ("localShutdown" | lengthDelimited(shutdownCodec)) :: + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: + ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] + + val DATA_NEGOTIATING_10_Codec: Codec[DATA_NEGOTIATING] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("localShutdown" | lengthDelimited(shutdownCodec)) :: + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: + ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] + + private val closingTxsCodec: Codec[ClosingTxs] = ( + ("localAndRemote_opt" | optional(bool8, closingTxCodec)) :: + ("localOnly_opt" | optional(bool8, closingTxCodec)) :: + ("remoteOnly_opt" | optional(bool8, closingTxCodec))).as[ClosingTxs] + + val DATA_NEGOTIATING_SIMPLE_17_Codec: Codec[DATA_NEGOTIATING_SIMPLE] = ( + ("commitments" | commitmentsCodec) :: + ("lastClosingFeerate" | feeratePerKw) :: + ("localScriptPubKey" | varsizebinarydata) :: + ("remoteScriptPubKey" | varsizebinarydata) :: + ("proposedClosingTxs" | listOfN(uint16, closingTxsCodec)) :: + ("publishedClosingTxs" | listOfN(uint16, closingTxCodec))).as[DATA_NEGOTIATING_SIMPLE] + + val DATA_CLOSING_07_Codec: Codec[DATA_CLOSING] = ( + ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: + ("waitingSince" | blockHeight) :: + ("finalScriptPubKey" | lengthDelimited(bytes)) :: + ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: + ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: + ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: + ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: + ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: + ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: + ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] + + val DATA_CLOSING_11_Codec: Codec[DATA_CLOSING] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("waitingSince" | blockHeight) :: + ("finalScriptPubKey" | lengthDelimited(bytes)) :: + ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: + ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: + ("localCommitPublished" | optional(bool8, localCommitPublishedCodec)) :: + ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: + ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: + ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: + ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] + + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_08_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: + ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] + + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_12_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] + } + + // Order matters! + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x19, Codecs.DATA_SHUTDOWN_19_Codec) + .typecase(0x18, Codecs.DATA_NORMAL_18_Codec) + .typecase(0x17, Codecs.DATA_NEGOTIATING_SIMPLE_17_Codec) + .typecase(0x16, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_READY_16_Codec) + .typecase(0x15, Codecs.DATA_WAIT_FOR_CHANNEL_READY_15_Codec) + .typecase(0x14, Codecs.DATA_NORMAL_14_Codec) + .typecase(0x13, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_13_Codec) + .typecase(0x12, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_12_Codec) + .typecase(0x11, Codecs.DATA_CLOSING_11_Codec) + .typecase(0x10, Codecs.DATA_NEGOTIATING_10_Codec) + .typecase(0x0f, Codecs.DATA_SHUTDOWN_0f_Codec) + .typecase(0x0e, Codecs.DATA_NORMAL_0e_Codec) + .typecase(0x0d, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_READY_0d_Codec) + .typecase(0x0c, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_0c_Codec) + .typecase(0x0b, Codecs.DATA_WAIT_FOR_CHANNEL_READY_0b_Codec) + .typecase(0x0a, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_0a_Codec) + .typecase(0x09, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_09_Codec) + .typecase(0x08, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_08_Codec) + .typecase(0x07, Codecs.DATA_CLOSING_07_Codec) + .typecase(0x06, Codecs.DATA_NEGOTIATING_06_Codec) + .typecase(0x05, Codecs.DATA_SHUTDOWN_05_Codec) + .typecase(0x04, Codecs.DATA_NORMAL_04_Codec) + .typecase(0x03, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_READY_03_Codec) + .typecase(0x02, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_02_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_01_Codec) + .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec) + +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelTypes5.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelTypes5.scala new file mode 100644 index 0000000000..e818a8b93a --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelTypes5.scala @@ -0,0 +1,87 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.internal.channel.version5 + +import fr.acinq.eclair.channel.LocalFundingStatus.ConfirmedFundingTx +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, Shutdown} +import fr.acinq.eclair.{Alias, RealShortChannelId} + +private[channel] object ChannelTypes5 { + + // We moved the real scid inside each commitment object when adding DATA_NORMAL_14_Codec. + case class ShortIds(real_opt: Option[RealShortChannelId], localAlias: Alias, remoteAlias_opt: Option[Alias]) + + // We moved the channel_announcement inside each commitment object when adding DATA_NORMAL_14_Codec. + case class DATA_NORMAL_0e(commitments: Commitments, + shortIds: ShortIds, + channelAnnouncement: Option[ChannelAnnouncement], + channelUpdate: ChannelUpdate, + localShutdown: Option[Shutdown], + remoteShutdown: Option[Shutdown], + closingFeerates: Option[ClosingFeerates], + spliceStatus: SpliceStatus) { + def migrate(): DATA_NORMAL = { + val commitments1 = commitments.copy( + active = commitments.active.map(c => setScidIfMatches(c, shortIds)), + inactive = commitments.inactive.map(c => setScidIfMatches(c, shortIds)), + ) + val aliases = ShortIdAliases(shortIds.localAlias, shortIds.remoteAlias_opt) + val closeStatus_opt = if (localShutdown.nonEmpty) { + Some(CloseStatus.Initiator(closingFeerates)) + } else if (remoteShutdown.nonEmpty) { + Some(CloseStatus.NonInitiator(closingFeerates)) + } else None + DATA_NORMAL(commitments1, aliases, channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closeStatus_opt, spliceStatus) + } + } + + case class DATA_WAIT_FOR_CHANNEL_READY_0b(commitments: Commitments, shortIds: ShortIds) { + def migrate(): DATA_WAIT_FOR_CHANNEL_READY = { + val commitments1 = commitments.copy( + active = commitments.active.map(c => setScidIfMatches(c, shortIds)), + inactive = commitments.inactive.map(c => setScidIfMatches(c, shortIds)), + ) + val aliases = ShortIdAliases(shortIds.localAlias, shortIds.remoteAlias_opt) + DATA_WAIT_FOR_CHANNEL_READY(commitments1, aliases) + } + } + + case class DATA_WAIT_FOR_DUAL_FUNDING_READY_0d(commitments: Commitments, shortIds: ShortIds) { + def migrate(): DATA_WAIT_FOR_DUAL_FUNDING_READY = { + val commitments1 = commitments.copy( + active = commitments.active.map(c => setScidIfMatches(c, shortIds)), + inactive = commitments.inactive.map(c => setScidIfMatches(c, shortIds)), + ) + val aliases = ShortIdAliases(shortIds.localAlias, shortIds.remoteAlias_opt) + DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments1, aliases) + } + } + + private def setScidIfMatches(c: Commitment, shortIds: ShortIds): Commitment = { + c.localFundingStatus match { + // We didn't support splicing on public channels in this version: the scid (if available) is for the initial + // funding transaction. For private channels we don't care about the real scid, it will be set correctly after + // the next splice. + case f: ConfirmedFundingTx if c.fundingTxIndex == 0 => + val scid = shortIds.real_opt.getOrElse(f.shortChannelId) + c.copy(localFundingStatus = f.copy(shortChannelId = scid)) + case _ => c + } + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index 7b561d3fa1..a3ab5a6ef5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -17,7 +17,11 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.{ByteVector64, Satoshi, TxId} +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, TxId} +import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel.{ChannelType, ChannelTypes} +import fr.acinq.eclair.wire.protocol.ChannelTlv.{nexLocalNonceTlvCodec, nexLocalNoncesTlvCodec} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tmillisatoshi} import fr.acinq.eclair.{Alias, FeatureSupport, Features, MilliSatoshi, UInt64} @@ -89,6 +93,13 @@ object ChannelTlv { */ case class UseFeeCredit(amount: MilliSatoshi) extends OpenDualFundedChannelTlv with SpliceInitTlv + case class NextLocalNonceTlv(nonce: IndividualNonce) extends OpenChannelTlv with AcceptChannelTlv with ChannelReadyTlv with ChannelReestablishTlv + + val nexLocalNonceTlvCodec: Codec[NextLocalNonceTlv] = tlvField(publicNonce) + + case class NextLocalNoncesTlv(nonces: List[IndividualNonce]) extends OpenChannelTlv with AcceptChannelTlv with ChannelReadyTlv with ChannelReestablishTlv + + val nexLocalNoncesTlvCodec: Codec[NextLocalNoncesTlv] = tlvField(list(publicNonce)) } object OpenChannelTlv { @@ -98,6 +109,7 @@ object OpenChannelTlv { val openTlvCodec: Codec[TlvStream[OpenChannelTlv]] = tlvStream(discriminated[OpenChannelTlv].by(varint) .typecase(UInt64(0), upfrontShutdownScriptCodec) .typecase(UInt64(1), channelTypeCodec) + .typecase(UInt64(4), nexLocalNonceTlvCodec) ) } @@ -109,6 +121,7 @@ object AcceptChannelTlv { val acceptTlvCodec: Codec[TlvStream[AcceptChannelTlv]] = tlvStream(discriminated[AcceptChannelTlv].by(varint) .typecase(UInt64(0), upfrontShutdownScriptCodec) .typecase(UInt64(1), channelTypeCodec) + .typecase(UInt64(4), nexLocalNonceTlvCodec) ) } @@ -205,16 +218,26 @@ object AcceptDualFundedChannelTlv { } +case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends FundingCreatedTlv with FundingSignedTlv + +object PartialSignatureWithNonceTlv { + val codec: Codec[PartialSignatureWithNonceTlv] = tlvField(partialSignatureWithNonce) +} + sealed trait FundingCreatedTlv extends Tlv object FundingCreatedTlv { - val fundingCreatedTlvCodec: Codec[TlvStream[FundingCreatedTlv]] = tlvStream(discriminated[FundingCreatedTlv].by(varint)) + val fundingCreatedTlvCodec: Codec[TlvStream[FundingCreatedTlv]] = tlvStream(discriminated[FundingCreatedTlv].by(varint) + .typecase(UInt64(2), PartialSignatureWithNonceTlv.codec) + ) } sealed trait FundingSignedTlv extends Tlv object FundingSignedTlv { - val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint)) + val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint) + .typecase(UInt64(2), PartialSignatureWithNonceTlv.codec) + ) } sealed trait ChannelReadyTlv extends Tlv @@ -227,6 +250,7 @@ object ChannelReadyTlv { val channelReadyTlvCodec: Codec[TlvStream[ChannelReadyTlv]] = tlvStream(discriminated[ChannelReadyTlv].by(varint) .typecase(UInt64(1), channelAliasTlvCodec) + .typecase(UInt64(4), nexLocalNonceTlvCodec) ) } @@ -253,6 +277,7 @@ object ChannelReestablishTlv { .typecase(UInt64(0), NextFundingTlv.codec) .typecase(UInt64(1), YourLastFundingLockedTlv.codec) .typecase(UInt64(3), MyCurrentFundingLockedTlv.codec) + .typecase(UInt64(4), nexLocalNoncesTlvCodec) ) } @@ -265,7 +290,13 @@ object UpdateFeeTlv { sealed trait ShutdownTlv extends Tlv object ShutdownTlv { - val shutdownTlvCodec: Codec[TlvStream[ShutdownTlv]] = tlvStream(discriminated[ShutdownTlv].by(varint)) + case class ShutdownNonce(nonce: IndividualNonce) extends ShutdownTlv + + private val shutdownNonceCodec: Codec[ShutdownNonce] = tlvField(publicNonce) + + val shutdownTlvCodec: Codec[TlvStream[ShutdownTlv]] = tlvStream(discriminated[ShutdownTlv].by(varint) + .typecase(UInt64(8), shutdownNonceCodec) + ) } sealed trait ClosingSignedTlv extends Tlv @@ -276,10 +307,14 @@ object ClosingSignedTlv { private val feeRange: Codec[FeeRange] = tlvField(("min_fee_satoshis" | satoshi) :: ("max_fee_satoshis" | satoshi)) + case class PartialSignature(partialSignature: ByteVector32) extends ClosingSignedTlv + + private val partialSignature: Codec[PartialSignature] = tlvField(bytes32) + val closingSignedTlvCodec: Codec[TlvStream[ClosingSignedTlv]] = tlvStream(discriminated[ClosingSignedTlv].by(varint) .typecase(UInt64(1), feeRange) + .typecase(UInt64(6), partialSignature) ) - } sealed trait ClosingTlv extends Tlv @@ -294,10 +329,21 @@ object ClosingTlv { /** Signature for a closing transaction containing the closer and closee's outputs. */ case class CloserAndCloseeOutputs(sig: ByteVector64) extends ClosingTlv + /** Signature for a closing transaction containing only the closer's output. */ + case class CloserOutputOnlyPartialSignature(partialSignature: ByteVector32) extends ClosingTlv + + /** Signature for a closing transaction containing only the closee's output. */ + case class CloseeOutputOnlyPartialSignature(partialSignature: ByteVector32) extends ClosingTlv + + /** Signature for a closing transaction containing the closer and closee's outputs. */ + case class CloserAndCloseeOutputsPartialSignature(partialSignature: ByteVector32) extends ClosingTlv + val closingTlvCodec: Codec[TlvStream[ClosingTlv]] = tlvStream(discriminated[ClosingTlv].by(varint) .typecase(UInt64(1), tlvField(bytes64.as[CloserOutputOnly])) .typecase(UInt64(2), tlvField(bytes64.as[CloseeOutputOnly])) .typecase(UInt64(3), tlvField(bytes64.as[CloserAndCloseeOutputs])) + .typecase(UInt64(4), tlvField(bytes32.as[CloserOutputOnlyPartialSignature])) + .typecase(UInt64(5), tlvField(bytes32.as[CloseeOutputOnlyPartialSignature])) + .typecase(UInt64(6), tlvField(bytes32.as[CloserAndCloseeOutputsPartialSignature])) ) - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 3018c02032..81202f81dd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -16,10 +16,12 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Satoshi, Transaction, TxHash, TxId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ShortIdAliases} +import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId} import org.apache.commons.codec.binary.Base32 @@ -156,6 +158,13 @@ object CommonCodecs { val publicKey: Codec[PublicKey] = catchAllCodec(bytes(33).xmap(bin => PublicKey(bin), pub => pub.value)) + val publicNonce: Codec[IndividualNonce] = Codec[IndividualNonce]( + (pub: IndividualNonce) => bytes(66).encode(ByteVector.view(pub.toByteArray)), + (wire: BitVector) => bytes(66).decode(wire).map(_.map(b => new IndividualNonce(b.toArray))) + ) + + val partialSignatureWithNonce: Codec[PartialSignatureWithNonce] = (bytes32 :: publicNonce).as[PartialSignatureWithNonce] + val rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b)) val txCodec: Codec[Transaction] = bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala index 7e608d36e5..799de25b83 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala @@ -16,8 +16,10 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream, tu16} import scodec.{Attempt, Codec, Err} @@ -81,14 +83,28 @@ object CommitSigTlv { val codec: Codec[BatchTlv] = tlvField(tu16) } + case class PartialSignatureWithNonceTlv(partialSigWithNonce: PartialSignatureWithNonce) extends CommitSigTlv + + object PartialSignatureWithNonceTlv { + val codec: Codec[PartialSignatureWithNonceTlv] = tlvField(partialSignatureWithNonce) + } + val commitSigTlvCodec: Codec[TlvStream[CommitSigTlv]] = tlvStream(discriminated[CommitSigTlv].by(varint) + .typecase(UInt64(2), PartialSignatureWithNonceTlv.codec) .typecase(UInt64(0x47010005), BatchTlv.codec) ) - } sealed trait RevokeAndAckTlv extends Tlv object RevokeAndAckTlv { - val revokeAndAckTlvCodec: Codec[TlvStream[RevokeAndAckTlv]] = tlvStream(discriminated[RevokeAndAckTlv].by(varint)) + case class NextLocalNoncesTlv(nonces: List[IndividualNonce]) extends RevokeAndAckTlv + + object NextLocalNoncesTlv { + val codec: Codec[NextLocalNoncesTlv] = tlvField(list(publicNonce)) + } + + val revokeAndAckTlvCodec: Codec[TlvStream[RevokeAndAckTlv]] = tlvStream(discriminated[RevokeAndAckTlv].by(varint) + .typecase(UInt64(4), NextLocalNoncesTlv.codec) + ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala index 96696d8356..ce1ab0c109 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala @@ -16,12 +16,14 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.{ByteVector64, TxId} import fr.acinq.eclair.UInt64 -import fr.acinq.eclair.wire.protocol.CommonCodecs.{bytes64, txIdAsHash, varint} +import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce +import fr.acinq.eclair.wire.protocol.CommonCodecs.{bytes64, partialSignatureWithNonce, publicNonce, txIdAsHash, varint} import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvField, tlvStream} import scodec.Codec -import scodec.codecs.discriminated +import scodec.codecs.{discriminated, list} /** * Created by t-bast on 08/04/2022. @@ -60,7 +62,19 @@ object TxRemoveOutputTlv { sealed trait TxCompleteTlv extends Tlv object TxCompleteTlv { - val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint)) + case class FundingNonces(nonces: List[IndividualNonce]) extends TxCompleteTlv + object FundingNonces { + val codec: Codec[FundingNonces] = list(publicNonce).xmap(l => FundingNonces(l), _.nonces.toList) + } + + case class CommitNonces(nonces: List[IndividualNonce]) extends TxCompleteTlv + object CommitNonces { + val codec: Codec[CommitNonces] = list(publicNonce).xmap(l => CommitNonces(l), _.nonces.toList) + } + val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint) + .typecase(UInt64(4), tlvField(FundingNonces.codec)) + .typecase(UInt64(6), tlvField(CommitNonces.codec)) + ) } sealed trait TxSignaturesTlv extends Tlv @@ -69,7 +83,14 @@ object TxSignaturesTlv { /** When doing a splice, each peer must provide their signature for the previous 2-of-2 funding output. */ case class PreviousFundingTxSig(sig: ByteVector64) extends TxSignaturesTlv + case class PreviousFundingTxPartialSig(partialSigWithNonce: PartialSignatureWithNonce) extends TxSignaturesTlv + + object PreviousFundingTxPartialSig { + val codec: Codec[PreviousFundingTxPartialSig] = tlvField(partialSignatureWithNonce) + } + val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint) + .typecase(UInt64(2), PreviousFundingTxPartialSig.codec) .typecase(UInt64(601), tlvField(bytes64.as[PreviousFundingTxSig])) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 11034b42d0..46144f3db9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -18,11 +18,14 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets import com.google.common.net.InetAddresses +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, OutPoint, Satoshi, SatoshiLong, ScriptWitness, Transaction, TxId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.RemoteSignature.PartialSignatureWithNonce import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.ChannelReadyTlv.ShortChannelIdTlv import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, isAsciiPrintable} import scodec.bits.ByteVector @@ -116,18 +119,34 @@ case class TxRemoveOutput(channelId: ByteVector32, tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId case class TxComplete(channelId: ByteVector32, - tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId + tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId { + val fundingNonces: List[IndividualNonce] = tlvStream.get[TxCompleteTlv.FundingNonces].map(_.nonces).getOrElse(List.empty[IndividualNonce]) + val commitNonces: List[IndividualNonce] = tlvStream.get[TxCompleteTlv.CommitNonces].map(_.nonces).getOrElse(List.empty[IndividualNonce]) +} + +object TxComplete { + def apply(channelId: ByteVector32) = new TxComplete(channelId, TlvStream.empty) + + def apply(channelId: ByteVector32, tlvStream: TlvStream[TxCompleteTlv]) = new TxComplete(channelId, tlvStream) + + def apply(channelId: ByteVector32, fundingNonces: List[IndividualNonce], commitNonces: List[IndividualNonce]) = new TxComplete(channelId, TlvStream(TxCompleteTlv.FundingNonces(fundingNonces), TxCompleteTlv.CommitNonces(commitNonces))) +} case class TxSignatures(channelId: ByteVector32, txId: TxId, witnesses: Seq[ScriptWitness], tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId { val previousFundingTxSig_opt: Option[ByteVector64] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].map(_.sig) + val previousFundingTxPartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxPartialSig].map(_.partialSigWithNonce) } object TxSignatures { - def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ByteVector64]): TxSignatures = { - TxSignatures(channelId, tx.txid, witnesses, TlvStream(previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig).toSet[TxSignaturesTlv])) + def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ByteVector64], previousFundingTxPartialSig_opt: Option[PartialSignatureWithNonce]): TxSignatures = { + val tlvs: Set[TxSignaturesTlv] = Set( + previousFundingSig_opt.map(TxSignaturesTlv.PreviousFundingTxSig), + previousFundingTxPartialSig_opt.map(p => TxSignaturesTlv.PreviousFundingTxPartialSig(p)) + ).flatten + TxSignatures(channelId, tx.txid, witnesses, TlvStream(tlvs)) } } @@ -187,6 +206,7 @@ case class ChannelReestablish(channelId: ByteVector32, val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId) val myCurrentFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].map(_.txId) val yourLastFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.YourLastFundingLockedTlv].map(_.txId) + val nextLocalNonces: List[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNoncesTlv].map(_.nonces).getOrElse(List.empty) } case class OpenChannel(chainHash: BlockHash, @@ -210,6 +230,7 @@ case class OpenChannel(chainHash: BlockHash, tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) + val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) } case class AcceptChannel(temporaryChannelId: ByteVector32, @@ -229,6 +250,7 @@ case class AcceptChannel(temporaryChannelId: ByteVector32, tlvStream: TlvStream[AcceptChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) + val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) } // NB: this message is named open_channel2 in the specification. @@ -289,16 +311,21 @@ case class FundingCreated(temporaryChannelId: ByteVector32, fundingTxId: TxId, fundingOutputIndex: Int, signature: ByteVector64, - tlvStream: TlvStream[FundingCreatedTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId + tlvStream: TlvStream[FundingCreatedTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { + val sigOrPartialSig: Either[ByteVector64, PartialSignatureWithNonce] = tlvStream.get[PartialSignatureWithNonceTlv].map(_.partialSigWithNonce).toRight(signature) +} case class FundingSigned(channelId: ByteVector32, signature: ByteVector64, - tlvStream: TlvStream[FundingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId + tlvStream: TlvStream[FundingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + val sigOrPartialSig: Either[ByteVector64, PartialSignatureWithNonce] = tlvStream.get[PartialSignatureWithNonceTlv].map(_.partialSigWithNonce).toRight(signature) +} case class ChannelReady(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, tlvStream: TlvStream[ChannelReadyTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { val alias_opt: Option[Alias] = tlvStream.get[ShortChannelIdTlv].map(_.alias) + val nexLocalNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelTlv.NextLocalNonceTlv].map(_.nonce) } case class Stfu(channelId: ByteVector32, initiator: Boolean) extends SetupMessage with HasChannelId @@ -355,25 +382,51 @@ case class SpliceLocked(channelId: ByteVector32, case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector, - tlvStream: TlvStream[ShutdownTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId with ForbiddenMessageWhenQuiescent + tlvStream: TlvStream[ShutdownTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId with ForbiddenMessageWhenQuiescent { + val shutdownNonce_opt: Option[IndividualNonce] = tlvStream.get[ShutdownTlv.ShutdownNonce].map(_.nonce) +} case class ClosingSigned(channelId: ByteVector32, feeSatoshis: Satoshi, signature: ByteVector64, tlvStream: TlvStream[ClosingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { val feeRange_opt = tlvStream.get[ClosingSignedTlv.FeeRange] + val partialSignature_opt = tlvStream.get[ClosingSignedTlv.PartialSignature] } case class ClosingComplete(channelId: ByteVector32, closerScriptPubKey: ByteVector, closeeScriptPubKey: ByteVector, fees: Satoshi, lockTime: Long, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { - val closerOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserOutputOnly].map(_.sig) - val closeeOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloseeOutputOnly].map(_.sig) - val closerAndCloseeOutputsSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndCloseeOutputs].map(_.sig) + //val closerOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserOutputOnly].map(_.sig) + //val closeeOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloseeOutputOnly].map(_.sig) + //val closerAndCloseeOutputsSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndCloseeOutputs].map(_.sig) + + val closerOutputOnlyPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingTlv.CloserOutputOnlyPartialSignature].map(_.partialSignature) + val closeeOutputOnlyPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingTlv.CloseeOutputOnlyPartialSignature].map(_.partialSignature) + val closerAndCloseeOutputsPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingTlv.CloserAndCloseeOutputsPartialSignature].map(_.partialSignature) + + val closerOutputOnlySigOrPartialSig_opt: Option[Either[ByteVector64, ByteVector32]] = tlvStream.get[ClosingTlv.CloserOutputOnly] + .map(tlv => Some(Left(tlv.sig))) + .getOrElse(tlvStream.get[ClosingTlv.CloserOutputOnlyPartialSignature].map(tlv => Right(tlv.partialSignature))) + + val closeeOutputOnlySigOrPartialSig_opt: Option[Either[ByteVector64, ByteVector32]] = tlvStream.get[ClosingTlv.CloseeOutputOnly] + .map(tlv => Some(Left(tlv.sig))) + .getOrElse(tlvStream.get[ClosingTlv.CloseeOutputOnlyPartialSignature].map(tlv => Right(tlv.partialSignature))) + + val closerAndCloseeOutputsSigOrPartialSig_opt: Option[Either[ByteVector64, ByteVector32]] = tlvStream.get[ClosingTlv.CloserAndCloseeOutputs] + .map(tlv => Some(Left(tlv.sig))) + .getOrElse(tlvStream.get[ClosingTlv.CloserAndCloseeOutputsPartialSignature].map(tlv => Right(tlv.partialSignature))) + + val closerOutputOnlySig_opt: Option[ByteVector64] = closerOutputOnlySigOrPartialSig_opt.flatMap(_.swap.toOption) + val closeeOutputOnlySig_opt: Option[ByteVector64] = closeeOutputOnlySigOrPartialSig_opt.flatMap(_.swap.toOption) + val closerAndCloseeOutputsSig_opt: Option[ByteVector64] = closerAndCloseeOutputsSigOrPartialSig_opt.flatMap(_.swap.toOption) } case class ClosingSig(channelId: ByteVector32, closerScriptPubKey: ByteVector, closeeScriptPubKey: ByteVector, fees: Satoshi, lockTime: Long, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { val closerOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserOutputOnly].map(_.sig) val closeeOutputOnlySig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloseeOutputOnly].map(_.sig) val closerAndCloseeOutputsSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndCloseeOutputs].map(_.sig) + val closerOutputOnlyPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingTlv.CloserOutputOnlyPartialSignature].map(_.partialSignature) + val closeeOutputOnlyPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingTlv.CloseeOutputOnlyPartialSignature].map(_.partialSignature) + val closerAndCloseeOutputsPartialSig_opt: Option[ByteVector32] = tlvStream.get[ClosingTlv.CloserAndCloseeOutputsPartialSignature].map(_.partialSignature) } case class UpdateAddHtlc(channelId: ByteVector32, @@ -432,12 +485,16 @@ case class CommitSig(channelId: ByteVector32, htlcSignatures: List[ByteVector64], tlvStream: TlvStream[CommitSigTlv] = TlvStream.empty) extends HtlcMessage with HasChannelId { val batchSize: Int = tlvStream.get[CommitSigTlv.BatchTlv].map(_.size).getOrElse(1) + val partialSignature_opt: Option[PartialSignatureWithNonce] = tlvStream.get[CommitSigTlv.PartialSignatureWithNonceTlv].map(_.partialSigWithNonce) + val sigOrPartialSig: Either[ByteVector64, PartialSignatureWithNonce] = partialSignature_opt.toRight(signature) } case class RevokeAndAck(channelId: ByteVector32, perCommitmentSecret: PrivateKey, nextPerCommitmentPoint: PublicKey, - tlvStream: TlvStream[RevokeAndAckTlv] = TlvStream.empty) extends HtlcMessage with HasChannelId + tlvStream: TlvStream[RevokeAndAckTlv] = TlvStream.empty) extends HtlcMessage with HasChannelId { + val nexLocalNonces: List[IndividualNonce] = tlvStream.get[protocol.RevokeAndAckTlv.NextLocalNoncesTlv].map(_.nonces).getOrElse(List.empty) +} case class UpdateFee(channelId: ByteVector32, feeratePerKw: FeeratePerKw, 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 89ed601e01..2aa23b28e4 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 @@ -33,13 +33,14 @@ import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.blockchain.{OnChainWallet, SingleKeyOnChainWallet} import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} +import fr.acinq.eclair.channel.states.ChannelStateTestsTags import fr.acinq.eclair.io.OpenChannelInterceptor.makeChannelParams -import fr.acinq.eclair.transactions.Transactions.InputInfo +import fr.acinq.eclair.transactions.Transactions.{InputInfo, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Feature, FeatureSupport, Features, InitFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, UInt64, randomBytes32, randomKey} -import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.{BeforeAndAfterAll, Tag} import scodec.bits.{ByteVector, HexStringSyntax} import java.util.UUID @@ -103,10 +104,20 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit private val firstPerCommitmentPointA = nodeParamsA.channelKeyManager.commitmentPoint(nodeParamsA.channelKeyManager.keyPath(channelParamsA.localParams, ChannelConfig.standard), 0) private val firstPerCommitmentPointB = nodeParamsB.channelKeyManager.commitmentPoint(nodeParamsB.channelKeyManager.keyPath(channelParamsB.localParams, ChannelConfig.standard), 0) - val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey))) + val fundingPubkeyA = nodeParamsA.channelKeyManager.fundingPublicKey(channelParamsA.localParams.fundingKeyPath, 0).publicKey + val fundingPubkeyB = nodeParamsB.channelKeyManager.fundingPublicKey(channelParamsB.localParams.fundingKeyPath, 0).publicKey + assert(channelParamsA.commitmentFormat == channelParamsB.commitmentFormat) + val fundingPubkeyScript: ByteVector = channelParamsA.commitmentFormat match { + case SimpleTaprootChannelCommitmentFormat => + Script.write(Scripts.Taproot.musig2FundingScript(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey)) + case _ => + Script.write(Script.pay2wsh(Scripts.multiSig2of2(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey))) + } + + Script.write(Script.pay2wsh(Scripts.multiSig2of2(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey))) def dummySharedInputB(amount: Satoshi): SharedFundingInput = { - val inputInfo = InputInfo.SegwitInput(OutPoint(randomTxId(), 3), TxOut(amount, fundingPubkeyScript), Nil) + val inputInfo = InputInfo.SegwitInput(OutPoint(randomTxId(), 3), TxOut(amount, fundingPubkeyScript), fundingPubkeyScript) val fundingTxIndex = fundingParamsA.sharedInput_opt match { case Some(input: Multisig2of2Input) => input.fundingTxIndex + 1 case _ => 0 @@ -211,8 +222,19 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } - private def createFixtureParams(fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false), nonInitiatorPaysCommitTxFees: Boolean = false): FixtureParams = { - val channelFeatures = ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Features[InitFeature](Features.DualFunding -> FeatureSupport.Optional), Features[InitFeature](Features.DualFunding -> FeatureSupport.Optional), announceChannel = true) + private def createFixtureParams(fundingAmountA: Satoshi, fundingAmountB: Satoshi, targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs = RequireConfirmedInputs(forLocal = false, forRemote = false), nonInitiatorPaysCommitTxFees: Boolean = false, useTaprootChannels: Boolean = false): FixtureParams = { + val channelFeatures = if (useTaprootChannels) + ChannelFeatures( + ChannelTypes.SimpleTaprootChannelsStaging(), + Features[InitFeature](Features.SimpleTaprootStaging -> FeatureSupport.Optional, Features.DualFunding -> FeatureSupport.Optional), + Features[InitFeature](Features.SimpleTaprootStaging -> FeatureSupport.Optional, Features.DualFunding -> FeatureSupport.Optional), + announceChannel = true) + else + ChannelFeatures( + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), + Features[InitFeature](Features.DualFunding -> FeatureSupport.Optional), + Features[InitFeature](Features.DualFunding -> FeatureSupport.Optional), + announceChannel = true) val Seq(nodeParamsA, nodeParamsB) = Seq(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams).map(_.copy(features = Features(channelFeatures.features.map(f => f -> FeatureSupport.Optional).toMap[Feature, FeatureSupport]))) val localParamsA = makeChannelParams(nodeParamsA, nodeParamsA.features.initFeatures(), None, None, isChannelOpener = true, paysCommitTxFees = !nonInitiatorPaysCommitTxFees, dualFunded = true, fundingAmountA, unlimitedMaxHtlcValueInFlight = false) val localParamsB = makeChannelParams(nodeParamsB, nodeParamsB.features.initFeatures(), None, None, isChannelOpener = false, paysCommitTxFees = nonInitiatorPaysCommitTxFees, dualFunded = true, fundingAmountB, unlimitedMaxHtlcValueInFlight = false) @@ -276,7 +298,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } - private def withFixture(fundingAmountA: Satoshi, utxosA: Seq[Satoshi], fundingAmountB: Satoshi, utxosB: Seq[Satoshi], targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None)(testFun: Fixture => Any): Unit = { + private def withFixture(fundingAmountA: Satoshi, utxosA: Seq[Satoshi], fundingAmountB: Satoshi, utxosB: Seq[Satoshi], targetFeerate: FeeratePerKw, dustLimit: Satoshi, lockTime: Long, requireConfirmedInputs: RequireConfirmedInputs, liquidityPurchase_opt: Option[LiquidityAds.Purchase] = None, useTaprootChannels: Boolean = false)(testFun: Fixture => Any): Unit = { // Initialize wallets with a few confirmed utxos. val probe = TestProbe() val rpcClientA = createWallet(UUID.randomUUID().toString) @@ -287,7 +309,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit utxosB.foreach(amount => addUtxo(walletB, amount, probe)) generateBlocks(1) - val fixtureParams = createFixtureParams(fundingAmountA, fundingAmountB, targetFeerate, dustLimit, lockTime, requireConfirmedInputs, nonInitiatorPaysCommitTxFees = liquidityPurchase_opt.nonEmpty) + val fixtureParams = createFixtureParams(fundingAmountA, fundingAmountB, targetFeerate, dustLimit, lockTime, requireConfirmedInputs, nonInitiatorPaysCommitTxFees = liquidityPurchase_opt.nonEmpty, useTaprootChannels) val alice = fixtureParams.spawnTxBuilderAlice(walletA, liquidityPurchase_opt = liquidityPurchase_opt) val bob = fixtureParams.spawnTxBuilderBob(walletB, liquidityPurchase_opt = liquidityPurchase_opt) testFun(Fixture(alice, bob, fixtureParams, walletA, rpcClientA, walletB, rpcClientB, TestProbe(), TestProbe())) @@ -370,13 +392,13 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } } - test("initiator funds less than non-initiator") { + test("initiator funds less than non-initiator (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { val targetFeerate = FeeratePerKw(3000 sat) val fundingA = 10_000 sat val utxosA = Seq(50_000 sat) val fundingB = 50_000 sat val utxosB = Seq(80_000 sat) - withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true)) { f => + withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = true, forRemote = true), useTaprootChannels = true) { f => import f._ alice ! Start(alice2bob.ref) @@ -2614,7 +2636,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.SegwitInput(OutPoint(fundingTx, 0), fundingTx.txOut.head, Nil), 0, randomKey().publicKey) + val sharedInput = Multisig2of2Input(InputInfo.SegwitInput(OutPoint(fundingTx, 0), fundingTx.txOut.head, Script.write(Script.pay2wpkh(randomKey().publicKey))), 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 @@ -2841,8 +2863,8 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit assert(initiatorTx.buildUnsignedTx().txid == unsignedTx.txid) assert(nonInitiatorTx.buildUnsignedTx().txid == unsignedTx.txid) - val initiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), None) - val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None) + val initiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), None, None) + val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None, None) val initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs, None) assert(initiatorSignedTx.feerate == FeeratePerKw(262 sat)) val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs, None) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 0f24ad8fbb..a2d564a65f 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 @@ -53,6 +53,8 @@ import scala.concurrent.duration.DurationInt class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with ChannelStateTestsBase with BeforeAndAfterAll { + val channelType: SupportedChannelType = ChannelTypes.SimpleTaprootChannelsStaging() // ChannelTypes.AnchorOutputsZeroFeeHtlcTx() + override def beforeAll(): Unit = { startBitcoind() waitForBitcoindReady() @@ -166,6 +168,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w case _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) case _: ChannelTypes.AnchorOutputs => Set(ChannelStateTestsTags.AnchorOutputs) case _: ChannelTypes.StaticRemoteKey => Set(ChannelStateTestsTags.StaticRemoteKey) + case _: ChannelTypes.SimpleTaprootChannelsStaging => Set(ChannelStateTestsTags.OptionSimpleTaprootStaging) case _ => Set.empty[String] } reachNormal(setup, testTags) @@ -220,7 +223,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough, not spending anchor output (local commit)") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate @@ -235,7 +238,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough, not spending anchor output (remote commit)") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate @@ -250,7 +253,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) @@ -267,7 +270,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough and commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate @@ -285,7 +288,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -304,7 +307,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("next remote commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ // Add a partially signed htlc Alice -> Bob. @@ -335,7 +338,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx published, not spending local anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -352,7 +355,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx replaces local commit tx, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -388,7 +391,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("not enough funds to increase commit tx feerate") { - withFixture(Seq(10.4 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(10.4 millibtc), channelType) { f => import f._ // close channel and wait for the commit tx to be published, anchor will not be published because we don't have enough funds @@ -407,7 +410,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate too low, spending anchor output (local commit)") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -432,7 +435,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt) val actualFee = mempoolTxs.map(_.fees).sum - assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee") + assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.2, s"actualFee=$actualFee targetFee=$targetFee") generateBlocks(6) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) @@ -477,7 +480,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt) val actualFee = mempoolTxs.map(_.fees).sum - assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee") + assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.2, s"actualFee=$actualFee targetFee=$targetFee") generateBlocks(6) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) @@ -488,19 +491,19 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate too low, spending anchor output (remote commit)") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => testSpendRemoteCommitAnchor(f, nextCommit = false) } } test("commit tx feerate too low, spending anchor output (next remote commit)") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => testSpendRemoteCommitAnchor(f, nextCommit = true) } } test("commit tx feerate too low, spending anchor output (feerate upper bound reached)") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -519,7 +522,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val targetFee = Transactions.weight2fee(maxFeerate, mempoolTxs.map(_.weight).sum.toInt) val actualFee = mempoolTxs.map(_.fees).sum - assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee") + assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.25, s"actualFee=$actualFee targetFee=$targetFee") generateBlocks(6) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) @@ -531,7 +534,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate too low, spending anchor output (fastest feerate threshold)") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -551,12 +554,12 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // We allow up to 25% more than the fastest feerate. val targetFee = Transactions.weight2fee(fastestFeerate * 1.25, mempoolTxs.map(_.weight).sum.toInt) val actualFee = mempoolTxs.map(_.fees).sum - assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee") + assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.25, s"actualFee=$actualFee targetFee=$targetFee") } } test("commit tx not published, publishing it and spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 32) @@ -572,7 +575,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt) val actualFee = mempoolTxs.map(_.fees).sum - assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee") + assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.25, s"actualFee=$actualFee targetFee=$targetFee") generateBlocks(6) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) @@ -584,7 +587,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx not published, publishing it and spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val commitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager).tx @@ -605,7 +608,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt) val actualFee = mempoolTxs.map(_.fees).sum - assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee") + assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.25, s"actualFee=$actualFee targetFee=$targetFee") generateBlocks(6) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) @@ -625,7 +628,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 12000 sat, 10000 sat ) - withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(utxos, channelType) { f => import f._ // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. @@ -653,7 +656,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx fees not increased when confirmation target is far and feerate hasn't changed") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -676,7 +679,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, lowering anchor output amount") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -714,7 +717,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, adding other wallet inputs") { - withFixture(Seq(10.2 millibtc, 0.15 millibtc, 0.15 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(10.2 millibtc, 0.15 millibtc, 0.15 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 40) @@ -759,7 +762,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, not enough funds to increase fees") { - withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(10.2 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 40) @@ -793,7 +796,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, cannot use new unconfirmed inputs to increase fees") { - withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(10.2 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 40) @@ -824,7 +827,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, updating confirmation target") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 40) @@ -847,7 +850,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(mempoolAnchorTx1.txid == anchorTxId1) val targetFee1 = Transactions.weight2fee(feerateLow, mempoolTxs1.map(_.weight).sum.toInt) val actualFee1 = mempoolTxs1.map(_.fees).sum - assert(targetFee1 * 0.9 <= actualFee1 && actualFee1 <= targetFee1 * 1.1, s"actualFee=$actualFee1 targetFee=$targetFee1") + assert(targetFee1 * 0.9 <= actualFee1 && actualFee1 <= targetFee1 * 1.25, s"actualFee=$actualFee1 targetFee=$targetFee1") // The confirmation target has changed (probably because we learnt a payment preimage). // We should now use the high feerate, which corresponds to that new target. @@ -867,7 +870,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when anchor tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc, 200 millibtc), channelType) { f => import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -902,7 +905,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock anchor utxos when stopped before completion") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -924,7 +927,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx confirmed, not publishing htlc tx") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ // Add htlcs in both directions and ensure that preimages are available. @@ -991,7 +994,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("next remote commit tx confirmed, not publishing htlc tx") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ // Add one htlc in the current commitment and one htlc in the next commitment. @@ -1078,7 +1081,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("not enough funds to increase htlc tx feerate") { - withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(10.5 millibtc), channelType) { f => import f._ val (_, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight()) @@ -1100,7 +1103,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val htlcSuccessTx = getMempoolTxs(1).head val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx.weight.toInt) - assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx.fees && htlcSuccessTx.fees <= htlcSuccessTargetFee * 1.2, s"actualFee=${htlcSuccessTx.fees} targetFee=$htlcSuccessTargetFee") + assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx.fees && htlcSuccessTx.fees <= htlcSuccessTargetFee * 1.25, s"actualFee=${htlcSuccessTx.fees} targetFee=$htlcSuccessTargetFee") assert(htlcSuccessTx.fees <= htlcSuccess.txInfo.amountIn) generateBlocks(6) @@ -1157,7 +1160,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate too low, adding wallet inputs") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputs()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val targetFeerate = FeeratePerKw(15_000 sat) @@ -1172,7 +1175,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate zero, adding wallet inputs") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + assume(channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + withFixture(Seq(500 millibtc), channelType) { f => import f._ val targetFeerate = FeeratePerKw(15_000 sat) @@ -1189,7 +1193,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate zero, high commit feerate, adding wallet inputs") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + assume(channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + withFixture(Seq(500 millibtc), channelType) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate @@ -1206,7 +1211,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate zero, adding wallet inputs (feerate upper bound reached)") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + assume(channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) + withFixture(Seq(500 millibtc), channelType) { f => import f._ val targetFeerate = FeeratePerKw(15_000 sat) @@ -1242,10 +1248,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 5200 sat, 5100 sat ) - withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(utxos, channelType) { f => import f._ - val targetFeerate = FeeratePerKw(8_000 sat) + val targetFeerate = FeeratePerKw(9_000 sat) val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30) // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. setFeerate(targetFeerate, blockTarget = 12) @@ -1257,7 +1263,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx not confirming, lowering output amount") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val initialFeerate = FeeratePerKw(15_000 sat) @@ -1291,7 +1297,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx not confirming, adding other wallet inputs") { - withFixture(Seq(1_010_000 sat, 10_000 sat), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(1_010_000 sat, 10_000 sat), channelType) { f => import f._ val initialFeerate = FeeratePerKw(3_000 sat) @@ -1320,12 +1326,12 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(htlcSuccessTx1.fees < htlcSuccessTx2.fees) assert(htlcSuccessInputs1 != htlcSuccessInputs2) val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx2.weight.toInt) - assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx2.fees && htlcSuccessTx2.fees <= htlcSuccessTargetFee * 1.1, s"actualFee=${htlcSuccessTx2.fees} targetFee=$htlcSuccessTargetFee") + assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx2.fees && htlcSuccessTx2.fees <= htlcSuccessTargetFee * 1.25, s"actualFee=${htlcSuccessTx2.fees} targetFee=$htlcSuccessTargetFee") } } test("htlc success tx double-spent by claim-htlc-timeout") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val feerate = FeeratePerKw(15_000 sat) @@ -1356,7 +1362,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx confirmation target reached, increasing fees") { - withFixture(Seq(50 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(50 millibtc), channelType) { f => import f._ val initialFeerate = FeeratePerKw(10_000 sat) @@ -1386,7 +1392,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc timeout tx not confirming, increasing fees") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ val feerate = FeeratePerKw(15_000 sat) @@ -1423,7 +1429,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("utxos count too low, setting short confirmation target") { - withFixture(Seq(15 millibtc, 10 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(15 millibtc, 10 millibtc, 5 millibtc), channelType) { f => import f._ val (_, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 144) @@ -1443,7 +1449,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when htlc tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc, 200 millibtc), channelType) { f => import f._ val targetFeerate = FeeratePerKw(5_000 sat) @@ -1477,7 +1483,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock htlc utxos when stopped before completion") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(500 millibtc), channelType) { f => import f._ setFeerate(FeeratePerKw(5_000 sat)) @@ -1495,7 +1501,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("local commit tx confirmed, not publishing claim htlc tx") { - withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(11 millibtc), channelType) { f => import f._ // Add htlcs in both directions and ensure that preimages are available. @@ -1661,7 +1667,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("claim htlc tx feerate high enough, not changing output amount") { - withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + withFixture(Seq(11 millibtc), channelType) { f => import f._ val currentFeerate = alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates.fast @@ -1857,7 +1863,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(mempoolTxs.map(_.txid).toSet == Set(claimHtlcSuccessTx.txid)) } } +} +class ReplaceableTxPublisherTaprootChannelsSpec extends ReplaceableTxPublisherSpec { + override val channelType: SupportedChannelType = ChannelTypes.SimpleTaprootChannelsStaging() } class ReplaceableTxPublisherWithEclairSignerSpec extends ReplaceableTxPublisherSpec { 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 3cc7c16a64..3f2ec5cc52 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 @@ -98,6 +98,8 @@ object ChannelStateTestsTags { val SimpleClose = "option_simple_close" /** If set, disable option_splice for one node. */ val DisableSplice = "disable_splice" + /** If set, channels weill use option_simple_taproot_staging */ + val OptionSimpleTaprootStaging = "option_simple_taproot_staging" } trait ChannelStateTestsBase extends Assertions with Eventually { @@ -192,6 +194,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootStaging))(_.updated(Features.SimpleTaprootStaging, FeatureSupport.Optional)) ) val nodeParamsB1 = nodeParamsB.copy(features = nodeParamsB.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableWumbo))(_.removed(Features.Wumbo)) @@ -205,6 +208,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableSplice))(_.removed(Features.SplicePrototype)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootStaging))(_.updated(Features.SimpleTaprootStaging, FeatureSupport.Optional)) ) (nodeParamsA1, nodeParamsB1) } @@ -250,7 +254,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val channelConfig = ChannelConfig.standard val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags, channelFlags) - val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) || tags.contains(ChannelStateTestsTags.OptionSimpleTaprootStaging)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val fundingAmount = TestConstants.fundingSatoshis val initiatorPushAmount = if (tags.contains(ChannelStateTestsTags.NoPushAmount)) None else Some(TestConstants.initiatorPushAmount) val nonInitiatorPushAmount = if (tags.contains(ChannelStateTestsTags.NonInitiatorPushAmount)) Some(TestConstants.nonInitiatorPushAmount) else None @@ -584,8 +588,10 @@ trait ChannelStateTestsBase extends Assertions with Eventually { assert(publishedLocalCommitTx.txid == commitTx.txid) val commitInput = closingState.commitments.latest.commitInput Transaction.correctlySpends(publishedLocalCommitTx, Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - if (closingState.commitments.params.commitmentFormat.isInstanceOf[Transactions.AnchorOutputsCommitmentFormat]) { - assert(s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) + closingState.commitments.params.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => + assert(s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) + case _ => () } // if s has a main output in the commit tx (when it has a non-dust balance), it should be claimed localCommitPublished.claimMainDelayedOutputTx.foreach(tx => s2blockchain.expectMsg(TxPublisher.PublishFinalTx(tx, tx.fee, None))) 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 45cb04a8cf..8038a5ed74 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 @@ -235,6 +235,38 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny assert(aliceData.latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx.txid == fundingTxId) } + test("complete interactive-tx protocol (simple taproot channels, with push amount)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.DualFunding), Tag("both_push_amount")) { f => + import f._ + + val listener = TestProbe() + alice.underlyingActor.context.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + + val expectedBalanceAlice = TestConstants.fundingSatoshis.toMilliSatoshi + TestConstants.nonInitiatorPushAmount - TestConstants.initiatorPushAmount + assert(expectedBalanceAlice == 900_000_000.msat) + val expectedBalanceBob = TestConstants.nonInitiatorFundingSatoshis.toMilliSatoshi + TestConstants.initiatorPushAmount - TestConstants.nonInitiatorPushAmount + assert(expectedBalanceBob == 600_000_000.msat) + + // Bob sends its signatures first as he contributed less than Alice. + bob2alice.expectMsgType[TxSignatures] + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + assert(bobData.commitments.latest.localCommit.spec.toLocal == expectedBalanceBob) + assert(bobData.commitments.latest.localCommit.spec.toRemote == expectedBalanceAlice) + + // Alice receives Bob's signatures and sends her own signatures. + bob2alice.forward(alice) + alice2bob.expectMsgType[TxSignatures] + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + assert(aliceData.commitments.latest.localCommit.spec.toLocal == expectedBalanceAlice) + assert(aliceData.commitments.latest.localCommit.spec.toRemote == expectedBalanceBob) + } + test("recv invalid CommitSig", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ 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 8397f210a1..5c300bac62 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 @@ -29,11 +29,11 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.ProcessCurrentBlockHeight import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, SetChannelId} +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, SetChannelId} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.ClaimLocalAnchorOutputTx +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, ClaimLocalAnchorOutputTx, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -1094,16 +1094,28 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].status == DualFundingStatus.WaitingForConfirmations) } - test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => + def receiveError(f: FixtureParam): Unit = { import f._ val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "dual funding d34d") awaitCond(alice.stateName == CLOSING) assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => alice2blockchain.expectMsgType[PublishReplaceableTx] // claim anchor + case Transactions.DefaultCommitmentFormat => () + } alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) } + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => + receiveError(f) + } + + test("recv Error (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DualFunding)) { f => + receiveError(f) + } + test("recv Error (remote commit published)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val aliceCommitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 2329b46a8e..a98715e306 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -34,6 +34,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFactory, PimpTestFSM} +import fr.acinq.eclair.channel.states.ChannelStateTestsTags.{AnchorOutputsZeroFeeHtlcTxs, OptionSimpleTaprootStaging, ZeroConf} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.db.RevokedHtlcInfoCleaner.ForgetHtlcInfos import fr.acinq.eclair.io.Peer.LiquidityPurchaseSigned @@ -41,6 +42,7 @@ import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, ClaimLocalAnchorOutputTx, InputInfo, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.wire.protocol._ import org.scalatest.Inside.inside import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -57,9 +59,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik type FixtureParam = SetupFixture implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging + val useTaproot = false override def withFixture(test: OneArgTest): Outcome = { - val tags = test.tags + ChannelStateTestsTags.DualFunding + val tags = if (useTaproot) { + test.tags + ChannelStateTestsTags.DualFunding + ChannelStateTestsTags.OptionSimpleTaprootStaging + ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs + } else { + test.tags + ChannelStateTestsTags.DualFunding + } val setup = init(tags = tags) import setup._ reachNormal(setup, tags) @@ -616,7 +623,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(commitment.localCommit.spec.toLocal == 650_000_000.msat) assert(commitment.localChannelReserve == 15_000.sat) val commitFees = Transactions.commitTxTotalCost(commitment.remoteParams.dustLimit, commitment.remoteCommit.spec, commitment.params.commitmentFormat) - assert(commitFees > 20_000.sat) + commitment.commitInput match { + case _: InputInfo.TaprootInput => + assert(commitFees > 7_000.sat) + case _ => + assert(commitFees > 20_000.sat) + } val sender = TestProbe() val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = None, Some(SpliceOut(630_000 sat, defaultSpliceOutScriptPubKey)), requestFunding_opt = None) @@ -1043,6 +1055,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } test("recv TxAbort (after CommitSig)") { f => + assume(!useTaproot) import f._ val sender = TestProbe() @@ -1569,6 +1582,38 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) } + test("recv CMD_ADD_HTLC with multiple commitments and reconnect (simple taproot channels", Tag(OptionSimpleTaprootStaging), Tag(AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + val sender = TestProbe() + alice ! CMD_ADD_HTLC(sender.ref, 500_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + alice ! CMD_SIGN() + assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) + assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) + // Bob disconnects before receiving Alice's commit_sig. + disconnect(f) + reconnect(f) + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) + alice2bob.forward(bob) + assert(alice2bob.expectMsgType[CommitSig].batchSize == 2) + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + assert(bob2alice.expectMsgType[CommitSig].batchSize == 2) + bob2alice.forward(alice) + assert(bob2alice.expectMsgType[CommitSig].batchSize == 2) + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(_.localCommit.spec.htlcs.size == 1)) + } + test("recv CMD_ADD_HTLC while a splice is requested") { f => import f._ val sender = TestProbe() @@ -2702,6 +2747,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2bob.expectMsgType[Error] val commitTx2 = assertPublished(alice2blockchain, "commit-tx") Transaction.correctlySpends(commitTx2, Seq(fundingTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val isAnchorOutputs = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => true + case _ => false + } + if (isAnchorOutputs) { + assertPublished(alice2blockchain, "local-anchor") + } val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") // Alice publishes her htlc timeout transactions. val htlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-timeout")) @@ -2709,6 +2762,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val watchConfirmedCommit2 = alice2blockchain.expectWatchTxConfirmed(commitTx2.txid) val watchConfirmedClaimMainDelayed2 = alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) + // watch for all htlc outputs from local commit-tx to be spent + if (isAnchorOutputs) { + alice2blockchain.expectMsgType[WatchOutputSpent] + } val watchHtlcsOut = htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) @@ -2738,6 +2795,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Bob's htlc-timeout txs confirm. bob ! WatchFundingSpentTriggered(commitTx2) + if (isAnchorOutputs) { + assertPublished(bob2blockchain, "local-anchor") + assertPublished(bob2blockchain, "remote-main-delayed") + } val bobHtlcsTxsOut = htlcs.bobToAlice.map(_ => assertPublished(bob2blockchain, "claim-htlc-timeout")) val remoteOutpoints = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.map(rcp => rcp.htlcTxs.filter(_._2.isEmpty).keys).toSeq.flatten assert(remoteOutpoints.size == htlcs.bobToAlice.size) @@ -3456,5 +3517,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(finalState.commitments.latest.localCommit.spec.toLocal == 805_000_000.msat) assert(finalState.commitments.latest.localCommit.spec.toRemote == 695_000_000.msat) } - } + +class NormalSplicesStateWithTaprootChannelsSpec extends NormalSplicesStateSpec { + override val useTaproot: Boolean = true +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 7766558f80..cdf3510a6d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -266,7 +266,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("resume htlc settlement", Tag(IgnoreChannelUpdates)) { f => + def resumeHTlcSettlement(f: FixtureParam): Unit = { import f._ // Successfully send a first payment. @@ -313,6 +313,14 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex == 4) } + test("resume htlc settlement", Tag(IgnoreChannelUpdates)) { f => + resumeHTlcSettlement(f) + } + + test("resume htlc settlement (simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(IgnoreChannelUpdates)) { f => + resumeHTlcSettlement(f) + } + test("reconnect with an outdated commitment", Tag(IgnoreChannelUpdates), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 5c587a0109..ce03d3a327 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -531,6 +531,54 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(bob.stateName == NEGOTIATING_SIMPLE) } + test("recv ClosingComplete (both outputs, simple taproot channels)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + aliceClose(f) + val aliceClosingComplete = alice2bob.expectMsgType[ClosingComplete] + assert(aliceClosingComplete.fees > 0.sat) + assert(aliceClosingComplete.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + assert(aliceClosingComplete.closerOutputOnlyPartialSig_opt.nonEmpty) + assert(aliceClosingComplete.closeeOutputOnlyPartialSig_opt.isEmpty) + val bobClosingComplete = bob2alice.expectMsgType[ClosingComplete] + assert(bobClosingComplete.fees > 0.sat) + assert(bobClosingComplete.closerAndCloseeOutputsPartialSig_opt.nonEmpty) + assert(bobClosingComplete.closerOutputOnlyPartialSig_opt.nonEmpty) + assert(bobClosingComplete.closeeOutputOnlyPartialSig_opt.isEmpty) + + alice2bob.forward(bob, aliceClosingComplete) + val bobClosingSig = bob2alice.expectMsgType[ClosingSig] + assert(bobClosingSig.fees == aliceClosingComplete.fees) + assert(bobClosingSig.lockTime == aliceClosingComplete.lockTime) + bob2alice.forward(alice, bobClosingSig) + val aliceTx = alice2blockchain.expectMsgType[PublishFinalTx] + assert(aliceTx.desc == "closing") + assert(aliceTx.fee > 0.sat) + alice2blockchain.expectWatchTxConfirmed(aliceTx.tx.txid) + inside(bob2blockchain.expectMsgType[PublishFinalTx]) { p => + assert(p.tx.txid == aliceTx.tx.txid) + assert(p.fee == 0.sat) + } + bob2blockchain.expectWatchTxConfirmed(aliceTx.tx.txid) + assert(alice.stateName == NEGOTIATING_SIMPLE) + + bob2alice.forward(alice, bobClosingComplete) + val aliceClosingSig = alice2bob.expectMsgType[ClosingSig] + assert(aliceClosingSig.fees == bobClosingComplete.fees) + assert(aliceClosingSig.lockTime == bobClosingComplete.lockTime) + alice2bob.forward(bob, aliceClosingSig) + val bobTx = bob2blockchain.expectMsgType[PublishFinalTx] + assert(bobTx.desc == "closing") + assert(bobTx.fee > 0.sat) + bob2blockchain.expectWatchTxConfirmed(bobTx.tx.txid) + inside(alice2blockchain.expectMsgType[PublishFinalTx]) { p => + assert(p.tx.txid == bobTx.tx.txid) + assert(p.fee == 0.sat) + } + assert(aliceTx.tx.txid != bobTx.tx.txid) + alice2blockchain.expectWatchTxConfirmed(bobTx.tx.txid) + assert(bob.stateName == NEGOTIATING_SIMPLE) + } + test("recv ClosingComplete (single output)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ aliceClose(f) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 45eaf70346..7364f9dc25 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 @@ -33,7 +33,7 @@ import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ -import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} @@ -400,7 +400,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob claims the htlc output from Alice's commit tx using its preimage. bob ! WatchFundingSpentTriggered(lcp.commitTx) - if (initialState.commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { + if (initialState.commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx) || initialState.commitments.params.channelFeatures.hasFeature(Features.SimpleTaprootStaging)) { assert(bob2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) bob2blockchain.expectMsgType[PublishFinalTx] // main-delayed } @@ -434,6 +434,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromClaimHtlcSuccess(f) } + test("recv WatchOutputSpentTriggered (extract preimage from Claim-HTLC-success tx, simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging)) { f => + extractPreimageFromClaimHtlcSuccess(f) + } + private def extractPreimageFromHtlcSuccess(f: FixtureParam): Unit = { import f._ @@ -476,6 +480,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromHtlcSuccess(f) } + test("recv WatchOutputSpentTriggered (extract preimage from HTLC-success tx, simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging)) { f => + extractPreimageFromHtlcSuccess(f) + } + private def extractPreimageFromRemovedHtlc(f: FixtureParam): Unit = { import f._ @@ -566,6 +574,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromRemovedHtlc(f) } + test("recv WatchOutputSpentTriggered (extract preimage for removed HTLC, simple taproot channels)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + extractPreimageFromRemovedHtlc(f) + } + private def extractPreimageFromNextHtlcs(f: FixtureParam): Unit = { import f._ @@ -603,7 +615,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice prepares Claim-HTLC-timeout transactions for each HTLC. alice ! WatchFundingSpentTriggered(rcp.commitTx) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { + if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx) || alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.SimpleTaprootStaging)) { assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) assert(alice2blockchain.expectMsgType[PublishFinalTx].desc == "remote-main-delayed") } @@ -611,7 +623,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get) assert(claimHtlcTimeoutTxs.size == 3) assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rcp.commitTx.txid) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { + if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx) || alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.SimpleTaprootStaging)) { alice2blockchain.expectMsgType[WatchTxConfirmed] // remote-main-delayed } assert(Set( @@ -656,7 +668,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with extractPreimageFromNextHtlcs(f) } - test("recv CMD_BUMP_FORCE_CLOSE_FEE (local commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv WatchOutputSpentTriggered (extract preimage for next batch of HTLCs, simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging)) { f => + extractPreimageFromNextHtlcs(f) + } + + def `recv CMD_BUMP_FORCE_CLOSE_FEE (local commit)`(f: FixtureParam): Unit = { import f._ localClose(alice, alice2blockchain) @@ -691,6 +707,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.contains(localCommitPublished2)) } + test("recv CMD_BUMP_FORCE_CLOSE_FEE (local commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + `recv CMD_BUMP_FORCE_CLOSE_FEE (local commit)`(f) + } + + test("recv CMD_BUMP_FORCE_CLOSE_FEE (local commit, simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging)) { f => + `recv CMD_BUMP_FORCE_CLOSE_FEE (local commit)`(f) + } + def testLocalCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ @@ -751,6 +775,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testLocalCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) } + test("recv WatchTxConfirmedTriggered (local commit, simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testLocalCommitTxConfirmed(f, ChannelFeatures(Features.SimpleTaprootStaging)) + } + test("recv WatchTxConfirmedTriggered (local commit with multiple htlcs for the same payment)") { f => import f._ @@ -1158,6 +1186,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit } + test("recv WatchFundingSpentTriggered (remote commit) taproot channel ", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val bobCommitTx = bobCommitTxs.last.commitTx.tx + assert(bobCommitTx.txOut.size == 4) // two main outputs + val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + assert(closingState.claimMainOutputTx.nonEmpty) + assert(closingState.claimHtlcTxs.isEmpty) + val txPublished = txListener.expectMsgType[TransactionPublished] + assert(txPublished.tx == bobCommitTx) + assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit + } + test("recv WatchFundingSpentTriggered (remote commit, public channel)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.DoNotInterceptGossip)) { f => import f._ @@ -1326,6 +1366,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testRemoteCommitTxWithHtlcsConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) } + test("recv WatchTxConfirmedTriggered (remote commit with multiple htlcs for the same payment, simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging)) { f => + testRemoteCommitTxWithHtlcsConfirmed(f, ChannelFeatures(Features.SimpleTaprootStaging)) + } + test("recv WatchTxConfirmedTriggered (remote commit) followed by CMD_FULFILL_HTLC") { f => import f._ // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. @@ -1373,6 +1417,56 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(100 millis) } + test("recv WatchTxConfirmedTriggered (remote commit, simple taproot channels) followed by CMD_FULFILL_HTLC", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. + val (r1, htlc1) = addHtlc(110000000 msat, CltvExpiryDelta(48), bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + alice2relayer.expectMsgType[RelayForward] + + // An HTLC Alice -> Bob is only signed by Alice: Bob has two spendable commit tx. + val (_, htlc2) = addHtlc(95000000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN() + alice2bob.expectMsgType[CommitSig] // We stop here: Alice sent her CommitSig, but doesn't hear back from Bob. + + // Now Bob publishes the first commit tx (force-close). + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + assert(bobCommitTx.txOut.length == 5) // two main outputs + two anchor outputs + 1 HTLC + val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + assert(closingState.claimMainOutputTx.nonEmpty) + assert(closingState.claimHtlcTxs.size == 1) + assert(getClaimHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet + assert(getClaimHtlcTimeoutTxs(closingState).isEmpty) + + // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. + alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) + alice2blockchain.expectMsgType[PublishReplaceableTx] + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == closingState.claimMainOutputTx.get.tx) + val claimHtlcSuccessTx = getClaimHtlcSuccessTxs(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get).head.tx + Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.asInstanceOf[ClaimHtlcSuccessTx] + assert(publishHtlcSuccessTx.tx == claimHtlcSuccessTx) + assert(publishHtlcSuccessTx.confirmationTarget == ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) + + // Alice resets watches on all relevant transactions. + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == closingState.claimMainOutputTx.get.tx.txid) + val watchHtlcSuccess = alice2blockchain.expectMsgType[WatchOutputSpent] + assert(watchHtlcSuccess.txId == bobCommitTx.txid) + assert(watchHtlcSuccess.outputIndex == claimHtlcSuccessTx.txIn.head.outPoint.index) + alice2blockchain.expectNoMessage(100 millis) + + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) + // The second htlc was not included in the commit tx published on-chain, so we can consider it failed + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc2) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.irrevocablySpent.values.toSet == Set(bobCommitTx, closingState.claimMainOutputTx.get.tx, claimHtlcSuccessTx)) + awaitCond(alice.stateName == CLOSED) + alice2blockchain.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) + } + test("recv INPUT_RESTORED (remote commit)") { f => import f._ @@ -1495,6 +1589,26 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } + test("recv WatchTxConfirmedTriggered (next remote commit, simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.SimpleTaprootStaging)) + val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, closingState.claimMainOutputTx.get.tx) + alice2relayer.expectNoMessage(100 millis) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) + alice2relayer.expectNoMessage(250 millis) + awaitCond(alice.stateName == CLOSED) + } + test("recv WatchTxConfirmedTriggered (next remote commit) followed by CMD_FULFILL_HTLC") { f => import f._ // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. @@ -1780,13 +1894,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet val spentOutpoints = penaltyTxs.flatMap(_.tx.txIn.map(_.outPoint)).toSet assert(spentOutpoints.forall(_.txid == bobRevokedTx.txid)) - if (channelFeatures.commitmentFormat.isInstanceOf[AnchorOutputsCommitmentFormat]) { - assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors - } - else if (channelFeatures.paysDirectlyToWallet) { - assert(spentOutpoints.size == bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet - } else { - assert(spentOutpoints.size == bobRevokedTx.txOut.size) + channelFeatures.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => + assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors + case _ if channelFeatures.paysDirectlyToWallet => + assert(spentOutpoints.size == bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet + case _ => + assert(spentOutpoints.size == bobRevokedTx.txOut.size) } // alice watches confirmation for the outputs only her can claim @@ -1836,6 +1950,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with testFundingSpentRevokedTx(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) } + test("recv WatchFundingSpentTriggered (one revoked tx, simple taproot channels)", Tag(ChannelStateTestsTags.OptionSimpleTaprootStaging), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testFundingSpentRevokedTx(f, ChannelFeatures(Features.SimpleTaprootStaging)) + } + test("recv WatchFundingSpentTriggered (multiple revoked tx)") { f => import f._ val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey)) 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 cd268f5f2c..0ab3d0e066 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 @@ -93,7 +93,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // and re-encode it with the new codec val bin_new = ByteVector(channelDataCodec.encode(data_new).require.toByteVector.toArray) // data should now be encoded under the new format - assert(bin_new.startsWith(hex"04000a")) + assert(bin_new.startsWith(hex"05000a")) // now let's decode it again val data_new2 = channelDataCodec.decode(bin_new.toBitVector).require.value // data should match perfectly @@ -169,7 +169,7 @@ class ChannelCodecsSpec extends AnyFunSuite { val dataClosingRemote = hex"0100250000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009157899e63ee4dc5f8ce060a1bab76e8ae829aa9b3cafe6ebe19f2a608a453ff680000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff160014c1c7289591b6e67b38e3f0a63a433da5493348410000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000229a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048602d93a6ec6fd9e9147cac7fa53c602ce68dcc35013e2edb7475a3f0b35557307e602c1d9c74cd082a8e25197d98ddbf62039113f3909122ed479b2005aab8b85836a03304a5e9c4fc78c68dbba22e21b76c3cd24ae7c87414f63ed6d429c67dc721d9103bd4c12aa179f4fe1ee2cf08c770f794caf8a83e7a9b091e7b44512ed3a845bb200000003229a82000000000000000007000300fd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000030000000000e4e1c0ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b100002494b30e6e77ee6cabb23c8a7c081a7179d5f712d843914abd26867855cc0271b4c4af2b65b2e568c9aaf41fa200afee1821aa546e177cae8e0f753ef0c15a8f47d06f1ac76e0bdd4d0e38926e908d92d7252bf6246018bab1bb2477c1265f1bd7dcabbba79d5b8df198640ae5c845bdd16df15d1b40380dabefd6c7b3ca5afb188f38c3ebb77c06c8eb783ff1fb029a8c194caa09b3469f779180e2a09eb0b40d78868fd9226c9f6d25349495a90351a4be2edf01105fc94310b764f8a3b18557934a9f9b39aa47a7ef69d3cf6d9201fcdb4537e12c9e6c589d5f4e800a49c4e090005d526d5515000e6855d0546acea7322cc195df08662a1b9a74644819285eae65b93818a3faf5de7769ec8bed30fdf57af35acfb6f88adc6b95920ae1edc795eb06a0039b16da091f0eab98c83b176ca208ab24c41978bc782457aa1018f49edb22ece7fa520d0053ee995f2850b76b095b298b55b6403b82a16608d369aafdf5a19364b1180b2066cf00d5419bd4e283cdc0c9535e4a0ad0d0615647a3601b95ff5f206538e4482731273e4e33235835cd227d97cab80e9a6c21853bbcb47ffa529a9ac7b0ed70af6d0574f9f1e979d99a94dacad5a0682623b9d25a4885f98bfb98222637c64ccefbbe9568ecf3cc103660c44301fb289104c5083b07ba9ce535c702dac687cefd4cd1d1d6cf9eb6dab0c9b643b6ec4fa3cf2126a65d6a7a344652523849b538d06e1d0089d10cc9edb58ddcc0e8d4f451712f36c38198325b71c9aedecb90845996714dc3671455462875f4cca75f3c2b0060b0fc0ec7e927e8900c1036ae93155ada9047186e06364ae4e63a6871fe6747d75b2f8ff30a925835504ec7f23bc2ba405df732e404542f2eae7d6646db3080d35e4c1dfa385370318945b530f2777e2503fd85818051b3482984accdbfebbdcc80e76bd8ab0cd3da3e0f5ab9d9f344016c11ce513ff3b6abc9928947da5f2785cfae693126ce5f6ce6049fbce41654aa5e0d86e32bd1dedb16114e8c66419fefd87d83a063e02f7f8a37db88cc8f5634caf033efc6e6867b81fedf436b6295633dff3bac0b325e9dd34cf3fa924d12b4daf2edf0ff58a2d78ff709f8711a536a34b31fa2daab34b5e877b88cd8ced38ea6d22230390c3567586d111aa21a0ab8bff630b86ae8aba818f62fc68e6169bbc88d3586a1e58652b9e90dc2f74809f83e2ffb47b6257bac95c487d8f32c72e8cdb24bff3d96dedf6d5fba9602ea8984abb3f2a36a7491d602767b14a712256d6a0b2f8bc6181aaa64b6ce0ce6bc64aba2432cc1b7762079abecc59f9e9c19d408ddbf0ea2294c2ad7c12717acc1ab0f4246bcd000ce886049a935b712dba203907d1defaa07d2a37c093eeec8ec5fbcb42341361611d9416f009ee22d21443eb58c7835bfe2082fdec855fbdc26c11f120eb5eb74574bb0f12429e401420350eef27ddc833dee32d96a909f49ef8e1d962ea9d0996f1f910dcde1609b0cb6338e4e29fed5327156fa5dbb82f9b5f7bd184c1eb24380fe3ad40727fa9a60bb9a5a14dfa5fa34e59243198bdbaa26e4cf7db298712f9ea41583dfd63e2f87262107e48241c24341cacab2f0ae64168b0f539b7a570991a44381c26b669a74f973907cab17cb8e8171e6d078379f29906ad926d4c2e85e0fb31cabf4019a797c065e0b1d3ba6c927d187365c596d009c79777eaa6b9d798d60ee76dd6c88dffc1f7a8619bb21b8a3f48ff774bff36b11e7c0f17efe58b38fe41c5099c0eef256ed66cab7483303b84d83188588a05aa775d2a5c28f912da358b4fc9f9e348f6312878d3c919567679931d547a3d05c401b324785e7e2c4387692792cf762925110db2ef8d85bec34f59bb98de736d479c3580729672af7b0eeb9b2eadf706b52c3dd16efca574616c00fd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000040000000000e4e1c0ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b1000038c08be032cff6f282a21a7a69a3bf3da8fa32f751f4978232ae774aea67295cd154028a32d0881dc27fb5a46beb9e5931926d35866103d8bef028dd6031cad768283018e7e8d49e81b033e42e24105d9681f6638f634bb482b38182d08b9863011e3c19949a86fe744f90212a3076e149125742a5e8ae8f72f0af19370fe1a995ddcc48d6cff9f7e3ff96d18fbdb4bc034a0dc23ee3d31489ae2d4438b46bb9a23d53de03d0686710db3a5a72e504a65fa370f21e5d2c023c0d1bd6e0821853bea11dad9c9ea88a4b12f2c6a18b47bfb35aeb45ff012ea413ff83de1ab0c2e5a9166efa17ebff5b83ca9c1d7b680e57e9d5abff81980d42403cac0b6e12070346545ca9d5b794f3b89066bd37391d4c32ec7b610efabe18d4d315109012430c346d145926681c5fbd82d63500b4c7cb4e061c096ef8f75f0695e0b17321dd32cfe1497d71b59d7132a3580d81632b7a9bfe9d621e707d4d5654721dabc154b522013a48974545a822045cdf649270c2b0ce422a8f87df4e3603dc3c1c6701e62c9a7908e9ab7dd1fb19d8b4fb08a59e7877fd4dc733c059dcff11c59b70912e07e5afa2e609e2d8af9a5351304b2fb79b9137cb8d3a82046ff3568af861c3376f64fd5fc6e056139a0f2fe180686db998bf9e956cb3548bfd6ecbaafbe31a35d1f496696adcd140b28430a2bdcd0ffd75f78e3272e3b3ef3c441d9884a748cfb08a4f00dbf5c754583acfe3f90e3caaed206e64a018fc58d26234740e079bc5d01b2633fe5bd81e256aeccbd74b8904b3bbc86c1527af0669c7c73f58160e02ddc6618a5a1169e8be3a5a7809689faecf535d5532c878533fd26a886b44c8e673c63324b22b2e28e3fd3674d75256ab6bdbf418fed262d30cfb55bf5a6ef02d595d947b2aca6ed178086ffbbbb1dd7863164e0c75d56587be40fd3c5543bbe87d89b653e062a67b3295a69e724ca9e9c80ff5289d53d346861d97a1f38d194d257f64516129cafa0cb7ec861fcb004c919a22016f96b0ab646c600882aaeb39d20d374162a048b8548879e6e5042e5b913a606abbb847422256c1883578524448dc3c6a11608b20206d9d908ec98e81163ceb586faa501889284c970ec4d6a8a0e142b497447db2d1c8cf7fa9fa51eb2dbf8f99e014be9128e2009bdd6fb1558e85fb528136d822ac60a00f1af625ea74938b05aeec2fec143e9ef1abd326166a424464932e2fdff2ce553e5404113383ca261a48fb8dacec0a72005b5a5d5c4459d795372bc6d9a7d7788a35cdb1769b728380baceed0a667f63a06c39eb4250c5b90da1d80c6f6dd2a8300ef8f028ef11c3ae7cb4a95a01f6777dad6b6a84e5f420e9510698d24963d2c81e85bb1fea6e66fe53fb108f417065dced6547eef1f2c1baa4cb2b477ca7cba58746ba7ad13d73030ec351631d162161be16270410bfa756e27d196467ac64d3fc4ae70b82cc7ee9651029259ed39b81b98e55793dbcd1e2a94b3c8a4c1c22827c176ce103d1789413eddc9d23f74046f9984d0753bc29611b34fdfeaf6bfc7f18f591cc0a689ebc1f7a4d53b21fc89bdef6cab1ca85f942508cf163f2c1b6b6aa5391b3a3ebbf30fe0eb67f5864052c86c5056e507513f422efd721e6c981c736ed886c55e914c08cd7bf5ec7a8db36e7e7fcc2defd601479b672ab608895cd43bf8554733b328757630ebd3f230aa0a30809e7b76dcf59a7d9649c236ff4ffb1271ce40a0ebe9cabfce5fb297cd8b291a5556917438924c5f76dbc6bb8ab50835a05eea952939b065f0c23669f115b9181cede8f9d583d5bd3a651114f7277291481555d8e66d923111a71c49d12bee354107737349acfb08ea7ab046728777a9753f631353d0880d6e3fe6eb92c8c12f923a50942a4bef3b492ede9bc0cfb387fb8b39e2581e8200fd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000050000000001312d00ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b0f0003c45571d40572e15114d1731236a3768c74cf13eda20d58aafaba564570a043fd6e6543c50cff6e1f8cc5342cc95f2e30b9003fa74ec84e2f7c95bfde69f464d5886d8ae0a99294ff5b18bf6c57f3ff0c8577030d4d7a9469302228d9510b5b85e962b9a48bb1bfb7710d43e3df7fb78d734f258c05886d532e108e55f2a2159537cbe48edd13245a612f41889f5014dd5d1734c0416315384ede850b25ee716f24d40b77cc17e341f5d6e61c4589d245f9923a501d2548716c5568eac4691476b60cfcdb2d94ba948cbb7ecb710871b23ce8a9179d5fdcea8fc94e4bc9b56000e425734dab1247a441a73598e879934bf161815a037bfa259b3038490ea6cfb577ca825ecbd4b39c4f1397902bfc9587c52680755097e3938113373aaddbe62ca55db19e45140f125d84ac69cfae7103d11079c2adc3d3c51f13797e82443a8bf69c2acb34e7646095e18d160338c9aafedebf6e30e72302ea579585d74ef2e0af3005f93e9c76902dc272871e91dd5124c99a19597adcaca362d2cf9afaa674958e19a9c6d37b76b6b2be9e3779297e5f65c733c19dd456d62a66a2452b0eb091d97a82b965f1b5536d11b7f6ee8643d5505224e37839798ca99d8623bdd1e2f50b6b60b450a8e0572f44dee06a8658014e650ba5cb614f727035a8da06a2f7a60564e22971c2488fdd1fffee6b8b9eb5e9596b623a447bc4b06294952a18a6a1cd988c9d2223fec1714724ba2168639db964e1a9e2cdef4efc1c1ac7c2c5c1ea81daa34ddc8278ba8e9e54cbc3234725fa874a9a3c0c58f629d3872fa0b765b6cff95593492782398dcf097bee861476a08f31d82eeefac4c8f868cb3d1acddea2ba4f5dfc3671c42dca6a584e5f3a0e7aebe1083a997a03279f27d7319c6928e9d190392f718bd7278b806846f5c2c2dd4cbc06db84261fe6a557e86439ee7fef09809930d4a78d0e43f3b11d163539461d971bf0a83b3a52d3f8fa87c600a369a06e36cfe2349798d9597362a8385a97d82a7b445a4d7f7c1541c8e51531728d432422f3478e3733b5582e3ddb1137af5e732bf85e5c9abec1c44dd1d472774d913aad656a7e3a45659e99ba048a5e85ffbdc4af146e79241ea4a45e0b0139bd3dae63c6f89b0515d6c31566d067198e51cd01f82a41cf7d04538c4393b4892332d94f8c584531426053f2b4e6e945b3d28b492f9a4364671bdc4ee878cdf9fec1ab90ca2dc131c086e0df17bfa59b0269ea1d5a1bdde7c826c2af89da485b046e17cb8ef7c55fbbbb42a409661b6a1cefd62f05c906384d475764e51c4bae7f73a904a77622bc3515913a9fcf393ff14851c779679b4b0df58d9aa8216a390b847206f0f83ec4d982462ad88c569f878368f94fd2940975bca95669471c00894420bf2fab9bb09a3372774b716a9d1bc7d991cae8ffd759fca9c3b5ecfc1323db3f4c777308af23ef7530cacb4e4bfdaee107f059b90a2b495058bc7400ffb31e12660cf8e4fe22d3a01511595bd864cdccf6123545d024a611ba380096da71b89412c13f8be3287ec4db4c86cd4f023d53d6d4a0f7102956c3d1b0769516b6dc83c66ea73c8a5131ce74a2444c3ed9accfa085ed2ae9862ac1094c1a84db8a99c82f96f370c450c53c4372d931daffe0663d4e7b3b651fe995ee3cf09fde460b8d3360b567dda5df1483407c5286f0df33567e9407f8539eaf5e117900bef904f4f6c1081c0cc57afafce7d618cf865da5d1729e1028bf2c3a58af700690650a13340dd9217f4d3e564dc7aa3c7823caafce7ae4d3220c263b3bddf8e6624ed57c5c177abec462f39b24e8f44020490bc3098525d341a51b6b91ccff06df8dc771b911659414faa0708d30902b4461dbd6621904e3a506dd1888660e22b4b46c814135f8d033dc509a37ea7af5cbeb018f000009c40000000008f0d180000000002faf080024bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a283000000002b40420f000000000022002031589753b0c53bac3611dc7043ed3fd39f3e028d084f4ccced10610770c61eed47522103074b1de9739810ce2a885ac85c0f919a17018e8826aefbe6ab92935267e25cf721031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048652aefd023d02000000000101bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a2830000000000f8719480074a0100000000000022002040c49f845d675d6dd824933252c4f85caaa4dc7b023d3c6accc9e57323dcbcff4a0100000000000022002044e4c2c048661a29bc9e194687e5e973b14379028f3d86bea22b68c4a4a25b96983a0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f8427983a0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f8427204e0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f84275837020000000000220020433f81252b8fbd947c2ade785fb393184fea5b3d6df574528efa6b01495f812700350c00000000002200207f0ec90c88cba1da4cfcd677a9962f617097c470a07b5a157d42e22f6f8c12fd040047304402204b4bdba0b08185935323d31c398004c6476b0d3781045bcdfb1f8917eaa0486f02203db71b78186cf7a500bc7b4e3216f2c8b9bd37a2911e34777adbaabccd50dbfb01483045022100a08231029e1d326be1eaf23f947766ec11b22d2a774f818fe5a6700b07ea379002202f365ea341df3d7678be1bf136d05efc60794209b4b68a7fd1e7760d2b8656fa0147522103074b1de9739810ce2a885ac85c0f919a17018e8826aefbe6ab92935267e25cf721031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048652ae1e7512200003000324baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c020000002b983a0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f84278876a914b639a79d1aa3426d75a36b98d3ef3cb79db674358763ac6721039d5c3528a43f5f532bf649ddbb05b350e34453610003e2a4a159c81dfbdb45ad7c820120876475527c21025f47871e3cf61241abadb85d3badad8c789676754a5f77d0ae5009458d2c395152ae67a914120850f1eeb36e375b36d78a9a3ecc632c9d442888ac6851b275685e0200000001baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c020000000001000000011734000000000000220020433f81252b8fbd947c2ade785fb393184fea5b3d6df574528efa6b01495f8127101b0600404693180da32f1d5e3a5cf74a174a22a363150e638208f838efb7ad8a0b43427424154595262a91ee14af9432f6c857f72cea7868a0e49c0e54b344593ef3d78d406b4ac7294e4519eb1d0b9e451955a4961f87693ae723013e24073a27928e580652aa6c394b7c98dd4f97e6245768124997b14f3d049ca0bdd4666ac97b02d65c000324baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c030000002b983a0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f84278876a914b639a79d1aa3426d75a36b98d3ef3cb79db674358763ac6721039d5c3528a43f5f532bf649ddbb05b350e34453610003e2a4a159c81dfbdb45ad7c820120876475527c21025f47871e3cf61241abadb85d3badad8c789676754a5f77d0ae5009458d2c395152ae67a914120850f1eeb36e375b36d78a9a3ecc632c9d442888ac6851b275685e0200000001baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c030000000001000000011734000000000000220020433f81252b8fbd947c2ade785fb393184fea5b3d6df574528efa6b01495f8127101b06004045b8a6b6a6611e6d5aba881c9d6cf58c0314e2596da2e4a202afa424d551ac44494b1b9bdfe85e3079f90db9ec0f0281e6796d2e2b0825ba8771b23bc41d894d409b9ae17cf065f8bf57c6aa38297af2ccf76ff61a8e289fc9dc8edcde898a9a6c4db11ecccb2d97ea9135f9360844aa1c6b595e42a353f80cb8347cb2d8413978000324baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c040000002b204e0000000000002200208930be41bd0d33eb7b5714a4d9bf3179170daef6fd827e79e39401157f5f84278876a914b639a79d1aa3426d75a36b98d3ef3cb79db674358763ac6721039d5c3528a43f5f532bf649ddbb05b350e34453610003e2a4a159c81dfbdb45ad7c820120876475527c21025f47871e3cf61241abadb85d3badad8c789676754a5f77d0ae5009458d2c395152ae67a914120850f1eeb36e375b36d78a9a3ecc632c9d442888ac6851b275685e0200000001baa7cb41926907ca87f2875f857786966641cc7cae8ce9b3c2a50d4661bf368c040000000001000000019f47000000000000220020433f81252b8fbd947c2ade785fb393184fea5b3d6df574528efa6b01495f81270f1b060040c395381f0200842628e2e4a42c6bfea2a2134509f622e4838743e21576e8dec0504690bc58416694470a9b3f8f8d3d3e81c112a19acd607532f07526a04fcf7e401e169a806c424664e39e2ae979256894e50ce777ffd3330a68444e9f57ea653831fa96cb65eb5e5b8bdb88f87c6fa0eb7c1aa54ab026159fa4e31ebb58073aef00000000000000070003fffd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000030000000000e4e1c0ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b100002494b30e6e77ee6cabb23c8a7c081a7179d5f712d843914abd26867855cc0271b4c4af2b65b2e568c9aaf41fa200afee1821aa546e177cae8e0f753ef0c15a8f47d06f1ac76e0bdd4d0e38926e908d92d7252bf6246018bab1bb2477c1265f1bd7dcabbba79d5b8df198640ae5c845bdd16df15d1b40380dabefd6c7b3ca5afb188f38c3ebb77c06c8eb783ff1fb029a8c194caa09b3469f779180e2a09eb0b40d78868fd9226c9f6d25349495a90351a4be2edf01105fc94310b764f8a3b18557934a9f9b39aa47a7ef69d3cf6d9201fcdb4537e12c9e6c589d5f4e800a49c4e090005d526d5515000e6855d0546acea7322cc195df08662a1b9a74644819285eae65b93818a3faf5de7769ec8bed30fdf57af35acfb6f88adc6b95920ae1edc795eb06a0039b16da091f0eab98c83b176ca208ab24c41978bc782457aa1018f49edb22ece7fa520d0053ee995f2850b76b095b298b55b6403b82a16608d369aafdf5a19364b1180b2066cf00d5419bd4e283cdc0c9535e4a0ad0d0615647a3601b95ff5f206538e4482731273e4e33235835cd227d97cab80e9a6c21853bbcb47ffa529a9ac7b0ed70af6d0574f9f1e979d99a94dacad5a0682623b9d25a4885f98bfb98222637c64ccefbbe9568ecf3cc103660c44301fb289104c5083b07ba9ce535c702dac687cefd4cd1d1d6cf9eb6dab0c9b643b6ec4fa3cf2126a65d6a7a344652523849b538d06e1d0089d10cc9edb58ddcc0e8d4f451712f36c38198325b71c9aedecb90845996714dc3671455462875f4cca75f3c2b0060b0fc0ec7e927e8900c1036ae93155ada9047186e06364ae4e63a6871fe6747d75b2f8ff30a925835504ec7f23bc2ba405df732e404542f2eae7d6646db3080d35e4c1dfa385370318945b530f2777e2503fd85818051b3482984accdbfebbdcc80e76bd8ab0cd3da3e0f5ab9d9f344016c11ce513ff3b6abc9928947da5f2785cfae693126ce5f6ce6049fbce41654aa5e0d86e32bd1dedb16114e8c66419fefd87d83a063e02f7f8a37db88cc8f5634caf033efc6e6867b81fedf436b6295633dff3bac0b325e9dd34cf3fa924d12b4daf2edf0ff58a2d78ff709f8711a536a34b31fa2daab34b5e877b88cd8ced38ea6d22230390c3567586d111aa21a0ab8bff630b86ae8aba818f62fc68e6169bbc88d3586a1e58652b9e90dc2f74809f83e2ffb47b6257bac95c487d8f32c72e8cdb24bff3d96dedf6d5fba9602ea8984abb3f2a36a7491d602767b14a712256d6a0b2f8bc6181aaa64b6ce0ce6bc64aba2432cc1b7762079abecc59f9e9c19d408ddbf0ea2294c2ad7c12717acc1ab0f4246bcd000ce886049a935b712dba203907d1defaa07d2a37c093eeec8ec5fbcb42341361611d9416f009ee22d21443eb58c7835bfe2082fdec855fbdc26c11f120eb5eb74574bb0f12429e401420350eef27ddc833dee32d96a909f49ef8e1d962ea9d0996f1f910dcde1609b0cb6338e4e29fed5327156fa5dbb82f9b5f7bd184c1eb24380fe3ad40727fa9a60bb9a5a14dfa5fa34e59243198bdbaa26e4cf7db298712f9ea41583dfd63e2f87262107e48241c24341cacab2f0ae64168b0f539b7a570991a44381c26b669a74f973907cab17cb8e8171e6d078379f29906ad926d4c2e85e0fb31cabf4019a797c065e0b1d3ba6c927d187365c596d009c79777eaa6b9d798d60ee76dd6c88dffc1f7a8619bb21b8a3f48ff774bff36b11e7c0f17efe58b38fe41c5099c0eef256ed66cab7483303b84d83188588a05aa775d2a5c28f912da358b4fc9f9e348f6312878d3c919567679931d547a3d05c401b324785e7e2c4387692792cf762925110db2ef8d85bec34f59bb98de736d479c3580729672af7b0eeb9b2eadf706b52c3dd16efca574616cfffd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000040000000000e4e1c0ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b1000038c08be032cff6f282a21a7a69a3bf3da8fa32f751f4978232ae774aea67295cd154028a32d0881dc27fb5a46beb9e5931926d35866103d8bef028dd6031cad768283018e7e8d49e81b033e42e24105d9681f6638f634bb482b38182d08b9863011e3c19949a86fe744f90212a3076e149125742a5e8ae8f72f0af19370fe1a995ddcc48d6cff9f7e3ff96d18fbdb4bc034a0dc23ee3d31489ae2d4438b46bb9a23d53de03d0686710db3a5a72e504a65fa370f21e5d2c023c0d1bd6e0821853bea11dad9c9ea88a4b12f2c6a18b47bfb35aeb45ff012ea413ff83de1ab0c2e5a9166efa17ebff5b83ca9c1d7b680e57e9d5abff81980d42403cac0b6e12070346545ca9d5b794f3b89066bd37391d4c32ec7b610efabe18d4d315109012430c346d145926681c5fbd82d63500b4c7cb4e061c096ef8f75f0695e0b17321dd32cfe1497d71b59d7132a3580d81632b7a9bfe9d621e707d4d5654721dabc154b522013a48974545a822045cdf649270c2b0ce422a8f87df4e3603dc3c1c6701e62c9a7908e9ab7dd1fb19d8b4fb08a59e7877fd4dc733c059dcff11c59b70912e07e5afa2e609e2d8af9a5351304b2fb79b9137cb8d3a82046ff3568af861c3376f64fd5fc6e056139a0f2fe180686db998bf9e956cb3548bfd6ecbaafbe31a35d1f496696adcd140b28430a2bdcd0ffd75f78e3272e3b3ef3c441d9884a748cfb08a4f00dbf5c754583acfe3f90e3caaed206e64a018fc58d26234740e079bc5d01b2633fe5bd81e256aeccbd74b8904b3bbc86c1527af0669c7c73f58160e02ddc6618a5a1169e8be3a5a7809689faecf535d5532c878533fd26a886b44c8e673c63324b22b2e28e3fd3674d75256ab6bdbf418fed262d30cfb55bf5a6ef02d595d947b2aca6ed178086ffbbbb1dd7863164e0c75d56587be40fd3c5543bbe87d89b653e062a67b3295a69e724ca9e9c80ff5289d53d346861d97a1f38d194d257f64516129cafa0cb7ec861fcb004c919a22016f96b0ab646c600882aaeb39d20d374162a048b8548879e6e5042e5b913a606abbb847422256c1883578524448dc3c6a11608b20206d9d908ec98e81163ceb586faa501889284c970ec4d6a8a0e142b497447db2d1c8cf7fa9fa51eb2dbf8f99e014be9128e2009bdd6fb1558e85fb528136d822ac60a00f1af625ea74938b05aeec2fec143e9ef1abd326166a424464932e2fdff2ce553e5404113383ca261a48fb8dacec0a72005b5a5d5c4459d795372bc6d9a7d7788a35cdb1769b728380baceed0a667f63a06c39eb4250c5b90da1d80c6f6dd2a8300ef8f028ef11c3ae7cb4a95a01f6777dad6b6a84e5f420e9510698d24963d2c81e85bb1fea6e66fe53fb108f417065dced6547eef1f2c1baa4cb2b477ca7cba58746ba7ad13d73030ec351631d162161be16270410bfa756e27d196467ac64d3fc4ae70b82cc7ee9651029259ed39b81b98e55793dbcd1e2a94b3c8a4c1c22827c176ce103d1789413eddc9d23f74046f9984d0753bc29611b34fdfeaf6bfc7f18f591cc0a689ebc1f7a4d53b21fc89bdef6cab1ca85f942508cf163f2c1b6b6aa5391b3a3ebbf30fe0eb67f5864052c86c5056e507513f422efd721e6c981c736ed886c55e914c08cd7bf5ec7a8db36e7e7fcc2defd601479b672ab608895cd43bf8554733b328757630ebd3f230aa0a30809e7b76dcf59a7d9649c236ff4ffb1271ce40a0ebe9cabfce5fb297cd8b291a5556917438924c5f76dbc6bb8ab50835a05eea952939b065f0c23669f115b9181cede8f9d583d5bd3a651114f7277291481555d8e66d923111a71c49d12bee354107737349acfb08ea7ab046728777a9753f631353d0880d6e3fe6eb92c8c12f923a50942a4bef3b492ede9bc0cfb387fb8b39e2581e82fffd05aabd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a28300000000000000050000000001312d00ea851289c7e6144af83059520a132678410f370c0349d53ace3fb2d4b11761d000061b0f0003c45571d40572e15114d1731236a3768c74cf13eda20d58aafaba564570a043fd6e6543c50cff6e1f8cc5342cc95f2e30b9003fa74ec84e2f7c95bfde69f464d5886d8ae0a99294ff5b18bf6c57f3ff0c8577030d4d7a9469302228d9510b5b85e962b9a48bb1bfb7710d43e3df7fb78d734f258c05886d532e108e55f2a2159537cbe48edd13245a612f41889f5014dd5d1734c0416315384ede850b25ee716f24d40b77cc17e341f5d6e61c4589d245f9923a501d2548716c5568eac4691476b60cfcdb2d94ba948cbb7ecb710871b23ce8a9179d5fdcea8fc94e4bc9b56000e425734dab1247a441a73598e879934bf161815a037bfa259b3038490ea6cfb577ca825ecbd4b39c4f1397902bfc9587c52680755097e3938113373aaddbe62ca55db19e45140f125d84ac69cfae7103d11079c2adc3d3c51f13797e82443a8bf69c2acb34e7646095e18d160338c9aafedebf6e30e72302ea579585d74ef2e0af3005f93e9c76902dc272871e91dd5124c99a19597adcaca362d2cf9afaa674958e19a9c6d37b76b6b2be9e3779297e5f65c733c19dd456d62a66a2452b0eb091d97a82b965f1b5536d11b7f6ee8643d5505224e37839798ca99d8623bdd1e2f50b6b60b450a8e0572f44dee06a8658014e650ba5cb614f727035a8da06a2f7a60564e22971c2488fdd1fffee6b8b9eb5e9596b623a447bc4b06294952a18a6a1cd988c9d2223fec1714724ba2168639db964e1a9e2cdef4efc1c1ac7c2c5c1ea81daa34ddc8278ba8e9e54cbc3234725fa874a9a3c0c58f629d3872fa0b765b6cff95593492782398dcf097bee861476a08f31d82eeefac4c8f868cb3d1acddea2ba4f5dfc3671c42dca6a584e5f3a0e7aebe1083a997a03279f27d7319c6928e9d190392f718bd7278b806846f5c2c2dd4cbc06db84261fe6a557e86439ee7fef09809930d4a78d0e43f3b11d163539461d971bf0a83b3a52d3f8fa87c600a369a06e36cfe2349798d9597362a8385a97d82a7b445a4d7f7c1541c8e51531728d432422f3478e3733b5582e3ddb1137af5e732bf85e5c9abec1c44dd1d472774d913aad656a7e3a45659e99ba048a5e85ffbdc4af146e79241ea4a45e0b0139bd3dae63c6f89b0515d6c31566d067198e51cd01f82a41cf7d04538c4393b4892332d94f8c584531426053f2b4e6e945b3d28b492f9a4364671bdc4ee878cdf9fec1ab90ca2dc131c086e0df17bfa59b0269ea1d5a1bdde7c826c2af89da485b046e17cb8ef7c55fbbbb42a409661b6a1cefd62f05c906384d475764e51c4bae7f73a904a77622bc3515913a9fcf393ff14851c779679b4b0df58d9aa8216a390b847206f0f83ec4d982462ad88c569f878368f94fd2940975bca95669471c00894420bf2fab9bb09a3372774b716a9d1bc7d991cae8ffd759fca9c3b5ecfc1323db3f4c777308af23ef7530cacb4e4bfdaee107f059b90a2b495058bc7400ffb31e12660cf8e4fe22d3a01511595bd864cdccf6123545d024a611ba380096da71b89412c13f8be3287ec4db4c86cd4f023d53d6d4a0f7102956c3d1b0769516b6dc83c66ea73c8a5131ce74a2444c3ed9accfa085ed2ae9862ac1094c1a84db8a99c82f96f370c450c53c4372d931daffe0663d4e7b3b651fe995ee3cf09fde460b8d3360b567dda5df1483407c5286f0df33567e9407f8539eaf5e117900bef904f4f6c1081c0cc57afafce7d618cf865da5d1729e1028bf2c3a58af700690650a13340dd9217f4d3e564dc7aa3c7823caafce7ae4d3220c263b3bddf8e6624ed57c5c177abec462f39b24e8f44020490bc3098525d341a51b6b91ccff06df8dc771b911659414faa0708d30902b4461dbd6621904e3a506dd1888660e22b4b46c814135f8d033dc509a37ea7af5cbeb018f000009c4000000002faf08000000000008f0d18037073c01e5e7fdc85bee624bcc26b237744faa71acc095a5d456c306e0b84c570254366cdc771b7bf8b81d90c93c86d9fa27d602d49d72e0b8bb30bb7dab6cc7a100000000000000000000000000000000000000060000000000000000000300000000000000030003da107ef61715474784465d8d7a4c9b460000000000000004000380d777b851b445a4882760ed591ac96000000000000000050003ab7783e399d84ce28eb9d6c64e1cc1ebff02d009cfedda76c975c77dc72543b16b6288e702ac1d989c9d76e8c7d6be3d8b5e24bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a283000000002b40420f000000000022002031589753b0c53bac3611dc7043ed3fd39f3e028d084f4ccced10610770c61eed47522103074b1de9739810ce2a885ac85c0f919a17018e8826aefbe6ab92935267e25cf721031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048652ae0003003e0000fffffffffffc0081f719ad9ced864fec5efa6b78e12e255bc98a916a4bc5f9b5cb8f32e697106a8c00fc0003ffffffffffe80105a101c31ea6ddba8de1c0bb5450b4a974eb55537e794338926645ec773aafe38802000007ffffffffffc80105b32c7c8b2bbfa59f8cf725a987e9f8a61a2fec4ac26cdee7bb64601081749db40003ffffffffffe4bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a283000000000000061a800000000000fffd023e02000000000101bd8de2389047c2515292ce12da17b431dcface72910e502c97f8779eda57a2830000000000f8719480074a0100000000000022002040c49f845d675d6dd824933252c4f85caaa4dc7b023d3c6accc9e57323dcbcff4a0100000000000022002044e4c2c048661a29bc9e194687e5e973b14379028f3d86bea22b68c4a4a25b96983a000000000000220020e2928b9a0be275a3d3ff2f0a4a694d353b54903908b5aa665641cc3c3ceefea3983a000000000000220020e2928b9a0be275a3d3ff2f0a4a694d353b54903908b5aa665641cc3c3ceefea3204e0000000000002200206e5e8c4f2aea44a87052c41297157ac18f217623a59fa1353abef22a75d998e2583702000000000022002028d8bbb17d6825838406905b42f8086e03f0654cd8d61d0ee2fee623d8190e0300350c00000000002200200117fc94b3c5d70b727cd6e9aad460adab3cec2a453ec40e9dfd75569d4ae54d04004830450221008b3f1d003dd2c41d72c928c08a765f7a769c8b1081600138de4d5f4bb9fd5580022018ed8947befd01b8d06dbd1daecb906b310a558d4ca297cae2f5f066a477f66f014830450221009ad38cdf4595dbbef080ce00011b4bbb676d6b300e3b252c093a285f3a69303c022025dd87f45aba9ffe2c520cb3ed7bb55b5b386070ed48ce7fd4e118f4a771c1980147522103074b1de9739810ce2a885ac85c0f919a17018e8826aefbe6ab92935267e25cf721031015fdff76ce8a1d7c1281c28e981300256525ad3b533f5464ff453a1ba2048652ae1e751220ffc402000000000101574cb8e006c356d4a595c0ac71aa4f7437b226cc4b62ee5bc8fde7e5013c0737050000000001000000011426020000000000160014c1c7289591b6e67b38e3f0a63a433da54933484102483045022100de26022c9a370b05d8f6e200d32efbe3e0f50ed462581bcf69392a7edea08b4102207f738cecdf403341487fefa58c85655b28eab8fa5bf500f25c04e91c85e9049a01252102dbee0f1efff1842122d3523429101c7bd5691e191f99a5e341df467d9db88492ad51b20000000000000003fd012e02000000000101574cb8e006c356d4a595c0ac71aa4f7437b226cc4b62ee5bc8fde7e5013c0737020000000001000000013025000000000000160014c1c7289591b6e67b38e3f0a63a433da54933484103483045022100ef73a23215a1b32d15c9ba5fa1f18e861864a5c6f58dad40d8da2aab70e916c702203e24080b6ff3d0be32836ce3ea1cefc5ab02aee69a38532277aa24b6f273686201008e76a9140ab41ae30b3e5520b9285bc70a1706dd18f584538763ac672102243182d8f24376ce7d563bc6ce9f0c8be8ceef940cdbc6c90d7cfd8910efd6197c8201208763a914120850f1eeb36e375b36d78a9a3ecc632c9d442888527c21027aeba4eeccb5cf053a8bf33109abf45c4e718df202948fa4b2d99d4ddd9b6b9052ae677503101b06b175ac6851b27568101b0600fd012d02000000000101574cb8e006c356d4a595c0ac71aa4f7437b226cc4b62ee5bc8fde7e5013c0737030000000001000000013025000000000000160014c1c7289591b6e67b38e3f0a63a433da54933484103473044022063af75e673d37952609ff9ef56bcb29b3db945dd26514e34bb3519aa7f4725a702204b30378d31c16ce8713b2bbe6db99db2d3589b10e797575e95c451f70bb418a101008e76a9140ab41ae30b3e5520b9285bc70a1706dd18f584538763ac672102243182d8f24376ce7d563bc6ce9f0c8be8ceef940cdbc6c90d7cfd8910efd6197c8201208763a914120850f1eeb36e375b36d78a9a3ecc632c9d442888527c21027aeba4eeccb5cf053a8bf33109abf45c4e718df202948fa4b2d99d4ddd9b6b9052ae677503101b06b175ac6851b27568101b0600fd012d02000000000101574cb8e006c356d4a595c0ac71aa4f7437b226cc4b62ee5bc8fde7e5013c073704000000000100000001b838000000000000160014c1c7289591b6e67b38e3f0a63a433da5493348410347304402204f9fcba505bf2cabcbb503c32bdc4d42b2d057b0159252a747f20756a55fc7f002207f448fbc3b38d93171a18dee85b4580ad4445a120dc568329042b1485ab3058601008e76a9140ab41ae30b3e5520b9285bc70a1706dd18f584538763ac672102243182d8f24376ce7d563bc6ce9f0c8be8ceef940cdbc6c90d7cfd8910efd6197c8201208763a914120850f1eeb36e375b36d78a9a3ecc632c9d442888527c21027aeba4eeccb5cf053a8bf33109abf45c4e718df202948fa4b2d99d4ddd9b6b9052ae6775030f1b06b175ac6851b275680f1b0600000000000000" val dataClosingRevoked = hex"0100250000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094906719a3060b6b7a8b2cd5b0b7ffb12daa3a7407d0cedef637bbcd52601b3a080000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff1600144aa46655da279b5eedda63abf309f1a805ed087c0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b1031c918d3f8fb25f30cbe149e9061d0249eb0a98e53c2c7589593ecc8c36e14dc803f0b100abb8898ef76144b486b2daa59fedc616bc366e380f66ed2e1da5663c4303e4dee6072779d8ee287ea4599573583513270eaaf20082aa0cd2b63c910a8ee803208519c8ace6160bbc39b6c56037772d1991175541903c889debc59bb7b2286a00000003028a8200000000000000000b000000002710000000000bebc200000000002faf08002481c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d666000000002b40420f00000000002200209e56159a6e582da6e14db7461a5936a58a55baa243c83231611cd367ad4f9649475221030efc1f0cb52dfc48335810d38630a98e6884ea75e1e5fbd7c05a4b888beeebec2103b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b152aefd01590200000000010181c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d66600000000008786e08002f8f0020000000000220020ff7e851fb97475ad8d8c30429ac0c7cbb02b47bb985f82554a0f436a3dd79e6500350c00000000001600142044ca0a60adefac91f19ae938bd98cc6cea69b0040047304402203b05b39a4fadc7ab82fe2afd8088e69db334e0b5e481ea39ebd9ef53a529724e02204103fa2839221efdad56038a254977fcf10222821295f619fec5841fa3438b560147304402200949fffc854052ecaccb8dd071ea0c4b7085ae575ac60103251ada0847fda81f0220234c0752cb1c3937175a8002e0cbd8f71b7ee63c286e1548b335c6cdb98205ea01475221030efc1f0cb52dfc48335810d38630a98e6884ea75e1e5fbd7c05a4b888beeebec2103b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b152aeacf333200000000000000000000c000000002710000000002faf0800000000000bebc200fc852480fce78897590d18931f3d0697af871b67f1428e8c59d21c4aa1ddfb1702186519b93a8c9bec55b3496c9483242a0587525bff5603d9484742100b53afe9000000000000000000000000000000000000000500000000000000020000ff020f63b47126beb7dc930d1639f9e0971a0d147a557d00fa6b713f92f07e8a73bb2481c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d666000000002b40420f00000000002200209e56159a6e582da6e14db7461a5936a58a55baa243c83231611cd367ad4f9649475221030efc1f0cb52dfc48335810d38630a98e6884ea75e1e5fbd7c05a4b888beeebec2103b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b152ae0002003d0000fffffffffff8010650c983a9aa663bb634b47c144792150fd9cd0e2621dd26d40b7604352765071801f00007ffffffffffa0040d41ea1fb5689120f08b5f97c7929cbc72fece2f9cb179c2ef08043575dcc6f230000fffffffffff4081c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d666000000000000061a8000000000000000000001fd02050200000000010181c26703ee350c2defc5f06186e513b95038f0070263155a11a55b110019d66600000000008786e0800650460000000000002200208d17f8ce0fb5199bf3e11b7a955ea7daaa11571057fd9455d735b05aafd6d7f8204e000000000000220020907e62299a1b92c8836bf03a7d1a9c887dc5c3e0ffe66feff9609bd81cf7390da8610000000000002200202f5d49e4c427129d546154f7ba80cc67d93fb720965bc64bf65b2a5b73889d90b88800000000000022002026bc9c36e6ed935e183c0eb92de335a9c8da6038e48c8dcf8361101de37c956bb8eb010000000000160014fc8bf8fcffad6adab3501a470afac2426df90c8190a00b0000000000220020b80866b204a5b57648b7c3ebd06eddf8936ded233ec5550d6b82081c4d7685930400473044022063a11e78bcf2d0241bc74005b6ea50604b030085b69bb135b972c7718f2f95f40220231d562020bceecc452e78616813d8c0790967ed1441ffc8d5546f51380866a10147304402200cb958272d3a7d9bc66015fc9cd661512c5db8150810c3cc157d83f9932cbdf602206dd941faab7aa65757d44d35418641c73e0f6435624286cb6e1d0d342fc0f70c01475221030efc1f0cb52dfc48335810d38630a98e6884ea75e1e5fbd7c05a4b888beeebec2103b99798090319eaea87ab11d7d007e88d438ac147ec6e2165eb87171e08a1c2b152aeadf33320ffc00200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb040000000000000000019cda0100000000001600144aa46655da279b5eedda63abf309f1a805ed087c02483045022100e3b47f5e6962f8cd79e93ce4fe7632fa7f6d249dd17b86433d4a2f79c957c2ba022039e951552a72f5c884f7fc765abe54331eed07beaf361b0dbd522e67d9b80d7b012103241283b366441780a43655750be5a57971f8b2130c30188a0d7f2184519a076500000000ffee0200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0500000000ffffffff01a88d0b00000000001600144aa46655da279b5eedda63abf309f1a805ed087c03483045022100d63cd44d813ce92f0fd65140b954f84fd381f5d35b387e4e2d681e8ef1a5adf7022066706a758c18d1c193b0090f747b0eb85c2add3c11ace0d16c94997740e12e410101014d6321020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b67029000b275210232d85a8549f7c760b20b062899c097a9999a6bf7c46e960def69c0cc2aa9c9b868ac000000000004fd01450200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0000000000ffffffff0138310000000000001600144aa46655da279b5eedda63abf309f1a805ed087c0347304402205c0ed89c351d43af0c7abf6a24ce30636dfe55261105aaaa03f017b61b97fcdb022013872d525dcf2367d110a939a8d1850dc3f2124b11dc66f1bfb4e0e487f1a93e0121020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b8576a914ba607324990f54b8449be0e46f783c254675991c8763ac67210224cdb71653a2a77768106e6710fc335045c23399172c1e8b5444ee17580090fd7c820120876475527c210386000084d80729cbeb94e9816fc91ac124011b79ca68a0629212a46675b662f352ae67a914d9d01c45f608fa6d73753a83863469b973b64d3288ac686800000000fd01460200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0100000000ffffffff0108390000000000001600144aa46655da279b5eedda63abf309f1a805ed087c03483045022100de4f0a5d5f225303ac1e5185620e33e32b970e67561a1a94d3b28b08bc39e85c02202a43ef5ae02ad0ca79eabac2e844aa5e0c02aca2b46c200b613fa5de51ac278c0121020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b8576a914ba607324990f54b8449be0e46f783c254675991c8763ac67210224cdb71653a2a77768106e6710fc335045c23399172c1e8b5444ee17580090fd7c820120876475527c210386000084d80729cbeb94e9816fc91ac124011b79ca68a0629212a46675b662f352ae67a914f43516dcc02db1dbfc68b53a1129f3578116dd4388ac686800000000fd014b0200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0200000000ffffffff01544c0000000000001600144aa46655da279b5eedda63abf309f1a805ed087c0347304402204238cf6593e137b4f8087a7661335566713badd6ab823b58cfb01e8aa04d984502205da1af983b813831e2f0f4dc0510f334cc9f20c19a9f66d01f9ce107008747310121020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b8b76a914ba607324990f54b8449be0e46f783c254675991c8763ac67210224cdb71653a2a77768106e6710fc335045c23399172c1e8b5444ee17580090fd7c8201208763a914bffcd1ff89d9a58a7fbce039452ada42b9ad5dd088527c210386000084d80729cbeb94e9816fc91ac124011b79ca68a0629212a46675b662f352ae677503101b06b175ac686800000000fd014b0200000000010144a2a76d9c8ca0533c23f0a4bb4fb2152bc6cead6981ac750a88e54951e16bcb0300000000ffffffff0164730000000000001600144aa46655da279b5eedda63abf309f1a805ed087c03473044022049b9e9d5780960c6e93b6ee1398179020059c8688e31ba906b19547f73690f5d02203db4bc7331d6b8f61ea2b8deb491d0e998ce9a189f287f967f3f0865c1f1a3700121020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b8b76a914ba607324990f54b8449be0e46f783c254675991c8763ac67210224cdb71653a2a77768106e6710fc335045c23399172c1e8b5444ee17580090fd7c8201208763a914965a3bfb147aa783bd0c850cca40f558a4ebdbce88527c210386000084d80729cbeb94e9816fc91ac124011b79ca68a0629212a46675b662f352ae677503101b06b175ac6868000000000002ed02000000000101a4cb5d4a3a460408b582cb720ff78e22e29019deb21d7399fbb7a299cf01d0400000000000ffffffff015a5a0000000000001600144aa46655da279b5eedda63abf309f1a805ed087c034730440220683a50c89b9770f0cb072e73fad0c97a3728532f8e007ffcdea0a526d791aad80220658115603831d9062a5d4e19e0fad5c79b6df34f565791ccb7bf373932a970e30101014d6321020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b67029000b275210232d85a8549f7c760b20b062899c097a9999a6bf7c46e960def69c0cc2aa9c9b868ac00000000ee020000000001019586cb37e5cd9d1fac6cc4f51d7b452ae3b30ec1abac2f1b971d51c69da3ee960000000000ffffffff0182190000000000001600144aa46655da279b5eedda63abf309f1a805ed087c03483045022100dc35cab8904ed96334111419bbbd7b18327cd17515462af18052b99448e7e69102204120ead6f0d3de63911d5d56fc6f70507716c34b7dc59fac54d1e811cf65b21f0101014d6321020373586870387b0483cd65641fba5b3af25dc15c72e44f641299697411b64e8b67029000b275210232d85a8549f7c760b20b062899c097a9999a6bf7c46e960def69c0cc2aa9c9b868ac000000000000" - Seq(dataNormal, dataWaitForFundingConfirmed, dataShutdown, dataNegotiating, dataClosingLocal, dataClosingRemote, dataClosingRevoked).foreach(oldBin => { + Seq(dataNegotiating).foreach(oldBin => { // check that this data has been encoded with the old codec assert(oldBin.startsWith(hex"0100")) // we decode with the new codec @@ -177,7 +177,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // and we encode with the new codec val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec - assert(newBin.startsWith(hex"0400")) + assert(newBin.startsWith(hex"0500")) // make sure that round-trip yields the same data val decoded2 = channelDataCodec.decode(newBin.bits).require.value assert(decoded1 == decoded2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index ec318af73c..62e1cc7b0b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -212,9 +212,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TxRemoveOutput(channelId1, UInt64(1)) -> hex"0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001", TxComplete(channelId1) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", TxComplete(channelId1, TlvStream(Set.empty[TxCompleteTlv], Set(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", - TxSignatures(channelId1, tx2, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87")), ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484", - TxSignatures(channelId2, tx1, Nil, None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000", - TxSignatures(channelId2, tx1, Nil, Some(signature)) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxSignatures(channelId1, tx2, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87")), ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None, None) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484", + TxSignatures(channelId2, tx1, Nil, None, None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000", + TxSignatures(channelId2, tx1, Nil, Some(signature), None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 1_500_000 sat, requireConfirmedInputs = true, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 0 sat, requireConfirmedInputs = false, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000", @@ -291,6 +291,11 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } } + test("decode open_channel with simple_taproot_channel extension") { + val raw = "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f85c4f4bf75b2cb938d4c3e75bd53949f12d708b0b8d6db817e10ac3437ffb29f00000000000186a0000000000000000000000000000001620000000005e69ec000000000000003e80000000000000001000009c4009001e303d01507c5d81a04650898e6ce017a3ed8349b83dd1f592e7ec8b9d6bdb064950c02a54a8591a5fdc5f082f23d0f3e83ff74b6de433f71e40123c44b20a56a5bb9f502a8e31e0707b1ac67b9fd938e5c9d59e3607fb84e0ab6e0824ad582e4f8f88df802721e2a2757ff1c60a92716a366f89c3a7df6a48e71bc8824e23b1ae47d9f596503df8191d861c265ab1f0539bdc04f8ac94847511abd6c70ed0775aea3f6c3821202c2fdb53245754e0e033a71e260e64f0c0959ac4a994e9c5159708ae05559e9ad00000001171000000000000000000000000000000000000000000000044203a8c947da4dae605ee05f7894e22a9d6d51e23c5523e63f8fc5dc7aea90835a9403f68dbb02e8cba1a97ea42bd6a963942187ff0da465dda3dc35cf0d260bcdcece" + assert(openChannelCodec.decode(BitVector.fromValidHex(raw)).isSuccessful) + } + test("decode invalid open_channel") { val defaultEncoded = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000100000000000000010000000000000001000000000000000100000000000000010000000100010001031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00" val testCases = Seq( diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index fd5dd468b5..32b92d09d4 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -48,7 +48,11 @@ trait Channel { ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true), - ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true) + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), + ChannelTypes.SimpleTaprootChannelsStaging(), + ChannelTypes.SimpleTaprootChannelsStaging(zeroConf = true), + ChannelTypes.SimpleTaprootChannelsStaging(scidAlias = true), + ChannelTypes.SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true), ).map(ct => ct.toString -> ct).toMap // we use the toString method as name in the api val open: Route = postRequest("open") { implicit t =>