From e5671a111972daddde37e6c185c8f80ec6f945d9 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 6 Jun 2024 11:03:45 +0200 Subject: [PATCH] Implement simple taproot channels --- .../kotlin/fr/acinq/lightning/Features.kt | 12 +- .../kotlin/fr/acinq/lightning/NodeParams.kt | 1 + .../lightning/channel/ChannelException.kt | 1 + .../lightning/channel/ChannelFeatures.kt | 11 + .../fr/acinq/lightning/channel/Commitments.kt | 148 +++- .../fr/acinq/lightning/channel/Helpers.kt | 212 +++-- .../acinq/lightning/channel/InteractiveTx.kt | 209 ++++- .../acinq/lightning/channel/states/Channel.kt | 79 +- .../acinq/lightning/channel/states/Closing.kt | 4 +- .../lightning/channel/states/Negotiating.kt | 28 +- .../acinq/lightning/channel/states/Normal.kt | 93 ++- .../acinq/lightning/channel/states/Offline.kt | 3 +- .../acinq/lightning/channel/states/Syncing.kt | 51 +- .../channel/states/WaitForAcceptChannel.kt | 12 +- .../channel/states/WaitForChannelReady.kt | 5 +- .../channel/states/WaitForFundingConfirmed.kt | 6 +- .../channel/states/WaitForFundingCreated.kt | 7 +- .../channel/states/WaitForFundingSigned.kt | 8 +- .../lightning/channel/states/WaitForInit.kt | 13 +- .../channel/states/WaitForOpenChannel.kt | 30 +- .../fr/acinq/lightning/crypto/KeyManager.kt | 34 + .../acinq/lightning/json/JsonSerializers.kt | 32 + .../serialization/v4/Deserialization.kt | 37 +- .../serialization/v4/Serialization.kt | 24 + .../acinq/lightning/transactions/Scripts.kt | 116 +++ .../lightning/transactions/Transactions.kt | 743 +++++++++++++++--- .../fr/acinq/lightning/wire/ChannelTlv.kt | 119 ++- .../acinq/lightning/wire/InteractiveTxTlv.kt | 20 + .../acinq/lightning/wire/LightningMessages.kt | 62 +- .../channel/ChannelDataTestsCommon.kt | 2 +- .../lightning/channel/HelpersTestsCommon.kt | 2 +- .../channel/InteractiveTxTestsCommon.kt | 55 +- .../lightning/channel/RecoveryTestsCommon.kt | 3 +- .../fr/acinq/lightning/channel/TestsHelper.kt | 37 +- .../channel/states/ClosingTestsCommon.kt | 61 +- .../channel/states/NormalTestsCommon.kt | 41 +- .../channel/states/OfflineTestsCommon.kt | 18 +- .../channel/states/SpliceTestsCommon.kt | 59 +- .../states/WaitForChannelReadyTestsCommon.kt | 32 + .../WaitForFundingCreatedTestsCommon.kt | 35 + .../states/WaitForFundingSignedTestsCommon.kt | 25 + .../StateSerializationTestsCommon.kt | 34 + .../transactions/AnchorOutputsTestsCommon.kt | 5 +- .../transactions/TransactionsTestsCommon.kt | 523 +++++++++++- .../wire/LightningCodecsTestsCommon.kt | 28 +- 45 files changed, 2668 insertions(+), 412 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt index 3f834bc8f..dc28449d1 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt @@ -270,6 +270,12 @@ sealed class Feature { override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) } + @Serializable + object SimpleTaprootStaging : Feature() { + override val rfcName get() = "option_simple_taproot_staging" + override val mandatory get() = 180 + override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) + } } @Serializable @@ -353,7 +359,8 @@ data class Features(val activated: Map, val unknown: Se Feature.ChannelBackupProvider, Feature.ExperimentalSplice, Feature.OnTheFlyFunding, - Feature.FundingFeeCredit + Feature.FundingFeeCredit, + Feature.SimpleTaprootStaging ) operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray()) @@ -387,7 +394,8 @@ data class Features(val activated: Map, val unknown: Se Feature.TrampolinePayment to listOf(Feature.PaymentSecret), Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret), Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice), - Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding) + Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding), + Feature.SimpleTaprootStaging to listOf(Feature.AnchorOutputs, Feature.StaticRemoteKey) ) class FeatureException(message: String) : IllegalArgumentException(message) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt index 94f519965..7253c5940 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -211,6 +211,7 @@ data class NodeParams( Feature.ExperimentalSplice to FeatureSupport.Optional, Feature.OnTheFlyFunding to FeatureSupport.Optional, Feature.FundingFeeCredit to FeatureSupport.Optional, + Feature.SimpleTaprootStaging to FeatureSupport.Optional ), dustLimit = 546.sat, maxRemoteDustLimit = 600.sat, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt index a005ac003..820ecd8b7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt @@ -22,6 +22,7 @@ data class InvalidPushAmount (override val channelId: Byte data class InvalidMaxAcceptedHtlcs (override val channelId: ByteVector32, val maxAcceptedHtlcs: Int, val max: Int) : ChannelException(channelId, "invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)") data class InvalidChannelType (override val channelId: ByteVector32, val ourChannelType: ChannelType, val theirChannelType: ChannelType) : ChannelException(channelId, "invalid channel_type=${theirChannelType.name}, expected channel_type=${ourChannelType.name}") data class MissingChannelType (override val channelId: ByteVector32) : ChannelException(channelId, "option_channel_type was negotiated but channel_type is missing") +data class MissingNextLocalNonces (override val channelId: ByteVector32) : ChannelException(channelId, "missing next local nonces") data class DustLimitTooSmall (override val channelId: ByteVector32, val dustLimit: Satoshi, val min: Satoshi) : ChannelException(channelId, "dustLimit=$dustLimit is too small (min=$min)") data class DustLimitTooLarge (override val channelId: ByteVector32, val dustLimit: Satoshi, val max: Satoshi) : ChannelException(channelId, "dustLimit=$dustLimit is too large (max=$max)") data class ToSelfDelayTooHigh (override val channelId: ByteVector32, val toSelfDelay: CltvExpiryDelta, val max: CltvExpiryDelta) : ChannelException(channelId, "unreasonable to_self_delay=$toSelfDelay (max=$max)") diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt index ed359c5c9..ebfc1b813 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt @@ -59,6 +59,15 @@ sealed class ChannelType { override val features: Set get() = setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels) } + object SimpleTaprootStaging : SupportedChannelType() { + override val name: String get() = "simple_taproot_staging" + override val features: Set get() = setOf(Feature.SimpleTaprootStaging, Feature.StaticRemoteKey, Feature.AnchorOutputs) + } + + object SimpleTaprootStagingZeroReserve : SupportedChannelType() { + override val name: String get() = "simple_taproot_staging_zero_reserve" + override val features: Set get() = setOf(Feature.SimpleTaprootStaging, Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels) + } } data class UnsupportedChannelType(val featureBits: Features) : ChannelType() { @@ -71,6 +80,8 @@ sealed class ChannelType { // NB: Bolt 2: features must exactly match in order to identify a channel type. fun fromFeatures(features: Features): ChannelType = when (features) { // @formatter:off + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory, Feature.SimpleTaprootStaging to FeatureSupport.Mandatory) -> SupportedChannelType.SimpleTaprootStagingZeroReserve + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.SimpleTaprootStaging to FeatureSupport.Mandatory) -> SupportedChannelType.SimpleTaprootStaging Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory) -> SupportedChannelType.AnchorOutputsZeroReserve Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory) -> SupportedChannelType.AnchorOutputs else -> UnsupportedChannelType(features) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt index a094f456c..30b7bb0db 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt @@ -2,10 +2,14 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Crypto.sha256 +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.bitcoin.utils.Try +import fr.acinq.bitcoin.utils.flatMap import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Feature +import fr.acinq.lightning.Features import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -46,6 +50,8 @@ data class ChannelParams( require(channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) { "FundingPubKeyBasedChannelKeyPath option must be enabled" } } + val isTaprootChannel = Features.canUseFeature(localParams.features, remoteParams.features, Feature.SimpleTaprootStaging) + fun updateFeatures(localInit: Init, remoteInit: Init) = this.copy( localParams = localParams.copy(features = localInit.features), remoteParams = remoteParams.copy(features = remoteInit.features) @@ -94,13 +100,15 @@ data class CommitmentChanges(val localChanges: LocalChanges, val remoteChanges: data class HtlcTxAndSigs(val txinfo: HtlcTx, val localSig: ByteVector64, val remoteSig: ByteVector64) data class PublishableTxs(val commitTx: CommitTx, val htlcTxsAndSigs: List) +data class PartialSignatureWithNonce(val partialSig: ByteVector32, val nonce: IndividualNonce) /** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishableTxs: PublishableTxs) { companion object { fun fromCommitSig(keyManager: KeyManager.ChannelKeys, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, commit: CommitSig, - localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, log: MDCLogger): Either { + localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, localNonce: Pair?, log: MDCLogger + ): Either { val (localCommitTx, sortedHtlcTxs) = Commitments.makeLocalTxs( keyManager, commitTxNumber = localCommitIndex, @@ -112,10 +120,26 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishabl localPerCommitmentPoint = localPerCommitmentPoint, spec ) - val sig = Transactions.sign(localCommitTx, keyManager.fundingKey(fundingTxIndex)) + val localFundingKey = keyManager.fundingKey(fundingTxIndex) + val signedCommitTx = when (commit.sigOrPartialSig) { + is Either.Left -> { + val sig = localCommitTx.sign(localFundingKey) + Transactions.addSigs(localCommitTx, localFundingKey.publicKey(), remoteFundingPubKey, sig, commit.signature) + } + + is Either.Right -> { + val remoteSig = commit.sigOrPartialSig.right + val signed = Transactions.partialSign(localCommitTx, localFundingKey, localFundingKey.publicKey(), remoteFundingPubKey, localNonce!!, remoteSig.nonce) + .flatMap { localSig -> Transactions.aggregatePartialSignatures(localCommitTx, localSig, remoteSig.partialSig, localFundingKey.publicKey(), remoteFundingPubKey, localNonce.second, remoteSig.nonce) } + .map { aggSig -> Transactions.addAggregatedSignature(localCommitTx, aggSig) } + if (signed.isLeft) { + return Either.Left(InvalidCommitmentSignature(params.channelId, localCommitTx.tx.txid)) + } + signed.right!! + } + } // no need to compute htlc sigs if commit sig doesn't check out - val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPubKey(fundingTxIndex), remoteFundingPubKey, sig, commit.signature) when (val check = Transactions.checkSpendable(signedCommitTx)) { is Try.Failure -> { log.error(check.error) { "remote signature $commit is invalid" } @@ -140,7 +164,7 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishabl is HtlcTx.HtlcSuccessTx -> { // we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig // which was created with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) { + if (!htlcTx.checkSig(remoteSig, remoteHtlcPubkey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) { return Either.Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) } HtlcTxAndSigs(htlcTx, localSig, remoteSig) @@ -154,7 +178,7 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishabl /** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxId, val remotePerCommitmentPoint: PublicKey) { - fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo): CommitSig { + fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, remoteNonce: IndividualNonce?): CommitSig { val (remoteCommitTx, sortedHtlcsTxs) = Commitments.makeRemoteTxs( channelKeys, index, @@ -166,14 +190,25 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI remotePerCommitmentPoint = remotePerCommitmentPoint, spec ) - val sig = Transactions.sign(remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val sig = remoteCommitTx.sign(channelKeys.fundingKey(fundingTxIndex)) + val partialSig = when (remoteNonce) { + null -> null + else -> { + val localNonce = channelKeys.signingNonce(fundingTxIndex) + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + PartialSignatureWithNonce( + Transactions.partialSign(remoteCommitTx, fundingKey, fundingKey.publicKey(), remoteFundingPubKey, localNonce, remoteNonce).right!!, + localNonce.second + ) + } + } // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - val htlcSigs = sortedHtlcsTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } - return CommitSig(params.channelId, sig, htlcSigs.toList()) + val htlcSigs = sortedHtlcsTxs.map { it.sign(channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } + return CommitSig(params.channelId, sig, htlcSigs.toList(), TlvStream(setOfNotNull(partialSig?.let { CommitSigTlv.PartialSignatureWithNonceTlv(it) }))) } - fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, signingSession: InteractiveTxSigningSession): CommitSig = - sign(channelKeys, params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubkey, signingSession.commitInput) + fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, signingSession: InteractiveTxSigningSession, remoteNonce: IndividualNonce?): CommitSig = + sign(channelKeys, params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubkey, signingSession.commitInput, remoteNonce) } /** We have the next remote commit when we've sent our commit_sig but haven't yet received their revoke_and_ack. */ @@ -254,9 +289,9 @@ data class Commitment( val balanceNoFees = (reduced.toRemote - localChannelReserve(params).toMilliSatoshi()).coerceAtLeast(0.msat) return if (params.localParams.paysCommitTxFees) { // The initiator always pays the on-chain fees, so we must subtract that from the amount we can send. - val commitFees = commitTxFeeMsat(params.remoteParams.dustLimit, reduced) + val commitFees = commitTxFeeMsat(params.remoteParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) // the initiator needs to keep a "initiator fee buffer" (see explanation above) - val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) + htlcOutputFee(reduced.feerate * 2) val amountToReserve = commitFees.coerceAtLeast(initiatorFeeBuffer) if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(params.remoteParams.dustLimit, reduced).toMilliSatoshi()) { // htlc will be trimmed @@ -283,9 +318,9 @@ data class Commitment( balanceNoFees } else { // The initiator always pays the on-chain fees, so we must subtract that from the amount we can receive. - val commitFees = commitTxFeeMsat(params.localParams.dustLimit, reduced) + val commitFees = commitTxFeeMsat(params.localParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) // we expected the initiator to keep a "initiator fee buffer" (see explanation above) - val initiatorFeeBuffer = commitTxFeeMsat(params.localParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer = commitTxFeeMsat(params.localParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) + htlcOutputFee(reduced.feerate * 2) val amountToReserve = commitFees.coerceAtLeast(initiatorFeeBuffer) if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(params.localParams.dustLimit, reduced).toMilliSatoshi()) { // htlc will be trimmed @@ -351,10 +386,10 @@ data class Commitment( val outgoingHtlcs = reduced.htlcs.incomings() // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment - val fees = commitTxFee(params.remoteParams.dustLimit, reduced) + val fees = commitTxFee(params.remoteParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) // the initiator needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid // getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728). - val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) + htlcOutputFee(reduced.feerate * 2) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. val missingForSender = reduced.toRemote - localChannelReserve(params).toMilliSatoshi() - (if (params.localParams.paysCommitTxFees) fees.toMilliSatoshi().coerceAtLeast(initiatorFeeBuffer) else 0.msat) @@ -403,7 +438,7 @@ data class Commitment( val incomingHtlcs = reduced.htlcs.incomings() // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment - val fees = commitTxFee(params.localParams.dustLimit, reduced) + val fees = commitTxFee(params.localParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) // NB: we don't enforce the initiatorFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. val missingForSender = reduced.toRemote - remoteChannelReserve(params).toMilliSatoshi() - (if (params.localParams.paysCommitTxFees) 0.sat else fees).toMilliSatoshi() @@ -436,7 +471,7 @@ data class Commitment( val reduced = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is initiator remote doesn't pay the fees - val fees = commitTxFee(params.remoteParams.dustLimit, reduced) + val fees = commitTxFee(params.remoteParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) val missing = reduced.toRemote.truncateToSatoshi() - localChannelReserve(params) - fees return if (missing < 0.sat) { Either.Left(CannotAffordFees(params.channelId, -missing, localChannelReserve(params), fees)) @@ -453,7 +488,7 @@ data class Commitment( // It is easier to do it here because under certain (race) conditions spec allows a lower-than-normal fee to be paid, // and it would be tricky to check if the conditions are met at signing // (it also means that we need to check the fee of the initial commitment tx somewhere) - val fees = commitTxFee(params.localParams.dustLimit, reduced) + val fees = commitTxFee(params.localParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) val missing = reduced.toRemote.truncateToSatoshi() - remoteChannelReserve(params) - fees return if (missing < 0.sat) { Either.Left(CannotAffordFees(params.channelId, -missing, remoteChannelReserve(params), fees)) @@ -462,7 +497,15 @@ data class Commitment( } } - fun sendCommit(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, log: MDCLogger): Pair { + fun sendCommit( + channelKeys: KeyManager.ChannelKeys, + params: ChannelParams, + changes: CommitmentChanges, + remoteNextPerCommitmentPoint: PublicKey, + batchSize: Int, + remoteNonce: IndividualNonce?, + log: MDCLogger + ): Pair { // remote commitment will include all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val (remoteCommitTx, sortedHtlcTxs) = Commitments.makeRemoteTxs( @@ -477,6 +520,17 @@ data class Commitment( spec ) val sig = Transactions.sign(remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val partialSig = when (remoteNonce) { + null -> null + else -> { + // we generate a new nonce each time we sign their commit tx + val localNonce = channelKeys.signingNonce(fundingTxIndex) + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val psig = Transactions.partialSign(remoteCommitTx, fundingKey, fundingKey.publicKey(), remoteFundingPubkey, localNonce, remoteNonce).right!! + log.debug { "sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint" } + PartialSignatureWithNonce(psig, localNonce.second) + } + } // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remoteNextPerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } @@ -501,13 +555,16 @@ data class Commitment( if (batchSize > 1) { add(CommitSigTlv.Batch(batchSize)) } + if (partialSig != null) { + add(CommitSigTlv.PartialSignatureWithNonceTlv(partialSig)) + } } val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), TlvStream(tlvs)) val commitment1 = copy(nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint))) return Pair(commitment1, commitSig) } - fun receiveCommit(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, changes: CommitmentChanges, commit: CommitSig, log: MDCLogger): Either { + fun receiveCommit(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, changes: CommitmentChanges, commit: CommitSig, localNonce: Pair?, log: MDCLogger): Either { // they sent us a signature for *their* view of *our* next commit tx // so in terms of rev.hashes and indexes we have: // ourCommit.index -> our current revocation hash, which is about to become our old revocation hash @@ -523,7 +580,7 @@ data class Commitment( val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommit.index + 1) - return LocalCommit.fromCommitSig(channelKeys, params, fundingTxIndex, remoteFundingPubkey, commitInput, commit, localCommit.index + 1, spec, localPerCommitmentPoint, log).map { localCommit1 -> + return LocalCommit.fromCommitSig(channelKeys, params, fundingTxIndex, remoteFundingPubkey, commitInput, commit, localCommit.index + 1, spec, localPerCommitmentPoint, localNonce, log).map { localCommit1 -> log.info { val htlcsIn = spec.htlcs.incomings().map { it.id }.joinToString(",") val htlcsOut = spec.htlcs.outgoings().map { it.id }.joinToString(",") @@ -554,6 +611,7 @@ data class FullCommitment( params.channelFeatures.hasFeature(Feature.ZeroReserveChannels) -> 0.sat else -> (fundingAmount / 100).max(params.localParams.dustLimit) } + val isTaprootChannel = params.isTaprootChannel } data class WaitingForRevocation(val sentAfterLocalCommitIndex: Long) @@ -566,7 +624,10 @@ data class Commitments( val payments: Map, // for outgoing htlcs, maps to paymentId val remoteNextCommitInfo: Either, // this one is tricky, it must be kept in sync with Commitment.nextRemoteCommit val remotePerCommitmentSecrets: ShaChain, - val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty + val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty, + val nextRemoteNonces: List = listOf(), + val closingNonce: Pair? = null, + val pendingRemoteNextLocalNonce: IndividualNonce? = null ) { init { require(active.isNotEmpty()) { "there must be at least one active commitment" } @@ -575,6 +636,7 @@ data class Commitments( val channelId: ByteVector32 = params.channelId val localNodeId: PublicKey = params.localParams.nodeId val remoteNodeId: PublicKey = params.remoteParams.nodeId + val isTaprootChannel = params.isTaprootChannel // Commitment numbers are the same for all active commitments. val localCommitIndex = active.first().localCommit.index @@ -758,7 +820,11 @@ data class Commitments( fun sendCommit(channelKeys: KeyManager.ChannelKeys, log: MDCLogger): Either>> { val remoteNextPerCommitmentPoint = remoteNextCommitInfo.right ?: return Either.Left(CannotSignBeforeRevocation(channelId)) if (!changes.localHasChanges()) return Either.Left(CannotSignWithoutChanges(channelId)) - val (active1, sigs) = active.map { it.sendCommit(channelKeys, params, changes, remoteNextPerCommitmentPoint, active.size, log) }.unzip() + val (active1, sigs) = when (isTaprootChannel) { + true -> active.zip(nextRemoteNonces).map { it.first.sendCommit(channelKeys, params, changes, remoteNextPerCommitmentPoint, active.size, it.second, log) }.unzip() + false -> active.map { it.sendCommit(channelKeys, params, changes, remoteNextPerCommitmentPoint, active.size, null, log) }.unzip() + } + val commitments1 = copy( active = active1, remoteNextCommitInfo = Either.Left(WaitingForRevocation(localCommitIndex)), @@ -779,7 +845,11 @@ data class Commitments( } // 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 { - when (val commitment1 = it.first.receiveCommit(channelKeys, params, changes, it.second, log)) { + val localNonce = when (this.isTaprootChannel) { + true -> channelKeys.verificationNonce(it.first.fundingTxIndex, localCommitIndex + 1) + false -> null + } + when (val commitment1 = it.first.receiveCommit(channelKeys, params, changes, it.second, localNonce, log)) { is Either.Left -> return Either.Left(commitment1.value) is Either.Right -> commitment1.value } @@ -787,7 +857,15 @@ data class Commitments( // we will send our revocation preimage + our next revocation hash val localPerCommitmentSecret = channelKeys.commitmentSecret(localCommitIndex) val localNextPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex + 2) - val revocation = RevokeAndAck(channelId, localPerCommitmentSecret, localNextPerCommitmentPoint) + val tlvStream: TlvStream = when (isTaprootChannel) { + true -> { + val nonces = active.map { channelKeys.verificationNonce(it.fundingTxIndex, localCommitIndex + 2) } + TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map { it.second })) + } + + false -> TlvStream.empty() + } + val revocation = RevokeAndAck(channelId, localPerCommitmentSecret, localNextPerCommitmentPoint, tlvStream) val commitments1 = copy( active = active1, changes = changes.copy( @@ -805,6 +883,8 @@ data class Commitments( val remoteCommit = active.first().remoteCommit if (revocation.perCommitmentSecret.publicKey() != remoteCommit.remotePerCommitmentPoint) return Either.Left(InvalidRevocation(channelId)) + if (isTaprootChannel && revocation.nextLocalNonces.isEmpty()) return Either.Left(MissingNextLocalNonces(channelId)) + // the outgoing following htlcs have been completed (fulfilled or failed) when we received this revocation // they have been removed from both local and remote commitment // since fulfill/fail are sent by remote, they are (1) signed by them, (2) revoked by us, (3) signed by us, (4) revoked by them @@ -849,7 +929,8 @@ data class Commitments( remoteNextCommitInfo = Either.Right(revocation.nextPerCommitmentPoint), remotePerCommitmentSecrets = remotePerCommitmentSecrets.addHash(revocation.perCommitmentSecret.value, 0xFFFFFFFFFFFFL - remoteCommitIndex), payments = payments1, - remoteChannelData = revocation.channelData + remoteChannelData = revocation.channelData, + nextRemoteNonces = revocation.nextLocalNonces ) return Either.Right(Pair(commitments1, actions.toList())) } @@ -982,6 +1063,7 @@ data class Commitments( val ANCHOR_AMOUNT = 330.sat const val COMMIT_WEIGHT = 1124 + const val COMMIT_WEIGHT_TAPROOT = 968 const val HTLC_OUTPUT_WEIGHT = 172 const val HTLC_TIMEOUT_WEIGHT = 666 const val HTLC_SUCCESS_WEIGHT = 706 @@ -1028,6 +1110,7 @@ data class Commitments( val remoteHtlcPubkey = remoteParams.htlcBasepoint.deriveForCommitment(localPerCommitmentPoint) val localRevocationPubkey = remoteParams.revocationBasepoint.deriveForRevocation(localPerCommitmentPoint) val localPaymentBasepoint = channelKeys.paymentBasepoint + val isTaprootChannel = Features.canUseFeature(localParams.features, remoteParams.features, Feature.SimpleTaprootStaging) val outputs = makeCommitTxOutputs( channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, @@ -1039,10 +1122,11 @@ data class Commitments( remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, - spec + spec, + isTaprootChannel ) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isChannelOpener, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feerate, outputs) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feerate, outputs, isTaprootChannel) return Pair(commitTx, htlcTxs) } @@ -1062,6 +1146,7 @@ data class Commitments( val remoteDelayedPaymentPubkey = remoteParams.delayedPaymentBasepoint.deriveForCommitment(remotePerCommitmentPoint) val remoteHtlcPubkey = remoteParams.htlcBasepoint.deriveForCommitment(remotePerCommitmentPoint) val remoteRevocationPubkey = channelKeys.revocationBasepoint.deriveForRevocation(remotePerCommitmentPoint) + val isTaprootChannel = Features.canUseFeature(localParams.features, remoteParams.features, Feature.SimpleTaprootStaging) val outputs = makeCommitTxOutputs( remoteFundingPubKey, channelKeys.fundingPubKey(fundingTxIndex), @@ -1073,11 +1158,12 @@ data class Commitments( localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, - spec + spec, + isTaprootChannel ) // NB: we are creating the remote commit tx, so local/remote parameters are inverted. val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentPubkey, !localParams.isChannelOpener, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feerate, outputs) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feerate, outputs, isTaprootChannel) return Pair(commitTx, htlcTxs) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt index 4c55be458..d1c070fea 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt @@ -3,10 +3,14 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Crypto.ripemd160 import fr.acinq.bitcoin.Crypto.sha256 +import fr.acinq.bitcoin.Script.pay2tr import fr.acinq.bitcoin.Script.pay2wsh import fr.acinq.bitcoin.Script.write +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.bitcoin.utils.Try +import fr.acinq.bitcoin.utils.flatMap import fr.acinq.bitcoin.utils.runTrying import fr.acinq.lightning.Feature import fr.acinq.lightning.Features @@ -30,6 +34,7 @@ import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.logging.* import fr.acinq.lightning.transactions.* import fr.acinq.lightning.transactions.Scripts.multiSig2of2 +import fr.acinq.lightning.transactions.Scripts.musig2FundingScript import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx @@ -99,6 +104,10 @@ object Helpers { return Either.Left(FeerateTooDifferent(open.temporaryChannelId, FeeratePerKw.CommitmentFeerate, open.commitmentFeerate)) } + if (channelType is ChannelType.SupportedChannelType.SimpleTaprootStaging && open.nextLocalNonces.size < 2) { + return Either.Left(MissingNextLocalNonces(open.temporaryChannelId)) + } + return Either.Right(channelType) } @@ -134,6 +143,10 @@ object Helpers { return Either.Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.maxToLocalDelayBlocks)) } + if (accept.channelType is ChannelType.SupportedChannelType.SimpleTaprootStaging && accept.nextLocalNonces.size < 2) { + return Either.Left(MissingNextLocalNonces(open.temporaryChannelId)) + } + return Either.Right(init.channelType) } @@ -201,8 +214,11 @@ object Helpers { } } - fun makeFundingPubKeyScript(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey): ByteVector { - return write(pay2wsh(multiSig2of2(localFundingPubkey, remoteFundingPubkey))).toByteVector() + fun makeFundingPubKeyScript(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, isTaprootChannel: Boolean): ByteVector { + return when (isTaprootChannel) { + true -> write(musig2FundingScript(localFundingPubkey, remoteFundingPubkey)).toByteVector() + else -> write(pay2wsh(multiSig2of2(localFundingPubkey, remoteFundingPubkey))).toByteVector() + } } fun makeFundingInputInfo( @@ -210,15 +226,18 @@ object Helpers { fundingTxOutputIndex: Int, fundingAmount: Satoshi, fundingPubkey1: PublicKey, - fundingPubkey2: PublicKey + fundingPubkey2: PublicKey, + isTaprootChannel: Boolean ): Transactions.InputInfo { - val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) - val fundingTxOut = TxOut(fundingAmount, pay2wsh(fundingScript)) - return Transactions.InputInfo( - OutPoint(fundingTxId, fundingTxOutputIndex.toLong()), - fundingTxOut, - ByteVector(write(fundingScript)) - ) + if (isTaprootChannel) { + val fundingScript = musig2FundingScript(fundingPubkey1, fundingPubkey2) + val fundingTxOut = TxOut(fundingAmount, fundingScript) + return Transactions.InputInfo(OutPoint(fundingTxId, fundingTxOutputIndex.toLong()), fundingTxOut, ByteVector(write(fundingScript))) + } else { + val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) + val fundingTxOut = TxOut(fundingAmount, pay2wsh(fundingScript)) + return Transactions.InputInfo(OutPoint(fundingTxId, fundingTxOutputIndex.toLong()), fundingTxOut, ByteVector(write(fundingScript))) + } } data class PairOfCommitTxs(val localSpec: CommitmentSpec, val localCommitTx: Transactions.TransactionWithInputInfo.CommitTx, val localHtlcTxs: List, val remoteSpec: CommitmentSpec, val remoteCommitTx: Transactions.TransactionWithInputInfo.CommitTx, val remoteHtlcTxs: List) @@ -249,12 +268,13 @@ object Helpers { val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote) val remoteSpec = CommitmentSpec(localHtlcs.map{ it.opposite() }.toSet(), commitTxFeerate, toLocal = toRemote, toRemote = toLocal) + val isTaprootChannel = Features.canUseFeature(localParams.features, remoteParams.features, Feature.SimpleTaprootStaging) if (!localParams.paysCommitTxFees) { // They are responsible for paying the commitment transaction fee: we need to make sure they can afford it! // Note that the reserve may not be always be met: we could be using dual funding with a large funding amount on // our side and a small funding amount on their side. But we shouldn't care as long as they can pay the fees for // the commitment transaction. - val fees = commitTxFee(remoteParams.dustLimit, remoteSpec) + val fees = commitTxFee(remoteParams.dustLimit, remoteSpec, isTaprootChannel) val missing = fees - remoteSpec.toLocal.truncateToSatoshi() if (missing > 0.sat) { return Either.Left(CannotAffordFirstCommitFees(channelId, missing = missing, fees = fees)) @@ -262,7 +282,7 @@ object Helpers { } val fundingPubKey = channelKeys.fundingPubKey(fundingTxIndex) - val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, fundingPubKey, remoteFundingPubkey) + val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, fundingPubKey, remoteFundingPubkey, isTaprootChannel) val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommitmentIndex) val (localCommitTx, localHtlcTxs) = Commitments.makeLocalTxs( channelKeys, @@ -330,10 +350,12 @@ object Helpers { commitment: FullCommitment, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, - requestedFeerate: ClosingFeerates + requestedFeerate: ClosingFeerates, + localClosingNonce: Pair? = null, + remoteClosingNonce: IndividualNonce? = null ): Pair { val closingFees = firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, requestedFeerate) - return makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFees) + return makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFees, localClosingNonce, remoteClosingNonce) } fun makeClosingTx( @@ -341,15 +363,33 @@ object Helpers { commitment: FullCommitment, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, - closingFees: ClosingFees + closingFees: ClosingFees, + localClosingNonce: Pair? = null, + remoteClosingNonce: IndividualNonce? = null ): Pair { val allowAnySegwit = Features.canUseFeature(commitment.params.localParams.features, commitment.params.remoteParams.features, Feature.ShutdownAnySegwit) require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit)) { "invalid localScriptPubkey" } require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit)) { "invalid remoteScriptPubkey" } val dustLimit = commitment.params.localParams.dustLimit.max(commitment.params.remoteParams.dustLimit) val closingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.params.localParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) - val localClosingSig = Transactions.sign(closingTx, channelKeys.fundingKey(commitment.fundingTxIndex)) - val closingSigned = ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) + val closingSigned = when (commitment.isTaprootChannel) { + true -> { + val localClosingPartialSig = Transactions.partialSign( + closingTx, + channelKeys.fundingKey(commitment.fundingTxIndex), + channelKeys.fundingKey(commitment.fundingTxIndex).publicKey(), + commitment.remoteFundingPubkey, + localClosingNonce!!, + remoteClosingNonce!! + ).right!! + ClosingSigned(commitment.channelId, closingFees.preferred, ByteVector64.Zeroes, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max), ClosingSignedTlv.PartialSignature(localClosingPartialSig))) + } + + else -> { + val localClosingSig = Transactions.sign(closingTx, channelKeys.fundingKey(commitment.fundingTxIndex)) + ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) + } + } return Pair(closingTx, closingSigned) } @@ -373,6 +413,37 @@ object Helpers { } } + fun checkClosingSignature( + channelKeys: KeyManager.ChannelKeys, + commitment: FullCommitment, + localScriptPubkey: ByteArray, + remoteScriptPubkey: ByteArray, + remoteClosingFee: Satoshi, + localClosingNonce: Pair, + remoteClosingNonce: IndividualNonce, + remoteClosingPartialSig: ByteVector32 + ): Either> { + val (closingTx, closingSigned) = makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee), localClosingNonce, remoteClosingNonce) + return if (checkClosingDustAmounts(closingTx)) { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val signingAttempt = + Transactions.aggregatePartialSignatures(closingTx, closingSigned.partialSignature!!, remoteClosingPartialSig, fundingKey.publicKey(), commitment.remoteFundingPubkey, localClosingNonce.second, remoteClosingNonce) + .map { aggsig -> Transactions.addAggregatedSignature(closingTx, aggsig) } + when (signingAttempt) { + is Either.Left -> Either.Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + is Either.Right -> { + val signedClosingTx = signingAttempt.right + when (Transactions.checkSpendable(signedClosingTx)) { + is Try.Success -> Either.Right(Pair(signedClosingTx, closingSigned)) + is Try.Failure -> Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + } + } + } + } else { + Either.Left(InvalidCloseAmountBelowDust(commitment.channelId, closingTx.tx.txid)) + } + } + /** * Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk * that the closing transaction will not be relayed to miners' mempool and will not confirm. @@ -416,10 +487,11 @@ object Helpers { commitment.params.remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey.toByteArray(), - feerateDelayed + feerateDelayed, + commitment.isTaprootChannel ) }?.let { - val sig = Transactions.sign(it, channelKeys.delayedPaymentKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) + val sig = it.sign(channelKeys.delayedPaymentKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) Transactions.addSigs(it, sig) } @@ -443,14 +515,15 @@ object Helpers { // all htlc output to us are delayed, so we need to claim them as soon as the delay is over val htlcDelayedTxs = htlcTxs.values.filterNotNull().mapNotNull { txInfo -> generateTx("claim-htlc-delayed") { - Transactions.makeClaimLocalDelayedOutputTx( + Transactions.makeHtlcDelayedTx( txInfo.tx, localParams.dustLimit, localRevocationPubkey, commitment.params.remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey.toByteArray(), - feerateDelayed + feerateDelayed, + commitment.isTaprootChannel ) }?.let { val sig = Transactions.sign(it, channelKeys.delayedPaymentKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) @@ -509,7 +582,8 @@ object Helpers { localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, - remoteCommit.spec + remoteCommit.spec, + commitment.isTaprootChannel ) // we need to use a rather high fee for htlc-claim because we compete with the counterparty @@ -570,7 +644,7 @@ object Helpers { }.toMap() // we claim our output and add the htlc txs we just created - return claimRemoteCommitMainOutput(channelKeys, commitment.params, tx, feerates.claimMainFeerate).copy(claimHtlcTxs = claimHtlcTxs) + return claimRemoteCommitMainOutput(channelKeys, commitment.params, tx, feerates.claimMainFeerate, commitment.isTaprootChannel).copy(claimHtlcTxs = claimHtlcTxs) } /** @@ -579,7 +653,7 @@ object Helpers { * @param tx the remote commitment transaction that has just been published. * @return a transaction to claim our main output. */ - internal fun LoggingContext.claimRemoteCommitMainOutput(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, tx: Transaction, claimMainFeerate: FeeratePerKw): RemoteCommitPublished { + internal fun LoggingContext.claimRemoteCommitMainOutput(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, tx: Transaction, claimMainFeerate: FeeratePerKw, isTaprootChannel: Boolean): RemoteCommitPublished { val localPaymentPoint = channelKeys.paymentBasepoint val mainTx = generateTx("claim-remote-delayed-output") { @@ -588,7 +662,8 @@ object Helpers { params.localParams.dustLimit, localPaymentPoint, params.localParams.defaultFinalScriptPubKey, - claimMainFeerate + claimMainFeerate, + isTaprootChannel ) }?.let { val sig = Transactions.sign(it, channelKeys.paymentKey) @@ -629,7 +704,7 @@ object Helpers { * When a revoked commitment transaction spending the funding tx is detected, we build a set of transactions that * will punish our peer by stealing all their funds. */ - fun LoggingContext.claimRevokedRemoteCommitTxOutputs(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, remotePerCommitmentSecret: PrivateKey, commitTx: Transaction, feerates: OnChainFeerates): RevokedCommitPublished { + fun LoggingContext.claimRevokedRemoteCommitTxOutputs(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, remotePerCommitmentSecret: PrivateKey, commitTx: Transaction, feerates: OnChainFeerates, isTaprootChannel: Boolean): RevokedCommitPublished { val localPaymentPoint = channelKeys.paymentBasepoint val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey() val remoteDelayedPaymentPubkey = params.remoteParams.delayedPaymentBasepoint.deriveForCommitment(remotePerCommitmentPoint) @@ -646,7 +721,8 @@ object Helpers { params.localParams.dustLimit, localPaymentPoint, params.localParams.defaultFinalScriptPubKey, - feerateMain + feerateMain, + isTaprootChannel ) }?.let { val sig = Transactions.sign(it, channelKeys.paymentKey) @@ -662,7 +738,8 @@ object Helpers { params.localParams.defaultFinalScriptPubKey.toByteArray(), params.localParams.toSelfDelay, remoteDelayedPaymentPubkey, - feeratePenalty + feeratePenalty, + isTaprootChannel ) }?.let { val sig = Transactions.sign(it, channelKeys.revocationKey.deriveForRevocation(remotePerCommitmentSecret)) @@ -691,27 +768,58 @@ object Helpers { // we retrieve the information needed to rebuild htlc scripts logger.info { "found ${htlcInfos.size} htlcs for txid=${revokedCommitPublished.commitTx.txid}" } - val htlcsRedeemScripts = htlcInfos.flatMap { htlcInfo -> - val htlcReceived = Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash), htlcInfo.cltvExpiry) - val htlcOffered = Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash)) - listOf(htlcReceived, htlcOffered) - }.associate { redeemScript -> write(pay2wsh(redeemScript)).toByteVector() to write(redeemScript).toByteVector() } - - // and finally we steal the htlc outputs - val htlcPenaltyTxs = revokedCommitPublished.commitTx.txOut.mapIndexedNotNull { outputIndex, txOut -> - htlcsRedeemScripts[txOut.publicKeyScript]?.let { redeemScript -> - generateTx("htlc-penalty") { - Transactions.makeHtlcPenaltyTx( - revokedCommitPublished.commitTx, - outputIndex, - redeemScript.toByteArray(), - params.localParams.dustLimit, - params.localParams.defaultFinalScriptPubKey.toByteArray(), - feeratePenalty - ) - }?.let { htlcPenaltyTx -> - val sig = Transactions.sign(htlcPenaltyTx, channelKeys.revocationKey.deriveForRevocation(revokedCommitPublished.remotePerCommitmentSecret)) - Transactions.addSigs(htlcPenaltyTx, sig, remoteRevocationPubkey) + val htlcPenaltyTxs = when (params.isTaprootChannel) { + true -> { + val scriptTrees = htlcInfos.flatMap { htlcInfo -> + val receivedTree = Scripts.Taproot.receivedHtlcTree(remoteHtlcPubkey, localHtlcPubkey, htlcInfo.paymentHash, htlcInfo.cltvExpiry) + val offeredTree = Scripts.Taproot.offeredHtlcTree(remoteHtlcPubkey, localHtlcPubkey, htlcInfo.paymentHash) + listOf(receivedTree, offeredTree) + }.associate { scriptTree -> write(pay2tr(remoteRevocationPubkey.xOnly(), scriptTree)).byteVector() to scriptTree } + + // and finally we steal the htlc outputs + revokedCommitPublished.commitTx.txOut.mapIndexedNotNull { outputIndex, txOut -> + scriptTrees[txOut.publicKeyScript]?.let { scriptTree -> + generateTx("htlc-penalty") { + Transactions.makeHtlcPenaltyTx( + revokedCommitPublished.commitTx, + outputIndex, + Transactions.ScriptTreeAndInternalKey(scriptTree, remoteRevocationPubkey.xOnly()), + params.localParams.dustLimit, + params.localParams.defaultFinalScriptPubKey.toByteArray(), + feeratePenalty + ) + }?.let { htlcPenaltyTx -> + val sig = Transactions.sign(htlcPenaltyTx, channelKeys.revocationKey.deriveForRevocation(revokedCommitPublished.remotePerCommitmentSecret)) + Transactions.addSigs(htlcPenaltyTx, sig, remoteRevocationPubkey) + } + } + } + } + + else -> { + val htlcsRedeemScripts = htlcInfos.flatMap { htlcInfo -> + val htlcReceived = Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash), htlcInfo.cltvExpiry) + val htlcOffered = Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash)) + listOf(htlcReceived, htlcOffered) + }.associate { redeemScript -> write(pay2wsh(redeemScript)).toByteVector() to write(redeemScript).toByteVector() } + + // and finally we steal the htlc outputs + revokedCommitPublished.commitTx.txOut.mapIndexedNotNull { outputIndex, txOut -> + htlcsRedeemScripts[txOut.publicKeyScript]?.let { redeemScript -> + generateTx("htlc-penalty") { + Transactions.makeHtlcPenaltyTx( + revokedCommitPublished.commitTx, + outputIndex, + redeemScript.toByteArray(), + params.localParams.dustLimit, + params.localParams.defaultFinalScriptPubKey.toByteArray(), + feeratePenalty + ) + }?.let { htlcPenaltyTx -> + val sig = Transactions.sign(htlcPenaltyTx, channelKeys.revocationKey.deriveForRevocation(revokedCommitPublished.remotePerCommitmentSecret)) + Transactions.addSigs(htlcPenaltyTx, sig, remoteRevocationPubkey) + } + } } } } @@ -737,7 +845,8 @@ object Helpers { params: ChannelParams, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, - feerates: OnChainFeerates + feerates: OnChainFeerates, + isTaprootChannel: Boolean ): Pair> { val claimTxs = buildList { revokedCommitPublished.claimMainOutputTx?.let { add(it) } @@ -762,7 +871,8 @@ object Helpers { params.localParams.toSelfDelay, remoteDelayedPaymentPubkey, params.localParams.defaultFinalScriptPubKey.toByteArray(), - feeratePenalty + feeratePenalty, + isTaprootChannel ).mapNotNull { claimDelayedOutputPenaltyTx -> generateTx("claim-htlc-delayed-penalty") { claimDelayedOutputPenaltyTx diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index a1280ecf1..be3475974 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -51,6 +51,24 @@ sealed class SharedFundingInput { const val weight: Int = 388 } } + + data class Musig2Input(override val info: Transactions.InputInfo, val fundingTxIndex: Long, val remoteFundingPubkey: PublicKey) : SharedFundingInput() { + + constructor(commitment: Commitment) : this( + info = commitment.commitInput, + fundingTxIndex = commitment.fundingTxIndex, + remoteFundingPubkey = commitment.remoteFundingPubkey + ) + + // This value was computed assuming 73 bytes signatures (worst-case scenario). + override val weight: Int = Musig2Input.weight + + override fun sign(channelKeys: KeyManager.ChannelKeys, tx: Transaction): ByteVector64 = ByteVector64.Zeroes + + companion object { + const val weight: Int = 234 + } + } } /** The current balances of a [[SharedFundingInput]]. */ @@ -94,9 +112,13 @@ data class InteractiveTxParams( // BOLT 2: the initiator's serial IDs MUST use even values and the non-initiator odd values. val serialIdParity = if (isInitiator) 0 else 1 - fun fundingPubkeyScript(channelKeys: KeyManager.ChannelKeys): ByteVector { - val fundingTxIndex = (sharedInput as? SharedFundingInput.Multisig2of2)?.let { it.fundingTxIndex + 1 } ?: 0 - return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey) + fun fundingPubkeyScript(channelKeys: KeyManager.ChannelKeys, isTaprootChannel: Boolean): ByteVector { + val fundingTxIndex = when (sharedInput) { + is SharedFundingInput.Multisig2of2 -> sharedInput.fundingTxIndex + 1 + is SharedFundingInput.Musig2Input -> sharedInput.fundingTxIndex + 1 + null -> 0 + } + return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey, isTaprootChannel) } fun liquidityFees(purchase: LiquidityAds.Purchase?): MilliSatoshi = purchase?.let { l -> @@ -270,9 +292,10 @@ data class FundingContributions(val inputs: List, v swapInKeys: KeyManager.SwapInOnChainKeys, params: InteractiveTxParams, walletInputs: List, - liquidityPurchase: LiquidityAds.Purchase? + liquidityPurchase: LiquidityAds.Purchase?, + isTaprootChannel: Boolean = false ): Either { - return create(channelKeys, swapInKeys, params, null, walletInputs, listOf(), liquidityPurchase) + return create(channelKeys, swapInKeys, params, null, walletInputs, listOf(), liquidityPurchase, isTaprootChannel = isTaprootChannel) } /** @@ -289,7 +312,8 @@ data class FundingContributions(val inputs: List, v walletInputs: List, localOutputs: List, liquidityPurchase: LiquidityAds.Purchase?, - changePubKey: PublicKey? = null + changePubKey: PublicKey? = null, + isTaprootChannel: Boolean = false ): Either { walletInputs.forEach { utxo -> if (utxo.previousTx.txOut.size <= utxo.outputIndex) return Either.Left(FundingContributionFailure.InputOutOfBounds(utxo.txId, utxo.outputIndex)) @@ -313,7 +337,7 @@ data class FundingContributions(val inputs: List, v return Either.Left(FundingContributionFailure.InvalidFundingBalances(params.fundingAmount, nextLocalBalanceAfterPush, nextRemoteBalanceAfterPush)) } - val fundingPubkeyScript = params.fundingPubkeyScript(channelKeys) + val fundingPubkeyScript = params.fundingPubkeyScript(channelKeys, isTaprootChannel) // We use local and remote balances before amounts are pushed to allow computing the local and remote mining fees. val sharedOutput = listOf(InteractiveTxOutput.Shared(0, fundingPubkeyScript, nextLocalBalanceBeforePush, nextRemoteBalanceBeforePush, sharedUtxo?.second?.toHtlcs ?: 0.msat)) val nonChangeOutputs = localOutputs.map { o -> InteractiveTxOutput.Local.NonChange(0, o.amount, o.publicKeyScript) } @@ -460,18 +484,29 @@ data class SharedTransaction( fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { val unsignedTx = buildUnsignedTx() - val sharedSig = fundingParams.sharedInput?.sign(keyManager.channelKeys(localParams.fundingKeyPath), unsignedTx) + val sharedSig = when (fundingParams.sharedInput) { + is SharedFundingInput.Multisig2of2 -> + fundingParams.sharedInput.sign(keyManager.channelKeys(localParams.fundingKeyPath), unsignedTx) + + else -> ByteVector64.Zeroes + } // NB: the order in this list must match the order of the transaction's inputs. val previousOutputs = unsignedTx.txIn.map { spentOutputs[it.outPoint]!! } - // Public nonces for all the musig2 swap-in inputs (local and remote). - // We have verified that one nonce was provided for each input when receiving `tx_complete`. - val remoteNonces: Map = when (session.txCompleteReceived) { - null -> mapOf() - else -> (localInputs.filterIsInstance() + remoteInputs.filterIsInstance()) - .sortedBy { it.serialId } - .zip(session.txCompleteReceived.publicNonces) - .associate { it.first.serialId to it.second } + val sharedPartialSig = when (fundingParams.sharedInput) { + is SharedFundingInput.Musig2Input -> { + val sharedInputs = session.localInputs.filterIsInstance() + session.remoteInputs.filterIsInstance() + // there should be a single shared input + val serialId = sharedInputs.first().serialId + val localNonce = session.secretNonces[serialId]!! + val fundingKey = keyManager.channelKeys(localParams.fundingKeyPath).fundingKey(fundingParams.sharedInput.fundingTxIndex) + val inputIndex = unsignedTx.txIn.indexOfFirst { it.outPoint == fundingParams.sharedInput.info.outPoint } + val remoteNonce = session.remoteNonces[serialId]!! + val psig = Transactions.partialSign(fundingKey, unsignedTx, inputIndex, previousOutputs, fundingKey.publicKey(), fundingParams.sharedInput.remoteFundingPubkey, localNonce, remoteNonce) + PartialSignatureWithNonce(psig.right!!, localNonce.second) + } + + else -> null } // If we are swapping funds in, we provide our partial signatures to the corresponding inputs. @@ -488,7 +523,7 @@ data class SharedTransaction( ?.let { input -> // We generate our secret nonce when sending the corresponding input, we know it exists in the map. val userNonce = session.secretNonces[input.serialId]!! - val serverNonce = remoteNonces[input.serialId]!! + val serverNonce = session.remoteNonces[input.serialId]!! keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, previousOutputs, userNonce.first, userNonce.second, serverNonce, input.addressIndex) .map { TxSignaturesTlv.PartialSignature(it, userNonce.second, serverNonce) } .getOrDefault(null) @@ -515,14 +550,14 @@ data class SharedTransaction( val swapInProtocol = SwapInProtocol(input.userKey, serverKey.publicKey(), input.userRefundKey, input.refundDelay) // We generate our secret nonce when receiving the corresponding input, we know it exists in the map. val serverNonce = session.secretNonces[input.serialId]!! - val userNonce = remoteNonces[input.serialId]!! + val userNonce = session.remoteNonces[input.serialId]!! swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, serverKey, serverNonce.first, userNonce, serverNonce.second) .map { TxSignaturesTlv.PartialSignature(it, userNonce, serverNonce.second) } .getOrDefault(null) } }.filterNotNull() - return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) + return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, sharedPartialSig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) } } @@ -547,6 +582,8 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, if (remoteSigs.swapInServerPartialSigs.size != tx.localInputs.filterIsInstance().size) return null if (remoteSigs.witnesses.size != tx.remoteOnlyInputs().size) return null if (remoteSigs.txId != localSigs.txId) return null + if (fundingParams.sharedInput is SharedFundingInput.Musig2Input && remoteSigs.previousFundingTxPartialSig == null) return null + val sharedSigs = fundingParams.sharedInput?.let { when (it) { is SharedFundingInput.Multisig2of2 -> Scripts.witness2of2( @@ -555,6 +592,22 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, channelKeys.fundingPubKey(it.fundingTxIndex), it.remoteFundingPubkey, ) + + is SharedFundingInput.Musig2Input -> { + val localFundingPubkey = channelKeys.fundingPubKey(it.fundingTxIndex) + val unsignedTx = this.tx.buildUnsignedTx() + val inputIndex = unsignedTx.txIn.indexOfFirst { i -> i.outPoint == it.info.outPoint } + val aggSig = Musig2.aggregateTaprootSignatures( + listOf(localSigs.previousFundingTxPartialSig!!.partialSig, remoteSigs.previousFundingTxPartialSig!!.partialSig), + unsignedTx, + inputIndex, + unsignedTx.txIn.map { i -> tx.spentOutputs[i.outPoint]!! }, + Scripts.sort(listOf(localFundingPubkey, it.remoteFundingPubkey)), + listOf(localSigs.previousFundingTxPartialSig.nonce, remoteSigs.previousFundingTxPartialSig.nonce), + null + ) + Script.witnessKeyPathPay2tr(aggSig.right!!) + } } } val fullySignedTx = FullySignedSharedTransaction(tx, localSigs, remoteSigs, sharedSigs) @@ -656,6 +709,7 @@ data class InteractiveTxSession( val txCompleteReceived: TxComplete? = null, val inputsReceivedCount: Int = 0, val outputsReceivedCount: Int = 0, + val firstRemoteNonce: IndividualNonce? = null, val secretNonces: Map> = mapOf() ) { @@ -682,7 +736,8 @@ data class InteractiveTxSession( previousRemoteBalance: MilliSatoshi, localHtlcs: Set, fundingContributions: FundingContributions, - previousTxs: List = listOf() + previousTxs: List = listOf(), + firstRemoteNonce: IndividualNonce? = null ) : this( remoteNodeId, channelKeys, @@ -691,17 +746,40 @@ data class InteractiveTxSession( SharedFundingInputBalances(previousLocalBalance, previousRemoteBalance, localHtlcs.map { it.add.amountMsat }.sum()), fundingContributions.inputs.map { i -> Either.Left(i) } + fundingContributions.outputs.map { o -> Either.Right(o) }, previousTxs, - localHtlcs + localHtlcs, + firstRemoteNonce = firstRemoteNonce ) val isComplete: Boolean = txCompleteSent != null && txCompleteReceived != null + // Public nonces for all the musig2 swap-in inputs (local and remote). + // We have verified that one nonce was provided for each input when receiving `tx_complete`. + private val sharedInputsThatNeedANonce = when (firstRemoteNonce) { + null -> listOf() + else -> localInputs.filterIsInstance() + remoteInputs.filterIsInstance() + } + val remoteNonces: Map = when (txCompleteReceived) { + null -> mapOf() + else -> (localInputs.filterIsInstance() + remoteInputs.filterIsInstance() + sharedInputsThatNeedANonce) + .sortedBy { it.serialId } + .zip(txCompleteReceived.publicNonces) + .associate { it.first.serialId to it.second } + } + fun send(): Pair { return when (val msg = toSend.firstOrNull()) { null -> { val localSwapIns = localInputs.filterIsInstance() val remoteSwapIns = remoteInputs.filterIsInstance() - val publicNonces = (localSwapIns + remoteSwapIns) + val sharedLocalInputs = when (this.firstRemoteNonce) { + null -> listOf() + else -> localInputs.filterIsInstance() + } + val sharedRemoteInputs = when (this.firstRemoteNonce) { + null -> listOf() + else -> remoteInputs.filterIsInstance() + } + val publicNonces = (localSwapIns + remoteSwapIns + sharedLocalInputs + sharedRemoteInputs) .map { it.serialId } .sorted() // We generate secret nonces whenever we send and receive tx_add_input, so we know they exist in the map. @@ -736,7 +814,21 @@ data class InteractiveTxSession( val secretNonce = Musig2.generateNonce(randomBytes32(), swapInKeys.userPrivateKey, listOf(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey)) secretNonces + (inputOutgoing.serialId to secretNonce) } - else -> secretNonces + else -> { + secretNonces + } + } + + is InteractiveTxInput.Shared -> when (firstRemoteNonce) { + null -> secretNonces + else -> { + val fundingTxIndex = when (val input = fundingParams.sharedInput) { + is SharedFundingInput.Musig2Input -> input.fundingTxIndex + else -> return Pair(this, InteractiveTxSessionAction.InvalidSharedInput(fundingParams.channelId, inputOutgoing.serialId)) + } + val secretNonce = channelKeys.signingNonce(fundingTxIndex) + secretNonces + (inputOutgoing.serialId to secretNonce) + } } else -> secretNonces } @@ -748,7 +840,9 @@ data class InteractiveTxSession( val next = copy(toSend = toSend.tail(), localOutputs = localOutputs + outputOutgoing, txCompleteSent = null) val txAddOutput = when (outputOutgoing) { is InteractiveTxOutput.Local -> TxAddOutput(fundingParams.channelId, outputOutgoing.serialId, outputOutgoing.amount, outputOutgoing.pubkeyScript) - is InteractiveTxOutput.Shared -> TxAddOutput(fundingParams.channelId, outputOutgoing.serialId, outputOutgoing.amount, outputOutgoing.pubkeyScript) + is InteractiveTxOutput.Shared -> { + TxAddOutput(fundingParams.channelId, outputOutgoing.serialId, outputOutgoing.amount, outputOutgoing.pubkeyScript) + } } Pair(next, InteractiveTxSessionAction.SendMessage(txAddOutput)) } @@ -815,14 +909,15 @@ data class InteractiveTxSession( if (message.sequence > 0xfffffffdU) { return Either.Left(InteractiveTxSessionAction.NonReplaceableInput(message.channelId, message.serialId, input.outPoint.txid, input.outPoint.index, message.sequence.toLong())) } - val secretNonces1 = when (input) { - // Generate a secret nonce for this input if we don't already have one. - is InteractiveTxInput.RemoteSwapIn -> when (secretNonces[input.serialId]) { - null -> { - val secretNonce = Musig2.generateNonce(randomBytes32(), swapInKeys.localServerPrivateKey(remoteNodeId), listOf(input.userKey, input.serverKey)) - secretNonces + (input.serialId to secretNonce) - } - else -> secretNonces + val secretNonces1 = when { + input is InteractiveTxInput.RemoteSwapIn && secretNonces[input.serialId] == null -> { + val secretNonce = Musig2.generateNonce(randomBytes32(), swapInKeys.localServerPrivateKey(remoteNodeId), listOf(input.userKey, input.serverKey)) + secretNonces + (input.serialId to secretNonce) + } + + input is InteractiveTxInput.Shared && this.fundingParams.sharedInput is SharedFundingInput.Musig2Input -> { + val secretNonce = channelKeys.signingNonce(fundingParams.sharedInput.fundingTxIndex) + secretNonces + (input.serialId to secretNonce) } else -> secretNonces } @@ -837,9 +932,9 @@ data class InteractiveTxSession( Either.Left(InteractiveTxSessionAction.DuplicateSerialId(message.channelId, message.serialId)) } else if (message.amount < fundingParams.dustLimit) { Either.Left(InteractiveTxSessionAction.OutputBelowDust(message.channelId, message.serialId, message.amount, fundingParams.dustLimit)) - } else if (message.pubkeyScript == fundingParams.fundingPubkeyScript(channelKeys) && message.amount != fundingParams.fundingAmount) { + } else if (message.pubkeyScript == fundingParams.fundingPubkeyScript(channelKeys, firstRemoteNonce != null) && message.amount != fundingParams.fundingAmount) { Either.Left(InteractiveTxSessionAction.InvalidTxSharedAmount(message.channelId, message.serialId, message.amount, fundingParams.fundingAmount)) - } else if (message.pubkeyScript == fundingParams.fundingPubkeyScript(channelKeys)) { + } else if (message.pubkeyScript == fundingParams.fundingPubkeyScript(channelKeys, firstRemoteNonce != null)) { val localAmount = previousFunding.toLocal + fundingParams.localContribution.toMilliSatoshi() val remoteAmount = previousFunding.toRemote + fundingParams.remoteContribution.toMilliSatoshi() Either.Right(InteractiveTxOutput.Shared(message.serialId, message.pubkeyScript, localAmount, remoteAmount, previousFunding.toHtlcs)) @@ -936,7 +1031,8 @@ data class InteractiveTxSession( // Our peer must send us one nonce for each swap input (local and remote), ordered by serial_id. val swapInputsCount = localInputs.count { it is InteractiveTxInput.LocalSwapIn } + remoteInputs.count { it is InteractiveTxInput.RemoteSwapIn } - if (txCompleteReceived.publicNonces.size != swapInputsCount) { + val sharedInputsCount = localInputs.count { it is InteractiveTxInput.Shared && this.firstRemoteNonce != null } + remoteInputs.count { it is InteractiveTxInput.Shared && this.firstRemoteNonce != null } + if (txCompleteReceived.publicNonces.size != swapInputsCount + sharedInputsCount) { return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, swapInputsCount, txCompleteReceived.publicNonces.size) } @@ -1034,7 +1130,23 @@ data class InteractiveTxSigningSession( is Either.Left -> { val localCommitIndex = localCommit.value.index val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex) - when (val signedLocalCommit = LocalCommit.fromCommitSig(channelKeys, channelParams, fundingTxIndex, fundingParams.remoteFundingPubkey, commitInput, remoteCommitSig, localCommitIndex, localCommit.value.spec, localPerCommitmentPoint, logger)) { + val localNonce = when (remoteCommitSig.sigOrPartialSig.isRight) { + true -> channelKeys.verificationNonce(fundingTxIndex, localCommitIndex) + else -> null + } + when (val signedLocalCommit = LocalCommit.fromCommitSig( + channelKeys, + channelParams, + fundingTxIndex, + fundingParams.remoteFundingPubkey, + commitInput, + remoteCommitSig, + localCommitIndex, + localCommit.value.spec, + localPerCommitmentPoint, + localNonce, + logger + )) { is Either.Left -> { val fundingKey = channelKeys.fundingKey(fundingTxIndex) val localSigOfLocalTx = Transactions.sign(localCommit.value.commitTx, fundingKey) @@ -1096,7 +1208,7 @@ data class InteractiveTxSigningSession( ): Either> { val channelKeys = channelParams.localParams.channelKeys(keyManager) val unsignedTx = sharedTx.buildUnsignedTx() - val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) } + val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys, session.firstRemoteNonce != null) } val liquidityFees = fundingParams.liquidityFees(liquidityPurchase) return Helpers.Funding.makeCommitTxs( channelKeys, @@ -1113,8 +1225,20 @@ data class InteractiveTxSigningSession( remoteFundingPubkey = fundingParams.remoteFundingPubkey, remotePerCommitmentPoint = remotePerCommitmentPoint ).map { firstCommitTx -> - val localSigOfRemoteCommitTx = Transactions.sign(firstCommitTx.remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) - val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } + val localSigOfRemoteCommitTx = firstCommitTx.remoteCommitTx.sign(channelKeys.fundingKey(fundingTxIndex)) + val localPartialSigOfRemoteTx = when (session.firstRemoteNonce) { + null -> null + else -> { + val localNonce = channelKeys.signingNonce(fundingTxIndex) + val psig = Transactions.partialSign( + firstCommitTx.remoteCommitTx, channelKeys.fundingKey(fundingTxIndex), + channelKeys.fundingKey(fundingTxIndex).publicKey(), session.fundingParams.remoteFundingPubkey, + localNonce, session.firstRemoteNonce + ).right!! + CommitSigTlv.PartialSignatureWithNonceTlv(PartialSignatureWithNonce(psig, localNonce.second)) + } + } + val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { it.sign(channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } val alternativeSigs = if (firstCommitTx.remoteHtlcTxs.isEmpty()) { val commitSigTlvs = Commitments.alternativeFeerates.map { feerate -> @@ -1133,11 +1257,12 @@ data class InteractiveTxSigningSession( val sig = Transactions.sign(alternativeRemoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) CommitSigTlv.AlternativeFeerateSig(feerate, sig) } - TlvStream(CommitSigTlv.AlternativeFeerateSigs(commitSigTlvs) as CommitSigTlv) + CommitSigTlv.AlternativeFeerateSigs(commitSigTlvs) } else { - TlvStream.empty() + null } - val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, alternativeSigs) + val tlvStream = TlvStream(setOf(localPartialSigOfRemoteTx, alternativeSigs).filterNotNull().toSet()) + val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, tlvStream) // We haven't received the remote commit_sig: we don't have local htlc txs yet. val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf()) val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt index 3083829f0..bb245d331 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt @@ -2,10 +2,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* import fr.acinq.bitcoin.utils.Either -import fr.acinq.lightning.CltvExpiryDelta -import fr.acinq.lightning.Feature -import fr.acinq.lightning.NodeParams -import fr.acinq.lightning.SensitiveTaskEvents +import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.* import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.channel.* @@ -81,6 +78,7 @@ sealed class ChannelState { else -> it } } + else -> actions } @@ -106,10 +104,13 @@ sealed class ChannelState { when { spliceStatusBefore !is SpliceStatus.InProgress && spliceStatusAfter is SpliceStatus.InProgress -> // splice initiated staticParams.nodeParams._nodeEvents.tryEmit(SensitiveTaskEvents.TaskStarted(SensitiveTaskEvents.TaskIdentifier.InteractiveTx(spliceStatusAfter.spliceSession.fundingParams))) + spliceStatusBefore is SpliceStatus.InProgress && spliceStatusAfter !is SpliceStatus.WaitingForSigs -> // splice aborted before reaching signing phase staticParams.nodeParams._nodeEvents.tryEmit(SensitiveTaskEvents.TaskEnded(SensitiveTaskEvents.TaskIdentifier.InteractiveTx(spliceStatusBefore.spliceSession.fundingParams))) + spliceStatusBefore is SpliceStatus.WaitingForSigs && spliceStatusAfter !is SpliceStatus.WaitingForSigs -> // splice leaving signing phase (successfully or not) staticParams.nodeParams._nodeEvents.tryEmit(SensitiveTaskEvents.TaskEnded(SensitiveTaskEvents.TaskIdentifier.InteractiveTx(spliceStatusBefore.session.fundingParams))) + else -> {} } } @@ -134,6 +135,7 @@ sealed class ChannelState { ) ) } + else -> { // this is a force close, the closing tx is a commit tx // since force close scenarios may be complicated with multiple layers of transactions, we estimate global fees by listing all the final outputs @@ -238,8 +240,10 @@ sealed class ChannelState { ) Pair(nextState, actions) } + else -> forceClose(state) } + is Closing -> { if (state.mutualClosePublished.isNotEmpty()) { // we already have published a mutual close tx, it's always better to use that @@ -251,6 +255,7 @@ sealed class ChannelState { } ?: state.run { spendLocalCurrent() } } } + is Closed -> Pair(state, emptyList()) is Aborted -> Pair(state, emptyList()) is Offline -> state.run { handleLocalError(cmd, t) } @@ -281,6 +286,7 @@ sealed class ChannelState { }) } } + is WaitForFundingSigned -> Pair(Aborted, listOf(ChannelAction.Storage.RemoveChannel(this@ChannelState))) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) is ChannelStateWithCommitments -> this.spendLocalCurrent() @@ -304,24 +310,37 @@ sealed class PersistedChannelState : ChannelState() { internal fun ChannelContext.createChannelReestablish(): HasEncryptedChannelData = when (val state = this@PersistedChannelState) { is WaitForFundingSigned -> { val myFirstPerCommitmentPoint = keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).commitmentPoint(0) + val myNextLocalNonce = when (state.channelParams.isTaprootChannel) { + true -> keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).verificationNonce(0, 1).second + else -> null + } ChannelReestablish( channelId = channelId, nextLocalCommitmentNumber = 1, nextRemoteRevocationNumber = 0, yourLastCommitmentSecret = PrivateKey(ByteVector32.Zeroes), myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint, - TlvStream(ChannelReestablishTlv.NextFunding(state.signingSession.fundingTx.txId)) + TlvStream(setOfNotNull(ChannelReestablishTlv.NextFunding(state.signingSession.fundingTx.txId), myNextLocalNonce?.let { ChannelReestablishTlv.NextLocalNoncesTlv(listOf(it)) })) ).withChannelData(state.remoteChannelData, logger) } + is ChannelStateWithCommitments -> { + val channelKeys = keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath) val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes - val myCurrentPerCommitmentPoint = keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath).commitmentPoint(state.commitments.localCommitIndex) + val myCurrentPerCommitmentPoint = channelKeys.commitmentPoint(state.commitments.localCommitIndex) + val myNextLocalNonces = when (state.commitments.isTaprootChannel) { + true -> state.commitments.active.map { channelKeys.verificationNonce(it.fundingTxIndex, state.commitments.localCommitIndex + 1).second } + else -> null + } val unsignedFundingTxId = when (state) { is WaitForFundingConfirmed -> state.getUnsignedFundingTxId() is Normal -> state.getUnsignedFundingTxId() // a splice was in progress, we tell our peer that we are remembering it and are expecting signatures else -> null } - val tlvs: TlvStream = unsignedFundingTxId?.let { TlvStream(ChannelReestablishTlv.NextFunding(it)) } ?: TlvStream.empty() + val tlvs: TlvStream = TlvStream(setOfNotNull( + unsignedFundingTxId?.let { ChannelReestablishTlv.NextFunding(it) }, + myNextLocalNonces?.let { ChannelReestablishTlv.NextLocalNoncesTlv(it) } + )) ChannelReestablish( channelId = channelId, nextLocalCommitmentNumber = state.commitments.localCommitIndex + 1, @@ -333,6 +352,37 @@ sealed class PersistedChannelState : ChannelState() { } } + internal fun ChannelContext.createChannelReady(): ChannelReady = when (val state = this@PersistedChannelState) { + is WaitForFundingSigned -> { + val nextPerCommitmentPoint = keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).commitmentPoint(1) + val nextLocalNonce = when (state.channelParams.isTaprootChannel) { + true -> keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).verificationNonce(0, 1).second + false -> null + } + val tlvStream = TlvStream( + setOfNotNull( + ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)), + nextLocalNonce?.let { ChannelReadyTlv.NextLocalNonceTlv(it) }) + ) + ChannelReady(channelId, nextPerCommitmentPoint, tlvStream) + } + + is ChannelStateWithCommitments -> { + val channelKeys = keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath) + val nextPerCommitmentPoint = keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath).commitmentPoint(1) + val nextLocalNonce = when (state.commitments.isTaprootChannel) { + true -> channelKeys.verificationNonce(0, 1).second + else -> null + } + val tlvStream = TlvStream( + setOfNotNull( + ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)), + nextLocalNonce?.let { ChannelReadyTlv.NextLocalNonceTlv(it) }) + ) + ChannelReady(channelId, nextPerCommitmentPoint, tlvStream) + } + } + companion object { // this companion object is used by static extended function `fun PersistedChannelState.Companion.from` in Encryption.kt } @@ -410,6 +460,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { val watch = ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, w.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_ALTERNATIVE_COMMIT_TX_CONFIRMED)) spendLocalCurrent().run { copy(second = second + watch) } } + else -> { logger.warning { "unrecognized tx=${w.tx.txid}" } // This case can happen in the following (harmless) scenario: @@ -438,6 +489,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, remoteCommitPublished = remoteCommitPublished ) + else -> Closing( commitments = commitments, waitingSinceBlock = currentBlockHeight.toLong(), @@ -467,6 +519,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, nextRemoteCommitPublished = remoteCommitPublished ) + else -> Closing( commitments = commitments, waitingSinceBlock = currentBlockHeight.toLong(), @@ -484,7 +537,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { logger.warning { "funding tx spent in txid=${tx.txid}" } return getRemotePerCommitmentSecret(channelKeys(), commitments.params, commitments.remotePerCommitmentSecrets, tx)?.let { (remotePerCommitmentSecret, commitmentNumber) -> logger.warning { "txid=${tx.txid} was a revoked commitment, publishing the penalty tx" } - val revokedCommitPublished = claimRevokedRemoteCommitTxOutputs(channelKeys(), commitments.params, remotePerCommitmentSecret, tx, currentOnChainFeerates()) + val revokedCommitPublished = claimRevokedRemoteCommitTxOutputs(channelKeys(), commitments.params, remotePerCommitmentSecret, tx, currentOnChainFeerates(), commitments.isTaprootChannel) val ex = FundingTxSpent(channelId, tx.txid) val error = Error(channelId, ex.message) val nextState = when (this@ChannelStateWithCommitments) { @@ -493,12 +546,14 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { } else { this@ChannelStateWithCommitments.copy(revokedCommitPublished = this@ChannelStateWithCommitments.revokedCommitPublished + revokedCommitPublished) } + is Negotiating -> Closing( commitments = commitments, waitingSinceBlock = currentBlockHeight.toLong(), mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, revokedCommitPublished = listOf(revokedCommitPublished) ) + else -> Closing( commitments = commitments, waitingSinceBlock = currentBlockHeight.toLong(), @@ -515,7 +570,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { when (this@ChannelStateWithCommitments) { is WaitForRemotePublishFutureCommitment -> { logger.warning { "they published their future commit (because we asked them to) in txid=${tx.txid}" } - val remoteCommitPublished = claimRemoteCommitMainOutput(channelKeys(), commitments.params, tx, currentOnChainFeerates().claimMainFeerate) + val remoteCommitPublished = claimRemoteCommitMainOutput(channelKeys(), commitments.params, tx, currentOnChainFeerates().claimMainFeerate, commitments.isTaprootChannel) val nextState = Closing( commitments = commitments, waitingSinceBlock = currentBlockHeight.toLong(), @@ -526,6 +581,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { addAll(remoteCommitPublished.run { doPublish(channelId, staticParams.nodeParams.minDepthBlocks.toLong()) }) }) } + else -> { // Our peer may publish an alternative version of their commitment using a different feerate. when (val remoteCommit = Commitments.alternativeFeerateCommits(commitments, channelKeys()).find { it.txid == tx.txid }) { @@ -543,9 +599,10 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { // So, the best thing to do here is to ignore the spending tx. Pair(this@ChannelStateWithCommitments, listOf()) } + else -> { logger.warning { "they published an alternative commitment with feerate=${remoteCommit.spec.feerate} txid=${tx.txid}" } - val remoteCommitPublished = claimRemoteCommitMainOutput(channelKeys(), commitments.params, tx, currentOnChainFeerates().claimMainFeerate) + val remoteCommitPublished = claimRemoteCommitMainOutput(channelKeys(), commitments.params, tx, currentOnChainFeerates().claimMainFeerate, commitments.isTaprootChannel) val nextState = when (this@ChannelStateWithCommitments) { is Closing -> this@ChannelStateWithCommitments.copy(remoteCommitPublished = remoteCommitPublished) is Negotiating -> Closing(commitments, waitingSinceBlock = currentBlockHeight.toLong(), mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, remoteCommitPublished = remoteCommitPublished) @@ -588,6 +645,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { mutualCloseProposed = closingTxProposed.flatten().map { it.unsignedTx }, localCommitPublished = localCommitPublished ) + else -> Closing( commitments = commitments, waitingSinceBlock = currentBlockHeight.toLong(), @@ -633,6 +691,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { addAll(doPublish(bestUnpublishedClosingTx, nexState.channelId)) }) } + else -> { val error = Error(channelId, channelEx.message) spendLocalCurrent().run { copy(second = second + ChannelAction.Message.Send(error)) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt index be378bca1..7dace7763 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt @@ -208,7 +208,7 @@ data class Closing( val revokedCommitPublishActions = mutableListOf() val revokedCommitPublished1 = revokedCommitPublished.map { rev -> - val (newRevokedCommitPublished, penaltyTxs) = claimRevokedHtlcTxOutputs(channelKeys(), commitments.params, rev, watch.tx, currentOnChainFeerates()) + val (newRevokedCommitPublished, penaltyTxs) = claimRevokedHtlcTxOutputs(channelKeys(), commitments.params, rev, watch.tx, currentOnChainFeerates(), commitments.isTaprootChannel) penaltyTxs.forEach { revokedCommitPublishActions += ChannelAction.Blockchain.PublishTx(it) revokedCommitPublishActions += ChannelAction.Blockchain.SendWatch(WatchSpent(channelId, watch.tx, it.input.outPoint.index.toInt(), BITCOIN_OUTPUT_SPENT)) @@ -319,7 +319,7 @@ data class Closing( // note spendingTx != Nil (that's a requirement of DATA_CLOSING) val exc = FundingTxSpent(channelId, spendingTxs.first().txid) val error = Error(channelId, exc.message) - Pair(this@Closing, listOf(ChannelAction.Message.Send(error))) + Pair(this@Closing, listOf(ChannelAction.Message.Send(error))) // README: we don't need to update nonces, we're already in CLOSING state } is Error -> { logger.error { "peer sent error: ascii=${cmd.message.toAscii()} bin=${cmd.message.data.toHex()}" } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt index 321f2062a..4a32a4dbc 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt @@ -30,8 +30,29 @@ data class Negotiating( override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> { return when (cmd) { - is ChannelCommand.MessageReceived -> when (cmd.message) { - is ClosingSigned -> { + is ChannelCommand.MessageReceived -> when { + cmd.message is ClosingSigned && commitments.isTaprootChannel -> { + val remoteClosingFee = cmd.message.feeSatoshis + logger.info { "received closing fee=$remoteClosingFee" } + when (val result = Helpers.Closing.checkClosingSignature( + channelKeys(), + commitments.latest, + localShutdown.scriptPubKey.toByteArray(), + remoteShutdown.scriptPubKey.toByteArray(), + cmd.message.feeSatoshis, + commitments.closingNonce!!, + this@Negotiating.remoteShutdown.shutdownNonce!!, + cmd.message.partialSignature!! + )) { + is Either.Left -> handleLocalError(cmd, result.value) + is Either.Right -> { + // with simple taproot channels there is no fee negotiation + completeMutualClose(result.value.first, result.value.second) + } + } + } + + cmd.message is ClosingSigned -> { val remoteClosingFee = cmd.message.feeSatoshis logger.info { "received closing fee=$remoteClosingFee" } when (val result = @@ -142,7 +163,8 @@ data class Negotiating( } } } - is Error -> handleRemoteError(cmd.message) + + cmd.message is Error -> handleRemoteError(cmd.message) else -> unhandled(cmd) } is ChannelCommand.WatchReceived -> when (val watch = cmd.watch) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index ad0e24621..e6637de91 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -96,8 +96,12 @@ data class Normal( commitments.changes.localHasUnsignedOutgoingUpdateFee() -> handleCommandError(cmd, CannotCloseWithUnsignedOutgoingUpdateFee(channelId), channelUpdate) !Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit) -> handleCommandError(cmd, InvalidFinalScript(channelId), channelUpdate) else -> { - val shutdown = Shutdown(channelId, localScriptPubkey) - val newState = this@Normal.copy(localShutdown = shutdown, closingFeerates = cmd.feerates) + val closingNonce = when (commitments.isTaprootChannel) { + true -> channelKeys().signingNonce(commitments.latest.fundingTxIndex) + false -> null + } + val shutdown = Shutdown(channelId, localScriptPubkey, TlvStream(setOfNotNull(closingNonce?.let { ShutdownTlv.ShutdownNonce(it.second) }))) + val newState = this@Normal.copy(localShutdown = shutdown, closingFeerates = cmd.feerates).updateCommitments(this@Normal.commitments.copy(closingNonce = closingNonce)) val actions = listOf(ChannelAction.Storage.StoreState(newState), ChannelAction.Message.Send(shutdown)) Pair(newState, actions) } @@ -232,11 +236,15 @@ data class Normal( } val nextState = if (remoteShutdown != null && !commitments1.changes.localHasUnsignedOutgoingHtlcs()) { // we were waiting for our pending htlcs to be signed before replying with our local shutdown - val localShutdown = Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey) + val closingNonce = when (commitments.isTaprootChannel) { + true -> channelKeys().signingNonce(commitments.latest.fundingTxIndex) + else -> null + } + val localShutdown = Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey, TlvStream(setOfNotNull(closingNonce?.let { ShutdownTlv.ShutdownNonce(it.second) }))) actions.add(ChannelAction.Message.Send(localShutdown)) if (commitments1.latest.remoteCommit.spec.htlcs.isNotEmpty()) { // we just signed htlcs that need to be resolved now - ShuttingDown(commitments1, localShutdown, remoteShutdown, closingFeerates) + ShuttingDown(commitments1.copy(closingNonce = closingNonce), localShutdown, remoteShutdown, closingFeerates) } else { logger.warning { "we have no htlcs but have not replied with our Shutdown yet, this should never happen" } val closingTxProposed = if (paysClosingFees) { @@ -286,6 +294,7 @@ data class Normal( // there are no changes => go to NEGOTIATING when { !Helpers.Closing.isValidFinalScriptPubkey(cmd.message.scriptPubKey, allowAnySegwit) -> handleLocalError(cmd, InvalidFinalScript(channelId)) + commitments.isTaprootChannel && cmd.message.shutdownNonce == null -> handleLocalError(cmd, MissingNextLocalNonces(channelId)) commitments.changes.remoteHasUnsignedOutgoingHtlcs() -> handleLocalError(cmd, CannotCloseWithUnsignedOutgoingHtlcs(channelId)) commitments.changes.remoteHasUnsignedOutgoingUpdateFee() -> handleLocalError(cmd, CannotCloseWithUnsignedOutgoingUpdateFee(channelId)) commitments.changes.localHasUnsignedOutgoingHtlcs() -> { @@ -306,9 +315,23 @@ data class Normal( else -> { // so we don't have any unsigned outgoing changes val actions = mutableListOf() - val localShutdown = this@Normal.localShutdown ?: Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey) + val (commitments0, localShutdown) = when (val s = this@Normal.localShutdown) { + null -> when (commitments.isTaprootChannel) { + true -> { + val closingNonce = channelKeys().signingNonce(commitments.latest.fundingTxIndex) + Pair( + commitments.copy(closingNonce = closingNonce), + Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey, TlvStream(ShutdownTlv.ShutdownNonce(closingNonce.second))) + ) + } + + else -> commitments to Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey) + } + + else -> commitments to s + } if (this@Normal.localShutdown == null) actions.add(ChannelAction.Message.Send(localShutdown)) - val commitments1 = commitments.copy(remoteChannelData = cmd.message.channelData) + val commitments1 = commitments0.copy(remoteChannelData = cmd.message.channelData) when { commitments1.hasNoPendingHtlcsOrFeeUpdate() && paysClosingFees -> { val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( @@ -317,6 +340,8 @@ data class Normal( localShutdown.scriptPubKey.toByteArray(), cmd.message.scriptPubKey.toByteArray(), closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate), + commitments1.closingNonce, + cmd.message.shutdownNonce ) val nextState = Negotiating( commitments1, @@ -386,7 +411,7 @@ data class Normal( targetFeerate = spliceStatus.command.feerate ) val commitTxFees = when { - paysCommitTxFees -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec) + paysCommitTxFees -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec, commitments.isTaprootChannel) else -> 0.sat } val liquidityFees = when (val requestRemoteFunding = spliceStatus.command.requestRemoteFunding) { @@ -413,6 +438,14 @@ data class Normal( val action = listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidSpliceRequest(channelId).message))) Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), action) } else { + val nextLocalNonces = when (commitments.isTaprootChannel) { + true -> listOf( + channelKeys().verificationNonce(parentCommitment.fundingTxIndex + 1, commitments.localCommitIndex).second, + channelKeys().verificationNonce(parentCommitment.fundingTxIndex + 1, commitments.localCommitIndex + 1).second, + ) + + else -> listOf() + } val spliceInit = SpliceInit( channelId, fundingContribution = fundingContribution, @@ -420,6 +453,7 @@ data class Normal( feerate = spliceStatus.command.feerate, fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), requestFunding = spliceStatus.command.requestRemoteFunding, + nextLocalNonces = nextLocalNonces ) logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution}" } Pair(this@Normal.copy(spliceStatus = SpliceStatus.Requested(spliceStatus.command, spliceInit)), listOf(ChannelAction.Message.Send(spliceInit))) @@ -454,18 +488,33 @@ data class Normal( if (commitments.isQuiescent()) { logger.info { "accepting splice with remote.amount=${cmd.message.fundingContribution}" } val parentCommitment = commitments.active.first() + val nextLocalNonces = when (commitments.isTaprootChannel) { + true -> { + listOf( + channelKeys().verificationNonce(parentCommitment.fundingTxIndex + 1, commitments.localCommitIndex).second, + channelKeys().verificationNonce(parentCommitment.fundingTxIndex + 1, commitments.localCommitIndex + 1).second, + ) + } + + else -> listOf() + } val spliceAck = SpliceAck( channelId, fundingContribution = 0.sat, // only remote contributes to the splice fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), willFund = null, + nextLocalNonces = nextLocalNonces ) + val sharedInput = when (commitments.isTaprootChannel) { + true -> SharedFundingInput.Musig2Input(parentCommitment) + else -> SharedFundingInput.Multisig2of2(parentCommitment) + } val fundingParams = InteractiveTxParams( channelId = channelId, isInitiator = false, localContribution = spliceAck.fundingContribution, remoteContribution = cmd.message.fundingContribution, - sharedInput = SharedFundingInput.Multisig2of2(parentCommitment), + sharedInput = sharedInput, remoteFundingPubkey = cmd.message.fundingPubkey, localOutputs = emptyList(), lockTime = cmd.message.lockTime, @@ -481,9 +530,11 @@ data class Normal( previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, localHtlcs = parentCommitment.localCommit.spec.htlcs, fundingContributions = FundingContributions(emptyList(), emptyList()), // as non-initiator we don't contribute to this splice for now - previousTxs = emptyList() + previousTxs = emptyList(), + firstRemoteNonce = cmd.message.firstRemoteNonce ) val nextState = this@Normal.copy( + commitments = this@Normal.commitments.copy(pendingRemoteNextLocalNonce = cmd.message.secondRemoteNonce), spliceStatus = SpliceStatus.InProgress( replyTo = null, session, @@ -512,7 +563,7 @@ data class Normal( spliceStatus.command.requestRemoteFunding, remoteNodeId, channelId, - Helpers.Funding.makeFundingPubKeyScript(spliceStatus.spliceInit.fundingPubkey, cmd.message.fundingPubkey), + Helpers.Funding.makeFundingPubKeyScript(spliceStatus.spliceInit.fundingPubkey, cmd.message.fundingPubkey, isTaprootChannel = commitments.isTaprootChannel), cmd.message.fundingContribution, spliceStatus.spliceInit.feerate, isChannelCreation = false, @@ -526,7 +577,10 @@ data class Normal( } is Either.Right -> { val parentCommitment = commitments.active.first() - val sharedInput = SharedFundingInput.Multisig2of2(parentCommitment) + val sharedInput = when (commitments.isTaprootChannel) { + true -> SharedFundingInput.Musig2Input(parentCommitment) + else -> SharedFundingInput.Multisig2of2(parentCommitment) + } val fundingParams = InteractiveTxParams( channelId = channelId, isInitiator = true, @@ -547,7 +601,8 @@ data class Normal( walletInputs = spliceStatus.command.spliceIn?.walletInputs ?: emptyList(), localOutputs = spliceStatus.command.spliceOutputs, liquidityPurchase = liquidityPurchase.value, - changePubKey = null // we don't want a change output: we're spending every funds available + changePubKey = null, // we don't want a change output: we're spending every funds available, + isTaprootChannel = commitments.isTaprootChannel )) { is Either.Left -> { logger.error { "could not create splice contributions: ${fundingContributions.value}" } @@ -565,11 +620,13 @@ data class Normal( previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, localHtlcs = parentCommitment.localCommit.spec.htlcs, fundingContributions = fundingContributions.value, - previousTxs = emptyList() + previousTxs = emptyList(), + firstRemoteNonce = cmd.message.firstRemoteNonce ).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { val nextState = this@Normal.copy( + commitments = this@Normal.commitments.copy(pendingRemoteNextLocalNonce = cmd.message.secondRemoteNonce), spliceStatus = SpliceStatus.InProgress( replyTo = spliceStatus.command.replyTo, interactiveTxSession, @@ -670,7 +727,10 @@ data class Normal( } is Either.Right -> { val action: InteractiveTxSigningSessionAction.SendTxSigs = res.value - sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityPurchase, cmd.message.channelData) + val (nextState, actions) = sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityPurchase, cmd.message.channelData) + val nextRemoteNonces = nextState.commitments.pendingRemoteNextLocalNonce?.let { listOf(it) + nextState.commitments.nextRemoteNonces } ?: nextState.commitments.nextRemoteNonces + val commitments1 = nextState.commitments.copy(nextRemoteNonces = nextRemoteNonces) + Pair(nextState.updateCommitments(commitments1), actions) } } } @@ -689,7 +749,10 @@ data class Normal( is Either.Left -> Pair(this@Normal, listOf()) is Either.Right -> { logger.info { "received remote funding signatures, publishing txId=${fullySignedTx.signedTx.txid} fundingTxIndex=${commitments.latest.fundingTxIndex}" } - val nextState = this@Normal.copy(commitments = res.value.first) + val commitments = res.value.first + val nextRemoteNonces = commitments.pendingRemoteNextLocalNonce?.let { listOf(it) + commitments.nextRemoteNonces } ?: commitments.nextRemoteNonces + val commitments1 = commitments.copy(nextRemoteNonces = nextRemoteNonces) + val nextState = this@Normal.copy(commitments = commitments1) val actions = buildList { add(ChannelAction.Blockchain.PublishTx(fullySignedTx.signedTx, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) add(ChannelAction.Storage.StoreState(nextState)) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt index 10264fb9e..d72fb81a4 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt @@ -65,8 +65,7 @@ data class Offline(val state: PersistedChannelState) : ChannelState() { val nextState = when (state) { is WaitForFundingConfirmed -> { logger.info { "was confirmed while offline at blockHeight=${watch.blockHeight} txIndex=${watch.txIndex} with funding txid=${watch.tx.txid}" } - val nextPerCommitmentPoint = commitments1.params.localParams.channelKeys(keyManager).commitmentPoint(1) - val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) + val channelReady = state.run { createChannelReady() } val shortChannelId = ShortChannelId(watch.blockHeight, watch.txIndex, commitments1.latest.commitInput.outPoint.index.toInt()) WaitForChannelReady(commitments1, shortChannelId, channelReady) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt index a14f9e471..4d8172b6d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt @@ -30,22 +30,23 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: when (cmd.message.nextFundingTxId) { // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. state.signingSession.fundingTx.txId -> { - val commitSig = state.signingSession.remoteCommit.sign(channelKeys(), state.channelParams, state.signingSession) - Pair(state, listOf(ChannelAction.Message.Send(commitSig))) + val commitSig = state.signingSession.remoteCommit.sign(channelKeys(), state.channelParams, state.signingSession, cmd.message.nextLocalNonces.firstOrNull()) + Pair(state.copy(secondRemoteNonce = cmd.message.nextLocalNonces.firstOrNull()), listOf(ChannelAction.Message.Send(commitSig))) } - else -> Pair(state, listOf()) + + else -> Pair(state.copy(secondRemoteNonce = cmd.message.nextLocalNonces.firstOrNull()), listOf()) } } is WaitForFundingConfirmed -> { when (cmd.message.nextFundingTxId) { - null -> Pair(state, listOf()) + null -> Pair(state.copy(commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), listOf()) else -> { if (state.rbfStatus is RbfStatus.WaitingForSigs && state.rbfStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) { // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. logger.info { "re-sending commit_sig for rbf attempt with fundingTxId=${cmd.message.nextFundingTxId}" } - val commitSig = state.rbfStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.rbfStatus.session) + val commitSig = state.rbfStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.rbfStatus.session, cmd.message.nextLocalNonces.firstOrNull()) val actions = listOf(ChannelAction.Message.Send(commitSig)) - Pair(state, actions) + Pair(state.copy(commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), actions) } else if (state.latestFundingTx.txId == cmd.message.nextFundingTxId) { val actions = buildList { if (state.latestFundingTx.sharedTx is PartiallySignedSharedTransaction) { @@ -56,19 +57,23 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.params, fundingTxIndex = 0, state.commitments.latest.remoteFundingPubkey, - state.commitments.latest.commitInput + state.commitments.latest.commitInput, + cmd.message.nextLocalNonces.firstOrNull() ) add(ChannelAction.Message.Send(commitSig)) } logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" } add(ChannelAction.Message.Send(state.latestFundingTx.sharedTx.localSigs)) } - Pair(state, actions) + Pair(state.copy(commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), actions) } else { // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving their tx_complete). // We tell them to abort that RBF attempt. logger.info { "aborting obsolete rbf attempt for fundingTxId=${cmd.message.nextFundingTxId}" } - Pair(state.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(state.channelId, RbfAttemptAborted(state.channelId).message)))) + Pair( + state.copy(rbfStatus = RbfStatus.RbfAborted, commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), + listOf(ChannelAction.Message.Send(TxAbort(state.channelId, RbfAttemptAborted(state.channelId).message))) + ) } } } @@ -86,7 +91,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.params, fundingTxIndex = state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, - state.commitments.latest.commitInput + state.commitments.latest.commitInput, + cmd.message.nextLocalNonces.firstOrNull() ) actions.add(ChannelAction.Message.Send(commitSig)) } @@ -101,11 +107,10 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: } logger.debug { "re-sending channel_ready" } - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) - val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint) + val channelReady = state.run { createChannelReady() } actions.add(ChannelAction.Message.Send(channelReady)) - Pair(state, actions) + Pair(state.copy(commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), actions) } is LegacyWaitForFundingLocked -> { logger.debug { "re-sending channel_ready" } @@ -125,8 +130,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (state.commitments.latest.fundingTxIndex == 0L && cmd.message.nextLocalCommitmentNumber == 1L && state.commitments.localCommitIndex == 0L) { // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT logger.debug { "re-sending channel_ready" } - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) - val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint) + val channelReady = state.run { createChannelReady() } actions.add(ChannelAction.Message.Send(channelReady)) } @@ -134,7 +138,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: val spliceStatus1 = if (state.spliceStatus is SpliceStatus.WaitingForSigs && state.spliceStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) { // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. logger.info { "re-sending commit_sig for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" } - val commitSig = state.spliceStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.spliceStatus.session) + val commitSig = state.spliceStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.spliceStatus.session, cmd.message.nextLocalNonces.firstOrNull()) actions.add(ChannelAction.Message.Send(commitSig)) state.spliceStatus } else if (state.commitments.latest.fundingTxId == cmd.message.nextFundingTxId) { @@ -148,7 +152,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.params, fundingTxIndex = state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, - state.commitments.latest.commitInput + state.commitments.latest.commitInput, + cmd.message.nextLocalNonces.firstOrNull() ) actions.add(ChannelAction.Message.Send(commitSig)) } @@ -190,7 +195,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: actions.addAll(syncResult.retransmit.map { ChannelAction.Message.Send(it) }) // then we clean up unsigned updates - val commitments1 = discardUnsignedUpdates(state.commitments) + val commitments1 = discardUnsignedUpdates(state.commitments).copy(nextRemoteNonces = cmd.message.nextLocalNonces) if (commitments1.changes.localHasChanges()) { actions.add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign)) @@ -305,8 +310,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: val nextState = when (state) { is WaitForFundingConfirmed -> { logger.info { "was confirmed while syncing at blockHeight=${watch.blockHeight} txIndex=${watch.txIndex} with funding txid=${watch.tx.txid}" } - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) - val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) + val channelReady = state.run { createChannelReady() } val shortChannelId = ShortChannelId(watch.blockHeight, watch.txIndex, commitments1.latest.commitInput.outPoint.index.toInt()) WaitForChannelReady(commitments1, shortChannelId, channelReady) } @@ -473,10 +477,15 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // they just sent a new commit_sig, we have received it but they didn't receive our revocation val localPerCommitmentSecret = channelKeys.commitmentSecret(commitments.localCommitIndex - 1) val localNextPerCommitmentPoint = channelKeys.commitmentPoint(commitments.localCommitIndex + 1) + val nextLocalNonces = when (commitments.isTaprootChannel) { + true -> commitments.active.map { channelKeys.verificationNonce(it.fundingTxIndex, commitments.localCommitIndex + 1).second } + false -> null + } val revocation = RevokeAndAck( channelId = commitments.channelId, perCommitmentSecret = localPerCommitmentSecret, - nextPerCommitmentPoint = localNextPerCommitmentPoint + nextPerCommitmentPoint = localNextPerCommitmentPoint, + tlvStream = TlvStream(setOfNotNull(nextLocalNonces?.let { RevokeAndAckTlv.NextLocalNoncesTlv(it) })) ) checkRemoteCommit(remoteChannelReestablish, retransmitRevocation = revocation) } else if (commitments.localCommitIndex > remoteChannelReestablish.nextRemoteRevocationNumber + 1) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt index 007e519c0..a4c310ead 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ChannelEvents +import fr.acinq.lightning.Feature import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.Helpers.Funding.computeChannelId import fr.acinq.lightning.utils.msat @@ -55,7 +56,7 @@ data class WaitForAcceptChannel( lastSent.requestFunding, staticParams.remoteNodeId, channelId, - fundingParams.fundingPubkeyScript(channelKeys), + fundingParams.fundingPubkeyScript(channelKeys, isTaprootChannel = channelFeatures.hasFeature(Feature.SimpleTaprootStaging)), accept.fundingAmount, lastSent.fundingFeerate, isChannelCreation = true, @@ -72,7 +73,8 @@ data class WaitForAcceptChannel( keyManager.swapInOnChainWallet, fundingParams, init.walletInputs, - liquidityPurchase.value + liquidityPurchase.value, + isTaprootChannel = channelFeatures.hasFeature(Feature.SimpleTaprootStaging) )) { is Either.Left -> { logger.error { "could not fund channel: ${fundingContributions.value}" } @@ -89,7 +91,8 @@ data class WaitForAcceptChannel( 0.msat, 0.msat, emptySet(), - fundingContributions.value + fundingContributions.value, + firstRemoteNonce = cmd.message.firstRemoteNonce ).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { @@ -105,7 +108,8 @@ data class WaitForAcceptChannel( init.channelConfig, channelFeatures, liquidityPurchase.value, - channelOrigin + channelOrigin, + cmd.message.secondRemoteNonce ) val actions = listOf( ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt index cff67ea29..40aa5c970 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt @@ -59,6 +59,9 @@ data class WaitForChannelReady( Pair(this@WaitForChannelReady, listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidRbfTxConfirmed(channelId, commitments.latest.fundingTxId).message)))) } is ChannelReady -> { + if (commitments.isTaprootChannel) { + require(cmd.message.nextLocalNonce != null) { "missing next local nonce" } + } // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced val initialChannelUpdate = Announcements.makeChannelUpdate( staticParams.nodeParams.chainHash, @@ -73,7 +76,7 @@ data class WaitForChannelReady( enable = Helpers.aboveReserve(commitments) ) val nextState = Normal( - commitments, + commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonce?.let { listOf(it) } ?: listOf()), shortChannelId, initialChannelUpdate, null, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt index 7667c4e68..4d795b6e6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -126,7 +126,7 @@ data class WaitForFundingConfirmed( latestFundingTx.fundingParams.dustLimit, rbfStatus.command.targetFeerate ) - when (val contributions = FundingContributions.create(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, rbfStatus.command.walletInputs, null)) { + when (val contributions = FundingContributions.create(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, rbfStatus.command.walletInputs, null, isTaprootChannel = false)) { // FIXME is Either.Left -> { logger.warning { "error creating funding contributions: ${contributions.value}" } Pair(this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message)))) @@ -242,8 +242,8 @@ data class WaitForFundingConfirmed( is Either.Left -> Pair(this@WaitForFundingConfirmed, listOf()) is Either.Right -> { val (commitments1, commitment, actions) = res.value - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) - val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) + val channelReady = createChannelReady() + //ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) // this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel // as soon as it reaches NORMAL state, and before it is announced on the network // (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt index 535543b39..dd362bee7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PublicKey +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* @@ -41,7 +42,8 @@ data class WaitForFundingCreated( val channelConfig: ChannelConfig, val channelFeatures: ChannelFeatures, val liquidityPurchase: LiquidityAds.Purchase?, - val channelOrigin: Origin? + val channelOrigin: Origin?, + val secondRemoteNonce: IndividualNonce? ) : ChannelState() { val channelId: ByteVector32 = interactiveTxSession.fundingParams.channelId @@ -94,7 +96,8 @@ data class WaitForFundingCreated( session, remoteSecondPerCommitmentPoint, liquidityPurchase, - channelOrigin + channelOrigin, + secondRemoteNonce ) val actions = buildList { interactiveTxAction.txComplete?.let { add(ChannelAction.Message.Send(it)) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt index 7e8ed19d9..44bc34cea 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PublicKey import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK @@ -41,6 +42,7 @@ data class WaitForFundingSigned( val remoteSecondPerCommitmentPoint: PublicKey, val liquidityPurchase: LiquidityAds.Purchase?, val channelOrigin: Origin?, + val secondRemoteNonce: IndividualNonce?, val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty ) : PersistedChannelState() { override val channelId: ByteVector32 = channelParams.channelId @@ -111,7 +113,8 @@ data class WaitForFundingSigned( payments = mapOf(), remoteNextCommitInfo = Either.Right(remoteSecondPerCommitmentPoint), remotePerCommitmentSecrets = ShaChain.init, - remoteChannelData = remoteChannelData + remoteChannelData = remoteChannelData, + nextRemoteNonces = secondRemoteNonce?.let { listOf(it) } ?: listOf() ) val commonActions = buildList { action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) } @@ -146,8 +149,7 @@ data class WaitForFundingSigned( } return if (staticParams.useZeroConf) { logger.info { "channel is using 0-conf, we won't wait for the funding tx to confirm" } - val nextPerCommitmentPoint = channelParams.localParams.channelKeys(keyManager).commitmentPoint(1) - val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) + val channelReady = createChannelReady() // We use part of the funding txid to create a dummy short channel id. // This gives us a probability of collisions of 0.1% for 5 0-conf channels and 1% for 20 // Collisions mean that users may temporarily see incorrect numbers for their 0-conf channels (until they've been confirmed). diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt index a29d5fcbd..46387bfff 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt @@ -4,10 +4,7 @@ import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_SPENT import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchSpent -import fr.acinq.lightning.channel.ChannelAction -import fr.acinq.lightning.channel.ChannelCommand -import fr.acinq.lightning.channel.Helpers -import fr.acinq.lightning.channel.LocalFundingStatus +import fr.acinq.lightning.channel.* import fr.acinq.lightning.utils.msat import fr.acinq.lightning.wire.ChannelTlv import fr.acinq.lightning.wire.OpenDualFundedChannel @@ -55,6 +52,14 @@ data object WaitForInit : ChannelState() { buildSet { add(ChannelTlv.ChannelTypeTlv(cmd.channelType)) cmd.requestRemoteFunding?.let { add(ChannelTlv.RequestFundingTlv(it)) } + if (cmd.channelType == ChannelType.SupportedChannelType.SimpleTaprootStaging || cmd.channelType == ChannelType.SupportedChannelType.SimpleTaprootStagingZeroReserve) add( + ChannelTlv.NextLocalNoncesTlv( + listOf( + channelKeys.verificationNonce(0, 0).second, + channelKeys.verificationNonce(0, 1).second, + ) + ) + ) } ) ) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt index 0ff314252..e1346c22d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt @@ -42,7 +42,12 @@ data class WaitForOpenChannel( val minimumDepth = if (staticParams.useZeroConf) 0 else Helpers.minDepthForFunding(staticParams.nodeParams, open.fundingAmount) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) val localFundingPubkey = channelKeys.fundingPubKey(0) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey) + val isTaprootChannel = when (channelType) { + is ChannelType.SupportedChannelType.SimpleTaprootStaging -> true + is ChannelType.SupportedChannelType.SimpleTaprootStagingZeroReserve -> true + else -> false + } + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey, isTaprootChannel) val requestFunding = open.requestFunding val willFund = when { fundingRates == null -> null @@ -70,6 +75,14 @@ data class WaitForOpenChannel( buildSet { add(ChannelTlv.ChannelTypeTlv(channelType)) willFund?.let { add(ChannelTlv.ProvideFundingTlv(it.willFund)) } + if (isTaprootChannel) add( + ChannelTlv.NextLocalNoncesTlv( + listOf( + channelKeys.verificationNonce(0, 0).second, + channelKeys.verificationNonce(0, 1).second, + ) + ) + ) } ), ) @@ -90,14 +103,24 @@ data class WaitForOpenChannel( val remoteFundingPubkey = open.fundingPubkey val dustLimit = open.dustLimit.max(localParams.dustLimit) val fundingParams = InteractiveTxParams(channelId, false, fundingAmount, open.fundingAmount, remoteFundingPubkey, open.lockTime, dustLimit, open.fundingFeerate) - when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, walletInputs, null)) { + when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, walletInputs, null, isTaprootChannel)) { is Either.Left -> { logger.error { "could not fund channel: ${fundingContributions.value}" } replyTo.complete(ChannelFundingResponse.Failure.FundingFailure(fundingContributions.value)) Pair(Aborted, listOf(ChannelAction.Message.Send(Error(temporaryChannelId, ChannelFundingError(temporaryChannelId).message)))) } is Either.Right -> { - val interactiveTxSession = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value) + val interactiveTxSession = InteractiveTxSession( + staticParams.remoteNodeId, + channelKeys, + keyManager.swapInOnChainWallet, + fundingParams, + 0.msat, + 0.msat, + emptySet(), + fundingContributions.value, + firstRemoteNonce = open.firstRemoteNonce + ) val nextState = WaitForFundingCreated( replyTo, // If our peer asks us to pay the commit tx fees, we accept (only used in tests, as we're otherwise always the channel opener). @@ -112,6 +135,7 @@ data class WaitForOpenChannel( channelFeatures, willFund?.purchase, channelOrigin = null, + open.secondRemoteNonce ) val actions = listOf( ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 0a1767f8d..2c2f07edd 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.DeterministicWallet.hardened import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.utils.Either @@ -15,6 +16,8 @@ import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.LightningCodecs +import io.ktor.utils.io.core.* +import kotlin.random.Random interface KeyManager { @@ -66,8 +69,39 @@ interface KeyManager { val delayedPaymentBasepoint: PublicKey = delayedPaymentKey.publicKey() val revocationBasepoint: PublicKey = revocationKey.publicKey() val temporaryChannelId: ByteVector32 = (ByteVector(ByteArray(33) { 0 }) + revocationBasepoint.value).sha256() + fun nonceSeed(fundingTxIndex: Long): ByteVector32 { + val seed = Crypto.hmac512("taproot-rev-root".toByteArray(), Crypto.sha256(shaSeed.toByteArray())).byteVector32() + return Bolt3Derivation.perCommitSecret(seed, fundingTxIndex).value + } fun commitmentPoint(index: Long): PublicKey = Bolt3Derivation.perCommitPoint(shaSeed, index) fun commitmentSecret(index: Long): PrivateKey = Bolt3Derivation.perCommitSecret(shaSeed, index) + + /** + * Verification nonces are sent to our peer, and used to verify * their * signature of * our * commitment tx. + * They generated deterministically and don't need to be persisted. + * @param fundingTxIndex funding tx index + * @param commitIndex commitment index + * @return a deterministic verification nonce for a given funding and commitment index + */ + fun verificationNonce(fundingTxIndex: Long, commitIndex: Long): Pair { + val fundingPrivateKey = fundingKey(fundingTxIndex) + val sessionId = Bolt3Derivation.perCommitSecret(nonceSeed(fundingTxIndex), commitIndex).value + val nonce = Musig2.generateNonce(sessionId, fundingPrivateKey, listOf(fundingPrivateKey.publicKey())) + return nonce + } + + /** + * Signing nonces are used to sign our peer's commitment signature. They are generated on-the-fly, random (not deterministic) and + * do not need to be persisted. + * @param fundingTxIndex funding tx index + * @return a random musig2 nonce for a given funding index + */ + fun signingNonce(fundingTxIndex: Long): Pair { + val fundingPrivateKey = fundingKey(fundingTxIndex) + val sessionId = Random.nextBytes(32).byteVector32() + val nonce = Musig2.generateNonce(sessionId, fundingPrivateKey, listOf(fundingPrivateKey.publicKey())) + return nonce + } } data class Bip84OnChainKeys( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt b/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt index 20ba6aac0..657c2c3f4 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt @@ -27,6 +27,7 @@ JsonSerializers.KeyPathSerializer::class, JsonSerializers.SatoshiSerializer::class, JsonSerializers.MilliSatoshiSerializer::class, + JsonSerializers.PartialSignatureWithNonceSerializer::class, JsonSerializers.CltvExpirySerializer::class, JsonSerializers.CltvExpiryDeltaSerializer::class, JsonSerializers.FeeratePerKwSerializer::class, @@ -41,6 +42,8 @@ JsonSerializers.TransactionSerializer::class, JsonSerializers.OutPointSerializer::class, JsonSerializers.TxOutSerializer::class, + JsonSerializers.IndividualNonceSerializer::class, + JsonSerializers.SecretNonceSerializer::class, JsonSerializers.ClosingTxProposedSerializer::class, JsonSerializers.LocalCommitPublishedSerializer::class, JsonSerializers.RemoteCommitPublishedSerializer::class, @@ -81,6 +84,7 @@ JsonSerializers.ChannelReadySerializer::class, JsonSerializers.ChannelReadyTlvShortChannelIdTlvSerializer::class, JsonSerializers.ClosingSignedTlvFeeRangeSerializer::class, + JsonSerializers.ClosingSignedTlvPartialSignatureSerializer::class, JsonSerializers.ShutdownTlvChannelDataSerializer::class, JsonSerializers.GenericTlvSerializer::class, JsonSerializers.TlvStreamSerializer::class, @@ -91,6 +95,7 @@ JsonSerializers.CommitSigTlvAlternativeFeerateSigSerializer::class, JsonSerializers.CommitSigTlvAlternativeFeerateSigsSerializer::class, JsonSerializers.CommitSigTlvBatchSerializer::class, + JsonSerializers.CommitSigTlvPartialSignatureWithNonceSerializer::class, JsonSerializers.CommitSigTlvSerializer::class, JsonSerializers.UUIDSerializer::class, JsonSerializers.ClosingSerializer::class, @@ -108,6 +113,8 @@ package fr.acinq.lightning.json import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -205,10 +212,14 @@ object JsonSerializers { } polymorphic(Tlv::class) { subclass(ChannelReadyTlv.ShortChannelIdTlv::class, ChannelReadyTlvShortChannelIdTlvSerializer) + subclass(ChannelReadyTlv.NextLocalNonceTlv::class, ChannelReadyTlvNextLocalNonceTlvSerializer) subclass(CommitSigTlv.AlternativeFeerateSigs::class, CommitSigTlvAlternativeFeerateSigsSerializer) subclass(CommitSigTlv.Batch::class, CommitSigTlvBatchSerializer) + subclass(CommitSigTlv.PartialSignatureWithNonceTlv::class, CommitSigTlvPartialSignatureWithNonceSerializer) subclass(ShutdownTlv.ChannelData::class, ShutdownTlvChannelDataSerializer) + subclass(ShutdownTlv.ShutdownNonce::class, ShutdownTlvShutdownNonceSerializer) subclass(ClosingSignedTlv.FeeRange::class, ClosingSignedTlvFeeRangeSerializer) + subclass(ClosingSignedTlv.PartialSignature::class, ClosingSignedTlvPartialSignatureSerializer) subclass(UpdateAddHtlcTlv.Blinding::class, UpdateAddHtlcTlvBlindingSerializer) } // TODO The following declarations are required because serializers for [TransactionWithInputInfo] @@ -220,6 +231,8 @@ object JsonSerializers { contextual(TransactionSerializer) contextual(ByteVectorSerializer) contextual(ByteVector32Serializer) + contextual(IndividualNonceSerializer) + contextual(SecretNonceSerializer) contextual(Bolt11InvoiceSerializer) contextual(OfferSerializer) @@ -278,6 +291,7 @@ object JsonSerializers { transform = { i -> when (i) { is SharedFundingInput.Multisig2of2 -> SharedFundingInputSurrogate(i.info.outPoint, i.info.txOut.amount) + is SharedFundingInput.Musig2Input -> SharedFundingInputSurrogate(i.info.outPoint, i.info.txOut.amount) } }, delegateSerializer = SharedFundingInputSurrogate.serializer() @@ -404,6 +418,9 @@ object JsonSerializers { object OutPointSerializer : StringSerializer({ "${it.txid}:${it.index}" }) object TransactionSerializer : StringSerializer() + object IndividualNonceSerializer : StringSerializer({ "${it.data} " }) + object SecretNonceSerializer : StringSerializer({ "" }) + @Serializer(forClass = PublishableTxs::class) object PublishableTxsSerializer @@ -524,12 +541,21 @@ object JsonSerializers { @Serializer(forClass = ChannelReadyTlv.ShortChannelIdTlv::class) object ChannelReadyTlvShortChannelIdTlvSerializer + @Serializer(forClass = ChannelReadyTlv.NextLocalNonceTlv::class) + object ChannelReadyTlvNextLocalNonceTlvSerializer + @Serializer(forClass = ClosingSignedTlv.FeeRange::class) object ClosingSignedTlvFeeRangeSerializer + @Serializer(forClass = ClosingSignedTlv.PartialSignature::class) + object ClosingSignedTlvPartialSignatureSerializer + @Serializer(forClass = ShutdownTlv.ChannelData::class) object ShutdownTlvChannelDataSerializer + @Serializer(forClass = ShutdownTlv.ShutdownNonce::class) + object ShutdownTlvShutdownNonceSerializer + @Serializer(forClass = ShutdownTlv::class) object ShutdownTlvSerializer @@ -542,6 +568,12 @@ object JsonSerializers { @Serializer(forClass = CommitSigTlv.Batch::class) object CommitSigTlvBatchSerializer + @Serializer(forClass = PartialSignatureWithNonce::class) + object PartialSignatureWithNonceSerializer + + @Serializer(forClass = CommitSigTlv.PartialSignatureWithNonceTlv::class) + object CommitSigTlvPartialSignatureWithNonceSerializer + @Serializer(forClass = CommitSigTlv::class) object CommitSigTlvSerializer diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 5492af611..214115eb0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -71,7 +71,8 @@ object Deserialization { signingSession = readInteractiveTxSigningSession(), remoteSecondPerCommitmentPoint = readPublicKey(), liquidityPurchase = readNullable { readLiquidityPurchase() }, - channelOrigin = readNullable { readChannelOrigin() } + channelOrigin = readNullable { readChannelOrigin() }, + secondRemoteNonce = null, ) private fun Input.readWaitForFundingSignedWithPushAmount(): WaitForFundingSigned { @@ -83,7 +84,7 @@ object Deserialization { val remoteSecondPerCommitmentPoint = readPublicKey() val liquidityPurchase = readNullable { readLiquidityPurchase() } val channelOrigin = readNullable { readChannelOrigin() } - return WaitForFundingSigned(channelParams, signingSession, remoteSecondPerCommitmentPoint, liquidityPurchase, channelOrigin) + return WaitForFundingSigned(channelParams, signingSession, remoteSecondPerCommitmentPoint, liquidityPurchase, channelOrigin, secondRemoteNonce = null) } private fun Input.readWaitForFundingSignedLegacy(): WaitForFundingSigned { @@ -94,7 +95,7 @@ object Deserialization { readNumber() val remoteSecondPerCommitmentPoint = readPublicKey() val channelOrigin = readNullable { readChannelOrigin() } - return WaitForFundingSigned(channelParams, signingSession, remoteSecondPerCommitmentPoint, liquidityPurchase = null, channelOrigin) + return WaitForFundingSigned(channelParams, signingSession, remoteSecondPerCommitmentPoint, liquidityPurchase = null, channelOrigin, secondRemoteNonce = null) } private fun Input.readWaitForFundingConfirmedWithPushAmount(): WaitForFundingConfirmed { @@ -240,6 +241,11 @@ object Deserialization { fundingTxIndex = readNumber(), remoteFundingPubkey = readPublicKey() ) + 0x02 -> SharedFundingInput.Musig2Input( + info = readInputInfo(), + fundingTxIndex = readNumber(), + remoteFundingPubkey = readPublicKey() + ) else -> error("unknown discriminator $discriminator for class ${SharedFundingInput::class}") } @@ -670,7 +676,10 @@ object Deserialization { lastIndex = readNullable { readNumber() } ) val remoteChannelData = EncryptedChannelData(readDelimitedByteArray().toByteVector()) - return Commitments(params, changes, active, inactive, payments, remoteNextCommitInfo, remotePerCommitmentSecrets, remoteChannelData) + val nextRemoteNonces = if (params.isTaprootChannel) { + readCollection { readPublicNonce() } + } else listOf() + return Commitments(params, changes, active, inactive, payments, remoteNextCommitInfo, remotePerCommitmentSecrets, remoteChannelData, nextRemoteNonces.toList()) } private fun Input.readDirectedHtlc(): DirectedHtlc = when (val discriminator = read()) { @@ -705,12 +714,24 @@ object Deserialization { toRemote = readNumber().msat ) - private fun Input.readInputInfo(): Transactions.InputInfo = Transactions.InputInfo( - outPoint = readOutPoint(), - txOut = TxOut.read(readDelimitedByteArray()), - redeemScript = readDelimitedByteArray().toByteVector() + private fun Input.readScriptTree(): ScriptTree = ScriptTree.read(ByteArrayInput(readDelimitedByteArray())) + + private fun Input.readScriptTreeAndInternalKey(): Transactions.ScriptTreeAndInternalKey = Transactions.ScriptTreeAndInternalKey( + readScriptTree(), + XonlyPublicKey(readByteVector32()) ) + private fun Input.readInputInfo(): Transactions.InputInfo { + val outPoint = readOutPoint() + val txOut = TxOut.read(readDelimitedByteArray()) + val redeemScript = readDelimitedByteArray().toByteVector() + val scriptTreeAndInternalKey = when (redeemScript.isEmpty()) { + true -> readNullable { readScriptTreeAndInternalKey() } + else -> null + } + return Transactions.InputInfo(outPoint, txOut, redeemScript, scriptTreeAndInternalKey) + } + private fun Input.readOutPoint(): OutPoint = OutPoint.read(readDelimitedByteArray()) private fun Input.readTxOut(): TxOut = TxOut.read(readDelimitedByteArray()) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index 637fdfc10..8b6675a6c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.serialization.v4 import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Output import fr.acinq.bitcoin.utils.Either @@ -234,6 +235,12 @@ object Serialization { writeNumber(i.fundingTxIndex) writePublicKey(i.remoteFundingPubkey) } + is SharedFundingInput.Musig2Input -> { + write(0x02) + writeInputInfo(i.info) + writeNumber(i.fundingTxIndex) + writePublicKey(i.remoteFundingPubkey) + } } private fun Output.writeInteractiveTxParams(o: InteractiveTxParams) = o.run { @@ -611,6 +618,9 @@ object Serialization { writeNullable(lastIndex) { writeNumber(it) } } writeDelimited(remoteChannelData.data.toByteArray()) + if (o.isTaprootChannel) { + writeCollection(o.nextRemoteNonces) { writePublicNonce(it) } + } } private fun Output.writeDirectedHtlc(htlc: DirectedHtlc) = htlc.run { @@ -642,10 +652,22 @@ object Serialization { writeNumber(toRemote.toLong()) } + private fun Output.writeScriptTree(tree: ScriptTree): Unit = tree.run { + writeDelimited(this.write()) + } + + private fun Output.writeScriptTreeAndInternalKey(scriptTreeAndInternalKey: Transactions.ScriptTreeAndInternalKey): Unit = scriptTreeAndInternalKey.run { + writeScriptTree(scriptTree) + writeByteVector32(internalKey.value) + } + private fun Output.writeInputInfo(o: Transactions.InputInfo): Unit = o.run { writeBtcObject(outPoint) writeBtcObject(txOut) writeDelimited(redeemScript.toByteArray()) + if (redeemScript.isEmpty()) { + writeNullable(scriptTreeAndInternalKey) { writeScriptTreeAndInternalKey(it) } + } } private fun Output.writeTransactionWithInputInfo(o: Transactions.TransactionWithInputInfo) { @@ -715,6 +737,8 @@ object Serialization { private fun Output.writeTxId(o: TxId) = write(o.value.toByteArray()) + private fun Output.writePublicNonce(o: IndividualNonce) = write(o.toByteArray()) + private fun Output.writeDelimited(o: ByteArray) { writeNumber(o.size) write(o) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt index 985d09dec..f456ac08d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt @@ -2,8 +2,10 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* import fr.acinq.bitcoin.ScriptEltMapping.code2elt +import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.transactions.Transactions.NUMS_POINT import fr.acinq.lightning.utils.sat /** @@ -30,6 +32,12 @@ object Scripts { ScriptWitness(listOf(ByteVector.empty, der(sig2, SigHash.SIGHASH_ALL), der(sig1, SigHash.SIGHASH_ALL), ByteVector(Script.write(multiSig2of2(pubkey1, pubkey2))))) } + fun sort(pubkeys: List): List = pubkeys.sortedWith { a, b -> LexicographicalOrdering.compare(a, b) } + + fun musig2Aggregate(pubkey1: PublicKey, pubkey2: PublicKey): XonlyPublicKey = Musig2.aggregateKeys(sort(listOf(pubkey1, pubkey2))) + + fun musig2FundingScript(pubkey1: PublicKey, pubkey2: PublicKey): List = Script.pay2tr(musig2Aggregate(pubkey1, pubkey2), null as ByteVector32?) + /** * minimal encoding of a number into a script element: * - OP_0 to OP_16 if 0 <= n <= 16 @@ -241,4 +249,112 @@ object Scripts { fun witnessHtlcWithRevocationSig(revocationSig: ByteVector64, revocationPubkey: PublicKey, htlcScript: ByteVector) = ScriptWitness(listOf(der(revocationSig, SigHash.SIGHASH_ALL), revocationPubkey.value, htlcScript)) + /** + * Specific scripts for taproot channels + */ + object Taproot { + val anchorScript: List = listOf(OP_16, OP_CHECKSEQUENCEVERIFY) + + val anchorScriptTree = ScriptTree.Leaf(anchorScript) + + fun toRevokeScript(revocationPubkey: PublicKey, localDelayedPaymentPubkey: PublicKey) = + listOf(OP_PUSHDATA(localDelayedPaymentPubkey.xOnly()), OP_DROP, OP_PUSHDATA(revocationPubkey.xOnly()), OP_CHECKSIG) + + fun toDelayScript(localDelayedPaymentPubkey: PublicKey, toLocalDelay: CltvExpiryDelta) = + listOf(OP_PUSHDATA(localDelayedPaymentPubkey.xOnly()), OP_CHECKSIG, encodeNumber(toLocalDelay.toLong()), OP_CHECKSEQUENCEVERIFY, OP_DROP) + + /** + * Taproot channels to-local key, used for the delayed to-local output + * + * @param revocationPubkey revocation key + * @param toSelfDelay self CsV delay + * @param localDelayedPaymentPubkey local delayed payment key + * @return an (XonlyPubkey, Parity) pair + */ + fun toLocalKey(revocationPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey): Pair { + val revokeScript = toRevokeScript(revocationPubkey, localDelayedPaymentPubkey) + val delayScript = toDelayScript(localDelayedPaymentPubkey, toSelfDelay) + val scriptTree = ScriptTree.Branch( + ScriptTree.Leaf(delayScript), + ScriptTree.Leaf(revokeScript), + ) + return XonlyPublicKey(NUMS_POINT).outputKey(Crypto.TaprootTweak.ScriptTweak(scriptTree)) + } + + /** + * + * @param revocationPubkey revocation key + * @param toSelfDelay to-self CSV delay + * @param localDelayedPaymentPubkey local delayed payment key + * @return a script tree with two leaves (to self with delay, and to revocation key) + */ + fun toLocalScriptTree(revocationPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey): ScriptTree.Branch { + return ScriptTree.Branch( + ScriptTree.Leaf(toDelayScript(localDelayedPaymentPubkey, toSelfDelay)), + ScriptTree.Leaf(toRevokeScript(revocationPubkey, localDelayedPaymentPubkey)), + ) + } + + fun toRemoteScript(remotePaymentPubkey: PublicKey) = listOf(OP_PUSHDATA(remotePaymentPubkey.xOnly()), OP_CHECKSIG, OP_1, OP_CHECKSEQUENCEVERIFY, OP_DROP) + + /** + * taproot channel to-remote key, used for the to-remote output + * + * @param remotePaymentPubkey remote key + * @return a (XonlyPubkey, Parity) pair + */ + fun toRemoteKey(remotePaymentPubkey: PublicKey): Pair { + val remoteScript = toRemoteScript(remotePaymentPubkey) + val scriptTree = ScriptTree.Leaf(remoteScript) + return XonlyPublicKey(NUMS_POINT).outputKey(scriptTree) + } + + /** + * + * @param remotePaymentPubkey remote key + * @return a script tree with a single leaf (to remote key, with a 1-block CSV delay) + */ + fun toRemoteScriptTree(remotePaymentPubkey: PublicKey) = ScriptTree.Leaf(toRemoteScript(remotePaymentPubkey)) + + fun offeredHtlcTimeoutScript(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey) = listOf(OP_PUSHDATA(localHtlcPubkey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(remoteHtlcPubkey.xOnly()), OP_CHECKSIG) + + fun offeredHtlcSuccessScript(remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32) = listOf( + // @formatter:off + OP_SIZE, encodeNumber(32), OP_EQUALVERIFY, + OP_HASH160, OP_PUSHDATA(Crypto.ripemd160(paymentHash)), OP_EQUALVERIFY, + OP_PUSHDATA(remoteHtlcPubkey.xOnly()), OP_CHECKSIG, + OP_1, OP_CHECKSEQUENCEVERIFY, OP_DROP + // @formatter:on + ) + + fun offeredHtlcTree(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32) = + ScriptTree.Branch( + ScriptTree.Leaf(offeredHtlcTimeoutScript(localHtlcPubkey, remoteHtlcPubkey)), + ScriptTree.Leaf(offeredHtlcSuccessScript(remoteHtlcPubkey, paymentHash)) + ) + + fun receivedHtlcTimeoutScript(remoteHtlcPubkey: PublicKey, lockTime: CltvExpiry) = listOf( + // @formatter:off + OP_PUSHDATA(remoteHtlcPubkey.xOnly()), OP_CHECKSIG, + OP_1, OP_CHECKSEQUENCEVERIFY, OP_DROP, + encodeNumber(lockTime.toLong()), OP_CHECKLOCKTIMEVERIFY, OP_DROP + // @formatter:on + ) + + fun receivedHtlcSuccessScript(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32) = listOf( + // @formatter:off + OP_SIZE, encodeNumber(32), OP_EQUALVERIFY, + OP_HASH160, OP_PUSHDATA(Crypto.ripemd160(paymentHash)), OP_EQUALVERIFY, + OP_PUSHDATA(localHtlcPubkey.xOnly()), OP_CHECKSIGVERIFY, + OP_PUSHDATA(remoteHtlcPubkey.xOnly()), OP_CHECKSIG + // @formatter:on + ) + + fun receivedHtlcTree(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32, lockTime: CltvExpiry): ScriptTree.Branch { + return ScriptTree.Branch( + ScriptTree.Leaf(receivedHtlcTimeoutScript(remoteHtlcPubkey, lockTime)), + ScriptTree.Leaf(receivedHtlcSuccessScript(localHtlcPubkey, remoteHtlcPubkey, paymentHash)), + ) + } + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index fe755c57b..e07d45bd3 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -18,6 +18,10 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.Musig2 +import fr.acinq.bitcoin.crypto.musig2.SecretNonce +import fr.acinq.bitcoin.utils.Either import fr.acinq.bitcoin.utils.Try import fr.acinq.bitcoin.utils.runTrying import fr.acinq.lightning.CltvExpiryDelta @@ -27,10 +31,13 @@ import fr.acinq.lightning.channel.Commitments import fr.acinq.lightning.io.* import fr.acinq.lightning.transactions.CommitmentOutput.InHtlc import fr.acinq.lightning.transactions.CommitmentOutput.OutHtlc +import fr.acinq.lightning.transactions.Scripts.Taproot +import fr.acinq.lightning.transactions.Scripts.witnessToLocalDelayedAfterDelay import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.UpdateAddHtlc import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient /** Type alias for a collection of commitment output links */ typealias TransactionsCommitmentOutputs = List> @@ -41,14 +48,31 @@ typealias TransactionsCommitmentOutputs = List) : this(outPoint, txOut, ByteVector(Script.write(redeemScript))) + init { + require(redeemScript.isEmpty() == (scriptTreeAndInternalKey != null)) + } + constructor(outPoint: OutPoint, txOut: TxOut, scriptTreeAndInternalKey: ScriptTreeAndInternalKey) : this(outPoint, txOut, ByteVector.empty, scriptTreeAndInternalKey) + constructor(outPoint: OutPoint, txOut: TxOut, redeemScript: List) : this(outPoint, txOut, ByteVector(Script.write(redeemScript)), null) } @Serializable @@ -62,6 +86,17 @@ object Transactions { return (FeeratePerKw.MinimumRelayFeeRate * vsize / 1000).sat } + open fun sign(key: PrivateKey, sigHash: Int = SigHash.SIGHASH_ALL): ByteVector64 { + val inputIndex = tx.txIn.indexOfFirst { it.outPoint == input.outPoint } + require(inputIndex >= 0) { "transaction doesn't spend the input to sign" } + return sign(tx, inputIndex, input.redeemScript.toByteArray(), input.txOut.amount, key, sigHash) + } + + open fun checkSig(sig: ByteVector64, pubKey: PublicKey, sigHash: Int = SigHash.SIGHASH_ALL): Boolean { + val data = Transaction.hashForSigning(tx, 0, input.redeemScript.toByteArray(), sigHash, input.txOut.amount, SigVersion.SIGVERSION_WITNESS_V0) + return Crypto.verifySignature(data, sig, pubKey) + } + @Serializable data class SpliceTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() @@ -78,10 +113,42 @@ object Transactions { @Contextual override val tx: Transaction, @Contextual val paymentHash: ByteVector32, override val htlcId: Long - ) : HtlcTx() + ) : HtlcTx() { + override fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + return when (input.scriptTreeAndInternalKey) { + null -> super.sign(key, sigHash) + else -> { + val branch = input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Branch + Transaction.signInputTaprootScriptPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY, branch.right.hash()) + } + } + } + + override fun checkSig(sig: ByteVector64, pubKey: PublicKey, sigHash: Int): Boolean { + return when (input.scriptTreeAndInternalKey) { + null -> super.checkSig(sig, pubKey, sigHash) + else -> { + val sighash = SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY + val branch = input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Branch + val data = Transaction.hashForSigningTaprootScriptPath(tx, inputIndex = 0, listOf(input.txOut), sighash, branch.right.hash()) + Crypto.verifySignatureSchnorr(data, sig, pubKey.xOnly()) + } + } + } + } @Serializable - data class HtlcTimeoutTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : HtlcTx() + data class HtlcTimeoutTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : HtlcTx() { + override fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + return when (val tree = input.scriptTreeAndInternalKey) { + null -> super.sign(key, sigHash) + else -> { + val branch = tree.scriptTree as ScriptTree.Branch + Transaction.signInputTaprootScriptPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY, branch.left.hash()) + } + } + } + } } @Serializable @@ -89,10 +156,30 @@ object Transactions { abstract val htlcId: Long @Serializable - data class ClaimHtlcSuccessTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : ClaimHtlcTx() + data class ClaimHtlcSuccessTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : ClaimHtlcTx() { + override fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + return when (input.scriptTreeAndInternalKey) { + null -> super.sign(key, sigHash) + else -> { + val branch = input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Branch + Transaction.signInputTaprootScriptPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_DEFAULT, branch.right.hash()) + } + } + } + } @Serializable - data class ClaimHtlcTimeoutTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : ClaimHtlcTx() + data class ClaimHtlcTimeoutTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : ClaimHtlcTx() { + override fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + return when (input.scriptTreeAndInternalKey) { + null -> super.sign(key, sigHash) + else -> { + val htlcTree = input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Branch + Transaction.signInputTaprootScriptPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_DEFAULT, htlcTree.left.hash()) + } + } + } + } } @Serializable @@ -105,23 +192,76 @@ object Transactions { } @Serializable - data class ClaimLocalDelayedOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class ClaimLocalDelayedOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + override fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + return when (input.scriptTreeAndInternalKey) { + null -> super.sign(key, sigHash) + else -> { + when (val tree = input.scriptTreeAndInternalKey.scriptTree) { + is ScriptTree.Leaf -> { + // this tx claims an HTLC delayed output + Transaction.signInputTaprootScriptPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_DEFAULT, tree.hash()) + } + + is ScriptTree.Branch -> { + // this tx claims a delayed to-local output + Transaction.signInputTaprootScriptPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_DEFAULT, tree.left.hash()) + } + } + } + } + } + } @Serializable sealed class ClaimRemoteCommitMainOutputTx : TransactionWithInputInfo() { // TODO: once we deprecate v2/v3 serialization, we can remove the class nesting. @Serializable - data class ClaimRemoteDelayedOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimRemoteCommitMainOutputTx() + data class ClaimRemoteDelayedOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimRemoteCommitMainOutputTx() { + override fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + return when (input.scriptTreeAndInternalKey) { + null -> super.sign(key, sigHash) + else -> { + val toRemoteScriptTree = input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Leaf + Transaction.signInputTaprootScriptPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_DEFAULT, toRemoteScriptTree.hash()) + } + } + } + } } @Serializable - data class MainPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class MainPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + override fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + return when (input.scriptTreeAndInternalKey) { + null -> super.sign(key, sigHash) + else -> { + val toLocalScriptTree = input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Branch + Transaction.signInputTaprootScriptPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_DEFAULT, toLocalScriptTree.right.hash()) + } + } + } + } @Serializable - data class HtlcPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class HtlcPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + override fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + return when (input.scriptTreeAndInternalKey) { + null -> super.sign(key, sigHash) + else -> Transaction.signInputTaprootKeyPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_DEFAULT, input.scriptTreeAndInternalKey.scriptTree) + } + } + } @Serializable - data class ClaimHtlcDelayedOutputPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class ClaimHtlcDelayedOutputPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + override fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + return when (input.scriptTreeAndInternalKey) { + null -> super.sign(key, sigHash) + else -> Transaction.signInputTaprootKeyPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_DEFAULT, input.scriptTreeAndInternalKey.scriptTree) + } + } + } @Serializable data class ClosingTx(override val input: InputInfo, @Contextual override val tx: Transaction, val toLocalIndex: Int?) : TransactionWithInputInfo() { @@ -163,6 +303,7 @@ object Transactions { */ // legacy swap-in. witness is 2 signatures (73 bytes) + redeem script (77 bytes) const val swapInputWeightLegacy = 392 + // musig2 swap-in. witness is a single Schnorr signature (64 bytes) const val swapInputWeight = 233 @@ -230,14 +371,18 @@ object Transactions { * If you are adding multiple fees together for example, you should always add them in MilliSatoshi and then round * down to Satoshi. */ - fun commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec): MilliSatoshi { + fun commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec, isTaprootChannel: Boolean): MilliSatoshi { val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec) val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec) - val weight = Commitments.COMMIT_WEIGHT + Commitments.HTLC_OUTPUT_WEIGHT * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) + val weight = if (isTaprootChannel) { + Commitments.COMMIT_WEIGHT_TAPROOT + Commitments.HTLC_OUTPUT_WEIGHT * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) + } else { + Commitments.COMMIT_WEIGHT + Commitments.HTLC_OUTPUT_WEIGHT * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) + } return weight2feeMsat(spec.feerate, weight) + (Commitments.ANCHOR_AMOUNT * 2).toMilliSatoshi() } - fun commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = commitTxFeeMsat(dustLimit, spec).truncateToSatoshi() + fun commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec, isTaprootChannel: Boolean): Satoshi = commitTxFeeMsat(dustLimit, spec, isTaprootChannel).truncateToSatoshi() /** * @param commitTxNumber commit tx number @@ -290,7 +435,12 @@ 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 */ - data class CommitmentOutputLink(val output: TxOut, val redeemScript: List, val commitmentOutput: T) : Comparable> { + data class CommitmentOutputLink(val output: TxOut, val redeemScript: List, val scriptTreeAndInternalKey: ScriptTreeAndInternalKey?, val commitmentOutput: T) : Comparable> { + + constructor(output: TxOut, redeemScript: List, commitmentOutput: T) : this(output, redeemScript, null, commitmentOutput) + + constructor(output: TxOut, scriptTreeAndInternalKey: ScriptTreeAndInternalKey, commitmentOutput: T) : this(output, listOf(), scriptTreeAndInternalKey, 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. @@ -317,9 +467,10 @@ object Transactions { remotePaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, - spec: CommitmentSpec + spec: CommitmentSpec, + isTaprootChannel: Boolean ): TransactionsCommitmentOutputs { - val commitFee = commitTxFee(localDustLimit, spec) + val commitFee = commitTxFee(localDustLimit, spec, isTaprootChannel) val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localPaysCommitTxFees) { Pair(spec.toLocal.truncateToSatoshi() - commitFee, spec.toRemote.truncateToSatoshi()) @@ -329,50 +480,130 @@ object Transactions { val outputs = ArrayList>() - if (toLocalAmount >= localDustLimit) outputs.add( - CommitmentOutputLink( - TxOut(toLocalAmount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))), - Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey), - CommitmentOutput.ToLocal - ) - ) + if (toLocalAmount >= localDustLimit) { + when (isTaprootChannel) { + true -> { + val toLocalScriptTree = Scripts.Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + outputs.add( + CommitmentOutputLink( + TxOut(toLocalAmount, Script.pay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree)), + ScriptTreeAndInternalKey(toLocalScriptTree, NUMS_POINT.xOnly()), + CommitmentOutput.ToLocal + ) + ) + } + + else -> outputs.add( + CommitmentOutputLink( + TxOut(toLocalAmount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))), + Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey), + CommitmentOutput.ToLocal + ) + ) + } + } if (toRemoteAmount >= localDustLimit) { - outputs.add( - CommitmentOutputLink( - TxOut(toRemoteAmount, Script.pay2wsh(Scripts.toRemoteDelayed(remotePaymentPubkey))), - Scripts.toRemoteDelayed(remotePaymentPubkey), - CommitmentOutput.ToRemote + when (isTaprootChannel) { + true -> { + val toRemoteScriptTree = Taproot.toRemoteScriptTree(remotePaymentPubkey) + outputs.add( + CommitmentOutputLink( + TxOut(toRemoteAmount, Script.pay2tr(XonlyPublicKey(NUMS_POINT), toRemoteScriptTree)), + ScriptTreeAndInternalKey(toRemoteScriptTree, NUMS_POINT.xOnly()), + CommitmentOutput.ToRemote + ) + ) + } + + else -> outputs.add( + CommitmentOutputLink( + TxOut(toRemoteAmount, Script.pay2wsh(Scripts.toRemoteDelayed(remotePaymentPubkey))), + Scripts.toRemoteDelayed(remotePaymentPubkey), + CommitmentOutput.ToRemote + ) ) - ) + + } } val untrimmedHtlcs = trimOfferedHtlcs(localDustLimit, spec).isNotEmpty() || trimReceivedHtlcs(localDustLimit, spec).isNotEmpty() - if (untrimmedHtlcs || toLocalAmount >= localDustLimit) - outputs.add( - CommitmentOutputLink( - TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2wsh(Scripts.toAnchor(localFundingPubkey))), - Scripts.toAnchor(localFundingPubkey), - CommitmentOutput.ToLocalAnchor(localFundingPubkey) + if (untrimmedHtlcs || toLocalAmount >= localDustLimit) { + when (isTaprootChannel) { + true -> { + outputs.add( + CommitmentOutputLink( + TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2tr(localDelayedPaymentPubkey.xOnly(), Scripts.Taproot.anchorScriptTree)), + Scripts.Taproot.anchorScript, + CommitmentOutput.ToLocalAnchor(localFundingPubkey) + ) + ) + } + + else -> outputs.add( + CommitmentOutputLink( + TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2wsh(Scripts.toAnchor(localFundingPubkey))), + Scripts.toAnchor(localFundingPubkey), + CommitmentOutput.ToLocalAnchor(localFundingPubkey) + ) ) - ) - if (untrimmedHtlcs || toRemoteAmount >= localDustLimit) - outputs.add( - CommitmentOutputLink( - TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2wsh(Scripts.toAnchor(remoteFundingPubkey))), - Scripts.toAnchor(remoteFundingPubkey), - CommitmentOutput.ToLocalAnchor(remoteFundingPubkey) + } + } + + if (untrimmedHtlcs || toRemoteAmount >= localDustLimit) { + when (isTaprootChannel) { + true -> outputs.add( + CommitmentOutputLink( + TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2tr(remotePaymentPubkey.xOnly(), Scripts.Taproot.anchorScriptTree)), + Scripts.Taproot.anchorScript, + CommitmentOutput.ToLocalAnchor(remoteFundingPubkey) + ) ) - ) + + else -> outputs.add( + CommitmentOutputLink( + TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2wsh(Scripts.toAnchor(remoteFundingPubkey))), + Scripts.toAnchor(remoteFundingPubkey), + CommitmentOutput.ToLocalAnchor(remoteFundingPubkey) + ) + ) + } + } trimOfferedHtlcs(localDustLimit, spec).forEach { htlc -> - val redeemScript = Scripts.htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray())) - outputs.add(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) + when (isTaprootChannel) { + true -> { + val offeredHtlcTree = Scripts.Taproot.offeredHtlcTree(localHtlcPubkey, remoteHtlcPubkey, htlc.add.paymentHash) + outputs.add( + CommitmentOutputLink( + TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2tr(localRevocationPubkey.xOnly(), offeredHtlcTree)), ScriptTreeAndInternalKey(offeredHtlcTree, localRevocationPubkey.xOnly()), OutHtlc(htlc) + ) + ) + } + + else -> { + val redeemScript = Scripts.htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray())) + outputs.add(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) + } + } } trimReceivedHtlcs(localDustLimit, spec).forEach { htlc -> - val redeemScript = Scripts.htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray()), htlc.add.cltvExpiry) - outputs.add(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) + when (isTaprootChannel) { + true -> { + val receivedHtlcTree = Scripts.Taproot.receivedHtlcTree(localHtlcPubkey, remoteHtlcPubkey, htlc.add.paymentHash, htlc.add.cltvExpiry) + outputs.add( + CommitmentOutputLink( + TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2tr(localRevocationPubkey.xOnly(), receivedHtlcTree)), ScriptTreeAndInternalKey(receivedHtlcTree, localRevocationPubkey.xOnly()), InHtlc(htlc) + ) + ) + } + + else -> { + val redeemScript = Scripts.htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray()), htlc.add.cltvExpiry) + outputs.add(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) + } + } } return outputs.apply { sort() } @@ -400,8 +631,14 @@ object Transactions { } sealed class TxResult { - data class Skipped(val why: TxGenerationSkipped) : TxResult() - data class Success(val result: T) : TxResult() + abstract fun map(f: (T) -> R): TxResult + + data class Skipped(val why: TxGenerationSkipped) : TxResult() { + override fun map(f: (T) -> R): TxResult = Skipped(why) + } + data class Success(val result: T) : TxResult() { + override fun map(f: (T) -> R): TxResult = Success(f(result)) + } } private fun makeHtlcTimeoutTx( @@ -412,7 +649,8 @@ object Transactions { localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, - feerate: FeeratePerKw + feerate: FeeratePerKw, + isTaprootChannel: Boolean ): TxResult { val fee = weight2fee(feerate, Commitments.HTLC_TIMEOUT_WEIGHT) val redeemScript = output.redeemScript @@ -421,14 +659,30 @@ object Transactions { return if (amount < localDustLimit) { TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) } else { - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), - txOut = listOf(TxOut(amount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))), - lockTime = htlc.cltvExpiry.toLong() - ) - TxResult.Success(TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx(input, tx, htlc.id)) + when (isTaprootChannel) { + true -> { + val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], output.scriptTreeAndInternalKey!!) + val tree = ScriptTree.Leaf(Taproot.toDelayScript(localDelayedPaymentPubkey, toLocalDelay)) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), + txOut = listOf(TxOut(amount, Script.pay2tr(localRevocationPubkey.xOnly(), tree))), + lockTime = htlc.cltvExpiry.toLong() + ) + TxResult.Success(TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx(input, tx, htlc.id)) + } + + else -> { + val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), + txOut = listOf(TxOut(amount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))), + lockTime = htlc.cltvExpiry.toLong() + ) + TxResult.Success(TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx(input, tx, htlc.id)) + } + } } } @@ -440,7 +694,8 @@ object Transactions { localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, - feerate: FeeratePerKw + feerate: FeeratePerKw, + isTaprootChannel: Boolean ): TxResult { val fee = weight2fee(feerate, Commitments.HTLC_SUCCESS_WEIGHT) val redeemScript = output.redeemScript @@ -449,14 +704,30 @@ object Transactions { return if (amount < localDustLimit) { TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) } else { - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), - txOut = listOf(TxOut(amount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))), - lockTime = 0 - ) - TxResult.Success(TransactionWithInputInfo.HtlcTx.HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id)) + when (isTaprootChannel) { + true -> { + val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], output.scriptTreeAndInternalKey!!) + val tree = ScriptTree.Leaf(Taproot.toDelayScript(localDelayedPaymentPubkey, toLocalDelay)) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), + txOut = listOf(TxOut(amount, Script.pay2tr(localRevocationPubkey.xOnly(), tree))), + lockTime = 0 + ) + TxResult.Success(TransactionWithInputInfo.HtlcTx.HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id)) + } + + else -> { + val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), + txOut = listOf(TxOut(amount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))), + lockTime = 0 + ) + TxResult.Success(TransactionWithInputInfo.HtlcTx.HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id)) + } + } } } @@ -467,21 +738,22 @@ object Transactions { toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, feerate: FeeratePerKw, - outputs: TransactionsCommitmentOutputs + outputs: TransactionsCommitmentOutputs, + isTaprootChannel: Boolean ): List { val htlcTimeoutTxs = outputs .mapIndexedNotNull map@{ outputIndex, link -> val outHtlc = link.commitmentOutput as? OutHtlc ?: return@map null - val co = CommitmentOutputLink(link.output, link.redeemScript, outHtlc) - makeHtlcTimeoutTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feerate) + val co = CommitmentOutputLink(link.output, link.redeemScript, link.scriptTreeAndInternalKey, outHtlc) + makeHtlcTimeoutTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feerate, isTaprootChannel) } .mapNotNull { (it as? TxResult.Success)?.result } val htlcSuccessTxs = outputs .mapIndexedNotNull map@{ outputIndex, link -> val inHtlc = link.commitmentOutput as? InHtlc ?: return@map null - val co = CommitmentOutputLink(link.output, link.redeemScript, inHtlc) - makeHtlcSuccessTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feerate) + val co = CommitmentOutputLink(link.output, link.redeemScript, link.scriptTreeAndInternalKey, inHtlc) + makeHtlcSuccessTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feerate, isTaprootChannel) } .mapNotNull { (it as? TxResult.Success)?.result } @@ -497,13 +769,16 @@ object Transactions { remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteArray, htlc: UpdateAddHtlc, - feerate: FeeratePerKw + feerate: FeeratePerKw, ): TxResult { val redeemScript = Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(htlc.paymentHash)) return outputs.withIndex() .firstOrNull { (it.value.commitmentOutput as? OutHtlc)?.outgoingHtlc?.add?.id == htlc.id } ?.let { (outputIndex, _) -> - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + val input = when (val tree = outputs[outputIndex].scriptTreeAndInternalKey) { + null -> InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + else -> InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], tree) + } val tx = Transaction( version = 2, txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), @@ -532,13 +807,16 @@ object Transactions { remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteArray, htlc: UpdateAddHtlc, - feerate: FeeratePerKw + feerate: FeeratePerKw, ): TxResult { val redeemScript = Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(htlc.paymentHash), htlc.cltvExpiry) return outputs.withIndex() .firstOrNull { (it.value.commitmentOutput as? InHtlc)?.incomingHtlc?.add?.id == htlc.id } ?.let { (outputIndex, _) -> - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + val input = when (val tree = outputs[outputIndex].scriptTreeAndInternalKey) { + null -> InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + else -> InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], tree) + } // unsigned tx val tx = Transaction( version = 2, @@ -563,16 +841,35 @@ object Transactions { commitTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, - feerate: FeeratePerKw + feerate: FeeratePerKw, + isTaprootChannel: Boolean ): TxResult { - val redeemScript = Scripts.toRemoteDelayed(localPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) + + + val (redeemScript, pubkeyScript, scriptTree_opt) = when(isTaprootChannel) { + true -> { + val toRemoteTree = Taproot.toRemoteScriptTree(localPaymentPubkey) + Triple( + Taproot.toRemoteScript(localPaymentPubkey), + Script.write(Script.pay2tr(XonlyPublicKey(NUMS_POINT), toRemoteTree)), + ScriptTreeAndInternalKey(toRemoteTree, NUMS_POINT.xOnly()) + ) + } + + else -> { + val redeemScript = Scripts.toRemoteDelayed(localPaymentPubkey) + Triple(redeemScript, Script.write(Script.pay2wsh(redeemScript)), null) + } + } return when (val pubkeyScriptIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)) { is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) is TxResult.Success -> { val outputIndex = pubkeyScriptIndex.result - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + val input = when (isTaprootChannel) { + true -> InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], scriptTree_opt!!) + else -> InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + } // unsigned transaction val tx = Transaction( version = 2, @@ -594,6 +891,52 @@ object Transactions { } } +fun makeHtlcDelayedTx(htlcTx: Transaction, + localDustLimit: Satoshi, + localRevocationPubkey: PublicKey, + toLocalDelay: CltvExpiryDelta, + localDelayedPaymentPubkey: PublicKey, + localFinalScriptPubKey: ByteArray, + feeratePerKw: FeeratePerKw, + isTaprootChannel: Boolean +): TxResult { + return when(isTaprootChannel) { + true -> { + val htlcTxTree = ScriptTree.Leaf(Taproot.toDelayScript(localDelayedPaymentPubkey, toLocalDelay)) + val ScriptTreeAndInternalKey = ScriptTreeAndInternalKey(htlcTxTree, localRevocationPubkey.xOnly()) + when (val pubkeyScriptIndex = findPubKeyScriptIndex(htlcTx, ScriptTreeAndInternalKey.publicKeyScript.toByteArray())) { + is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) + is TxResult.Success -> { + val outputIndex = pubkeyScriptIndex.result + val input = InputInfo(OutPoint(htlcTx, outputIndex.toLong()), htlcTx.txOut[outputIndex], ScriptTreeAndInternalKey) + // unsigned transaction + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toLong())), + txOut = listOf(TxOut(Satoshi(0), localFinalScriptPubKey)), + lockTime = 0 + ) + val weight = run { + val witness = Script.witnessScriptPathPay2tr(localRevocationPubkey.xOnly(), htlcTxTree, ScriptWitness(listOf(ByteVector64.Zeroes)), htlcTxTree) + tx.updateWitness(0, witness).weight() + } + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) + TxResult.Success(TransactionWithInputInfo.ClaimLocalDelayedOutputTx(input, tx1)) + } + } + } + } + else -> { + makeClaimLocalDelayedOutputTx(htlcTx, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localFinalScriptPubKey, feeratePerKw, isTaprootChannel) + } + } + } + fun makeClaimLocalDelayedOutputTx( delayedOutputTx: Transaction, localDustLimit: Satoshi, @@ -601,15 +944,31 @@ object Transactions { toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteArray, - feerate: FeeratePerKw + feerate: FeeratePerKw, + isTaprootChannel: Boolean ): TxResult { - val redeemScript = Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) + + val (redeemScript, pubkeyScript, scriptTree_opt) = when (isTaprootChannel) { + true -> { + val toLocalScriptTree = Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + Triple( + ByteVector.empty, + Script.write(Script.pay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree)), + ScriptTreeAndInternalKey(toLocalScriptTree, NUMS_POINT.xOnly()) + ) + } + + else -> { + val redeemScript = Script.write(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)).byteVector() + Triple(redeemScript, Script.write(Script.pay2wsh(redeemScript)), null) + } + } + return when (val pubkeyScriptIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript)) { is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) is TxResult.Success -> { val outputIndex = pubkeyScriptIndex.result - val input = InputInfo(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + val input = InputInfo(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], redeemScript, scriptTree_opt) // unsigned transaction val tx = Transaction( version = 2, @@ -618,7 +977,14 @@ object Transactions { lockTime = 0 ) // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(TransactionWithInputInfo.ClaimLocalDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() + val weight = when(isTaprootChannel) { + true -> { + val toLocalScriptTree = Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree.left as ScriptTree.Leaf, ScriptWitness(listOf(ByteVector64.Zeroes)), toLocalScriptTree) + tx.updateWitness(0, witness).weight() + } + else -> addSigs(TransactionWithInputInfo.ClaimLocalDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() + } val fee = weight2fee(feerate, weight) val amount = input.txOut.amount - fee if (amount < localDustLimit) { @@ -638,14 +1004,30 @@ object Transactions { toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteArray, - feerate: FeeratePerKw + feerate: FeeratePerKw, + isTaprootChannel: Boolean ): List> { - val redeemScript = Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) + val (redeemScript, pubkeyScript, scripTree) = when (isTaprootChannel) { + true -> { + val tree = ScriptTree.Leaf(Taproot.toDelayScript(localDelayedPaymentPubkey, toLocalDelay)) + Triple( + Taproot.toDelayScript(localDelayedPaymentPubkey, toLocalDelay), + Script.write(Script.pay2tr(localRevocationPubkey.xOnly(), tree)), + ScriptTreeAndInternalKey(tree, localRevocationPubkey.xOnly()) + ) + } + else -> { + val script = Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + Triple(script, Script.write(Script.pay2wsh(script)), null) + } + } return when (val pubkeyScriptIndexes = findPubKeyScriptIndexes(delayedOutputTx, pubkeyScript)) { is TxResult.Skipped -> listOf(TxResult.Skipped(pubkeyScriptIndexes.why)) is TxResult.Success -> pubkeyScriptIndexes.result.map { outputIndex -> - val input = InputInfo(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + val input = when (isTaprootChannel) { + true -> InputInfo(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], scripTree!!) + else -> InputInfo(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + } // unsigned transaction val tx = Transaction( version = 2, @@ -674,15 +1056,33 @@ object Transactions { localFinalScriptPubKey: ByteArray, toRemoteDelay: CltvExpiryDelta, remoteDelayedPaymentPubkey: PublicKey, - feerate: FeeratePerKw + feerate: FeeratePerKw, + isTaprootChannel: Boolean ): TxResult { - val redeemScript = Scripts.toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) + val redeemScript = when (isTaprootChannel) { + true -> Taproot.toDelayScript(remoteDelayedPaymentPubkey, toRemoteDelay) + else -> Scripts.toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) + } + val pubkeyScript = when (isTaprootChannel) { + true -> { + val toLocalScriptTree = Taproot.toLocalScriptTree(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) + Script.write(Script.pay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree)) + } + + else -> Script.write(Script.pay2wsh(redeemScript)) + } return when (val pubkeyScriptIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)) { is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) is TxResult.Success -> { val outputIndex = pubkeyScriptIndex.result - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + val input = when (isTaprootChannel) { + true -> { + val tree = ScriptTreeAndInternalKey(Taproot.toLocalScriptTree(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey), NUMS_POINT.xOnly()) + InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector.empty, tree) + } + + else -> InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) + } // unsigned transaction val tx = Transaction( version = 2, @@ -735,6 +1135,33 @@ object Transactions { } } + fun makeHtlcPenaltyTx( + commitTx: Transaction, + htlcOutputIndex: Int, + scriptTreeAndInternalKey: ScriptTreeAndInternalKey, + localDustLimit: Satoshi, + localFinalScriptPubKey: ByteArray, + feeratePerKw: FeeratePerKw): TxResult { + val input = InputInfo(OutPoint(commitTx, htlcOutputIndex.toLong()), commitTx.txOut[htlcOutputIndex], scriptTreeAndInternalKey) + // unsigned transaction + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), + txOut = listOf(TxOut(Satoshi(0), localFinalScriptPubKey)), + lockTime = 0) + // compute weight with a dummy 73 bytes signature (the largest you can get) + val weight = addSigs(TransactionWithInputInfo.MainPenaltyTx(input, tx), PlaceHolderSig).tx.weight() + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + return if (amount < localDustLimit) { + TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) + TxResult.Success(TransactionWithInputInfo.HtlcPenaltyTx(input, tx1)) + } + } + + fun makeClosingTx( commitTxInput: InputInfo, localScriptPubKey: ByteArray, @@ -806,9 +1233,40 @@ object Transactions { } fun sign(txInfo: TransactionWithInputInfo, key: PrivateKey, sigHash: Int = SigHash.SIGHASH_ALL): ByteVector64 { - val inputIndex = txInfo.tx.txIn.indexOfFirst { it.outPoint == txInfo.input.outPoint } - require(inputIndex >= 0) { "transaction doesn't spend the input to sign" } - return sign(txInfo.tx, inputIndex, txInfo.input.redeemScript.toByteArray(), txInfo.input.txOut.amount, key, sigHash) + return txInfo.sign(key, sigHash) + } + + fun partialSign( + key: PrivateKey, tx: Transaction, inputIndex: Int, spentOutputs: List, + localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, + localNonce: Pair, remoteNextLocalNonce: IndividualNonce + ): Either { + val publicKeys = Scripts.sort(listOf(localFundingPublicKey, remoteFundingPublicKey)) + return Musig2.signTaprootInput(key, tx, inputIndex, spentOutputs, publicKeys, localNonce.first, listOf(localNonce.second, remoteNextLocalNonce), null) + } + + fun partialSign( + txinfo: TransactionWithInputInfo, key: PrivateKey, + localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, + localNonce: Pair, remoteNextLocalNonce: IndividualNonce + ): Either { + val inputIndex = txinfo.tx.txIn.indexOfFirst { it.outPoint == txinfo.input.outPoint } + return partialSign(key, txinfo.tx, inputIndex, listOf(txinfo.input.txOut), localFundingPublicKey, remoteFundingPublicKey, localNonce, remoteNextLocalNonce) + } + + fun aggregatePartialSignatures( + txinfo: TransactionWithInputInfo, + localSig: ByteVector32, remoteSig: ByteVector32, + localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, + localNonce: IndividualNonce, remoteNonce: IndividualNonce + ): Either { + return Musig2.aggregateTaprootSignatures( + listOf(localSig, remoteSig), txinfo.tx, txinfo.tx.txIn.indexOfFirst { it.outPoint == txinfo.input.outPoint }, + listOf(txinfo.input.txOut), + Scripts.sort(listOf(localFundingPublicKey, remoteFundingPublicKey)), + listOf(localNonce, remoteNonce), + null + ) } fun addSigs( @@ -823,47 +1281,112 @@ object Transactions { } fun addSigs(mainPenaltyTx: TransactionWithInputInfo.MainPenaltyTx, revocationSig: ByteVector64): TransactionWithInputInfo.MainPenaltyTx { - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScript) + val witness = when (val tree = mainPenaltyTx.input.scriptTreeAndInternalKey) { + null -> Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScript) + else -> Script.witnessScriptPathPay2tr(tree.internalKey, (tree.scriptTree as ScriptTree.Branch).right as ScriptTree.Leaf, ScriptWitness(listOf(revocationSig)), tree.scriptTree) + } return mainPenaltyTx.copy(tx = mainPenaltyTx.tx.updateWitness(0, witness)) } fun addSigs(htlcPenaltyTx: TransactionWithInputInfo.HtlcPenaltyTx, revocationSig: ByteVector64, revocationPubkey: PublicKey): TransactionWithInputInfo.HtlcPenaltyTx { - val witness = Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, htlcPenaltyTx.input.redeemScript) + val witness = when (htlcPenaltyTx.input.scriptTreeAndInternalKey) { + null -> { + Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, htlcPenaltyTx.input.redeemScript) + } + else -> { + Script.witnessKeyPathPay2tr(revocationSig) + } + } return htlcPenaltyTx.copy(tx = htlcPenaltyTx.tx.updateWitness(0, witness)) } fun addSigs(htlcSuccessTx: TransactionWithInputInfo.HtlcTx.HtlcSuccessTx, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32): TransactionWithInputInfo.HtlcTx.HtlcSuccessTx { - val witness = Scripts.witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScript) + val witness = when (htlcSuccessTx.input.scriptTreeAndInternalKey) { + null -> Scripts.witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScript) + else -> { + val branch = htlcSuccessTx.input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Branch + val sigHash = (SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY).toByte() + Script.witnessScriptPathPay2tr(htlcSuccessTx.input.scriptTreeAndInternalKey.internalKey, branch.right as ScriptTree.Leaf, ScriptWitness(listOf(remoteSig.concat(sigHash), localSig.concat(sigHash), paymentPreimage)), branch) + } + } return htlcSuccessTx.copy(tx = htlcSuccessTx.tx.updateWitness(0, witness)) } fun addSigs(htlcTimeoutTx: TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx, localSig: ByteVector64, remoteSig: ByteVector64): TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx { - val witness = Scripts.witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScript) + val witness = when (htlcTimeoutTx.input.scriptTreeAndInternalKey) { + null -> Scripts.witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScript) + else -> { + val branch = htlcTimeoutTx.input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Branch + val sigHash = (SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY).toByte() + Script.witnessScriptPathPay2tr(htlcTimeoutTx.input.scriptTreeAndInternalKey.internalKey, branch.left as ScriptTree.Leaf, ScriptWitness(listOf(remoteSig.concat(sigHash), localSig.concat(sigHash))), branch) + } + } return htlcTimeoutTx.copy(tx = htlcTimeoutTx.tx.updateWitness(0, witness)) } fun addSigs(claimHtlcSuccessTx: TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx, localSig: ByteVector64, paymentPreimage: ByteVector32): TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx { - val witness = Scripts.witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, claimHtlcSuccessTx.input.redeemScript) + val witness = when (val tree = claimHtlcSuccessTx.input.scriptTreeAndInternalKey) { + null -> Scripts.witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, claimHtlcSuccessTx.input.redeemScript) + else -> Script.witnessScriptPathPay2tr(tree.internalKey, (tree.scriptTree as ScriptTree.Branch).right as ScriptTree.Leaf, ScriptWitness(listOf(localSig, paymentPreimage)), tree.scriptTree) + } + return claimHtlcSuccessTx.copy(tx = claimHtlcSuccessTx.tx.updateWitness(0, witness)) } fun addSigs(claimHtlcTimeoutTx: TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx, localSig: ByteVector64): TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx { - val witness = Scripts.witnessClaimHtlcTimeoutFromCommitTx(localSig, claimHtlcTimeoutTx.input.redeemScript) + val witness = when (val tree = claimHtlcTimeoutTx.input.scriptTreeAndInternalKey) { + null -> Scripts.witnessClaimHtlcTimeoutFromCommitTx(localSig, claimHtlcTimeoutTx.input.redeemScript) + else -> Script.witnessScriptPathPay2tr(tree.internalKey, (tree.scriptTree as ScriptTree.Branch).left as ScriptTree.Leaf, ScriptWitness(listOf(localSig)), tree.scriptTree) + + } return claimHtlcTimeoutTx.copy(tx = claimHtlcTimeoutTx.tx.updateWitness(0, witness)) } +// fun addSigs(htlcDelayedTx: TransactionWithInputInfo.HtlcDelayedTx, localSig: ByteVector64): TransactionWithInputInfo.HtlcDelayedTx { +// val witness = when (val tree = htlcDelayedTx.input.scriptTreeAndInternalKey) { +// null -> witnessToLocalDelayedAfterDelay(localSig, htlcDelayedTx.input.redeemScript) +// else -> Script.witnessScriptPathPay2tr(tree.internalKey, tree.scriptTree as ScriptTree.Leaf, ScriptWitness(listOf(localSig)), tree.scriptTree) +// } +// return htlcDelayedTx.copy(tx = htlcDelayedTx.tx.updateWitness(0, witness)) +// } + fun addSigs(claimRemoteDelayed: TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx, localSig: ByteVector64): TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx { - val witness = Scripts.witnessToRemoteDelayedAfterDelay(localSig, claimRemoteDelayed.input.redeemScript) + val witness = when (val tree = claimRemoteDelayed.input.scriptTreeAndInternalKey) { + null -> Scripts.witnessToRemoteDelayedAfterDelay(localSig, claimRemoteDelayed.input.redeemScript) + else -> { + val leaf = claimRemoteDelayed.input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Leaf + Script.witnessScriptPathPay2tr(tree.internalKey, leaf, ScriptWitness(listOf(localSig)), leaf) + } + } return claimRemoteDelayed.copy(tx = claimRemoteDelayed.tx.updateWitness(0, witness)) } fun addSigs(claimLocalDelayed: TransactionWithInputInfo.ClaimLocalDelayedOutputTx, localSig: ByteVector64): TransactionWithInputInfo.ClaimLocalDelayedOutputTx { - val witness = Scripts.witnessToLocalDelayedAfterDelay(localSig, claimLocalDelayed.input.redeemScript) + val witness = when (val tree = claimLocalDelayed.input.scriptTreeAndInternalKey) { + null -> Scripts.witnessToLocalDelayedAfterDelay(localSig, claimLocalDelayed.input.redeemScript) + else -> { + when (tree.scriptTree) { + is ScriptTree.Branch -> { + // claim a to-local delayed output + Script.witnessScriptPathPay2tr(tree.internalKey, tree.scriptTree.left as ScriptTree.Leaf, ScriptWitness(listOf(localSig)), tree.scriptTree) + } + + is ScriptTree.Leaf -> { + // claim a delayed HTLC output + Script.witnessScriptPathPay2tr(tree.internalKey, tree.scriptTree, ScriptWitness(listOf(localSig)), tree.scriptTree) + } + + } + } + } return claimLocalDelayed.copy(tx = claimLocalDelayed.tx.updateWitness(0, witness)) } fun addSigs(claimHtlcDelayedPenalty: TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx, revocationSig: ByteVector64): TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx { - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScript) + val witness = when (claimHtlcDelayedPenalty.input.scriptTreeAndInternalKey) { + null -> Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScript) + else -> Script.witnessKeyPathPay2tr(revocationSig) + } return claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness)) } @@ -872,6 +1395,14 @@ object Transactions { return closingTx.copy(tx = closingTx.tx.updateWitness(0, witness)) } + fun addAggregatedSignature(commitTx: TransactionWithInputInfo.CommitTx, aggregatedSignature: ByteVector64): TransactionWithInputInfo.CommitTx { + return commitTx.copy(tx = commitTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregatedSignature))) + } + + fun addAggregatedSignature(closingTx: TransactionWithInputInfo.ClosingTx, aggregatedSignature: ByteVector64): TransactionWithInputInfo.ClosingTx { + return closingTx.copy(tx = closingTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregatedSignature))) + } + fun checkSpendable(txinfo: TransactionWithInputInfo): Try = runTrying { txinfo.tx.correctlySpends(mapOf(txinfo.tx.txIn.first().outPoint to txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt index 227c7c5e5..0a42e9f3d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output import fr.acinq.lightning.Features @@ -8,10 +9,10 @@ import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelType -import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.utils.toByteVector -import fr.acinq.lightning.utils.toByteVector64 +import fr.acinq.lightning.utils.* +import fr.acinq.lightning.channel.Origin +import fr.acinq.lightning.channel.PartialSignatureWithNonce +import fr.acinq.lightning.wire.ChannelReadyTlv.NextLocalNonceTlv sealed class ChannelTlv : Tlv { /** Commitment to where the funds will go in case of a mutual close, which remote node will enforce in case we're compromised. */ @@ -100,6 +101,23 @@ sealed class ChannelTlv : Tlv { override fun read(input: Input): FeeCreditUsedTlv = FeeCreditUsedTlv(LightningCodecs.tu64(input).msat) } } + + data class NextLocalNoncesTlv(val nonces: List) : ChannelTlv() { + override val tag: Long get() = NextLocalNoncesTlv.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): NextLocalNoncesTlv { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return NextLocalNoncesTlv(nonces) + } + } + } } sealed class ChannelReadyTlv : Tlv { @@ -112,6 +130,19 @@ sealed class ChannelReadyTlv : Tlv { override fun read(input: Input): ShortChannelIdTlv = ShortChannelIdTlv(ShortChannelId(LightningCodecs.u64(input))) } } + + data class NextLocalNonceTlv(val nonce: IndividualNonce) : ChannelReadyTlv() { + override val tag: Long get() = NextLocalNonceTlv.tag + + override fun write(out: Output) { + LightningCodecs.writeBytes(nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): NextLocalNonceTlv = NextLocalNonceTlv(IndividualNonce(LightningCodecs.bytes(input, 66))) + } + } } sealed class CommitSigTlv : Tlv { @@ -165,6 +196,27 @@ sealed class CommitSigTlv : Tlv { override fun read(input: Input): Batch = Batch(size = LightningCodecs.tu16(input)) } } + + data class PartialSignatureWithNonceTlv(val psig: PartialSignatureWithNonce) : CommitSigTlv() { + override val tag: Long get() = PartialSignatureWithNonceTlv.tag + + override fun write(out: Output) { + LightningCodecs.writeBytes(psig.partialSig, out) + LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 2 + override fun read(input: Input): PartialSignatureWithNonceTlv { + return PartialSignatureWithNonceTlv( + PartialSignatureWithNonce( + LightningCodecs.bytes(input, 32).byteVector32(), + IndividualNonce(LightningCodecs.bytes(input, 66)) + ) + ) + } + } + } } sealed class RevokeAndAckTlv : Tlv { @@ -177,6 +229,23 @@ sealed class RevokeAndAckTlv : Tlv { override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) } } + + data class NextLocalNoncesTlv(val nonces: List) : RevokeAndAckTlv() { + override val tag: Long get() = NextLocalNoncesTlv.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): NextLocalNoncesTlv { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return NextLocalNoncesTlv(nonces) + } + } + } } sealed class ChannelReestablishTlv : Tlv { @@ -199,6 +268,23 @@ sealed class ChannelReestablishTlv : Tlv { override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) } } + + data class NextLocalNoncesTlv(val nonces: List) : ChannelReestablishTlv() { + override val tag: Long get() = NextLocalNoncesTlv.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): NextLocalNoncesTlv { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return NextLocalNoncesTlv(nonces) + } + } + } } sealed class ShutdownTlv : Tlv { @@ -211,6 +297,20 @@ sealed class ShutdownTlv : Tlv { override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) } } + + data class ShutdownNonce(val nonce: IndividualNonce) : ShutdownTlv() { + override val tag: Long get() = ShutdownNonce.tag + + override fun write(out: Output) { + LightningCodecs.writeBytes(nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 8 + + override fun read(input: Input): ShutdownNonce = ShutdownNonce(IndividualNonce(LightningCodecs.bytes(input, 66))) + } + } } sealed class ClosingSignedTlv : Tlv { @@ -237,4 +337,15 @@ sealed class ClosingSignedTlv : Tlv { override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) } } + + data class PartialSignature(val partialSignature: ByteVector32) : ClosingSignedTlv() { + override val tag: Long get() = PartialSignature.tag + + override fun write(out: Output) = LightningCodecs.writeBytes(partialSignature, out) + + companion object : TlvValueReader { + const val tag: Long = 6 + override fun read(input: Input): PartialSignature = PartialSignature(LightningCodecs.bytes(input, 32).toByteVector32()) + } + } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index b3c2e6f68..df9b29365 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt @@ -4,8 +4,10 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.lightning.channel.PartialSignatureWithNonce import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector +import fr.acinq.lightning.utils.toByteVector32 import fr.acinq.lightning.utils.toByteVector64 sealed class TxAddInputTlv : Tlv { @@ -102,6 +104,24 @@ sealed class TxSignaturesTlv : Tlv { } } + data class PreviousFundingTxPartialSig(val partialSigWithNonce: PartialSignatureWithNonce) : TxSignaturesTlv() { + override val tag: Long get() = PreviousFundingTxPartialSig.tag + override fun write(out: Output) { + LightningCodecs.writeBytes(partialSigWithNonce.partialSig.toByteArray(), out) + LightningCodecs.writeBytes(partialSigWithNonce.nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 2 + override fun read(input: Input): PreviousFundingTxPartialSig = PreviousFundingTxPartialSig( + PartialSignatureWithNonce( + LightningCodecs.bytes(input, 32).byteVector32(), + IndividualNonce(LightningCodecs.bytes(input, 66)) + ) + ) + } + } + /** Signatures from the swap user for inputs that belong to them. */ data class SwapInUserSigs(val sigs: List) : TxSignaturesTlv() { override val tag: Long get() = SwapInUserSigs.tag diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 608e0715e..484fbd3a2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -6,11 +6,15 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelFlags import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.logging.MDCLogger +import fr.acinq.lightning.channel.Origin +import fr.acinq.lightning.channel.PartialSignatureWithNonce +import fr.acinq.lightning.logging.* import fr.acinq.lightning.router.Announcements import fr.acinq.lightning.utils.* import fr.acinq.secp256k1.Hex @@ -497,6 +501,7 @@ data class TxSignatures( tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, + previousFundingPartialSig: PartialSignatureWithNonce?, swapInUserSigs: List, swapInServerSigs: List, swapInUserPartialSigs: List, @@ -508,6 +513,7 @@ data class TxSignatures( TlvStream( setOfNotNull( previousFundingSig?.let { TxSignaturesTlv.PreviousFundingTxSig(it) }, + previousFundingPartialSig?.let { TxSignaturesTlv.PreviousFundingTxPartialSig(it) }, if (swapInUserSigs.isNotEmpty()) TxSignaturesTlv.SwapInUserSigs(swapInUserSigs) else null, if (swapInServerSigs.isNotEmpty()) TxSignaturesTlv.SwapInServerSigs(swapInServerSigs) else null, if (swapInUserPartialSigs.isNotEmpty()) TxSignaturesTlv.SwapInUserPartialSigs(swapInUserPartialSigs) else null, @@ -519,6 +525,7 @@ data class TxSignatures( override val type: Long get() = TxSignatures.type val previousFundingTxSig: ByteVector64? = tlvs.get()?.sig + val previousFundingTxPartialSig: PartialSignatureWithNonce? = tlvs.get()?.partialSigWithNonce val swapInUserSigs: List = tlvs.get()?.sigs ?: listOf() val swapInServerSigs: List = tlvs.get()?.sigs ?: listOf() val swapInUserPartialSigs: List = tlvs.get()?.psigs ?: listOf() @@ -546,6 +553,7 @@ data class TxSignatures( @Suppress("UNCHECKED_CAST") val readers = mapOf( TxSignaturesTlv.PreviousFundingTxSig.tag to TxSignaturesTlv.PreviousFundingTxSig.Companion as TlvValueReader, + TxSignaturesTlv.PreviousFundingTxPartialSig.tag to TxSignaturesTlv.PreviousFundingTxPartialSig.Companion as TlvValueReader, TxSignaturesTlv.SwapInUserSigs.tag to TxSignaturesTlv.SwapInUserSigs.Companion as TlvValueReader, TxSignaturesTlv.SwapInServerSigs.tag to TxSignaturesTlv.SwapInServerSigs.Companion as TlvValueReader, TxSignaturesTlv.SwapInUserPartialSigs.tag to TxSignaturesTlv.SwapInUserPartialSigs.Companion as TlvValueReader, @@ -679,6 +687,9 @@ data class OpenDualFundedChannel( ) : ChannelMessage, HasTemporaryChannelId, HasChainHash { val channelType: ChannelType? get() = tlvStream.get()?.channelType val requestFunding: LiquidityAds.RequestFunding? get() = tlvStream.get()?.request + val nextLocalNonces: List get() = tlvStream.get()?.nonces ?: listOf() + val firstRemoteNonce: IndividualNonce? = if (nextLocalNonces.isEmpty()) null else nextLocalNonces[0] + val secondRemoteNonce: IndividualNonce? = if (nextLocalNonces.isEmpty()) null else nextLocalNonces[1] override val type: Long get() = OpenDualFundedChannel.type @@ -716,6 +727,7 @@ data class OpenDualFundedChannel( ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader, ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, ChannelTlv.RequestFundingTlv.tag to ChannelTlv.RequestFundingTlv as TlvValueReader, + ChannelTlv.NextLocalNoncesTlv.tag to ChannelTlv.NextLocalNoncesTlv.Companion as TlvValueReader, ) override fun read(input: Input): OpenDualFundedChannel { @@ -787,6 +799,9 @@ data class AcceptDualFundedChannel( val channelType: ChannelType? get() = tlvStream.get()?.channelType val willFund: LiquidityAds.WillFund? get() = tlvStream.get()?.willFund val feeCreditUsed: MilliSatoshi = tlvStream.get()?.amount ?: 0.msat + val nextLocalNonces: List get() = tlvStream.get()?.nonces ?: listOf() + val firstRemoteNonce: IndividualNonce? = if (nextLocalNonces.isEmpty()) null else nextLocalNonces[0] + val secondRemoteNonce: IndividualNonce? = if (nextLocalNonces.isEmpty()) null else nextLocalNonces[1] override val type: Long get() = AcceptDualFundedChannel.type @@ -819,6 +834,7 @@ data class AcceptDualFundedChannel( ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, ChannelTlv.ProvideFundingTlv.tag to ChannelTlv.ProvideFundingTlv as TlvValueReader, ChannelTlv.FeeCreditUsedTlv.tag to ChannelTlv.FeeCreditUsedTlv as TlvValueReader, + ChannelTlv.NextLocalNoncesTlv.tag to ChannelTlv.NextLocalNoncesTlv.Companion as TlvValueReader, ) override fun read(input: Input): AcceptDualFundedChannel = AcceptDualFundedChannel( @@ -901,6 +917,7 @@ data class ChannelReady( ) : ChannelMessage, HasChannelId { override val type: Long get() = ChannelReady.type val alias: ShortChannelId? = tlvStream.get()?.alias + val nextLocalNonce: IndividualNonce? = tlvStream.get()?.nonce override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) @@ -912,7 +929,9 @@ data class ChannelReady( const val type: Long = 36 @Suppress("UNCHECKED_CAST") - val readers = mapOf(ChannelReadyTlv.ShortChannelIdTlv.tag to ChannelReadyTlv.ShortChannelIdTlv.Companion as TlvValueReader) + val readers = mapOf( + ChannelReadyTlv.ShortChannelIdTlv.tag to ChannelReadyTlv.ShortChannelIdTlv.Companion as TlvValueReader, + ChannelReadyTlv.NextLocalNonceTlv.tag to ChannelReadyTlv.NextLocalNonceTlv.Companion as TlvValueReader) override fun read(input: Input) = ChannelReady( ByteVector32(LightningCodecs.bytes(input, 32)), @@ -956,8 +975,11 @@ data class SpliceInit( override val type: Long get() = SpliceInit.type val requireConfirmedInputs: Boolean = tlvStream.get()?.let { true } ?: false val requestFunding: LiquidityAds.RequestFunding? = tlvStream.get()?.request + val nextLocalNonces: List get() = tlvStream.get()?.nonces ?: listOf() + val firstRemoteNonce: IndividualNonce? = if (nextLocalNonces.isEmpty()) null else nextLocalNonces[0] + val secondRemoteNonce: IndividualNonce? = if (nextLocalNonces.isEmpty()) null else nextLocalNonces[1] - constructor(channelId: ByteVector32, fundingContribution: Satoshi, feerate: FeeratePerKw, lockTime: Long, fundingPubkey: PublicKey, requestFunding: LiquidityAds.RequestFunding?) : this( + constructor(channelId: ByteVector32, fundingContribution: Satoshi, feerate: FeeratePerKw, lockTime: Long, fundingPubkey: PublicKey, requestFunding: LiquidityAds.RequestFunding?, nextLocalNonces: List = listOf()) : this( channelId, fundingContribution, feerate, @@ -966,6 +988,7 @@ data class SpliceInit( TlvStream( setOfNotNull( requestFunding?.let { ChannelTlv.RequestFundingTlv(it) }, + if (nextLocalNonces.isNotEmpty()) ChannelTlv.NextLocalNoncesTlv(nextLocalNonces) else null ) ) ) @@ -986,6 +1009,7 @@ data class SpliceInit( private val readers = mapOf( ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, ChannelTlv.RequestFundingTlv.tag to ChannelTlv.RequestFundingTlv as TlvValueReader, + ChannelTlv.NextLocalNoncesTlv.tag to ChannelTlv.NextLocalNoncesTlv.Companion as TlvValueReader ) override fun read(input: Input): SpliceInit = SpliceInit( @@ -1009,14 +1033,18 @@ data class SpliceAck( val requireConfirmedInputs: Boolean = tlvStream.get()?.let { true } ?: false val willFund: LiquidityAds.WillFund? = tlvStream.get()?.willFund val feeCreditUsed: MilliSatoshi = tlvStream.get()?.amount ?: 0.msat + val nextLocalNonces: List get() = tlvStream.get()?.nonces ?: listOf() + val firstRemoteNonce: IndividualNonce? = if (nextLocalNonces.isEmpty()) null else nextLocalNonces[0] + val secondRemoteNonce: IndividualNonce? = if (nextLocalNonces.isEmpty()) null else nextLocalNonces[1] - constructor(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubkey: PublicKey, willFund: LiquidityAds.WillFund?) : this( + constructor(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubkey: PublicKey, willFund: LiquidityAds.WillFund?, nextLocalNonces: List = listOf()) : this( channelId, fundingContribution, fundingPubkey, TlvStream( setOfNotNull( - willFund?.let { ChannelTlv.ProvideFundingTlv(it) } + willFund?.let { ChannelTlv.ProvideFundingTlv(it) }, + if (nextLocalNonces.isNotEmpty()) ChannelTlv.NextLocalNoncesTlv(nextLocalNonces) else null )) ) @@ -1035,6 +1063,7 @@ data class SpliceAck( ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, ChannelTlv.ProvideFundingTlv.tag to ChannelTlv.ProvideFundingTlv as TlvValueReader, ChannelTlv.FeeCreditUsedTlv.tag to ChannelTlv.FeeCreditUsedTlv.Companion as TlvValueReader, + ChannelTlv.NextLocalNoncesTlv.tag to ChannelTlv.NextLocalNoncesTlv.Companion as TlvValueReader ) override fun read(input: Input): SpliceAck = SpliceAck( @@ -1231,6 +1260,8 @@ data class CommitSig( val alternativeFeerateSigs: List = tlvStream.get()?.sigs ?: listOf() val batchSize: Int = tlvStream.get()?.size ?: 1 + val partialSig = tlvStream.get()?.psig + val sigOrPartialSig: Either = partialSig?.let { Either.Right(it) } ?: Either.Left(signature) override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) @@ -1248,6 +1279,7 @@ data class CommitSig( CommitSigTlv.ChannelData.tag to CommitSigTlv.ChannelData.Companion as TlvValueReader, CommitSigTlv.AlternativeFeerateSigs.tag to CommitSigTlv.AlternativeFeerateSigs.Companion as TlvValueReader, CommitSigTlv.Batch.tag to CommitSigTlv.Batch.Companion as TlvValueReader, + CommitSigTlv.PartialSignatureWithNonceTlv.tag to CommitSigTlv.PartialSignatureWithNonceTlv.Companion as TlvValueReader, ) override fun read(input: Input): CommitSig { @@ -1274,6 +1306,8 @@ data class RevokeAndAck( override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): RevokeAndAck = copy(tlvStream = tlvStream.addOrUpdate(RevokeAndAckTlv.ChannelData(ecd))) + val nextLocalNonces = tlvStream.get()?.nonces ?: listOf() + override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) LightningCodecs.writeBytes(perCommitmentSecret.value, out) @@ -1285,7 +1319,10 @@ data class RevokeAndAck( const val type: Long = 133 @Suppress("UNCHECKED_CAST") - val readers = mapOf(RevokeAndAckTlv.ChannelData.tag to RevokeAndAckTlv.ChannelData.Companion as TlvValueReader) + val readers = mapOf( + RevokeAndAckTlv.ChannelData.tag to RevokeAndAckTlv.ChannelData.Companion as TlvValueReader, + RevokeAndAckTlv.NextLocalNoncesTlv.tag to RevokeAndAckTlv.NextLocalNoncesTlv.Companion as TlvValueReader + ) override fun read(input: Input): RevokeAndAck { return RevokeAndAck( @@ -1332,6 +1369,8 @@ data class ChannelReestablish( override val type: Long get() = ChannelReestablish.type val nextFundingTxId: TxId? = tlvStream.get()?.txId + val nextLocalNonces: List = tlvStream.get()?.nonces ?: listOf() + override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): ChannelReestablish = copy(tlvStream = tlvStream.addOrUpdate(ChannelReestablishTlv.ChannelData(ecd))) @@ -1351,6 +1390,7 @@ data class ChannelReestablish( val readers = mapOf( ChannelReestablishTlv.ChannelData.tag to ChannelReestablishTlv.ChannelData.Companion as TlvValueReader, ChannelReestablishTlv.NextFunding.tag to ChannelReestablishTlv.NextFunding.Companion as TlvValueReader, + ChannelReestablishTlv.NextLocalNoncesTlv.tag to ChannelReestablishTlv.NextLocalNoncesTlv.Companion as TlvValueReader, ) override fun read(input: Input): ChannelReestablish { @@ -1551,6 +1591,8 @@ data class Shutdown( override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): Shutdown = copy(tlvStream = tlvStream.addOrUpdate(ShutdownTlv.ChannelData(ecd))) + val shutdownNonce: IndividualNonce? = tlvStream.get()?.nonce + override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) LightningCodecs.writeU16(scriptPubKey.size(), out) @@ -1562,7 +1604,10 @@ data class Shutdown( const val type: Long = 38 @Suppress("UNCHECKED_CAST") - val readers = mapOf(ShutdownTlv.ChannelData.tag to ShutdownTlv.ChannelData.Companion as TlvValueReader) + val readers = mapOf( + ShutdownTlv.ChannelData.tag to ShutdownTlv.ChannelData.Companion as TlvValueReader, + ShutdownTlv.ShutdownNonce.tag to ShutdownTlv.ShutdownNonce.Companion as TlvValueReader, + ) override fun read(input: Input): Shutdown { return Shutdown( @@ -1585,6 +1630,8 @@ data class ClosingSigned( override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): ClosingSigned = copy(tlvStream = tlvStream.addOrUpdate(ClosingSignedTlv.ChannelData(ecd))) + val partialSignature = tlvStream.get()?.partialSignature + override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) LightningCodecs.writeU64(feeSatoshis.toLong(), out) @@ -1598,7 +1645,8 @@ data class ClosingSigned( @Suppress("UNCHECKED_CAST") val readers = mapOf( ClosingSignedTlv.FeeRange.tag to ClosingSignedTlv.FeeRange.Companion as TlvValueReader, - ClosingSignedTlv.ChannelData.tag to ClosingSignedTlv.ChannelData.Companion as TlvValueReader + ClosingSignedTlv.ChannelData.tag to ClosingSignedTlv.ChannelData.Companion as TlvValueReader, + ClosingSignedTlv.PartialSignature.tag to ClosingSignedTlv.PartialSignature.Companion as TlvValueReader ) override fun read(input: Input): ClosingSigned { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt index 394d67457..dfd2a6428 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt @@ -317,7 +317,7 @@ class ChannelDataTestsCommon : LightningTestSuite(), LoggingContext { companion object { private fun txInput(tx: Transaction): InputInfo { - return InputInfo(tx.txIn.first().outPoint, TxOut(0.sat, ByteVector.empty), ByteVector.empty) + return InputInfo(tx.txIn.first().outPoint, TxOut(0.sat, ByteVector.empty), Script.pay2wpkh(randomKey().publicKey())) } private fun createClosingTransactions(): Triple { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt index dae11c3c3..61a1d9442 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt @@ -66,7 +66,7 @@ class HelpersTestsCommon : LightningTestSuite() { ) fun toClosingTx(txOut: List): Transactions.TransactionWithInputInfo.ClosingTx { - val input = Transactions.InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000.sat, listOf()), listOf()) + val input = Transactions.InputInfo(OutPoint(TxId(ByteVector32.Zeroes), 0), TxOut(1000.sat, listOf()), Script.pay2wpkh(randomKey().publicKey())) return Transactions.TransactionWithInputInfo.ClosingTx(input, Transaction(2, listOf(), txOut, 0), null) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index cb266c319..d898d3cf7 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -29,8 +29,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingB = 100_000.sat val utxosB = listOf(30_000.sat, 100_000.sat) val legacyUtxosB = listOf(25_000.sat, 50_000.sat) + val isTaprootChannel = false val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 42) - assertEquals(f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), f.fundingParamsB.fundingPubkeyScript(f.channelKeysB)) + assertEquals(f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel), f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel)) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) @@ -78,7 +79,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA + fundingB }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == fundingA + fundingB }, 1) assertEquals(sharedTxA.sharedTx.localAmountIn, 215_000_000.msat) assertEquals(sharedTxA.sharedTx.remoteAmountIn, 205_000_000.msat) @@ -153,6 +154,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingB = 50_000.sat val utxosB = listOf(80_000.sat) val legacyUtxosB = listOf(30_000.sat) + val isTaprootChannel = false val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) @@ -183,7 +185,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA + fundingB }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == fundingA + fundingB }, 1) assertEquals(sharedTxA.sharedTx.totalAmountIn, 190_000.sat) assertEquals(sharedTxA.sharedTx.fees, 5130.sat) @@ -224,6 +226,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingB = 50_000.sat val utxosB = listOf(200_000.sat) val legacyUtxosB = listOf(30_000.sat) + val isTaprootChannel = false val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) @@ -253,7 +256,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA + fundingB }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == fundingA + fundingB }, 1) assertEquals(sharedTxA.sharedTx.totalAmountIn, 410_000.sat) assertEquals(sharedTxA.sharedTx.fees, 8550.sat) @@ -281,6 +284,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingA = 150_000.sat val utxosA = listOf(80_000.sat, 120_000.sat) val legacyUtxosA = listOf(30_000.sat) + val isTaprootChannel = false val f = createFixture(fundingA, utxosA, legacyUtxosA, 0.sat, listOf(), listOf(), targetFeerate, 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA) @@ -314,7 +318,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == fundingA }, 1) assertEquals(sharedTxA.sharedTx.totalAmountIn, 230_000.sat) assertEquals(sharedTxA.sharedTx.fees, 2985.sat) @@ -399,6 +403,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val balanceB = 50_000_600.msat val additionalFundingB = 20_000.sat val utxosB = listOf(80_000.sat) + val isTaprootChannel = false val f = createSpliceFixture(balanceA, additionalFundingA, utxosA, listOf(), balanceB, additionalFundingB, utxosB, listOf(), targetFeerate, 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, 200_000.sat) assertNotNull(f.fundingParamsA.sharedInput) @@ -431,7 +436,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared input and the shared output. assertEquals(listOf(inputA1, inputA2).count { it.sharedInput == f.fundingParamsA.sharedInput?.info?.outPoint }, 1) assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 200_000.sat }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == 200_000.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 129_999_400.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 70_000_600.msat) @@ -476,6 +481,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val balanceB = 90_000_300.msat val spliceOutputsB = listOf(TxOut(30_000.sat, Script.pay2wpkh(randomKey().publicKey()))) val subtractedFundingB = 30_500.sat + val isTaprootChannel = false + val f = createSpliceFixture(balanceA, -subtractedFundingA, listOf(), spliceOutputsA, balanceB, -subtractedFundingB, listOf(), spliceOutputsB, FeeratePerKw(1000.sat), 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, 108_500.sat) assertNotNull(f.fundingParamsA.sharedInput) @@ -505,7 +512,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNull(inputA.previousTx) assertEquals(inputA.sharedInput, f.fundingParamsA.sharedInput?.info?.outPoint) assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 108_500.sat }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == 108_500.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 48_999_700.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 59_500_300.msat) @@ -549,6 +556,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val balanceB = 99_999_175.msat val spliceOutputsB = listOf(25_000.sat, 15_000.sat).map { TxOut(it, Script.pay2wpkh(randomKey().publicKey())) } val subtractedFundingB = 40_500.sat + val isTaprootChannel = false + val f = createSpliceFixture(balanceA, -subtractedFundingA, listOf(), spliceOutputsA, balanceB, -subtractedFundingB, listOf(), spliceOutputsB, FeeratePerKw(1000.sat), 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, 158_500.sat) assertNotNull(f.fundingParamsA.sharedInput) @@ -585,7 +594,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared input and the shared output. assertNull(inputA.previousTx) assertEquals(inputA.sharedInput, f.fundingParamsA.sharedInput?.info?.outPoint) - assertEquals(listOf(outputA1, outputA2, outputA3, outputA4).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 158_500.sat }, 1) + assertEquals(listOf(outputA1, outputA2, outputA3, outputA4).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == 158_500.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 99_000_825.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 59_499_175.msat) @@ -627,6 +636,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val additionalFundingB = 15_000.sat val spliceOutputsB = listOf(TxOut(10_000.sat, Script.pay2wpkh(randomKey().publicKey()))) val utxosB = listOf(50_000.sat) + val isTaprootChannel = false + val f = createSpliceFixture(balanceA, additionalFundingA, utxosA, spliceOutputsA, balanceB, additionalFundingB, utxosB, spliceOutputsB, targetFeerate, 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, 290_000.sat) assertNotNull(f.fundingParamsA.sharedInput) @@ -662,7 +673,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared input and the shared output. assertEquals(listOf(inputA1, inputA2).count { it.sharedInput == f.fundingParamsA.sharedInput?.info?.outPoint }, 1) - assertEquals(listOf(outputA1, outputA2, outputA3).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 290_000.sat }, 1) + assertEquals(listOf(outputA1, outputA2, outputA3).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == 290_000.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 174_000_333.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 115_999_667.msat) @@ -990,14 +1001,15 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `multiple funding outputs`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob - val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob - val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 4, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 4, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_complete --> Bob val failure = receiveInvalidMessage(bob3, TxComplete(f.channelId)) assertIs(failure) @@ -1008,10 +1020,11 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val balanceA = 100_000_000.msat val spliceOutputA = TxOut(20_000.sat, Script.pay2wpkh(randomKey().publicKey())) val subtractedFundingA = 25_000.sat + val isTaprootChannel = false val f = createSpliceFixture(balanceA, -subtractedFundingA, listOf(), listOf(spliceOutputA), 0.msat, 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_output --> Bob - val (bob1, _) = receiveMessage(bob0, TxAddOutput(f.channelId, 0, 75_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob1, _) = receiveMessage(bob0, TxAddOutput(f.channelId, 0, 75_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, spliceOutputA.amount, spliceOutputA.publicKeyScript)) // Alice --- tx_complete --> Bob @@ -1073,12 +1086,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `invalid funding amount`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob - val failure = receiveInvalidMessage(bob1, TxAddOutput(f.channelId, 2, 100_001.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val failure = receiveInvalidMessage(bob1, TxAddOutput(f.channelId, 2, 100_001.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) assertIs(failure) assertEquals(failure.expected, 100_000.sat) assertEquals(failure.amount, 100_001.sat) @@ -1143,13 +1157,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `total input amount too low`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob - val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 4, 51_000.sat, validScript)) // Alice --- tx_complete --> Bob @@ -1159,13 +1174,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `minimum fee not met`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob - val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 4, 49_999.sat, validScript)) // Alice --- tx_complete --> Bob @@ -1176,8 +1192,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `previous attempts not double-spent`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val sharedOutput = InteractiveTxOutput.Shared(0, f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), 100_000_000.msat, 0.msat, 0.msat) + val sharedOutput = InteractiveTxOutput.Shared(0, f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel), 100_000_000.msat, 0.msat, 0.msat) val previousTx1 = Transaction(2, listOf(), listOf(TxOut(150_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) val previousTx2 = Transaction(2, listOf(), listOf(TxOut(160_000.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(200_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() @@ -1195,7 +1212,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, TxAddInput(f.channelId, 4, previousTx2, 1, 0u)) // Alice --- tx_add_output --> Bob - val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 6, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 6, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 8, 25_000.sat, validScript)) // Alice --- tx_complete --> Bob @@ -1245,14 +1262,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87") ) ) - val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, listOf(), listOf(), listOf(), listOf()) + val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, null, listOf(), listOf(), listOf(), listOf()) val nonInitiatorWitness = ScriptWitness( listOf( ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484") ) ) - val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, listOf(), listOf(), listOf(), listOf()) + val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, null, listOf(), listOf(), listOf(), listOf()) val initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs, null) assertEquals(initiatorSignedTx.feerate, FeeratePerKw(262.sat)) val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs, null) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt index 5888936e9..48ec96722 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt @@ -47,7 +47,8 @@ class RecoveryTestsCommon { TestConstants.Bob.nodeParams.dustLimit, localPaymentPoint, Script.write(Script.pay2wpkh(fundingKey)).toByteVector(), - FeeratePerKw(Satoshi(750)) + FeeratePerKw(Satoshi(750)), + isTaprootChannel = false ) return when (mainTx) { is Transactions.TxResult.Success -> { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index a962506bd..95930b450 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -121,14 +121,18 @@ data class LNChannel( is Normal -> when (state.spliceStatus) { is SpliceStatus.WaitingForSigs -> state else -> state.copy(spliceStatus = SpliceStatus.None) - } + }.updateCommitments(state.commitments.copy(closingNonce = null, pendingRemoteNextLocalNonce = null)) + + is WaitForFundingSigned -> state.copy(secondRemoteNonce = null) + is ChannelStateWithCommitments -> state.updateCommitments(state.commitments.copy(closingNonce = null, pendingRemoteNextLocalNonce = null)) else -> state } val serialized = Serialization.serialize(state) val deserialized = Serialization.deserialize(serialized).value + val filtered = removeTemporaryStatuses(state) - assertEquals(removeTemporaryStatuses(state), deserialized, "serialization error") + assertEquals(filtered, deserialized, "serialization error") } private fun checkSerialization(actions: List) { @@ -151,14 +155,27 @@ object TestsHelper { zeroConf: Boolean = false, channelOrigin: Origin? = null ): Triple, LNChannel, OpenDualFundedChannel> { + val isTaprootChannel = when (channelType) { + is ChannelType.SupportedChannelType.SimpleTaprootStaging -> true + is ChannelType.SupportedChannelType.SimpleTaprootStagingZeroReserve -> true + else -> false + } + val aliceFeatures1 = when (isTaprootChannel) { + true -> aliceFeatures.add(Feature.SimpleTaprootStaging to FeatureSupport.Mandatory) + false -> aliceFeatures + } + val bobFeatures1 = when (isTaprootChannel) { + true -> bobFeatures.add(Feature.SimpleTaprootStaging to FeatureSupport.Mandatory) + false -> bobFeatures + } val (aliceNodeParams, bobNodeParams) = when (zeroConf) { true -> Pair( - TestConstants.Alice.nodeParams.copy(features = aliceFeatures, zeroConfPeers = setOf(TestConstants.Bob.nodeParams.nodeId)), - TestConstants.Bob.nodeParams.copy(features = bobFeatures, zeroConfPeers = setOf(TestConstants.Alice.nodeParams.nodeId)) + TestConstants.Alice.nodeParams.copy(features = aliceFeatures1, zeroConfPeers = setOf(TestConstants.Bob.nodeParams.nodeId)), + TestConstants.Bob.nodeParams.copy(features = bobFeatures1, zeroConfPeers = setOf(TestConstants.Alice.nodeParams.nodeId)) ) false -> Pair( - TestConstants.Alice.nodeParams.copy(features = aliceFeatures), - TestConstants.Bob.nodeParams.copy(features = bobFeatures) + TestConstants.Alice.nodeParams.copy(features = aliceFeatures1), + TestConstants.Bob.nodeParams.copy(features = bobFeatures1) ) } val alice = LNChannel( @@ -181,10 +198,10 @@ object TestsHelper { ) val channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = requestRemoteFunding != null) - val aliceChannelParams = TestConstants.Alice.channelParams(payCommitTxFees = !channelFlags.nonInitiatorPaysCommitFees).copy(features = aliceFeatures.initFeatures()) - val bobChannelParams = TestConstants.Bob.channelParams(payCommitTxFees = channelFlags.nonInitiatorPaysCommitFees).copy(features = bobFeatures.initFeatures()) - val aliceInit = Init(aliceFeatures) - val bobInit = Init(bobFeatures) + val aliceChannelParams = TestConstants.Alice.channelParams(payCommitTxFees = !channelFlags.nonInitiatorPaysCommitFees).copy(features = aliceFeatures1.initFeatures()) + val bobChannelParams = TestConstants.Bob.channelParams(payCommitTxFees = channelFlags.nonInitiatorPaysCommitFees).copy(features = bobFeatures1.initFeatures()) + val aliceInit = Init(aliceFeatures1) + val bobInit = Init(bobFeatures1) val (alice1, actionsAlice1) = alice.process( ChannelCommand.Init.Initiator( CompletableDeferred(), diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt index 08c3e527d..37d45af78 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt @@ -107,6 +107,17 @@ class ClosingTestsCommon : LightningTestSuite() { assertContains(actions1, ChannelAction.Storage.SetLocked(mutualCloseTx.tx.txid)) } + @Test + fun `recv BITCOIN_TX_CONFIRMED -- mutual close -- simple taproot channel`() { + val (alice0, _, _) = initMutualClose(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val mutualCloseTx = alice0.state.mutualClosePublished.last() + + // actual test starts here + val (alice1, actions1) = alice0.process(ChannelCommand.WatchReceived(WatchEventConfirmed(ByteVector32.Zeroes, BITCOIN_TX_CONFIRMED(mutualCloseTx.tx), 0, 0, mutualCloseTx.tx))) + assertIs(alice1.state) + assertContains(actions1, ChannelAction.Storage.SetLocked(mutualCloseTx.tx.txid)) + } + @Test fun `recv BITCOIN_TX_CONFIRMED -- mutual close with external btc address`() { val pubKey = Lightning.randomKey().publicKey() @@ -212,6 +223,18 @@ class ClosingTestsCommon : LightningTestSuite() { assertTrue(actions1.isEmpty()) } + @Test + fun `recv BITCOIN_FUNDING_SPENT -- local commit -- simple taproot channels`() { + val (aliceNormal, _) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (aliceClosing, localCommitPublished) = localClose(aliceNormal) + + // actual test starts here + // we are notified afterwards from our watcher about the tx that we just published + val (alice1, actions1) = aliceClosing.process(ChannelCommand.WatchReceived(WatchEventSpent(aliceNormal.state.channelId, BITCOIN_FUNDING_SPENT, localCommitPublished.commitTx))) + assertEquals(aliceClosing, alice1) + assertTrue(actions1.isEmpty()) + } + @Test fun `recv BITCOIN_TX_CONFIRMED -- local commit`() { val (alice0, bob0) = reachNormal() @@ -283,9 +306,8 @@ class ClosingTestsCommon : LightningTestSuite() { actions3.find().also { assertEquals(localCommitPublished.commitTx.txid, it.txId) } } - @Test - fun `recv BITCOIN_TX_CONFIRMED -- local commit with multiple htlcs for the same payment`() { - val (alice0, bob0) = reachNormal() + fun `recv BITCOIN_TX_CONFIRMED -- local commit with multiple htlcs for the same payment -- internal`(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs) { + val (alice0, bob0) = reachNormal(channelType) // alice sends an htlc to bob val (aliceClosing, localCommitPublished) = run { val (nodes1, preimage, _) = addHtlc(30_000_000.msat, alice0, bob0) @@ -326,6 +348,16 @@ class ClosingTestsCommon : LightningTestSuite() { confirmWatchedTxs(aliceClosing, watchConfirmed) } + @Test + fun `recv BITCOIN_TX_CONFIRMED -- local commit with multiple htlcs for the same payment`() { + `recv BITCOIN_TX_CONFIRMED -- local commit with multiple htlcs for the same payment -- internal`() + } + + @Test + fun `recv BITCOIN_TX_CONFIRMED -- local commit with multiple htlcs for the same payment -- simple taproot channels`() { + `recv BITCOIN_TX_CONFIRMED -- local commit with multiple htlcs for the same payment -- internal`(ChannelType.SupportedChannelType.SimpleTaprootStaging) + } + @Test fun `recv BITCOIN_TX_CONFIRMED -- local commit with htlcs only signed by local`() { val (alice0, bob0) = reachNormal() @@ -1539,9 +1571,8 @@ class ClosingTestsCommon : LightningTestSuite() { assertTrue(addSettledFails.all { it.result is ChannelAction.HtlcResult.Fail.OnChainFail }) } - @Test - fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx`() { - val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose() + fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx -- internal`(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs) { + val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose(channelType) // bob publishes one of his revoked txs val bobRevokedTx = bobCommitTxs[2] @@ -1646,6 +1677,16 @@ class ClosingTestsCommon : LightningTestSuite() { } } + @Test + fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx`() { + `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx -- internal`() + } + + @Test + fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx -- simple taproot channels`() { + `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx -- internal`(ChannelType.SupportedChannelType.SimpleTaprootStaging) + } + @Test fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published aggregated htlc tx`() { val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose() @@ -1763,8 +1804,8 @@ class ClosingTestsCommon : LightningTestSuite() { } companion object { - fun initMutualClose(withPayments: Boolean = false): Triple, LNChannel, List> { - val (aliceInit, bobInit) = reachNormal() + fun initMutualClose(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, withPayments: Boolean = false): Triple, LNChannel, List> { + val (aliceInit, bobInit) = reachNormal(channelType = channelType) var mutableAlice: LNChannel = aliceInit var mutableBob: LNChannel = bobInit @@ -1805,8 +1846,8 @@ class ClosingTestsCommon : LightningTestSuite() { data class RevokedCloseFixture(val alice: LNChannel, val bob: LNChannel, val bobRevokedTxs: List, val htlcsAlice: List, val htlcsBob: List) - fun prepareRevokedClose(): RevokedCloseFixture { - val (aliceInit, bobInit) = reachNormal() + fun prepareRevokedClose(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs): RevokedCloseFixture { + val (aliceInit, bobInit) = reachNormal(channelType) var mutableAlice: LNChannel = aliceInit var mutableBob: LNChannel = bobInit diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt index bce65898d..01ddf520e 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt @@ -227,7 +227,7 @@ class NormalTestsCommon : LightningTestSuite() { val (_, alice4) = crossSign(bob3, alice3) val aliceCommit = alice4.commitments.active.first().localCommit assertTrue(aliceCommit.publishableTxs.commitTx.tx.txOut.all { txOut -> txOut.amount > 0.sat }) - val aliceBalance = aliceCommit.spec.toLocal - commitTxFeeMsat(alice4.commitments.params.localParams.dustLimit, aliceCommit.spec) + val aliceBalance = aliceCommit.spec.toLocal - commitTxFeeMsat(alice4.commitments.params.localParams.dustLimit, aliceCommit.spec, alice4.commitments.isTaprootChannel) assertTrue(aliceBalance >= 0.msat) assertTrue(aliceBalance < alice4.commitments.latest.localChannelReserve) } @@ -409,6 +409,18 @@ class NormalTestsCommon : LightningTestSuite() { ) } + @Test + fun `recv UpdateAddHtlc -- simple taproot channels`() { + val (_, bob0) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val add = UpdateAddHtlc(bob0.channelId, 0, 15_000.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(bob0.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket) + val (bob1, actions1) = bob0.process(ChannelCommand.MessageReceived(add)) + assertTrue(actions1.isEmpty()) + assertEquals( + bob0.copy(state = bob0.state.copy(commitments = bob0.commitments.copy(changes = bob0.commitments.changes.copy(remoteNextHtlcId = 1, remoteChanges = bob0.commitments.changes.remoteChanges.copy(proposed = listOf(add)))))), + bob1 + ) + } + @Test fun `recv UpdateAddHtlc -- zero-reserve`() { val (alice0, _) = reachNormal(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, bobFundingAmount = 10_000.sat) @@ -807,6 +819,33 @@ class NormalTestsCommon : LightningTestSuite() { assertEquals(3, alice9.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) } + @Test + fun `recv CommitSig -- multiple htlcs in both directions -- simple taproot channels`() { + val (alice0, bob0) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (nodes1, _, _) = addHtlc(50_000_000.msat, alice0, bob0) // a->b (regular) + val (alice1, bob1) = nodes1 + val (nodes2, _, _) = addHtlc(8_000_000.msat, alice1, bob1) // a->b (regular) + val (alice2, bob2) = nodes2 + val (nodes3, _, _) = addHtlc(300_000.msat, bob2, alice2) // b->a (dust) + val (bob3, alice3) = nodes3 + val (nodes4, _, _) = addHtlc(1_000_000.msat, alice3, bob3) // a->b (regular) + val (alice4, bob4) = nodes4 + val (nodes5, _, _) = addHtlc(50_000_000.msat, bob4, alice4) // b->a (regular) + val (bob5, alice5) = nodes5 + val (nodes6, _, _) = addHtlc(500_000.msat, alice5, bob5) // a->b (dust) + val (alice6, bob6) = nodes6 + val (nodes7, _, _) = addHtlc(4_000_000.msat, bob6, alice6) // b->a (regular) + val (bob7, alice7) = nodes7 + + val (alice8, bob8) = signAndRevack(alice7, bob7) + val (_, actionsBob9) = bob8.process(ChannelCommand.Commitment.Sign) + val commitSig = actionsBob9.findOutgoingMessage() + val (alice9, _) = alice8.process(ChannelCommand.MessageReceived(commitSig)) + assertIs>(alice9) + assertEquals(1, alice9.commitments.latest.localCommit.index) + assertEquals(3, alice9.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) + } + @Test fun `recv CommitSig -- multiple htlcs in both directions -- non-initiator pays commit fees`() { val (alice0, bob0) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt index 9761b524c..d714efbbb 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt @@ -221,10 +221,12 @@ class OfflineTestsCommon : LightningTestSuite() { assertEquals(1, bob4.commitments.changes.remoteNextHtlcId) } - @Test - fun `resume htlc settlement`() { + fun resumeHtlcSettlement(isTaprootChannel: Boolean) { val (alice0, bob0, revB) = run { - val (alice0, bob0) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) + val (alice0, bob0) = when (isTaprootChannel) { + true -> TestsHelper.reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging, bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) + else -> TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) + } val (nodes1, r1, htlc1) = TestsHelper.addHtlc(15_000_000.msat, bob0, alice0) val (bob1, alice1) = TestsHelper.crossSign(nodes1.first, nodes1.second) val (bob2, alice2) = TestsHelper.fulfillHtlc(htlc1.id, r1, bob1, alice1) @@ -280,6 +282,16 @@ class OfflineTestsCommon : LightningTestSuite() { assertEquals(4, bob5.commitments.localCommitIndex) } + @Test + fun `resume htlc settlement`() { + resumeHtlcSettlement(isTaprootChannel = false) + } + + @Test + fun `resume htlc settlement -- simple taproot channels`() { + resumeHtlcSettlement(isTaprootChannel = true) + } + @Test fun `discover that we have a revoked commitment`() { val (alice, aliceOld, bob) = run { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index 9c8422ba1..9cd3df80f 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -36,12 +36,24 @@ class SpliceTestsCommon : LightningTestSuite() { spliceOut(alice, bob, 50_000.sat) } + @Test + fun `splice funds out -- simple taproot channels`() { + val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + spliceOut(alice, bob, 50_000.sat) + } + @Test fun `splice funds in`() { val (alice, bob) = reachNormal() spliceIn(alice, bob, listOf(50_000.sat)) } + @Test + fun `splice funds in -- simple taproot channels`() { + val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + spliceIn(alice, bob, listOf(50_000.sat)) + } + @Test fun `splice funds in and out with pending htlcs`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() @@ -98,6 +110,12 @@ class SpliceTestsCommon : LightningTestSuite() { spliceIn(bob, alice, listOf(50_000.sat)) } + @Test + fun `splice funds in -- non-initiator -- simple taproot channels`() { + val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + spliceIn(bob, alice, listOf(50_000.sat)) + } + @Test fun `splice funds in -- many utxos`() { val (alice, bob) = reachNormal() @@ -188,6 +206,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice to purchase inbound liquidity`() { + val isTaprootChannel = false val (alice, bob) = reachNormal() val fundingRates = LiquidityAds.WillFundRates( fundingRates = listOf(LiquidityAds.FundingRate(100_000.sat, 500_000.sat, 0, 250 /* 2.5% */, 0.sat, 1000.sat)), @@ -203,7 +222,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) val defaultSpliceAck = actionsBob2.findOutgoingMessage() assertNull(defaultSpliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey, isTaprootChannel) run { val willFund = fundingRates.validateRequest(bob.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)?.willFund assertNotNull(willFund) @@ -283,7 +302,7 @@ class SpliceTestsCommon : LightningTestSuite() { val spliceAck = actionsAlice2.hasOutgoingMessage() // We don't implement the liquidity provider side, so we must fake it. assertNull(spliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, isTaprootChannel = false) val willFund = fundingRates.validateRequest(alice.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)!!.willFund val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceAck.copy(fundingContribution = liquidityRequest.requestedAmount, tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))))) assertEquals(1, actionsBob3.size) @@ -304,7 +323,7 @@ class SpliceTestsCommon : LightningTestSuite() { val spliceAck = actionsAlice2.hasOutgoingMessage() // We don't implement the liquidity provider side, so we must fake it. assertNull(spliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, isTaprootChannel = false) val willFund = fundingRates.validateRequest(alice.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)!!.willFund val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceAck.copy(fundingContribution = liquidityRequest.requestedAmount, tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))))) assertEquals(1, actionsBob3.size) @@ -370,7 +389,7 @@ class SpliceTestsCommon : LightningTestSuite() { val spliceAck = actionsAlice2.hasOutgoingMessage() // We don't implement the liquidity provider side, so we must fake it. assertNull(spliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, isTaprootChannel = false) val willFund = fundingRates.validateRequest(alice.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)!!.willFund val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceAck.copy(fundingContribution = fundingRequest.requestedAmount, tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))))) actionsBob3.hasOutgoingMessage() @@ -625,6 +644,38 @@ class SpliceTestsCommon : LightningTestSuite() { } } + @Test + fun `use channel before splice_locked -- zero-conf -- simple taproot channels`() { + val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging, zeroConf = true) + val (alice1, bob1) = spliceOut(alice, bob, 50_000.sat) + assertEquals(alice1.commitments.active.size, 2) + assertEquals(bob1.commitments.active.size, 2) + val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! + + val (nodes2, preimage, htlc) = addHtlc(15_000_000.msat, alice1, bob1) + val (alice3, bob3) = crossSign(nodes2.first, nodes2.second, commitmentsCount = 2) + + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(SpliceLocked(alice.channelId, spliceTx.txid))) + actionsAlice4.has() + assertEquals(alice4.commitments.active.size, 1) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(SpliceLocked(bob.channelId, spliceTx.txid))) + actionsBob4.has() + assertEquals(bob4.commitments.active.size, 1) + + val (alice5, bob5) = fulfillHtlc(htlc.id, preimage, alice4, bob4) + assertIs>(alice5) + assertIs>(bob5) + val (bob6, alice6) = crossSign(bob5, alice5, commitmentsCount = 1) + listOf(bob6, alice6).forEach { node -> + assertEquals(node.commitments.active.size, 1) + assertEquals(node.commitments.inactive.size, 1) + assertEquals(node.commitments.active.first().localCommit.index, node.commitments.inactive.first().localCommit.index + 1) + assertEquals(node.commitments.active.first().remoteCommit.index, node.commitments.inactive.first().remoteCommit.index + 1) + assertTrue(node.commitments.active.first().localCommit.spec.htlcs.isEmpty()) + assertTrue(node.commitments.inactive.first().localCommit.spec.htlcs.isNotEmpty()) + } + } + @Test fun `use channel during splice_locked -- zero-conf`() { val (alice, bob) = reachNormal(zeroConf = true) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt index 965cc58b5..5056d64db 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt @@ -37,6 +37,19 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { assertEquals(actionsBob2.findWatch().txId, fundingTx.txid) } + @Test + fun `recv TxSignatures and restart -- zero conf -- simple taproot channels`() { + val (alice, _, bob, _) = init(ChannelType.SupportedChannelType.SimpleTaprootStaging, zeroConf = true) + val txSigsAlice = getFundingSigs(alice) + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(txSigsAlice)) + val fundingTx = actionsBob1.find().tx + val (bob2, actionsBob2) = LNChannel(bob1.ctx, WaitForInit).process(ChannelCommand.Init.Restore(bob1.state as PersistedChannelState)) + assertIs(bob2.state) + assertEquals(actionsBob2.size, 2) + assertEquals(actionsBob2.find().tx, fundingTx) + assertEquals(actionsBob2.findWatch().txId, fundingTx.txid) + } + @Test fun `recv TxSignatures -- duplicate`() { val (alice, _, _, _) = init() @@ -74,6 +87,25 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { assertIs(actionsBob1.find().event) } + @Test + fun `recv ChannelReady -- simple taproot channels`() { + val (alice, channelReadyAlice, bob, channelReadyBob) = init(ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(channelReadyBob)) + assertIs(alice1.state) + actionsAlice1.find().also { + assertEquals(alice.commitments.latest.fundingTxId, it.txId) + } + actionsAlice1.has() + assertIs(actionsAlice1.find().event) + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(channelReadyAlice)) + assertIs(bob1.state) + actionsBob1.find().also { + assertEquals(bob.commitments.latest.fundingTxId, it.txId) + } + actionsBob1.has() + assertIs(actionsBob1.find().event) + } + @Test fun `recv BITCOIN_FUNDING_SPENT -- remote commit`() { val (alice, _, bob, _) = init() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt index f0e069a46..268d57724 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* import fr.acinq.lightning.Feature +import fr.acinq.lightning.FeatureSupport import fr.acinq.lightning.Features import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey @@ -69,6 +70,40 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { verifyCommits(alice2.state.signingSession, bob3.state.signingSession, TestConstants.aliceFundingAmount.toMilliSatoshi(), 0.msat) } + @Test + fun `complete interactive-tx protocol -- simple taproot channels`() { + val (alice, bob, inputAlice) = init( + ChannelType.SupportedChannelType.SimpleTaprootStaging, + aliceFeatures = TestConstants.Alice.nodeParams.features.initFeatures().add(Feature.SimpleTaprootStaging to FeatureSupport.Mandatory), + bobFeatures = TestConstants.Bob.nodeParams.features.initFeatures().add(Feature.SimpleTaprootStaging to FeatureSupport.Mandatory), + bobFundingAmount = 0.sat + ) + // Alice ---- tx_add_input ----> Bob + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(inputAlice)) + // Alice <--- tx_complete ----- Bob + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(actionsBob1.findOutgoingMessage())) + // Alice ---- tx_add_output ----> Bob + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(actionsAlice1.findOutgoingMessage())) + // Alice <--- tx_complete ----- Bob + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) + // Alice ---- tx_complete ----> Bob + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) + val commitSigAlice = actionsAlice2.findOutgoingMessage() + val commitSigBob = actionsBob3.findOutgoingMessage() + assertEquals(commitSigAlice.channelId, commitSigBob.channelId) + assertTrue(commitSigAlice.htlcSignatures.isEmpty()) + assertTrue(commitSigAlice.channelData.isEmpty()) + assertTrue(commitSigBob.htlcSignatures.isEmpty()) + assertFalse(commitSigBob.channelData.isEmpty()) + actionsAlice2.has() + actionsBob3.has() + assertIs(alice2.state) + assertIs(bob3.state) + assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding, Feature.SimpleTaprootStaging))) + assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.DualFunding, Feature.SimpleTaprootStaging))) + verifyCommits(alice2.state.signingSession, bob3.state.signingSession, TestConstants.aliceFundingAmount.toMilliSatoshi(), 0.msat) + } + @Test fun `complete interactive-tx protocol -- with non-initiator contributions`() { val (alice, bob, inputAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt index c8020305b..1f214319a 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt @@ -43,6 +43,31 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { } } + @Test + fun `recv CommitSig -- simple taproot channels`() { + val (alice, commitSigAlice, bob, commitSigBob) = init(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val commitInput = alice.state.signingSession.commitInput + run { + val (_, _) = alice.process(ChannelCommand.MessageReceived(commitSigBob)) + .also { (state, actions) -> + assertIs(state.state) + assertTrue(actions.isEmpty()) + } + } + run { + val (_, _) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) + .also { (state, actions) -> + assertIs(state.state) + assertEquals(actions.size, 5) + actions.hasOutgoingMessage().also { assertFalse(it.channelData.isEmpty()) } + actions.findWatch().also { assertEquals(WatchConfirmed(state.channelId, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, 3, BITCOIN_FUNDING_DEPTHOK), it) } + actions.find().also { assertEquals(TestConstants.bobFundingAmount.toMilliSatoshi(), it.amountReceived) } + actions.has() + actions.find().also { assertEquals(ChannelEvents.Created(state.state), it.event) } + } + } + } + @Test fun `recv CommitSig -- zero conf`() { val (alice, commitSigAlice, bob, commitSigBob) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index 5742b35eb..44bbfaac1 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -1,6 +1,10 @@ package fr.acinq.lightning.serialization +import fr.acinq.bitcoin.* import fr.acinq.lightning.Feature +import fr.acinq.lightning.Lightning.randomBytes +import fr.acinq.lightning.Lightning.randomBytes32 +import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.channel.* @@ -10,6 +14,7 @@ import fr.acinq.lightning.channel.states.PersistedChannelState import fr.acinq.lightning.channel.states.SpliceTestsCommon import fr.acinq.lightning.serialization.Encryption.from import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.value @@ -161,4 +166,33 @@ class StateSerializationTestsCommon : LightningTestSuite() { } } + @Test + fun `encode taproot specific fields`() { + val (alice, _) = TestsHelper.reachNormal() + val bytes = Serialization.serialize(alice.state) + val check = Serialization.deserialize(bytes).value + assertEquals(alice.state, check) + val input = alice.commitments.active[0].localCommit.publishableTxs.commitTx.input + val scriptTree = Transactions.ScriptTreeAndInternalKey(ScriptTree.Branch(ScriptTree.Leaf(Script.pay2tr(randomKey().xOnlyPublicKey())), ScriptTree.Leaf(Script.pay2wpkh(randomKey().publicKey()))), randomKey().xOnlyPublicKey()) + val input1 = input.copy(redeemScript = ByteVector.empty, scriptTreeAndInternalKey = scriptTree) + val alice1 = alice.state.copy( + commitments = alice.commitments.copy( + active = alice.commitments.active.updated( + 0, + alice.commitments.active[0].copy( + localCommit = alice.commitments.active[0].localCommit.copy( + publishableTxs = alice.commitments.active[0].localCommit.publishableTxs.copy( + commitTx = alice.commitments.active[0].localCommit.publishableTxs.commitTx.copy( + input = input1 + ) + ) + ) + ) + ) + ) + ) + val bytes1 = Serialization.serialize(alice1) + val check1 = Serialization.deserialize(bytes1).value + assertEquals(alice1, check1) + } } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt index 2aca071fd..7b02f60e3 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt @@ -184,7 +184,8 @@ class AnchorOutputsTestsCommon { remote_payment_privkey.publicKey(), local_htlc_privkey.publicKey(), remote_htlc_privkey.publicKey(), - spec + spec, + false ) val commitTx = Transactions.makeCommitTx( commitTxInput, @@ -201,7 +202,7 @@ class AnchorOutputsTestsCommon { val txs = testCase.HtlcDescs.map { it.ResolutionTx.txid to it.ResolutionTx }.toMap() val remoteHtlcSigs = testCase.HtlcDescs.map { it.ResolutionTx.txid to ByteVector(it.RemoteSigHex) }.toMap() - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, 546.sat, local_revocation_pubkey, CltvExpiryDelta(144), local_delayedpubkey, spec.feerate, outputs) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, 546.sat, local_revocation_pubkey, CltvExpiryDelta(144), local_delayedpubkey, spec.feerate, outputs, isTaprootChannel = false) assertTrue { remoteHtlcSigs.keys.containsAll(htlcTxs.map { it.tx.txid }) } htlcTxs.forEach { htlcTx -> val localHtlcSig = Transactions.sign(htlcTx, local_htlc_privkey, SigHash.SIGHASH_ALL) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index 6163254b7..6ab30851a 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -20,7 +20,9 @@ import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.transactions.CommitmentOutput.OutHtlc import fr.acinq.lightning.transactions.Scripts.htlcOffered import fr.acinq.lightning.transactions.Scripts.htlcReceived +import fr.acinq.lightning.transactions.Scripts.musig2Aggregate import fr.acinq.lightning.transactions.Scripts.toLocalDelayed +import fr.acinq.lightning.transactions.Transactions.NUMS_POINT import fr.acinq.lightning.transactions.Transactions.PlaceHolderSig import fr.acinq.lightning.transactions.Transactions.TxGenerationSkipped.AmountBelowDustLimit import fr.acinq.lightning.transactions.Transactions.TxGenerationSkipped.OutputNotFound @@ -46,6 +48,7 @@ import fr.acinq.lightning.transactions.Transactions.makeClaimRemoteDelayedOutput import fr.acinq.lightning.transactions.Transactions.makeClosingTx import fr.acinq.lightning.transactions.Transactions.makeCommitTx import fr.acinq.lightning.transactions.Transactions.makeCommitTxOutputs +import fr.acinq.lightning.transactions.Transactions.makeHtlcDelayedTx import fr.acinq.lightning.transactions.Transactions.makeHtlcPenaltyTx import fr.acinq.lightning.transactions.Transactions.makeHtlcTxs import fr.acinq.lightning.transactions.Transactions.makeMainPenaltyTx @@ -68,7 +71,7 @@ class TransactionsTestsCommon : LightningTestSuite() { private val remotePaymentPriv = PrivateKey(randomBytes32()) private val localHtlcPriv = PrivateKey(randomBytes32()) private val remoteHtlcPriv = PrivateKey(randomBytes32()) - private val commitInput = Funding.makeFundingInputInfo(TxId(randomBytes32()), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey()) + private val commitInput = Funding.makeFundingInputInfo(TxId(randomBytes32()), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), false) private val toLocalDelay = CltvExpiryDelta(144) private val localDustLimit = 546.sat private val feerate = FeeratePerKw(22_000.sat) @@ -105,7 +108,7 @@ class TransactionsTestsCommon : LightningTestSuite() { IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000.msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket)) ) val spec = CommitmentSpec(htlcs, feerate = FeeratePerKw(5_000.sat), toLocal = 0.msat, toRemote = 0.msat) - val fee = commitTxFee(546.sat, spec) + val fee = commitTxFee(546.sat, spec, isTaprootChannel = false) assertEquals(8000.sat, fee) } @@ -116,13 +119,14 @@ class TransactionsTestsCommon : LightningTestSuite() { val toLocalDelay = CltvExpiryDelta(144) val feeratePerKw = FeeratePerKw.MinimumFeeratePerKw val blockHeight = 400_000 + val isTaprootChannel = false run { // ClaimHtlcDelayedTx // first we create a fake htlcSuccessOrTimeoutTx tx, containing only the output that will be spent by the ClaimDelayedOutputTx val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey()))) val htlcSuccessOrTimeoutTx = Transaction(version = 0, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = listOf(TxOut(20000.sat, pubKeyScript)), lockTime = 0) - val claimHtlcDelayedTx = makeClaimLocalDelayedOutputTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey(), finalPubKeyScript, feeratePerKw) + val claimHtlcDelayedTx = makeClaimLocalDelayedOutputTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey(), finalPubKeyScript, feeratePerKw, isTaprootChannel) assertTrue(claimHtlcDelayedTx is Success, "is $claimHtlcDelayedTx") // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimHtlcDelayedTx.result, PlaceHolderSig).tx) @@ -134,7 +138,7 @@ class TransactionsTestsCommon : LightningTestSuite() { // first we create a fake commitTx tx, containing only the output that will be spent by the MainPenaltyTx val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey()))) val commitTx = Transaction(version = 0, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = listOf(TxOut(20000.sat, pubKeyScript)), lockTime = 0) - val mainPenaltyTx = makeMainPenaltyTx(commitTx, localDustLimit, localRevocationPriv.publicKey(), finalPubKeyScript, toLocalDelay, localPaymentPriv.publicKey(), feeratePerKw) + val mainPenaltyTx = makeMainPenaltyTx(commitTx, localDustLimit, localRevocationPriv.publicKey(), finalPubKeyScript, toLocalDelay, localPaymentPriv.publicKey(), feeratePerKw, isTaprootChannel) assertTrue(mainPenaltyTx is Success, "is $mainPenaltyTx") // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(mainPenaltyTx.result, PlaceHolderSig).tx) @@ -174,7 +178,8 @@ class TransactionsTestsCommon : LightningTestSuite() { remotePaymentPriv.publicKey(), localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), - spec + spec, + isTaprootChannel ) val commitTx = Transaction(version = 0, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = outputs.map { it.output }, lockTime = 0) val claimHtlcSuccessTx = @@ -203,7 +208,8 @@ class TransactionsTestsCommon : LightningTestSuite() { remotePaymentPriv.publicKey(), localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), - spec + spec, + isTaprootChannel ) val commitTx = Transaction(version = 0, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = outputs.map { it.output }, lockTime = 0) val claimHtlcTimeoutTx = @@ -216,10 +222,211 @@ class TransactionsTestsCommon : LightningTestSuite() { } } + @Test + fun `build taproot transactions`() { + + // funding tx sends to musig2 aggregate of local and remote funding keys + val fundingTxOutpoint = OutPoint(TxId(randomBytes32()), 0) + val fundingOutput = TxOut(Satoshi(100000), Script.pay2tr(musig2Aggregate(localFundingPriv.publicKey(), remoteFundingPriv.publicKey()), null as ByteVector32?)) + + // to-local output script tree, with 2 leaves + val toLocalScriptTree = ScriptTree.Branch( + ScriptTree.Leaf(Scripts.Taproot.toDelayScript(localDelayedPaymentPriv.publicKey(), toLocalDelay)), + ScriptTree.Leaf(Scripts.Taproot.toRevokeScript(localRevocationPriv.publicKey(), localDelayedPaymentPriv.publicKey())), + ) + + // to-remote output script tree, with a single leaf + val toRemoteScriptTree = ScriptTree.Leaf(Scripts.Taproot.toRemoteScript(remotePaymentPriv.publicKey())) + + // offered HTLC + val preimage = ByteVector32.fromValidHex("0101010101010101010101010101010101010101010101010101010101010101") + val paymentHash = sha256(preimage).byteVector32() + val offeredHtlcTree = Scripts.Taproot.offeredHtlcTree(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), paymentHash) + val receivedHtlcTree = Scripts.Taproot.receivedHtlcTree(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), paymentHash, CltvExpiry(300)) + + val txNumber = 0x404142434445L + val (sequence, lockTime) = encodeTxNumber(txNumber) + val commitTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(fundingTxOutpoint, sequence)), + txOut = listOf( + TxOut(30000000.sat, Script.pay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree)), + TxOut(40000000.sat, Script.pay2tr(XonlyPublicKey(NUMS_POINT), toRemoteScriptTree)), + TxOut(330.sat, Script.pay2tr(localDelayedPaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree)), + TxOut(330.sat, Script.pay2tr(remotePaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree)), + TxOut(100.sat, Script.pay2tr(localRevocationPriv.xOnlyPublicKey(), offeredHtlcTree)), + TxOut(150.sat, Script.pay2tr(localRevocationPriv.xOnlyPublicKey(), receivedHtlcTree)) + ), + lockTime + ) + + val localNonce = Musig2.generateNonce(randomBytes32(), localFundingPriv, listOf(localFundingPriv.publicKey())) + val remoteNonce = Musig2.generateNonce(randomBytes32(), remoteFundingPriv, listOf(remoteFundingPriv.publicKey())) + + val localPartialSig = Musig2.signTaprootInput( + localFundingPriv, + tx, 0, listOf(fundingOutput), + Scripts.sort(listOf(localFundingPriv.publicKey(), remoteFundingPriv.publicKey())), + localNonce.first, listOf(localNonce.second, remoteNonce.second), + null + ).right!! + + val remotePartialSig = Musig2.signTaprootInput( + remoteFundingPriv, + tx, 0, listOf(fundingOutput), + Scripts.sort(listOf(localFundingPriv.publicKey(), remoteFundingPriv.publicKey())), + remoteNonce.first, listOf(localNonce.second, remoteNonce.second), + null + ).right!! + + val aggSig = Musig2.aggregateTaprootSignatures( + listOf(localPartialSig, remotePartialSig), tx, 0, + listOf(fundingOutput), + Scripts.sort(listOf(localFundingPriv.publicKey(), remoteFundingPriv.publicKey())), + listOf(localNonce.second, remoteNonce.second), + null + ).right!! + + tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggSig)) + } + Transaction.correctlySpends(commitTx, mapOf(fundingTxOutpoint to fundingOutput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey())) + + val spendToLocalOutputTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 0), sequence = toLocalDelay.toLong())), + txOut = listOf(TxOut(30000000.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, listOf(commitTx.txOut[0]), SigHash.SIGHASH_DEFAULT, toLocalScriptTree.left.hash()) + val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree.left as ScriptTree.Leaf, ScriptWitness(listOf(sig)), toLocalScriptTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendToLocalOutputTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + + val spendToRemoteOutputTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 1), sequence = 1)), + txOut = listOf(TxOut(40000000.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootScriptPath(remotePaymentPriv, tx, 0, listOf(commitTx.txOut[1]), SigHash.SIGHASH_DEFAULT, toRemoteScriptTree.hash()) + val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(NUMS_POINT), toRemoteScriptTree, ScriptWitness(listOf(sig)), toRemoteScriptTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendToRemoteOutputTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val spendLocalAnchorTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 2), sequence = TxIn.SEQUENCE_FINAL)), + txOut = listOf(TxOut(330.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootKeyPath(localDelayedPaymentPriv, tx, 0, listOf(commitTx.txOut[2]), SigHash.SIGHASH_DEFAULT, Scripts.Taproot.anchorScriptTree) + val witness = Script.witnessKeyPathPay2tr(sig) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendLocalAnchorTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val spendRemoteAnchorTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 3), listOf(), sequence = TxIn.SEQUENCE_FINAL)), + txOut = listOf(TxOut(330.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootKeyPath(remotePaymentPriv, tx, 0, listOf(commitTx.txOut[3]), SigHash.SIGHASH_DEFAULT, Scripts.Taproot.anchorScriptTree) + val witness = Script.witnessKeyPathPay2tr(sig) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendRemoteAnchorTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val mainPenaltyTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 0), sequence = TxIn.SEQUENCE_FINAL)), + txOut = listOf(TxOut(330.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootScriptPath(localRevocationPriv, tx, 0, listOf(commitTx.txOut[0]), SigHash.SIGHASH_DEFAULT, toLocalScriptTree.right.hash()) + val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree.right as ScriptTree.Leaf, ScriptWitness(listOf(sig)), toLocalScriptTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(mainPenaltyTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + // sign and spend received HTLC with HTLC-Success tx + val htlcSuccessTree = ScriptTree.Leaf(Scripts.Taproot.toDelayScript(localDelayedPaymentPriv.publicKey(), toLocalDelay)) + val htlcSuccessTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 5), sequence = 1)), + txOut = listOf(TxOut(150.sat, Script.pay2tr(localRevocationPriv.xOnlyPublicKey(), htlcSuccessTree))), + lockTime = 0 + ) + val sigHash = SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY + val localSig = Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, listOf(commitTx.txOut[5]), sigHash, receivedHtlcTree.right.hash()).toByteArray() + sigHash.toByte() + val remoteSig = Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, listOf(commitTx.txOut[5]), sigHash, receivedHtlcTree.right.hash()).toByteArray() + sigHash.toByte() + val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), receivedHtlcTree.right as ScriptTree.Leaf, ScriptWitness(listOf(remoteSig.byteVector(), localSig.byteVector(), preimage)), receivedHtlcTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(htlcSuccessTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val spendHtlcSuccessTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(htlcSuccessTx, 0), sequence = toLocalDelay.toLong())), + txOut = listOf(TxOut(150.sat, finalPubKeyScript)), + lockTime = 0 + ) + val localSig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, listOf(htlcSuccessTx.txOut[0]), SigHash.SIGHASH_DEFAULT, htlcSuccessTree.hash()) + val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), htlcSuccessTree, ScriptWitness(listOf(localSig)), htlcSuccessTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendHtlcSuccessTx, listOf(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + // sign and spend offered HTLC with HTLC-Timeout tx + val htlcTimeoutTree = htlcSuccessTree + val htlcTimeoutTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 4), sequence = TxIn.SEQUENCE_FINAL)), + txOut = listOf(TxOut(100.sat, Script.pay2tr(localRevocationPriv.xOnlyPublicKey(), htlcTimeoutTree))), + lockTime = CltvExpiry(300).toLong() + ) + val sigHash = SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY + val localSig = Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, listOf(commitTx.txOut[4]), sigHash, offeredHtlcTree.left.hash()).toByteArray() + sigHash.toByte() + val remoteSig = Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, listOf(commitTx.txOut[4]), sigHash, offeredHtlcTree.left.hash()).toByteArray() + sigHash.toByte() + val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), offeredHtlcTree.left as ScriptTree.Leaf, ScriptWitness(listOf(remoteSig.byteVector(), localSig.byteVector())), offeredHtlcTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(htlcTimeoutTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val spendHtlcTimeoutTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(htlcTimeoutTx, 0), sequence = toLocalDelay.toLong())), + txOut = listOf(TxOut(100.sat, finalPubKeyScript)), + lockTime = 0 + ) + val localSig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, listOf(htlcTimeoutTx.txOut[0]), SigHash.SIGHASH_DEFAULT, htlcTimeoutTree.hash()) + val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), htlcTimeoutTree, ScriptWitness(listOf(localSig)), htlcTimeoutTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendHtlcTimeoutTx, listOf(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + } + @Test fun `generate valid commitment and htlc transactions`() { + val isTaprootChannel = false val finalPubKeyScript = write(pay2wpkh(PrivateKey(ByteVector32("01".repeat(32))).publicKey())) - val commitInput = Funding.makeFundingInputInfo(TxId(ByteVector32("02".repeat(32))), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey()) + val commitInput = Funding.makeFundingInputInfo(TxId(ByteVector32("02".repeat(32))), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), isTaprootChannel) // htlc1 and htlc2 are regular IN/OUT htlcs val paymentPreimage1 = ByteVector32("03".repeat(32)) @@ -268,7 +475,8 @@ class TransactionsTestsCommon : LightningTestSuite() { remotePaymentPriv.publicKey(), localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), - spec + spec, + isTaprootChannel ) val commitTxNumber = 0x404142434445L @@ -286,7 +494,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val check = ((commitTx.tx.txIn.first().sequence and 0xffffffL) shl 24) or (commitTx.tx.lockTime and 0xffffffL) assertEquals(commitTxNumber, check xor num) } - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), spec.feerate, outputs) + val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), spec.feerate, outputs, isTaprootChannel) assertEquals(4, htlcTxs.size) val htlcSuccessTxs = htlcTxs.filterIsInstance() assertEquals(2, htlcSuccessTxs.size) // htlc2 and htlc4 @@ -307,13 +515,13 @@ class TransactionsTestsCommon : LightningTestSuite() { } run { // local spends delayed output of htlc1 timeout tx - val claimHtlcDelayed = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) + val claimHtlcDelayed = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) assertTrue(claimHtlcDelayed is Success, "is $claimHtlcDelayed") val localSig = sign(claimHtlcDelayed.result, localDelayedPaymentPriv) val signedTx = addSigs(claimHtlcDelayed.result, localSig) assertTrue(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit - val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey(), finalPubKeyScript, feerate) + val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) assertEquals(Skipped(OutputNotFound), claimHtlcDelayed1) } run { @@ -342,19 +550,19 @@ class TransactionsTestsCommon : LightningTestSuite() { } run { // local spends delayed output of htlc2 success tx - val claimHtlcDelayed = makeClaimLocalDelayedOutputTx(htlcSuccessTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) + val claimHtlcDelayed = makeClaimLocalDelayedOutputTx(htlcSuccessTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) assertTrue(claimHtlcDelayed is Success, "is $claimHtlcDelayed") val localSig = sign(claimHtlcDelayed.result, localDelayedPaymentPriv) val signedTx = addSigs(claimHtlcDelayed.result, localSig) val csResult = checkSpendable(signedTx) assertTrue(csResult.isSuccess, "is $csResult") // local can't claim delayed output of htlc4 timeout tx because it is below the dust limit - val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcSuccessTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) + val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcSuccessTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) assertEquals(Skipped(AmountBelowDustLimit), claimHtlcDelayed1) } run { // remote spends main output - val claimP2WPKHOutputTx = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey(), finalPubKeyScript.toByteVector(), feerate) + val claimP2WPKHOutputTx = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey(), finalPubKeyScript.toByteVector(), feerate, isTaprootChannel) assertTrue(claimP2WPKHOutputTx is Success, "is $claimP2WPKHOutputTx") val localSig = sign(claimP2WPKHOutputTx.result, remotePaymentPriv) val signedTx = addSigs(claimP2WPKHOutputTx.result, localSig) @@ -363,7 +571,8 @@ class TransactionsTestsCommon : LightningTestSuite() { } run { // remote spends htlc1's htlc-timeout tx with revocation key - val claimHtlcDelayedPenaltyTxs = makeClaimDelayedOutputPenaltyTxs(htlcTimeoutTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) + val claimHtlcDelayedPenaltyTxs = + makeClaimDelayedOutputPenaltyTxs(htlcTimeoutTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) assertEquals(1, claimHtlcDelayedPenaltyTxs.size) val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") @@ -372,7 +581,8 @@ class TransactionsTestsCommon : LightningTestSuite() { val csResult = checkSpendable(signed) assertTrue(csResult.isSuccess, "is $csResult") // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit - val claimHtlcDelayedPenaltyTxsSkipped = makeClaimDelayedOutputPenaltyTxs(htlcTimeoutTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) + val claimHtlcDelayedPenaltyTxsSkipped = + makeClaimDelayedOutputPenaltyTxs(htlcTimeoutTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) assertEquals(listOf(Skipped(AmountBelowDustLimit)), claimHtlcDelayedPenaltyTxsSkipped) } run { @@ -387,7 +597,8 @@ class TransactionsTestsCommon : LightningTestSuite() { } run { // remote spends htlc2's htlc-success tx with revocation key - val claimHtlcDelayedPenaltyTxs = makeClaimDelayedOutputPenaltyTxs(htlcSuccessTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) + val claimHtlcDelayedPenaltyTxs = + makeClaimDelayedOutputPenaltyTxs(htlcSuccessTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) assertEquals(1, claimHtlcDelayedPenaltyTxs.size) val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") @@ -396,7 +607,8 @@ class TransactionsTestsCommon : LightningTestSuite() { val csResult = checkSpendable(signed) assertTrue(csResult.isSuccess, "is $csResult") // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit - val claimHtlcDelayedPenaltyTxsSkipped = makeClaimDelayedOutputPenaltyTxs(htlcSuccessTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) + val claimHtlcDelayedPenaltyTxsSkipped = + makeClaimDelayedOutputPenaltyTxs(htlcSuccessTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) assertEquals(listOf(Skipped(AmountBelowDustLimit)), claimHtlcDelayedPenaltyTxsSkipped) } run { @@ -404,7 +616,8 @@ class TransactionsTestsCommon : LightningTestSuite() { val txIn = htlcTimeoutTxs.flatMap { it.tx.txIn } + htlcSuccessTxs.flatMap { it.tx.txIn } val txOut = htlcTimeoutTxs.flatMap { it.tx.txOut } + htlcSuccessTxs.flatMap { it.tx.txOut } val aggregatedHtlcTx = Transaction(2, txIn, txOut, 0) - val claimHtlcDelayedPenaltyTxs = makeClaimDelayedOutputPenaltyTxs(aggregatedHtlcTx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate) + val claimHtlcDelayedPenaltyTxs = + makeClaimDelayedOutputPenaltyTxs(aggregatedHtlcTx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) assertEquals(4, claimHtlcDelayedPenaltyTxs.size) val skipped = claimHtlcDelayedPenaltyTxs.filterIsInstance>() assertEquals(2, skipped.size) @@ -442,6 +655,270 @@ class TransactionsTestsCommon : LightningTestSuite() { } } + @Test + fun `generate valid commitment and htlc transactions -- simple taproot channels`() { + val isTaprootChannel = true + val finalPubKeyScript = write(pay2wpkh(PrivateKey(ByteVector32("01".repeat(32))).publicKey())) + val commitInput = Funding.makeFundingInputInfo(TxId(ByteVector32("02".repeat(32))), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), isTaprootChannel) + + // htlc1 and htlc2 are regular IN/OUT htlcs + val paymentPreimage1 = ByteVector32("03".repeat(32)) + val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, 100.mbtc.toMilliSatoshi(), ByteVector32(sha256(paymentPreimage1)), CltvExpiry(300), TestConstants.emptyOnionPacket) + val paymentPreimage2 = ByteVector32("04".repeat(32)) + val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 200.mbtc.toMilliSatoshi(), ByteVector32(sha256(paymentPreimage2)), CltvExpiry(300), TestConstants.emptyOnionPacket) + // htlc3 and htlc4 are dust htlcs 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 = ByteVector32("05".repeat(32)) + val htlc3 = UpdateAddHtlc( + ByteVector32.Zeroes, + 2, + (localDustLimit + weight2fee(feerate, Commitments.HTLC_TIMEOUT_WEIGHT)).toMilliSatoshi(), + ByteVector32(sha256(paymentPreimage3)), + CltvExpiry(300), + TestConstants.emptyOnionPacket + ) + val paymentPreimage4 = ByteVector32("06".repeat(32)) + val htlc4 = UpdateAddHtlc( + ByteVector32.Zeroes, + 3, + (localDustLimit + weight2fee(feerate, Commitments.HTLC_SUCCESS_WEIGHT)).toMilliSatoshi(), + ByteVector32(sha256(paymentPreimage4)), + CltvExpiry(300), + TestConstants.emptyOnionPacket + ) + val spec = CommitmentSpec( + htlcs = setOf( + OutgoingHtlc(htlc1), + IncomingHtlc(htlc2), + OutgoingHtlc(htlc3), + IncomingHtlc(htlc4) + ), + feerate = feerate, + toLocal = 400.mbtc.toMilliSatoshi(), + toRemote = 300.mbtc.toMilliSatoshi() + ) + + val outputs = makeCommitTxOutputs( + localFundingPriv.publicKey(), + remoteFundingPriv.publicKey(), + true, + localDustLimit, + localRevocationPriv.publicKey(), + toLocalDelay, + localDelayedPaymentPriv.publicKey(), + remotePaymentPriv.publicKey(), + localHtlcPriv.publicKey(), + remoteHtlcPriv.publicKey(), + spec, + isTaprootChannel + ) + val localNonce = Musig2.generateNonce(randomBytes32(), localFundingPriv, listOf(localFundingPriv.publicKey())) + val remoteNonce = Musig2.generateNonce(randomBytes32(), remoteFundingPriv, listOf(remoteFundingPriv.publicKey())) + val commitTxNumber = 0x404142434445L + val commitTx = run { + val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey(), remotePaymentPriv.publicKey(), true, outputs) + when (isTaprootChannel) { + true -> { + val localSig = Transactions.partialSign(txInfo, localFundingPriv, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localNonce, remoteNonce.second).right!! + val remoteSig = Transactions.partialSign(txInfo, remoteFundingPriv, remoteFundingPriv.publicKey(), localFundingPriv.publicKey(), remoteNonce, localNonce.second).right!! + val aggSig = Transactions.aggregatePartialSignatures(txInfo, localSig, remoteSig, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localNonce.second, remoteNonce.second).right!! + Transactions.addAggregatedSignature(txInfo, aggSig) + } + + else -> { + val localSig = sign(txInfo, localPaymentPriv) + val remoteSig = sign(txInfo, remotePaymentPriv) + addSigs(txInfo, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localSig, remoteSig) + } + } + } + + run { + assertEquals(commitTxNumber, getCommitTxNumber(commitTx.tx, true, localPaymentPriv.publicKey(), remotePaymentPriv.publicKey())) + val hash = sha256(localPaymentPriv.publicKey().value + remotePaymentPriv.publicKey().value) + val num = Pack.int64BE(hash.takeLast(8).toByteArray()) and 0xffffffffffffL + val check = ((commitTx.tx.txIn.first().sequence and 0xffffffL) shl 24) or (commitTx.tx.lockTime and 0xffffffL) + assertEquals(commitTxNumber, check xor num) + } + val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), spec.feerate, outputs, isTaprootChannel) + assertEquals(4, htlcTxs.size) + val htlcSuccessTxs = htlcTxs.filterIsInstance() + assertEquals(2, htlcSuccessTxs.size) // htlc2 and htlc4 + assertEquals(setOf(1L, 3L), htlcSuccessTxs.map { it.htlcId }.toSet()) + val htlcTimeoutTxs = htlcTxs.filterIsInstance() + assertEquals(2, htlcTimeoutTxs.size) // htlc1 and htlc3 + assertEquals(setOf(0L, 2L), htlcTimeoutTxs.map { it.htlcId }.toSet()) + + run { + // either party spends local->remote htlc output with htlc timeout tx + for (htlcTimeoutTx in htlcTimeoutTxs) { + val localSig = htlcTimeoutTx.sign(localHtlcPriv) + val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) + val signed = addSigs(htlcTimeoutTx, localSig, remoteSig) + val csResult = checkSpendable(signed) + assertTrue(csResult.isSuccess, "is $csResult") + } + } + run { + // local spends delayed output of htlc1 timeout tx + val claimHtlcDelayed = makeHtlcDelayedTx(htlcTimeoutTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) + assertTrue(claimHtlcDelayed is Success, "is $claimHtlcDelayed") + val localSig = claimHtlcDelayed.result.sign(localDelayedPaymentPriv) + val signedTx = addSigs(claimHtlcDelayed.result, localSig) + assertTrue(checkSpendable(signedTx).isSuccess) + // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit + val claimHtlcDelayed1 = makeHtlcDelayedTx(htlcTimeoutTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) + assertEquals(Skipped(OutputNotFound), claimHtlcDelayed1) + } + run { + // remote spends local->remote htlc1/htlc3 output directly in case of success + for ((htlc, paymentPreimage) in listOf(htlc1 to paymentPreimage1, htlc3 to paymentPreimage3)) { + val claimHtlcSuccessTx = + makeClaimHtlcSuccessTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey(), localHtlcPriv.publicKey(), localRevocationPriv.publicKey(), finalPubKeyScript, htlc, feerate) + assertTrue(claimHtlcSuccessTx is Success, "is $claimHtlcSuccessTx") + val localSig = claimHtlcSuccessTx.result.sign(remoteHtlcPriv) + val signed = addSigs(claimHtlcSuccessTx.result, localSig, paymentPreimage) + val csResult = checkSpendable(signed) + assertTrue(csResult.isSuccess, "is $csResult") + } + } + run { + // local spends remote->local htlc2/htlc4 output with htlc success tx using payment preimage + for ((htlcSuccessTx, paymentPreimage) in listOf(htlcSuccessTxs[1] to paymentPreimage2, htlcSuccessTxs[0] to paymentPreimage4)) { + val localSig = htlcSuccessTx.sign(localHtlcPriv) + val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) + val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage) + val csResult = checkSpendable(signedTx) + assertTrue(csResult.isSuccess, "is $csResult") + // check remote sig + assertTrue(htlcSuccessTx.checkSig(remoteSig, remoteHtlcPriv.publicKey(), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) + } + } + run { + // local spends delayed output of htlc2 success tx + val claimHtlcDelayed = makeHtlcDelayedTx(htlcSuccessTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) + assertTrue(claimHtlcDelayed is Success, "is $claimHtlcDelayed") + val localSig = claimHtlcDelayed.result.sign(localDelayedPaymentPriv) + val signedTx = addSigs(claimHtlcDelayed.result, localSig) + val csResult = checkSpendable(signedTx) + assertTrue(csResult.isSuccess, "is $csResult") + // local can't claim delayed output of htlc4 timeout tx because it is below the dust limit + val claimHtlcDelayed1 = makeHtlcDelayedTx(htlcSuccessTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) + assertEquals(Skipped(AmountBelowDustLimit), claimHtlcDelayed1) + } + run { + // remote spends main output + val claimP2WPKHOutputTx = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey(), finalPubKeyScript.toByteVector(), feerate, isTaprootChannel) + assertTrue(claimP2WPKHOutputTx is Success, "is $claimP2WPKHOutputTx") + val localSig = claimP2WPKHOutputTx.result.sign(remotePaymentPriv) + val signedTx = addSigs(claimP2WPKHOutputTx.result, localSig) + val csResult = checkSpendable(signedTx) + assertTrue(csResult.isSuccess, "is $csResult") + } + run { + // remote spends htlc1's htlc-timeout tx with revocation key + val claimHtlcDelayedPenaltyTxs = + makeClaimDelayedOutputPenaltyTxs(htlcTimeoutTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) + assertEquals(1, claimHtlcDelayedPenaltyTxs.size) + val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() + assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") + val sig = claimHtlcDelayedPenaltyTx.result.sign(localRevocationPriv) + val signed = addSigs(claimHtlcDelayedPenaltyTx.result, sig) + val csResult = checkSpendable(signed) + assertTrue(csResult.isSuccess, "is $csResult") + // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit + val claimHtlcDelayedPenaltyTxsSkipped = + makeClaimDelayedOutputPenaltyTxs(htlcTimeoutTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) + assertEquals(listOf(Skipped(AmountBelowDustLimit)), claimHtlcDelayedPenaltyTxsSkipped) + } + run { + // remote spends remote->local htlc output directly in case of timeout + val claimHtlcTimeoutTx = + makeClaimHtlcTimeoutTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey(), localHtlcPriv.publicKey(), localRevocationPriv.publicKey(), finalPubKeyScript, htlc2, feerate) + assertTrue(claimHtlcTimeoutTx is Success, "is $claimHtlcTimeoutTx") + val remoteSig = claimHtlcTimeoutTx.result.sign(remoteHtlcPriv) + val signed = addSigs(claimHtlcTimeoutTx.result, remoteSig) + val csResult = checkSpendable(signed) + assertTrue(csResult.isSuccess, "is $csResult") + } + run { + // remote spends htlc2's htlc-success tx with revocation key + val claimHtlcDelayedPenaltyTxs = + makeClaimDelayedOutputPenaltyTxs(htlcSuccessTxs[1].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) + assertEquals(1, claimHtlcDelayedPenaltyTxs.size) + val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() + assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") + val sig = claimHtlcDelayedPenaltyTx.result.sign(localRevocationPriv) + val signed = addSigs(claimHtlcDelayedPenaltyTx.result, sig) + val csResult = checkSpendable(signed) + assertTrue(csResult.isSuccess, "is $csResult") + // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit + val claimHtlcDelayedPenaltyTxsSkipped = + makeClaimDelayedOutputPenaltyTxs(htlcSuccessTxs[0].tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) + assertEquals(listOf(Skipped(AmountBelowDustLimit)), claimHtlcDelayedPenaltyTxsSkipped) + } + run { + // remote spends all htlc txs aggregated in a single tx + val txIn = htlcTimeoutTxs.flatMap { it.tx.txIn } + htlcSuccessTxs.flatMap { it.tx.txIn } + val txOut = htlcTimeoutTxs.flatMap { it.tx.txOut } + htlcSuccessTxs.flatMap { it.tx.txOut } + val aggregatedHtlcTx = Transaction(2, txIn, txOut, 0) + val claimHtlcDelayedPenaltyTxs = + makeClaimDelayedOutputPenaltyTxs(aggregatedHtlcTx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), finalPubKeyScript, feerate, isTaprootChannel) + assertEquals(4, claimHtlcDelayedPenaltyTxs.size) + val skipped = claimHtlcDelayedPenaltyTxs.filterIsInstance>() + assertEquals(2, skipped.size) + val claimed = claimHtlcDelayedPenaltyTxs.filterIsInstance>() + assertEquals(2, claimed.size) + assertEquals(2, claimed.map { it.result.input.outPoint }.toSet().size) + } + run { + // remote spends offered HTLC output with revocation key + val htlcOutputIndex = outputs.indexOfFirst { + val outHtlc = (it.commitmentOutput as? OutHtlc)?.outgoingHtlc?.add + outHtlc != null && outHtlc.id == htlc1.id + } + val htlcPenaltyTx = when (isTaprootChannel) { + true -> { + val scriptTree = Scripts.Taproot.offeredHtlcTree(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), htlc1.paymentHash) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, Transactions.ScriptTreeAndInternalKey(scriptTree, localRevocationPriv.publicKey().xOnly()), localDustLimit, finalPubKeyScript, feerate) + } + + else -> { + val script = write(htlcOffered(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), localRevocationPriv.publicKey(), ripemd160(htlc1.paymentHash))) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feerate) + } + } + assertTrue(htlcPenaltyTx is Success, "is $htlcPenaltyTx") + val sig = htlcPenaltyTx.result.sign(localRevocationPriv) + val signed = addSigs(htlcPenaltyTx.result, sig, localRevocationPriv.publicKey()) + val csResult = checkSpendable(signed) + assertTrue(csResult.isSuccess, "is $csResult") + } + run { + // remote spends received HTLC output with revocation key + val htlcOutputIndex = outputs.indexOfFirst { + val inHtlc = (it.commitmentOutput as? CommitmentOutput.InHtlc)?.incomingHtlc?.add + inHtlc != null && inHtlc.id == htlc2.id + } + val htlcPenaltyTx = when (isTaprootChannel) { + true -> { + val scriptTree = Scripts.Taproot.receivedHtlcTree(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), htlc2.paymentHash, htlc2.cltvExpiry) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, Transactions.ScriptTreeAndInternalKey(scriptTree, localRevocationPriv.publicKey().xOnly()), localDustLimit, finalPubKeyScript, feerate) + } + + else -> { + val script = write(htlcReceived(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), localRevocationPriv.publicKey(), ripemd160(htlc2.paymentHash), htlc2.cltvExpiry)) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feerate) + } + } + + assertTrue(htlcPenaltyTx is Success, "is $htlcPenaltyTx") + val sig = htlcPenaltyTx.result.sign(localRevocationPriv) + val signed = addSigs(htlcPenaltyTx.result, sig, localRevocationPriv.publicKey()) + val csResult = checkSpendable(signed) + assertTrue(csResult.isSuccess, "is $csResult") + } + } + @Test fun `spend 2-of-2 legacy swap-in`() { val userWallet = TestConstants.Alice.keyManager.swapInOnChainWallet @@ -574,7 +1051,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val remotePaymentPriv = PrivateKey.fromHex("a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6") val localHtlcPriv = PrivateKey.fromHex("a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7") val remoteHtlcPriv = PrivateKey.fromHex("a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8") - val commitInput = Funding.makeFundingInputInfo(TxId("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey()) + val commitInput = Funding.makeFundingInputInfo(TxId("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), isTaprootChannel = false) // 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 @@ -602,6 +1079,7 @@ class TransactionsTestsCommon : LightningTestSuite() { ) val commitTxNumber = 0x404142434446L + val isTaprootChannel = false val (commitTx, outputs, htlcTxs) = run { val outputs = makeCommitTxOutputs( @@ -615,13 +1093,14 @@ class TransactionsTestsCommon : LightningTestSuite() { remotePaymentPriv.publicKey(), localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), - spec + spec, + isTaprootChannel ) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey(), remotePaymentPriv.publicKey(), true, outputs) val localSig = sign(txInfo, localPaymentPriv) val remoteSig = sign(txInfo, remotePaymentPriv) val commitTx = addSigs(txInfo, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localSig, remoteSig) - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), feerate, outputs) + val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), feerate, outputs, isTaprootChannel) Triple(commitTx, outputs, htlcTxs) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 45ba39bf2..3b303465d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -379,7 +379,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val nodeKey = PrivateKey.fromHex("57ac961f1b80ebfb610037bf9c96c6333699bde42257919a53974811c34649e3") val fundingLease = LiquidityAds.FundingRate(500_000.sat, 5_000_000.sat, 1100, 75, 0.sat, 1_500.sat) val requestFunds = LiquidityAds.RequestFunding(750_000.sat, fundingLease, LiquidityAds.PaymentDetails.FromChannelBalance) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(publicKey(1), publicKey(1)) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(publicKey(1), publicKey(1), isTaprootChannel = false) val willFund = LiquidityAds.WillFundRates(listOf(fundingLease), setOf(LiquidityAds.PaymentType.FromChannelBalance)).validateRequest(nodeKey, fundingScript, FeeratePerKw(5000.sat), requestFunds, isChannelCreation = true, 0.msat)!!.willFund // @formatter:off val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000.sat, 473.sat, 100_000_000, 1.msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), publicKey(7)) @@ -489,19 +489,19 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TxRemoveInput(channelId2, 561) to ByteVector("0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231"), TxRemoveOutput(channelId1, 1) to ByteVector("0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001"), TxComplete(channelId1) to ByteVector("0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - TxSignatures(channelId1, tx2, listOf(ScriptWitness(listOf(ByteVector("68656c6c6f2074686572652c2074686973206973206120626974636f6e212121"), ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), ScriptWitness(listOf(ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), - TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), - TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5 fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId1, tx2, listOf(ScriptWitness(listOf(ByteVector("68656c6c6f2074686572652c2074686973206973206120626974636f6e212121"), ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), ScriptWitness(listOf(ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))), null, null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), + TxSignatures(channelId2, tx1, listOf(), null, null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), + TxSignatures(channelId2, tx1, listOf(), null, null, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, null, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + TxSignatures(channelId2, tx1, listOf(), signature, null, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), listOf(), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5 fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), TxInitRbf(channelId1, 8388607, FeeratePerKw(4000.sat)) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(1_500_000.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(0.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000"),