diff --git a/build.gradle.kts b/build.gradle.kts index 34a174307..8e65c092c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,7 +27,7 @@ val currentOs = org.gradle.internal.os.OperatingSystem.current() kotlin { - val bitcoinKmpVersion = "0.19.0" // when upgrading bitcoin-kmp, keep secpJniJvmVersion in sync! + val bitcoinKmpVersion = "0.20.0-SNAPSHOT" // when upgrading bitcoin-kmp, keep secpJniJvmVersion in sync! val secpJniJvmVersion = "0.15.0" val serializationVersion = "1.6.2" diff --git a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt index 86c8235e4..bf54d9fa6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt @@ -256,6 +256,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 @@ -337,7 +343,8 @@ data class Features(val activated: Map, val unknown: Se Feature.ChannelBackupClient, Feature.ChannelBackupProvider, Feature.ExperimentalSplice, - Feature.Quiescence + Feature.Quiescence, + Feature.SimpleTaprootStaging ) operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray()) @@ -369,7 +376,8 @@ data class Features(val activated: Map, val unknown: Se Feature.BasicMultiPartPayment to listOf(Feature.PaymentSecret), Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey), Feature.TrampolinePayment to listOf(Feature.PaymentSecret), - Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret) + Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret), + 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 6abd258e2..ca77ac276 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -201,7 +201,8 @@ data class NodeParams( Feature.PayToOpenClient to FeatureSupport.Optional, Feature.ChannelBackupClient to FeatureSupport.Optional, Feature.ExperimentalSplice to FeatureSupport.Optional, - Feature.Quiescence to FeatureSupport.Mandatory + Feature.Quiescence to FeatureSupport.Mandatory, + Feature.SimpleTaprootStaging to FeatureSupport.Optional ), dustLimit = 546.sat, maxRemoteDustLimit = 600.sat, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index 6b2d5aa08..b7b3434ff 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -50,6 +50,7 @@ sealed class ChannelAction { is Transactions.TransactionWithInputInfo.CommitTx -> Type.CommitTx is Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx -> Type.HtlcSuccessTx is Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx -> Type.HtlcTimeoutTx + is Transactions.TransactionWithInputInfo.HtlcDelayedTx -> Type.ClaimHtlcTimeoutTx is Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx -> Type.ClaimHtlcSuccessTx is Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx -> Type.ClaimHtlcTimeoutTx is Transactions.TransactionWithInputInfo.ClaimAnchorOutputTx.ClaimLocalAnchorOutputTx -> Type.ClaimLocalAnchorOutputTx diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt index d6f3f1724..0bd668c64 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 c1b728c9b..5d08bc9ca 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" } @@ -254,9 +278,9 @@ data class Commitment( val balanceNoFees = (reduced.toRemote - localChannelReserve(params).toMilliSatoshi()).coerceAtLeast(0.msat) return if (params.localParams.isInitiator) { // 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 +307,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 +375,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.isInitiator) fees.toMilliSatoshi().coerceAtLeast(initiatorFeeBuffer) else 0.msat) @@ -403,7 +427,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.isInitiator) 0.sat else fees).toMilliSatoshi() @@ -436,7 +460,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 +477,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)) @@ -507,7 +531,7 @@ data class Commitment( 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 +547,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 +578,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) @@ -575,6 +600,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 @@ -779,7 +805,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 } @@ -982,6 +1012,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 +1059,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 +1071,12 @@ data class Commitments( remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, - spec + spec, + isTaprootChannel ) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isInitiator, 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 +1096,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 +1108,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.isInitiator, 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 c37a4e884..e154002eb 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt @@ -30,6 +30,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 @@ -104,6 +105,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) } @@ -143,6 +148,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) } @@ -210,8 +219,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( @@ -219,15 +231,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) @@ -257,13 +272,13 @@ object Helpers { ): Either { 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.isInitiator) { // They initiated the channel open, therefore they pay the 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)) @@ -271,7 +286,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, @@ -425,7 +440,8 @@ 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) @@ -459,7 +475,8 @@ 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) @@ -518,7 +535,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 @@ -579,7 +597,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) } /** @@ -588,7 +606,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") { @@ -597,7 +615,8 @@ object Helpers { params.localParams.dustLimit, localPaymentPoint, params.localParams.defaultFinalScriptPubKey, - claimMainFeerate + claimMainFeerate, + isTaprootChannel ) }?.let { val sig = Transactions.sign(it, channelKeys.paymentKey) @@ -638,7 +657,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) @@ -655,7 +674,8 @@ object Helpers { params.localParams.dustLimit, localPaymentPoint, params.localParams.defaultFinalScriptPubKey, - feerateMain + feerateMain, + isTaprootChannel ) }?.let { val sig = Transactions.sign(it, channelKeys.paymentKey) @@ -746,7 +766,8 @@ object Helpers { params: ChannelParams, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, - feerates: OnChainFeerates + feerates: OnChainFeerates, + isTaprootChannel: Boolean ): Pair> { val claimTxs = buildList { revokedCommitPublished.claimMainOutputTx?.let { add(it) } @@ -771,7 +792,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 5074949ee..6d42756af 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -56,6 +56,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]]. */ @@ -99,9 +117,9 @@ 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 { + fun fundingPubkeyScript(channelKeys: KeyManager.ChannelKeys, isTaprootChannel: Boolean): ByteVector { val fundingTxIndex = (sharedInput as? SharedFundingInput.Multisig2of2)?.let { it.fundingTxIndex + 1 } ?: 0 - return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey) + return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey, isTaprootChannel) } } @@ -245,8 +263,14 @@ data class FundingContributions(val inputs: List, v /** * @param walletInputs 2-of-2 swap-in wallet inputs. */ - fun create(channelKeys: KeyManager.ChannelKeys, swapInKeys: KeyManager.SwapInOnChainKeys, params: InteractiveTxParams, walletInputs: List): Either = - create(channelKeys, swapInKeys, params, null, walletInputs, listOf()) + fun create( + channelKeys: KeyManager.ChannelKeys, + swapInKeys: KeyManager.SwapInOnChainKeys, + params: InteractiveTxParams, + walletInputs: List, + isTaprootChannel: Boolean = false + ): Either = + create(channelKeys, swapInKeys, params, null, walletInputs, listOf(), isTaprootChannel = isTaprootChannel) /** * @param sharedUtxo previous input shared between the two participants (e.g. previous funding output when splicing) and our corresponding balance. @@ -261,7 +285,8 @@ data class FundingContributions(val inputs: List, v sharedUtxo: Pair?, walletInputs: List, localOutputs: List, - 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)) @@ -277,7 +302,7 @@ data class FundingContributions(val inputs: List, v } // We compute the fees that we should pay in the shared transaction. - val fundingPubkeyScript = params.fundingPubkeyScript(channelKeys) + val fundingPubkeyScript = params.fundingPubkeyScript(channelKeys, isTaprootChannel) val weightWithoutChange = computeWeightPaid(params.isInitiator, sharedUtxo?.first, fundingPubkeyScript, walletInputs, localOutputs) val weightWithChange = computeWeightPaid(params.isInitiator, sharedUtxo?.first, fundingPubkeyScript, walletInputs, localOutputs + listOf(TxOut(0.sat, Script.pay2wpkh(Transactions.PlaceHolderPubKey)))) val feesWithoutChange = totalAmountIn - totalAmountOut @@ -440,16 +465,6 @@ data class SharedTransaction( // 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 } - } - // If we are swapping funds in, we provide our partial signatures to the corresponding inputs. val legacySwapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs @@ -464,7 +479,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) @@ -491,14 +506,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, null, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) } } @@ -531,6 +546,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) @@ -632,6 +663,7 @@ data class InteractiveTxSession( val txCompleteReceived: TxComplete? = null, val inputsReceivedCount: Int = 0, val outputsReceivedCount: Int = 0, + val firstRemoteNonce: IndividualNonce? = null, val secretNonces: Map> = mapOf() ) { @@ -658,7 +690,8 @@ data class InteractiveTxSession( previousRemoteBalance: MilliSatoshi, localHtlcs: Set, fundingContributions: FundingContributions, - previousTxs: List = listOf() + previousTxs: List = listOf(), + firstRemoteNonce: IndividualNonce? = null ) : this( remoteNodeId, channelKeys, @@ -667,11 +700,26 @@ 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 { val msg = toSend.firstOrNull() return when (msg) { @@ -713,7 +761,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 } @@ -814,9 +876,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)) @@ -1010,7 +1072,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) @@ -1074,7 +1152,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 = liquidityLease?.fees?.total?.toMilliSatoshi() ?: 0.msat return Helpers.Funding.makeCommitTxs( channelKeys, @@ -1091,8 +1169,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 -> @@ -1111,11 +1201,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 1982c8edd..d4adf45da 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt @@ -482,7 +482,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) { @@ -513,7 +513,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(), @@ -543,7 +543,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { } 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) 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..5878e0c07 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)) 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 c88ec47c9..b8a8f9c4c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -389,7 +389,7 @@ data class Normal( targetFeerate = spliceStatus.command.feerate ) val commitTxFees = when { - commitments.params.localParams.isInitiator -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec) + commitments.params.localParams.isInitiator -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec, commitments.isTaprootChannel) else -> 0.sat } if (parentCommitment.localCommit.spec.toLocal + fundingContribution.toMilliSatoshi() < parentCommitment.localChannelReserve(commitments.params).max(commitTxFees)) { @@ -517,7 +517,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 = false), // FIXME cmd.message.fundingContribution, spliceStatus.spliceInit.feerate, cmd.message.willFund, 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 c00720b28..eb8426095 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 @@ -52,14 +53,25 @@ data class WaitForAcceptChannel( val remoteFundingPubkey = accept.fundingPubkey val dustLimit = accept.dustLimit.max(init.localParams.dustLimit) val fundingParams = InteractiveTxParams(channelId, true, init.fundingAmount, accept.fundingAmount, remoteFundingPubkey, lastSent.lockTime, dustLimit, lastSent.fundingFeerate) - when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, init.walletInputs)) { + when (val fundingContributions = + FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, init.walletInputs, isTaprootChannel = channelFeatures.hasFeature(Feature.SimpleTaprootStaging))) { is Either.Left -> { logger.error { "could not fund channel: ${fundingContributions.value}" } Pair(Aborted, listOf(ChannelAction.Message.Send(Error(channelId, ChannelFundingError(channelId).message)))) } is Either.Right -> { // The channel initiator always sends the first interactive-tx message. - val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value).send() + val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession( + staticParams.remoteNodeId, + channelKeys, + keyManager.swapInOnChainWallet, + fundingParams, + 0.msat, + 0.msat, + emptySet(), + fundingContributions.value, + firstRemoteNonce = cmd.message.firstRemoteNonce + ).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { val nextState = WaitForFundingCreated( @@ -74,7 +86,8 @@ data class WaitForAcceptChannel( lastSent.channelFlags, init.channelConfig, channelFeatures, - null + null, + cmd.message.secondRemoteNonce ) val actions = listOf( ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId), 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 02e092358..d3b014924 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -136,7 +136,7 @@ data class WaitForFundingConfirmed( latestFundingTx.fundingParams.dustLimit, rbfStatus.command.targetFeerate ) - when (val contributions = FundingContributions.create(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, rbfStatus.command.walletInputs)) { + when (val contributions = FundingContributions.create(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, rbfStatus.command.walletInputs, 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)))) 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 304ba5f8f..1372f9202 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.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -41,7 +42,8 @@ data class WaitForFundingCreated( val channelFlags: Byte, val channelConfig: ChannelConfig, val channelFeatures: ChannelFeatures, - val channelOrigin: Origin? + val channelOrigin: Origin?, + val secondRemoteNonce: IndividualNonce? ) : ChannelState() { val channelId: ByteVector32 = interactiveTxSession.fundingParams.channelId 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 c97ce807a..32207924f 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt @@ -52,6 +52,14 @@ data object WaitForInit : ChannelState() { add(ChannelTlv.ChannelTypeTlv(cmd.channelType)) if (cmd.pushAmount > 0.msat) add(ChannelTlv.PushAmountTlv(cmd.pushAmount)) if (cmd.channelOrigin != null) add(ChannelTlv.OriginTlv(cmd.channelOrigin)) + if (cmd.channelType == ChannelType.SupportedChannelType.SimpleTaprootStaging) 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 18dcdba66..1a824cc41 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt @@ -60,6 +60,14 @@ data class WaitForOpenChannel( buildSet { add(ChannelTlv.ChannelTypeTlv(channelType)) if (pushAmount > 0.msat) add(ChannelTlv.PushAmountTlv(pushAmount)) + if (channelType == ChannelType.SupportedChannelType.SimpleTaprootStaging) add( + ChannelTlv.NextLocalNoncesTlv( + listOf( + channelKeys.verificationNonce(0, 0).second, + channelKeys.verificationNonce(0, 1).second, + ) + ) + ) } ), ) @@ -86,7 +94,17 @@ data class WaitForOpenChannel( 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( localParams, remoteParams, @@ -99,7 +117,8 @@ data class WaitForOpenChannel( open.channelFlags, channelConfig, channelFeatures, - open.origin + open.origin, + 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 6ffe4847e..5cdeb3183 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 d4b963a5e..8c7e6d90b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt @@ -277,6 +277,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() 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 d7d5ff33a..f89633987 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -193,6 +193,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}") } @@ -605,12 +610,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(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 48aab0206..bd7db0539 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -239,6 +239,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 { @@ -640,10 +646,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) { @@ -690,6 +708,9 @@ object Serialization { is SpliceTx -> { write(0x0e); writeInputInfo(o.input); writeBtcObject(o.tx) } + is HtlcDelayedTx -> { + write(0x0f); writeInputInfo(o.input); writeBtcObject(o.tx) + } } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt index 985d09dec..fdb52ea55 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(0, 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(0, delayScript), + ScriptTree.Leaf(1, 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(0, toDelayScript(localDelayedPaymentPubkey, toSelfDelay)), + ScriptTree.Leaf(1, 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(0, 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(0, 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(0, offeredHtlcTimeoutScript(localHtlcPubkey, remoteHtlcPubkey)), + ScriptTree.Leaf(1, 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(0, receivedHtlcTimeoutScript(remoteHtlcPubkey, lockTime)), + ScriptTree.Leaf(1, 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 b826695a4..363bfb567 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,55 @@ 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 + data class HtlcDelayedTx(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 branch = input.scriptTreeAndInternalKey.scriptTree as ScriptTree.Leaf + Transaction.signInputTaprootScriptPath(key, tx, 0, listOf(input.txOut), SigHash.SIGHASH_DEFAULT, branch.hash()) + } + } + } } @Serializable @@ -89,10 +169,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 @@ -111,17 +211,51 @@ object Transactions { 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 +297,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 +365,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 +429,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 +461,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 (localIsInitiator) { Pair(spec.toLocal.truncateToSatoshi() - commitFee, spec.toRemote.truncateToSatoshi()) @@ -329,50 +474,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 +625,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 +643,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 +653,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(0, Scripts.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 +688,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 +698,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(0, Scripts.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 +732,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 +763,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 +801,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 +835,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 +885,53 @@ 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 (0, Scripts.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.HtlcDelayedTx(input, tx1)) + } + } + } + } + else -> { + makeClaimLocalDelayedOutputTx(htlcTx, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localFinalScriptPubKey, feeratePerKw, isTaprootChannel).map { + it -> TransactionWithInputInfo.HtlcDelayedTx(it.input, it.tx) + } + } + } + } + fun makeClaimLocalDelayedOutputTx( delayedOutputTx: Transaction, localDustLimit: Satoshi, @@ -601,15 +939,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 = Scripts.Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + Triple( + Scripts.Taproot.toDelayScript(localDelayedPaymentPubkey, toLocalDelay), + Script.write(Script.pay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree)), + ScriptTreeAndInternalKey(toLocalScriptTree, NUMS_POINT.xOnly()) + ) + } + + else -> { + val redeemScript = Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + 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], ByteVector(Script.write(redeemScript)), scriptTree_opt) // unsigned transaction val tx = Transaction( version = 2, @@ -618,7 +972,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 = Scripts.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 +999,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(0, 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, @@ -735,6 +1112,33 @@ object Transactions { } } + fun makeHtlcPenaltyTx( + commitTx: Transaction, + htlcOutputIndex: Int, + scriptTreeAndInternalKey: ScriptTreeAndInternalKey, + localDustLimit: Satoshi, + localFinalScriptPubKey: ByteVector, + 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, @@ -811,6 +1215,39 @@ object Transactions { return sign(txInfo.tx, inputIndex, txInfo.input.redeemScript.toByteArray(), txInfo.input.txOut.amount, 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( commitTx: TransactionWithInputInfo.CommitTx, localFundingPubkey: PublicKey, @@ -823,37 +1260,83 @@ 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)) } @@ -863,7 +1346,10 @@ object Transactions { } 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 +1358,10 @@ 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 checkSpendable(txinfo: TransactionWithInputInfo): Try = runTrying { Transaction.correctlySpends(txinfo.tx, 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 0ac111bab..97bdfa564 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 @@ -9,6 +10,7 @@ import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.channel.Origin +import fr.acinq.lightning.channel.PartialSignatureWithNonce import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector @@ -201,6 +203,23 @@ sealed class ChannelTlv : Tlv { override fun read(input: Input): PushAmountTlv = PushAmountTlv(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 { @@ -213,6 +232,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 { @@ -266,6 +298,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 { @@ -278,6 +331,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) : 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 ChannelReestablishTlv : Tlv { @@ -300,6 +370,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) : 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 ShutdownTlv : Tlv { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index b3c2e6f68..79311d174 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() = PreviousFundingTxSig.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 aba62a09a..0bf3656d0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -6,10 +6,12 @@ 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.ChannelType 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.* @@ -487,6 +489,7 @@ data class TxSignatures( tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, + previousFundingPartialSig: PartialSignatureWithNonce?, swapInUserSigs: List, swapInServerSigs: List, swapInUserPartialSigs: List, @@ -498,6 +501,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, @@ -509,6 +513,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() @@ -536,6 +541,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, @@ -671,6 +677,9 @@ data class OpenDualFundedChannel( val pushAmount: MilliSatoshi get() = tlvStream.get()?.amount ?: 0.msat val requestFunds: ChannelTlv.RequestFunds? get() = tlvStream.get() val origin: Origin? get() = tlvStream.get()?.origin + 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 @@ -708,6 +717,7 @@ data class OpenDualFundedChannel( ChannelTlv.RequestFunds.tag to ChannelTlv.RequestFunds as TlvValueReader, ChannelTlv.OriginTlv.tag to ChannelTlv.OriginTlv.Companion as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, + ChannelTlv.NextLocalNoncesTlv.tag to ChannelTlv.NextLocalNoncesTlv.Companion as TlvValueReader, ) override fun read(input: Input): OpenDualFundedChannel = OpenDualFundedChannel( @@ -756,6 +766,9 @@ data class AcceptDualFundedChannel( val channelType: ChannelType? get() = tlvStream.get()?.channelType val willFund: ChannelTlv.WillFund? get() = tlvStream.get() val pushAmount: MilliSatoshi get() = 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 @@ -788,6 +801,7 @@ data class AcceptDualFundedChannel( ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, ChannelTlv.WillFund.tag to ChannelTlv.WillFund as TlvValueReader, ChannelTlv.PushAmountTlv.tag to ChannelTlv.PushAmountTlv.Companion as TlvValueReader, + ChannelTlv.NextLocalNoncesTlv.tag to ChannelTlv.NextLocalNoncesTlv.Companion as TlvValueReader, ) override fun read(input: Input): AcceptDualFundedChannel = AcceptDualFundedChannel( @@ -881,7 +895,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)), @@ -1190,6 +1206,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) @@ -1207,6 +1225,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 { @@ -1244,7 +1263,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( @@ -1310,6 +1332,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 { 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 1ebc2f2e1..fc28fd296 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 955293a92..7ef0c3c2d 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) @@ -154,6 +155,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) @@ -184,7 +186,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) @@ -225,6 +227,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) @@ -254,7 +257,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) @@ -282,6 +285,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) @@ -315,7 +319,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) @@ -357,6 +361,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) @@ -389,7 +394,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) @@ -434,6 +439,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) @@ -463,7 +470,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) @@ -507,6 +514,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) @@ -543,7 +552,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) @@ -585,6 +594,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) @@ -620,7 +631,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) @@ -883,14 +894,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) @@ -901,10 +913,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 @@ -966,12 +979,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) @@ -1036,13 +1050,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 @@ -1052,13 +1067,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 @@ -1069,8 +1085,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() @@ -1088,7 +1105,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 @@ -1138,14 +1155,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 a533f9a8e..4d69e2487 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -150,16 +150,25 @@ object TestsHelper { alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, zeroConf: Boolean = false, + isTaprootChannel: Boolean = false, channelOrigin: Origin? = null ): Triple, LNChannel, OpenDualFundedChannel> { + 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( @@ -182,10 +191,10 @@ object TestsHelper { ) val channelFlags = 0.toByte() - val aliceChannelParams = TestConstants.Alice.channelParams().copy(features = aliceFeatures.initFeatures()) - val bobChannelParams = TestConstants.Bob.channelParams().copy(features = bobFeatures.initFeatures()) - val aliceInit = Init(aliceFeatures) - val bobInit = Init(bobFeatures) + val aliceChannelParams = TestConstants.Alice.channelParams().copy(features = aliceFeatures1) + val bobChannelParams = TestConstants.Bob.channelParams().copy(features = bobFeatures1) + val aliceInit = Init(aliceFeatures1) + val bobInit = Init(bobFeatures1) val (alice1, actionsAlice1) = alice.process( ChannelCommand.Init.Initiator( aliceFundingAmount, 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 f80517101..565e07b32 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt @@ -226,7 +226,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) } 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 f8d7eb039..dd2d6afcb 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -188,6 +188,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice to purchase inbound liquidity`() { + val isTaprootChannel = false val (alice, bob) = reachNormal() val leaseRate = LiquidityAds.LeaseRate(0, 250, 250 /* 2.5% */, 10.sat, 200, 100.msat) val liquidityRequest = LiquidityAds.RequestRemoteFunding(200_000.sat, alice.currentBlockHeight, leaseRate) @@ -200,7 +201,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 = leaseRate.signLease(bob.staticParams.nodeParams.nodePrivateKey, fundingScript, spliceInit.requestFunds!!) val spliceAck = SpliceAck(alice.channelId, liquidityRequest.fundingAmount, 0.msat, defaultSpliceAck.fundingPubkey, willFund) 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 a26995701..572a9bf36 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt @@ -40,6 +40,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() 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 2ae04aa62..583b5ab3b 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 @@ -66,6 +67,40 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { verifyCommits(alice2.state.signingSession, bob3.state.signingSession, TestConstants.aliceFundingAmount.toMilliSatoshi() - TestConstants.alicePushAmount, TestConstants.alicePushAmount) } + @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() - TestConstants.alicePushAmount, TestConstants.alicePushAmount) + } + @Test fun `complete interactive-tx protocol -- with non-initiator contributions`() { val (alice, bob, inputAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs) @@ -303,9 +338,10 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, zeroConf: Boolean = false, + isTaprootChannel: Boolean = false, channelOrigin: Origin? = null ): Fixture { - val (a, b, open) = TestsHelper.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, zeroConf, channelOrigin) + val (a, b, open) = TestsHelper.init(channelType, aliceFeatures, bobFeatures, currentHeight, aliceFundingAmount, bobFundingAmount, alicePushAmount, bobPushAmount, zeroConf, isTaprootChannel, channelOrigin) val (b1, actions) = b.process(ChannelCommand.MessageReceived(open)) val accept = actions.findOutgoingMessage() assertIs>(b1) 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 faf65e15e..4a101a7bd 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt @@ -45,6 +45,34 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { } } + @Test + fun `recv CommitSig -- simple taproot channels`() { + val (alice, commitSigAlice, bob, commitSigBob) = init( + channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging, + isTaprootChannel = true + ) + 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() + TestConstants.alicePushAmount - TestConstants.bobPushAmount, it.amount) } + 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) @@ -296,6 +324,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { alicePushAmount: MilliSatoshi = TestConstants.alicePushAmount, bobPushAmount: MilliSatoshi = TestConstants.bobPushAmount, zeroConf: Boolean = false, + isTaprootChannel: Boolean = false, channelOrigin: Origin? = null ): Fixture { val (alice, bob, inputAlice, walletAlice) = WaitForFundingCreatedTestsCommon.init( @@ -308,6 +337,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { alicePushAmount, bobPushAmount, zeroConf, + isTaprootChannel, channelOrigin ) val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(inputAlice)) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index f15a933fc..82adf5157 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -1,8 +1,9 @@ package fr.acinq.lightning.serialization -import fr.acinq.bitcoin.byteVector +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 @@ -13,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 @@ -166,4 +168,34 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertEquals(state1, Serialization.deserialize(Serialization.serialize(state1)).value) } } + + @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(0, Script.pay2tr(randomKey().xOnlyPublicKey())), ScriptTree.Leaf(1, 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 e57048707..2fa4d4b0f 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 eccfc4e4d..e6166f64c 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) @@ -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(0, Scripts.Taproot.toDelayScript(localDelayedPaymentPriv.publicKey(), toLocalDelay)), + ScriptTree.Leaf(1, Scripts.Taproot.toRevokeScript(localRevocationPriv.publicKey(), localDelayedPaymentPriv.publicKey())), + ) + + // to-remote output script tree, with a single leaf + val toRemoteScriptTree = ScriptTree.Leaf(0, 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(0, 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.byteVector(), 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.byteVector(), 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 eedb45424..8cf74b0f7 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -421,19 +421,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"),