diff --git a/gradle.properties b/gradle.properties index e39b7406e..b41fb3b07 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=fr.acinq.lightning -version=1.9.1-SNAPSHOT +version=1.9.1-TAPROOT-SNAPSHOT # gradle org.gradle.jvmargs=-Xmx1536m org.gradle.parallel=true diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt index 3f834bc8f..8a2c97f57 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt @@ -270,6 +270,12 @@ sealed class Feature { override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) } + @Serializable + object SimpleTaprootStaging : Feature() { + override val rfcName get() = "option_simple_taproot_staging" + override val mandatory get() = 182 // README: this is not the feature bit defined in the bolt proposal (180) because we don't support zero-fee anchor outputs + override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) + } } @Serializable @@ -353,7 +359,8 @@ data class Features(val activated: Map, val unknown: Se Feature.ChannelBackupProvider, Feature.ExperimentalSplice, Feature.OnTheFlyFunding, - Feature.FundingFeeCredit + Feature.FundingFeeCredit, + Feature.SimpleTaprootStaging ) operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray()) @@ -387,7 +394,8 @@ data class Features(val activated: Map, val unknown: Se Feature.TrampolinePayment to listOf(Feature.PaymentSecret), Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret), Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice), - Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding) + Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding), + Feature.SimpleTaprootStaging to listOf(Feature.StaticRemoteKey) ) class FeatureException(message: String) : IllegalArgumentException(message) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt index cef290e79..053a8ebb4 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -212,6 +212,7 @@ data class NodeParams( Feature.ExperimentalSplice to FeatureSupport.Optional, Feature.OnTheFlyFunding to FeatureSupport.Optional, Feature.FundingFeeCredit to FeatureSupport.Optional, + Feature.SimpleTaprootStaging to FeatureSupport.Optional, ), dustLimit = 546.sat, maxRemoteDustLimit = 600.sat, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt index 2e0c08f87..799522d7e 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt @@ -21,6 +21,7 @@ data class InvalidFundingAmount (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/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt index ed359c5c9..049fe92e6 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt @@ -59,6 +59,10 @@ 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.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels, Feature.SimpleTaprootStaging) + } } data class UnsupportedChannelType(val featureBits: Features) : ChannelType() { @@ -73,6 +77,7 @@ sealed class ChannelType { // @formatter:off 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 + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory, Feature.SimpleTaprootStaging to FeatureSupport.Mandatory) -> SupportedChannelType.SimpleTaprootStaging else -> UnsupportedChannelType(features) // @formatter:on } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt index 601cee6b4..a43fe0b80 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt +++ b/modules/core/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.ShortChannelId import fr.acinq.lightning.blockchain.fee.FeeratePerByte @@ -47,6 +51,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) @@ -95,13 +101,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, log: MDCLogger + ): Either { val (localCommitTx, sortedHtlcTxs) = Commitments.makeLocalTxs( keyManager, commitTxNumber = localCommitIndex, @@ -113,13 +121,27 @@ 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 localNonce = keyManager.verificationNonce(fundingTxIndex, localCommitIndex) + 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) } + 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" } + log.error(check.error) { "remote signature $commit is invalid, localNonce = ${keyManager.verificationNonce(fundingTxIndex, localCommitIndex)}" } return Either.Left(InvalidCommitmentSignature(params.channelId, signedCommitTx.tx.txid)) } else -> {} @@ -127,7 +149,7 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishabl if (commit.htlcSignatures.size != sortedHtlcTxs.size) { return Either.Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) } - val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, keyManager.htlcKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) } + val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, keyManager.htlcKey.deriveForCommitment(localPerCommitmentPoint)) } val remoteHtlcPubkey = params.remoteParams.htlcBasepoint.deriveForCommitment(localPerCommitmentPoint) // combine the sigs to make signed txs val htlcTxsAndSigs = Triple(sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped().map { (htlcTx, localSig, remoteSig) -> @@ -141,7 +163,7 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishabl is HtlcTx.HtlcSuccessTx -> { // we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig // which was created with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) { + if (!htlcTx.checkSig(remoteSig, remoteHtlcPubkey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) { return Either.Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) } HtlcTxAndSigs(htlcTx, localSig, remoteSig) @@ -155,7 +177,7 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishabl /** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxId, val remotePerCommitmentPoint: PublicKey) { - fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo): CommitSig { + fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, remoteNonce: IndividualNonce?): CommitSig { val (remoteCommitTx, sortedHtlcsTxs) = Commitments.makeRemoteTxs( channelKeys, index, @@ -167,14 +189,25 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI remotePerCommitmentPoint = remotePerCommitmentPoint, spec ) - val sig = Transactions.sign(remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val sig = remoteCommitTx.sign(channelKeys.fundingKey(fundingTxIndex)) + val partialSig = when (remoteNonce) { + null -> null + else -> { + val localNonce = channelKeys.signingNonce(fundingTxIndex) + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + PartialSignatureWithNonce( + Transactions.partialSign(remoteCommitTx, fundingKey, fundingKey.publicKey(), remoteFundingPubKey, localNonce, remoteNonce).right!!, + localNonce.second + ) + } + } // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - val htlcSigs = sortedHtlcsTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } - return CommitSig(params.channelId, sig, htlcSigs.toList()) + val htlcSigs = sortedHtlcsTxs.map { it.sign(channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } + return CommitSig(params.channelId, sig, htlcSigs.toList(), TlvStream(setOfNotNull(partialSig?.let { CommitSigTlv.PartialSignatureWithNonceTlv(it) }))) } - fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, signingSession: InteractiveTxSigningSession): CommitSig = - sign(channelKeys, params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubkey, signingSession.commitInput) + fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, signingSession: InteractiveTxSigningSession, remoteNonce: IndividualNonce?): CommitSig = + sign(channelKeys, params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubkey, signingSession.commitInput, remoteNonce) } /** We have the next remote commit when we've sent our commit_sig but haven't yet received their revoke_and_ack. */ @@ -259,9 +292,9 @@ data class Commitment( val balanceNoFees = (reduced.toRemote - localChannelReserve(params).toMilliSatoshi()).coerceAtLeast(0.msat) return if (params.localParams.paysCommitTxFees) { // The initiator always pays the on-chain fees, so we must subtract that from the amount we can send. - val commitFees = commitTxFeeMsat(params.remoteParams.dustLimit, reduced) + val commitFees = commitTxFeeMsat(params.remoteParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) // the initiator needs to keep a "initiator fee buffer" (see explanation above) - val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) + htlcOutputFee(reduced.feerate * 2) val amountToReserve = commitFees.coerceAtLeast(initiatorFeeBuffer) if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(params.remoteParams.dustLimit, reduced).toMilliSatoshi()) { // htlc will be trimmed @@ -288,9 +321,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 @@ -356,10 +389,10 @@ data class Commitment( val outgoingHtlcs = reduced.htlcs.incomings() // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment - val fees = commitTxFee(params.remoteParams.dustLimit, reduced) + val fees = commitTxFee(params.remoteParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) // the initiator needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid // getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728). - val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2)) + htlcOutputFee(reduced.feerate * 2) + val initiatorFeeBuffer = commitTxFeeMsat(params.remoteParams.dustLimit, reduced.copy(feerate = reduced.feerate * 2), params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) + htlcOutputFee(reduced.feerate * 2) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. val missingForSender = reduced.toRemote - localChannelReserve(params).toMilliSatoshi() - (if (params.localParams.paysCommitTxFees) fees.toMilliSatoshi().coerceAtLeast(initiatorFeeBuffer) else 0.msat) @@ -408,7 +441,7 @@ data class Commitment( val incomingHtlcs = reduced.htlcs.incomings() // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment - val fees = commitTxFee(params.localParams.dustLimit, reduced) + val fees = commitTxFee(params.localParams.dustLimit, reduced, params.channelFeatures.hasFeature(Feature.SimpleTaprootStaging)) // NB: we don't enforce the initiatorFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. val missingForSender = reduced.toRemote - remoteChannelReserve(params).toMilliSatoshi() - (if (params.localParams.paysCommitTxFees) 0.sat else fees).toMilliSatoshi() @@ -441,7 +474,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)) @@ -458,7 +491,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)) @@ -467,7 +500,15 @@ data class Commitment( } } - fun sendCommit(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, log: MDCLogger): Pair { + fun sendCommit( + channelKeys: KeyManager.ChannelKeys, + params: ChannelParams, + changes: CommitmentChanges, + remoteNextPerCommitmentPoint: PublicKey, + batchSize: Int, + remoteNonce: IndividualNonce?, + log: MDCLogger + ): Pair { // remote commitment will include all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val (remoteCommitTx, sortedHtlcTxs) = Commitments.makeRemoteTxs( @@ -482,6 +523,20 @@ data class Commitment( spec ) val sig = Transactions.sign(remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val partialSig = when (remoteCommitTx.input) { + is Transactions.InputInfo.SegwitInput -> { + null + } + + is Transactions.InputInfo.TaprootInput -> { + // we generate a new nonce each time we sign their commit tx + val localNonce = channelKeys.signingNonce(fundingTxIndex) + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val psig = Transactions.partialSign(remoteCommitTx, fundingKey, fundingKey.publicKey(), remoteFundingPubkey, localNonce, remoteNonce!!).right!! + log.debug { "sendCommit: creating partial sig $psig for remote commit tx ${remoteCommitTx.tx.txid} with remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint" } + PartialSignatureWithNonce(psig, localNonce.second) + } + } // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remoteNextPerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } @@ -495,18 +550,50 @@ data class Commitment( val tlvs = buildSet { if (spec.htlcs.isEmpty()) { - val alternativeSigs = Commitments.alternativeFeerates.map { feerate -> + val alternativeRemoteCommitTxs = Commitments.alternativeFeerates.map { feerate -> val alternativeSpec = spec.copy(feerate = feerate) - val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs(channelKeys, commitTxNumber = remoteCommit.index + 1, params.localParams, params.remoteParams, fundingTxIndex = fundingTxIndex, remoteFundingPubKey = remoteFundingPubkey, commitInput, remotePerCommitmentPoint = remoteNextPerCommitmentPoint, alternativeSpec) - val alternativeSig = Transactions.sign(alternativeRemoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) - CommitSigTlv.AlternativeFeerateSig(feerate, alternativeSig) + val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( + channelKeys, + commitTxNumber = remoteCommit.index + 1, + params.localParams, + params.remoteParams, + fundingTxIndex = fundingTxIndex, + remoteFundingPubKey = remoteFundingPubkey, + commitInput, + remotePerCommitmentPoint = remoteNextPerCommitmentPoint, + alternativeSpec + ) + feerate to alternativeRemoteCommitTx + } + when (remoteNonce) { + null -> { + val alternativeSigs = alternativeRemoteCommitTxs.map { + val alternativeSig = Transactions.sign(it.second, channelKeys.fundingKey(fundingTxIndex)) + CommitSigTlv.AlternativeFeerateSig(it.first, alternativeSig) + } + add(CommitSigTlv.AlternativeFeerateSigs(alternativeSigs)) + } + + else -> { + val alternativeSigs = alternativeRemoteCommitTxs.map { + val localNonce = channelKeys.signingNonce(fundingTxIndex) + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val alternativePsig = Transactions.partialSign(it.second, fundingKey, fundingKey.publicKey(), remoteFundingPubkey, localNonce, remoteNonce).right!! + log.debug { "sendCommit: creating partial sig $alternativePsig for alternative remote commit tx ${it.second.tx.txid} with remote nonce $remoteNonce and remoteNextPerCommitmentPoint = $remoteNextPerCommitmentPoint" } + CommitSigTlv.AlternativeFeeratePartialSig(it.first, PartialSignatureWithNonce(alternativePsig, localNonce.second)) + } + add(CommitSigTlv.AlternativeFeeratePartialSigs(alternativeSigs)) + } } - add(CommitSigTlv.AlternativeFeerateSigs(alternativeSigs)) } if (batchSize > 1) { add(CommitSigTlv.Batch(batchSize)) } + if (partialSig != null) { + add(CommitSigTlv.PartialSignatureWithNonceTlv(partialSig)) + } } + val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), TlvStream(tlvs)) val commitment1 = copy(nextRemoteCommit = NextRemoteCommit(commitSig, RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint))) return Pair(commitment1, commitSig) @@ -559,6 +646,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) @@ -571,7 +659,10 @@ data class Commitments( val payments: Map, // for outgoing htlcs, maps to paymentId val remoteNextCommitInfo: Either, // this one is tricky, it must be kept in sync with Commitment.nextRemoteCommit val remotePerCommitmentSecrets: ShaChain, - val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty + val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty, + val nextRemoteNonces: List = listOf(), + val closingNonce: Pair? = null, + val pendingRemoteNextLocalNonce: IndividualNonce? = null ) { init { require(active.isNotEmpty()) { "there must be at least one active commitment" } @@ -580,6 +671,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 @@ -767,7 +859,11 @@ data class Commitments( fun sendCommit(channelKeys: KeyManager.ChannelKeys, log: MDCLogger): Either>> { val remoteNextPerCommitmentPoint = remoteNextCommitInfo.right ?: return Either.Left(CannotSignBeforeRevocation(channelId)) if (!changes.localHasChanges()) return Either.Left(CannotSignWithoutChanges(channelId)) - val (active1, sigs) = active.map { it.sendCommit(channelKeys, params, changes, remoteNextPerCommitmentPoint, active.size, log) }.unzip() + val (active1, sigs) = when (isTaprootChannel) { + true -> active.zip(nextRemoteNonces).map { it.first.sendCommit(channelKeys, params, changes, remoteNextPerCommitmentPoint, active.size, it.second, log) }.unzip() + false -> active.map { it.sendCommit(channelKeys, params, changes, remoteNextPerCommitmentPoint, active.size, null, log) }.unzip() + } + val commitments1 = copy( active = active1, remoteNextCommitInfo = Either.Left(WaitingForRevocation(localCommitIndex)), @@ -796,7 +892,15 @@ data class Commitments( // we will send our revocation preimage + our next revocation hash val localPerCommitmentSecret = channelKeys.commitmentSecret(localCommitIndex) val localNextPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex + 2) - val revocation = RevokeAndAck(channelId, localPerCommitmentSecret, localNextPerCommitmentPoint) + val tlvStream: TlvStream = when (isTaprootChannel) { + true -> { + val nonces = active.map { channelKeys.verificationNonce(it.fundingTxIndex, localCommitIndex + 2) } + TlvStream(RevokeAndAckTlv.NextLocalNoncesTlv(nonces.map { it.second })) + } + + false -> TlvStream.empty() + } + val revocation = RevokeAndAck(channelId, localPerCommitmentSecret, localNextPerCommitmentPoint, tlvStream) val commitments1 = copy( active = active1, changes = changes.copy( @@ -814,6 +918,8 @@ data class Commitments( val remoteCommit = active.first().remoteCommit if (revocation.perCommitmentSecret.publicKey() != remoteCommit.remotePerCommitmentPoint) return Either.Left(InvalidRevocation(channelId)) + if (isTaprootChannel && revocation.nextLocalNonces.isEmpty()) return Either.Left(MissingNextLocalNonces(channelId)) + // the outgoing following htlcs have been completed (fulfilled or failed) when we received this revocation // they have been removed from both local and remote commitment // since fulfill/fail are sent by remote, they are (1) signed by them, (2) revoked by us, (3) signed by us, (4) revoked by them @@ -858,7 +964,8 @@ data class Commitments( remoteNextCommitInfo = Either.Right(revocation.nextPerCommitmentPoint), remotePerCommitmentSecrets = remotePerCommitmentSecrets.addHash(revocation.perCommitmentSecret.value, 0xFFFFFFFFFFFFL - remoteCommitIndex), payments = payments1, - remoteChannelData = revocation.channelData + remoteChannelData = revocation.channelData, + nextRemoteNonces = revocation.nextLocalNonces ) return Either.Right(Pair(commitments1, actions.toList())) } @@ -1004,6 +1111,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 @@ -1050,6 +1158,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, @@ -1061,7 +1170,8 @@ data class Commitments( remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, - spec + spec, + isTaprootChannel ) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isChannelOpener, outputs) val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feerate, outputs) @@ -1084,6 +1194,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), @@ -1095,7 +1206,8 @@ data class Commitments( localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, - spec + spec, + isTaprootChannel ) // NB: we are creating the remote commit tx, so local/remote parameters are inverted. val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentPubkey, !localParams.isChannelOpener, outputs) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt index 88e92a4ac..28ccf6d15 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt @@ -3,10 +3,14 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Crypto.ripemd160 import fr.acinq.bitcoin.Crypto.sha256 +import fr.acinq.bitcoin.Script.pay2tr import fr.acinq.bitcoin.Script.pay2wsh import fr.acinq.bitcoin.Script.write +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.bitcoin.utils.Try +import fr.acinq.bitcoin.utils.flatMap import fr.acinq.bitcoin.utils.runTrying import fr.acinq.lightning.Feature import fr.acinq.lightning.Features @@ -28,6 +32,8 @@ import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.logging.LoggingContext import fr.acinq.lightning.transactions.* import fr.acinq.lightning.transactions.Scripts.multiSig2of2 +import fr.acinq.lightning.transactions.Scripts.Taproot.musig2Aggregate +import fr.acinq.lightning.transactions.Scripts.Taproot.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 @@ -192,8 +198,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( @@ -201,15 +210,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)) - ) + return if (isTaprootChannel) { + val fundingScript = musig2FundingScript(fundingPubkey1, fundingPubkey2) + val fundingTxOut = TxOut(fundingAmount, fundingScript) + Transactions.InputInfo.TaprootInput(OutPoint(fundingTxId, fundingTxOutputIndex.toLong()), fundingTxOut, musig2Aggregate(fundingPubkey1, fundingPubkey2), Transactions.InputInfo.RedeemPath.KeyPath(null)) + } else { + val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) + val fundingTxOut = TxOut(fundingAmount, pay2wsh(fundingScript)) + Transactions.InputInfo.SegwitInput(OutPoint(fundingTxId, fundingTxOutputIndex.toLong()), fundingTxOut, ByteVector(write(fundingScript))) + } } data class PairOfCommitTxs( @@ -247,12 +259,13 @@ object Helpers { val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote) val remoteSpec = CommitmentSpec(localHtlcs.map { it.opposite() }.toSet(), commitTxFeerate, toLocal = toRemote, toRemote = toLocal) + val isTaprootChannel = Features.canUseFeature(localParams.features, remoteParams.features, Feature.SimpleTaprootStaging) if (!localParams.paysCommitTxFees) { // They are responsible for paying the commitment transaction fee: we need to make sure they can afford it! // Note that the reserve may not be always be met: we could be using dual funding with a large funding amount on // our side and a small funding amount on their side. But we shouldn't care as long as they can pay the fees for // the commitment transaction. - val fees = commitTxFee(remoteParams.dustLimit, remoteSpec) + val fees = commitTxFee(remoteParams.dustLimit, remoteSpec, isTaprootChannel) val missing = fees - remoteSpec.toLocal.truncateToSatoshi() if (missing > 0.sat) { return Either.Left(CannotAffordFirstCommitFees(channelId, missing = missing, fees = fees)) @@ -260,7 +273,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, @@ -328,10 +341,12 @@ object Helpers { commitment: FullCommitment, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, - requestedFeerate: ClosingFeerates + requestedFeerate: ClosingFeerates, + localClosingNonce: Pair? = null, + remoteClosingNonce: IndividualNonce? = null ): Pair { val closingFees = firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, requestedFeerate) - return makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFees) + return makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFees, localClosingNonce, remoteClosingNonce) } fun makeClosingTx( @@ -339,15 +354,33 @@ object Helpers { commitment: FullCommitment, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, - closingFees: ClosingFees + closingFees: ClosingFees, + localClosingNonce: Pair? = null, + remoteClosingNonce: IndividualNonce? = null ): Pair { val allowAnySegwit = Features.canUseFeature(commitment.params.localParams.features, commitment.params.remoteParams.features, Feature.ShutdownAnySegwit) require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit)) { "invalid localScriptPubkey" } require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit)) { "invalid remoteScriptPubkey" } val dustLimit = commitment.params.localParams.dustLimit.max(commitment.params.remoteParams.dustLimit) val closingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.params.localParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) - val localClosingSig = Transactions.sign(closingTx, channelKeys.fundingKey(commitment.fundingTxIndex)) - val closingSigned = ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) + val closingSigned = when (commitment.isTaprootChannel) { + true -> { + val localClosingPartialSig = Transactions.partialSign( + closingTx, + channelKeys.fundingKey(commitment.fundingTxIndex), + channelKeys.fundingKey(commitment.fundingTxIndex).publicKey(), + commitment.remoteFundingPubkey, + localClosingNonce!!, + remoteClosingNonce!! + ).right!! + ClosingSigned(commitment.channelId, closingFees.preferred, ByteVector64.Zeroes, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max), ClosingSignedTlv.PartialSignature(localClosingPartialSig))) + } + + else -> { + val localClosingSig = Transactions.sign(closingTx, channelKeys.fundingKey(commitment.fundingTxIndex)) + ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) + } + } return Pair(closingTx, closingSigned) } @@ -371,6 +404,37 @@ object Helpers { } } + fun checkClosingSignature( + channelKeys: KeyManager.ChannelKeys, + commitment: FullCommitment, + localScriptPubkey: ByteArray, + remoteScriptPubkey: ByteArray, + remoteClosingFee: Satoshi, + localClosingNonce: Pair, + remoteClosingNonce: IndividualNonce, + remoteClosingPartialSig: ByteVector32 + ): Either> { + val (closingTx, closingSigned) = makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee), localClosingNonce, remoteClosingNonce) + return if (checkClosingDustAmounts(closingTx)) { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val signingAttempt = + Transactions.aggregatePartialSignatures(closingTx, closingSigned.partialSignature!!, remoteClosingPartialSig, fundingKey.publicKey(), commitment.remoteFundingPubkey, localClosingNonce.second, remoteClosingNonce) + .map { aggsig -> Transactions.addAggregatedSignature(closingTx, aggsig) } + when (signingAttempt) { + is Either.Left -> Either.Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + is Either.Right -> { + val signedClosingTx = signingAttempt.right + when (Transactions.checkSpendable(signedClosingTx)) { + is Try.Success -> Either.Right(Pair(signedClosingTx, closingSigned)) + is Try.Failure -> Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + } + } + } + } else { + Either.Left(InvalidCloseAmountBelowDust(commitment.channelId, closingTx.tx.txid)) + } + } + /** * Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk * that the closing transaction will not be relayed to miners' mempool and will not confirm. @@ -417,7 +481,7 @@ object Helpers { feerateDelayed ) }?.let { - val sig = Transactions.sign(it, channelKeys.delayedPaymentKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) + val sig = it.sign(channelKeys.delayedPaymentKey.deriveForCommitment(localPerCommitmentPoint)) Transactions.addSigs(it, sig) } @@ -441,7 +505,7 @@ object Helpers { // all htlc output to us are delayed, so we need to claim them as soon as the delay is over val htlcDelayedTxs = htlcTxs.values.filterNotNull().mapNotNull { txInfo -> generateTx("claim-htlc-delayed") { - Transactions.makeClaimLocalDelayedOutputTx( + Transactions.makeHtlcDelayedTx( txInfo.tx, localParams.dustLimit, localRevocationPubkey, @@ -507,7 +571,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 @@ -660,7 +725,7 @@ object Helpers { params.localParams.defaultFinalScriptPubKey.toByteArray(), params.localParams.toSelfDelay, remoteDelayedPaymentPubkey, - feeratePenalty + feeratePenalty, ) }?.let { val sig = Transactions.sign(it, channelKeys.revocationKey.deriveForRevocation(remotePerCommitmentSecret)) @@ -689,27 +754,59 @@ object Helpers { // we retrieve the information needed to rebuild htlc scripts logger.info { "found ${htlcInfos.size} htlcs for txid=${revokedCommitPublished.commitTx.txid}" } - val htlcsRedeemScripts = htlcInfos.flatMap { htlcInfo -> - val htlcReceived = Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash), htlcInfo.cltvExpiry) - val htlcOffered = Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash)) - listOf(htlcReceived, htlcOffered) - }.associate { redeemScript -> write(pay2wsh(redeemScript)).toByteVector() to write(redeemScript).toByteVector() } - - // and finally we steal the htlc outputs - val htlcPenaltyTxs = revokedCommitPublished.commitTx.txOut.mapIndexedNotNull { outputIndex, txOut -> - htlcsRedeemScripts[txOut.publicKeyScript]?.let { redeemScript -> - generateTx("htlc-penalty") { - Transactions.makeHtlcPenaltyTx( - revokedCommitPublished.commitTx, - outputIndex, - redeemScript.toByteArray(), - params.localParams.dustLimit, - params.localParams.defaultFinalScriptPubKey.toByteArray(), - feeratePenalty - ) - }?.let { htlcPenaltyTx -> - val sig = Transactions.sign(htlcPenaltyTx, channelKeys.revocationKey.deriveForRevocation(revokedCommitPublished.remotePerCommitmentSecret)) - Transactions.addSigs(htlcPenaltyTx, sig, remoteRevocationPubkey) + val htlcPenaltyTxs = when (params.isTaprootChannel) { + true -> { + val scriptTrees = htlcInfos.flatMap { htlcInfo -> + val receivedTree = Scripts.Taproot.receivedHtlcTree(remoteHtlcPubkey, localHtlcPubkey, htlcInfo.paymentHash, htlcInfo.cltvExpiry) + val offeredTree = Scripts.Taproot.offeredHtlcTree(remoteHtlcPubkey, localHtlcPubkey, htlcInfo.paymentHash) + listOf(receivedTree, offeredTree) + }.associate { scriptTree -> write(pay2tr(remoteRevocationPubkey.xOnly(), scriptTree)).byteVector() to scriptTree } + + // and finally we steal the htlc outputs + revokedCommitPublished.commitTx.txOut.mapIndexedNotNull { outputIndex, txOut -> + scriptTrees[txOut.publicKeyScript]?.let { scriptTree -> + generateTx("htlc-penalty") { + Transactions.makeHtlcPenaltyTx( + revokedCommitPublished.commitTx, + outputIndex, + remoteRevocationPubkey.xOnly(), + scriptTree, + params.localParams.dustLimit, + params.localParams.defaultFinalScriptPubKey.toByteArray(), + feeratePenalty + ) + }?.let { htlcPenaltyTx -> + val sig = Transactions.sign(htlcPenaltyTx, channelKeys.revocationKey.deriveForRevocation(revokedCommitPublished.remotePerCommitmentSecret)) + Transactions.addSigs(htlcPenaltyTx, sig, remoteRevocationPubkey) + } + } + } + } + + else -> { + val htlcsRedeemScripts = htlcInfos.flatMap { htlcInfo -> + val htlcReceived = Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash), htlcInfo.cltvExpiry) + val htlcOffered = Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash)) + listOf(htlcReceived, htlcOffered) + }.associate { redeemScript -> write(pay2wsh(redeemScript)).toByteVector() to write(redeemScript).toByteVector() } + + // and finally we steal the htlc outputs + revokedCommitPublished.commitTx.txOut.mapIndexedNotNull { outputIndex, txOut -> + htlcsRedeemScripts[txOut.publicKeyScript]?.let { redeemScript -> + generateTx("htlc-penalty") { + Transactions.makeHtlcPenaltyTx( + revokedCommitPublished.commitTx, + outputIndex, + redeemScript.toByteArray(), + params.localParams.dustLimit, + params.localParams.defaultFinalScriptPubKey.toByteArray(), + feeratePenalty + ) + }?.let { htlcPenaltyTx -> + val sig = Transactions.sign(htlcPenaltyTx, channelKeys.revocationKey.deriveForRevocation(revokedCommitPublished.remotePerCommitmentSecret)) + Transactions.addSigs(htlcPenaltyTx, sig, remoteRevocationPubkey) + } + } } } } @@ -760,7 +857,7 @@ object Helpers { params.localParams.toSelfDelay, remoteDelayedPaymentPubkey, params.localParams.defaultFinalScriptPubKey.toByteArray(), - feeratePenalty + feeratePenalty, ).mapNotNull { claimDelayedOutputPenaltyTx -> generateTx("claim-htlc-delayed-penalty") { claimDelayedOutputPenaltyTx diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index d601dcb10..c1fc7c102 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -51,6 +51,24 @@ sealed class SharedFundingInput { const val weight: Int = 388 } } + + data class Musig2Input(override val info: Transactions.InputInfo, val fundingTxIndex: Long, val remoteFundingPubkey: PublicKey) : SharedFundingInput() { + + constructor(commitment: Commitment) : this( + info = commitment.commitInput, + fundingTxIndex = commitment.fundingTxIndex, + remoteFundingPubkey = commitment.remoteFundingPubkey + ) + + // This value was computed assuming 73 bytes signatures (worst-case scenario). + override val weight: Int = Musig2Input.weight + + override fun sign(channelKeys: KeyManager.ChannelKeys, tx: Transaction): ByteVector64 = ByteVector64.Zeroes + + companion object { + const val weight: Int = 234 + } + } } /** The current balances of a [[SharedFundingInput]]. */ @@ -94,9 +112,13 @@ data class InteractiveTxParams( // BOLT 2: the initiator's serial IDs MUST use even values and the non-initiator odd values. val serialIdParity = if (isInitiator) 0 else 1 - fun fundingPubkeyScript(channelKeys: KeyManager.ChannelKeys): ByteVector { - val fundingTxIndex = (sharedInput as? SharedFundingInput.Multisig2of2)?.let { it.fundingTxIndex + 1 } ?: 0 - return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey) + fun fundingPubkeyScript(channelKeys: KeyManager.ChannelKeys, isTaprootChannel: Boolean): ByteVector { + val fundingTxIndex = when (sharedInput) { + is SharedFundingInput.Multisig2of2 -> sharedInput.fundingTxIndex + 1 + is SharedFundingInput.Musig2Input -> sharedInput.fundingTxIndex + 1 + null -> 0 + } + return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey, isTaprootChannel) } fun liquidityFees(purchase: LiquidityAds.Purchase?): MilliSatoshi = purchase?.let { l -> @@ -270,9 +292,10 @@ data class FundingContributions(val inputs: List, v swapInKeys: KeyManager.SwapInOnChainKeys, params: InteractiveTxParams, walletInputs: List, - liquidityPurchase: LiquidityAds.Purchase? + liquidityPurchase: LiquidityAds.Purchase?, + isTaprootChannel: Boolean = false ): Either { - return create(channelKeys, swapInKeys, params, null, walletInputs, listOf(), liquidityPurchase) + return create(channelKeys, swapInKeys, params, null, walletInputs, listOf(), liquidityPurchase, isTaprootChannel = isTaprootChannel) } /** @@ -289,7 +312,8 @@ data class FundingContributions(val inputs: List, v walletInputs: List, localOutputs: List, liquidityPurchase: LiquidityAds.Purchase?, - changePubKey: PublicKey? = null + changePubKey: PublicKey? = null, + isTaprootChannel: Boolean = false ): Either { walletInputs.forEach { utxo -> if (utxo.previousTx.txOut.size <= utxo.outputIndex) return Either.Left(FundingContributionFailure.InputOutOfBounds(utxo.txId, utxo.outputIndex)) @@ -313,7 +337,7 @@ data class FundingContributions(val inputs: List, v return Either.Left(FundingContributionFailure.InvalidFundingBalances(params.fundingAmount, nextLocalBalanceAfterPush, nextRemoteBalanceAfterPush)) } - val fundingPubkeyScript = params.fundingPubkeyScript(channelKeys) + val fundingPubkeyScript = params.fundingPubkeyScript(channelKeys, isTaprootChannel) // We use local and remote balances before amounts are pushed to allow computing the local and remote mining fees. val sharedOutput = listOf(InteractiveTxOutput.Shared(0, fundingPubkeyScript, nextLocalBalanceBeforePush, nextRemoteBalanceBeforePush, sharedUtxo?.second?.toHtlcs ?: 0.msat)) val nonChangeOutputs = localOutputs.map { o -> InteractiveTxOutput.Local.NonChange(0, o.amount, o.publicKeyScript) } @@ -460,18 +484,29 @@ data class SharedTransaction( fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { val unsignedTx = buildUnsignedTx() - val sharedSig = fundingParams.sharedInput?.sign(keyManager.channelKeys(localParams.fundingKeyPath), unsignedTx) + val sharedSig = when (fundingParams.sharedInput) { + is SharedFundingInput.Multisig2of2 -> + fundingParams.sharedInput.sign(keyManager.channelKeys(localParams.fundingKeyPath), unsignedTx) + + else -> ByteVector64.Zeroes + } // NB: the order in this list must match the order of the transaction's inputs. val previousOutputs = unsignedTx.txIn.map { spentOutputs[it.outPoint]!! } - // Public nonces for all the musig2 swap-in inputs (local and remote). - // We have verified that one nonce was provided for each input when receiving `tx_complete`. - val remoteNonces: Map = when (session.txCompleteReceived) { - null -> mapOf() - else -> (localInputs.filterIsInstance() + remoteInputs.filterIsInstance()) - .sortedBy { it.serialId } - .zip(session.txCompleteReceived.publicNonces) - .associate { it.first.serialId to it.second } + val sharedPartialSig = when (fundingParams.sharedInput) { + is SharedFundingInput.Musig2Input -> { + val sharedInputs = session.localInputs.filterIsInstance() + session.remoteInputs.filterIsInstance() + // there should be a single shared input + val serialId = sharedInputs.first().serialId + val localNonce = session.secretNonces[serialId]!! + val fundingKey = keyManager.channelKeys(localParams.fundingKeyPath).fundingKey(fundingParams.sharedInput.fundingTxIndex) + val inputIndex = unsignedTx.txIn.indexOfFirst { it.outPoint == fundingParams.sharedInput.info.outPoint } + val remoteNonce = session.remoteNonces[serialId]!! + val psig = Transactions.partialSign(fundingKey, unsignedTx, inputIndex, previousOutputs, fundingKey.publicKey(), fundingParams.sharedInput.remoteFundingPubkey, localNonce, remoteNonce) + PartialSignatureWithNonce(psig.right!!, localNonce.second) + } + + else -> null } // If we are swapping funds in, we provide our partial signatures to the corresponding inputs. @@ -488,7 +523,7 @@ data class SharedTransaction( ?.let { input -> // We generate our secret nonce when sending the corresponding input, we know it exists in the map. val userNonce = session.secretNonces[input.serialId]!! - val serverNonce = remoteNonces[input.serialId]!! + val serverNonce = session.remoteNonces[input.serialId]!! keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, previousOutputs, userNonce.first, userNonce.second, serverNonce, input.addressIndex) .map { TxSignaturesTlv.PartialSignature(it, userNonce.second, serverNonce) } .getOrDefault(null) @@ -515,14 +550,14 @@ data class SharedTransaction( val swapInProtocol = SwapInProtocol(input.userKey, serverKey.publicKey(), input.userRefundKey, input.refundDelay) // We generate our secret nonce when receiving the corresponding input, we know it exists in the map. val serverNonce = session.secretNonces[input.serialId]!! - val userNonce = remoteNonces[input.serialId]!! + val userNonce = session.remoteNonces[input.serialId]!! swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, serverKey, serverNonce.first, userNonce, serverNonce.second) .map { TxSignaturesTlv.PartialSignature(it, userNonce, serverNonce.second) } .getOrDefault(null) } }.filterNotNull() - return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) + return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, sharedPartialSig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) } } @@ -547,6 +582,8 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, if (remoteSigs.swapInServerPartialSigs.size != tx.localInputs.filterIsInstance().size) return null if (remoteSigs.witnesses.size != tx.remoteOnlyInputs().size) return null if (remoteSigs.txId != localSigs.txId) return null + if (fundingParams.sharedInput is SharedFundingInput.Musig2Input && remoteSigs.previousFundingTxPartialSig == null) return null + val sharedSigs = fundingParams.sharedInput?.let { when (it) { is SharedFundingInput.Multisig2of2 -> Scripts.witness2of2( @@ -555,6 +592,22 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, channelKeys.fundingPubKey(it.fundingTxIndex), it.remoteFundingPubkey, ) + + is SharedFundingInput.Musig2Input -> { + val localFundingPubkey = channelKeys.fundingPubKey(it.fundingTxIndex) + val unsignedTx = this.tx.buildUnsignedTx() + val inputIndex = unsignedTx.txIn.indexOfFirst { i -> i.outPoint == it.info.outPoint } + val aggSig = Musig2.aggregateTaprootSignatures( + listOf(localSigs.previousFundingTxPartialSig!!.partialSig, remoteSigs.previousFundingTxPartialSig!!.partialSig), + unsignedTx, + inputIndex, + unsignedTx.txIn.map { i -> tx.spentOutputs[i.outPoint]!! }, + Scripts.sort(listOf(localFundingPubkey, it.remoteFundingPubkey)), + listOf(localSigs.previousFundingTxPartialSig.nonce, remoteSigs.previousFundingTxPartialSig.nonce), + null + ) + Script.witnessKeyPathPay2tr(aggSig.right!!) + } } } val fullySignedTx = FullySignedSharedTransaction(tx, localSigs, remoteSigs, sharedSigs) @@ -656,6 +709,10 @@ data class InteractiveTxSession( val txCompleteReceived: TxComplete? = null, val inputsReceivedCount: Int = 0, val outputsReceivedCount: Int = 0, + val fundingTxIndex: Long, + val localCommitmentIndex: Long, + val remoteCommitmentIndex: Long, + val useTaproot: Boolean = false, val secretNonces: Map> = mapOf() ) { @@ -682,7 +739,11 @@ data class InteractiveTxSession( previousRemoteBalance: MilliSatoshi, localHtlcs: Set, fundingContributions: FundingContributions, - previousTxs: List = listOf() + previousTxs: List = listOf(), + fundingTxIndex: Long = 0, + localCommitmentIndex: Long = 0, + remoteCommitmentIndex: Long = 0, + useTaproot: Boolean = false ) : this( remoteNodeId, channelKeys, @@ -691,22 +752,66 @@ 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, + fundingTxIndex = fundingTxIndex, + localCommitmentIndex = localCommitmentIndex, + remoteCommitmentIndex = remoteCommitmentIndex, + useTaproot = useTaproot ) 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 (useTaproot) { + false -> listOf() + else -> localInputs.filterIsInstance() + remoteInputs.filterIsInstance() + } + val remoteNonces: Map = when (txCompleteReceived) { + null -> mapOf() + else -> { + val swapInMap = (localInputs.filterIsInstance() + remoteInputs.filterIsInstance()) + .sortedBy { it.serialId } + .zip(txCompleteReceived.swapInNonces) + .associate { it.first.serialId to it.second } + val sharedInputMap = sharedInputsThatNeedANonce + .sortedBy { it.serialId } + .zip(txCompleteReceived.fundingNonces) + .associate { it.first.serialId to it.second } + swapInMap + sharedInputMap + } + } + fun send(): Pair { return when (val msg = toSend.firstOrNull()) { null -> { val localSwapIns = localInputs.filterIsInstance() val remoteSwapIns = remoteInputs.filterIsInstance() - val publicNonces = (localSwapIns + remoteSwapIns) + val sharedLocalInputs = when (this.useTaproot) { + false -> listOf() + else -> localInputs.filterIsInstance() + } + val sharedRemoteInputs = when (this.useTaproot) { + false -> listOf() + else -> remoteInputs.filterIsInstance() + } + val swapInNonces = (localSwapIns + remoteSwapIns) .map { it.serialId } .sorted() // We generate secret nonces whenever we send and receive tx_add_input, so we know they exist in the map. .map { serialId -> secretNonces[serialId]!!.second } - val txComplete = TxComplete(fundingParams.channelId, publicNonces) + val fundingNonces = (sharedLocalInputs + sharedRemoteInputs) + .map { it.serialId } + .sorted() + // We generate secret nonces whenever we send and receive tx_add_input, so we know they exist in the map. + .map { serialId -> secretNonces[serialId]!!.second } + val commitNonces = if (this.useTaproot) { + listOf( + channelKeys.verificationNonce(fundingTxIndex, localCommitmentIndex).second, + channelKeys.verificationNonce(fundingTxIndex, localCommitmentIndex + 1).second, + ) + } else listOf() + val txComplete = TxComplete(fundingParams.channelId, swapInNonces, fundingNonces, commitNonces) val next = copy(txCompleteSent = txComplete) if (next.isComplete) { Pair(next, next.validateTx(txComplete)) @@ -736,7 +841,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 (useTaproot) { + false -> secretNonces + else -> { + val fundingTxIndex = when (val input = fundingParams.sharedInput) { + is SharedFundingInput.Musig2Input -> input.fundingTxIndex + else -> return Pair(this, InteractiveTxSessionAction.InvalidSharedInput(fundingParams.channelId, inputOutgoing.serialId)) + } + val secretNonce = channelKeys.signingNonce(fundingTxIndex) + secretNonces + (inputOutgoing.serialId to secretNonce) + } } else -> secretNonces } @@ -748,7 +867,9 @@ data class InteractiveTxSession( val next = copy(toSend = toSend.tail(), localOutputs = localOutputs + outputOutgoing, txCompleteSent = null) val txAddOutput = when (outputOutgoing) { is InteractiveTxOutput.Local -> TxAddOutput(fundingParams.channelId, outputOutgoing.serialId, outputOutgoing.amount, outputOutgoing.pubkeyScript) - is InteractiveTxOutput.Shared -> TxAddOutput(fundingParams.channelId, outputOutgoing.serialId, outputOutgoing.amount, outputOutgoing.pubkeyScript) + is InteractiveTxOutput.Shared -> { + TxAddOutput(fundingParams.channelId, outputOutgoing.serialId, outputOutgoing.amount, outputOutgoing.pubkeyScript) + } } Pair(next, InteractiveTxSessionAction.SendMessage(txAddOutput)) } @@ -815,14 +936,15 @@ data class InteractiveTxSession( if (message.sequence > 0xfffffffdU) { return Either.Left(InteractiveTxSessionAction.NonReplaceableInput(message.channelId, message.serialId, input.outPoint.txid, input.outPoint.index, message.sequence.toLong())) } - val secretNonces1 = when (input) { - // Generate a secret nonce for this input if we don't already have one. - is InteractiveTxInput.RemoteSwapIn -> when (secretNonces[input.serialId]) { - null -> { - val secretNonce = Musig2.generateNonce(randomBytes32(), swapInKeys.localServerPrivateKey(remoteNodeId), listOf(input.userKey, input.serverKey)) - secretNonces + (input.serialId to secretNonce) - } - else -> secretNonces + val secretNonces1 = when { + input is InteractiveTxInput.RemoteSwapIn && secretNonces[input.serialId] == null -> { + val secretNonce = Musig2.generateNonce(randomBytes32(), swapInKeys.localServerPrivateKey(remoteNodeId), listOf(input.userKey, input.serverKey)) + secretNonces + (input.serialId to secretNonce) + } + + input is InteractiveTxInput.Shared && this.fundingParams.sharedInput is SharedFundingInput.Musig2Input -> { + val secretNonce = channelKeys.signingNonce(fundingParams.sharedInput.fundingTxIndex) + secretNonces + (input.serialId to secretNonce) } else -> secretNonces } @@ -837,9 +959,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, useTaproot) && 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, useTaproot)) { 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)) @@ -894,13 +1016,10 @@ data class InteractiveTxSession( } } - private fun validateTx(txComplete: TxComplete?): InteractiveTxSessionAction { - // tx_complete MUST have been sent and received for us to reach this state, require is used here to tell the compiler that txCompleteSent and txCompleteReceived are not null - require(txCompleteSent != null) - require(txCompleteReceived != null) + private fun buildSharedTx(): Either { if (localInputs.size + remoteInputs.size > 252 || localOutputs.size + remoteOutputs.size > 252) { - return InteractiveTxSessionAction.InvalidTxInputOutputCount(fundingParams.channelId, localInputs.size + remoteInputs.size, localOutputs.size + remoteOutputs.size) + return Either.Left(InteractiveTxSessionAction.InvalidTxInputOutputCount(fundingParams.channelId, localInputs.size + remoteInputs.size, localOutputs.size + remoteOutputs.size)) } val sharedInputs = localInputs.filterIsInstance() + remoteInputs.filterIsInstance() @@ -911,7 +1030,7 @@ data class InteractiveTxSession( val remoteOnlyOutputs = remoteOutputs.filterIsInstance() if (sharedOutputs.size != 1) { - return InteractiveTxSessionAction.InvalidTxSharedOutput(fundingParams.channelId) + return Either.Left(InteractiveTxSessionAction.InvalidTxSharedOutput(fundingParams.channelId)) } val sharedOutput = sharedOutputs.first() @@ -925,31 +1044,29 @@ data class InteractiveTxSession( // we added capacity to the channel with a splice-in. val remoteReserve = ((fundingParams.fundingAmount - fundingParams.localContribution) / 100).max(fundingParams.dustLimit) if (sharedOutput.remoteAmount < remoteReserve && remoteOnlyOutputs.isNotEmpty()) { - return InteractiveTxSessionAction.InvalidTxBelowReserve(fundingParams.channelId, sharedOutput.remoteAmount.truncateToSatoshi(), remoteReserve) + return Either.Left(InteractiveTxSessionAction.InvalidTxBelowReserve(fundingParams.channelId, sharedOutput.remoteAmount.truncateToSatoshi(), remoteReserve)) } } if (sharedInputs.size != 1) { - return InteractiveTxSessionAction.InvalidTxSharedInput(fundingParams.channelId) + return Either.Left(InteractiveTxSessionAction.InvalidTxSharedInput(fundingParams.channelId)) } sharedInputs.first() } // Our peer must send us one nonce for each swap input (local and remote), ordered by serial_id. val swapInputsCount = localInputs.count { it is InteractiveTxInput.LocalSwapIn } + remoteInputs.count { it is InteractiveTxInput.RemoteSwapIn } - if (txCompleteReceived.publicNonces.size != swapInputsCount) { - return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, swapInputsCount, txCompleteReceived.publicNonces.size) - } + val sharedInputsCount = localInputs.count { it is InteractiveTxInput.Shared && this.useTaproot } + remoteInputs.count { it is InteractiveTxInput.Shared && this.useTaproot } val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputs, remoteOnlyInputs, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) val tx = sharedTx.buildUnsignedTx() if (sharedTx.localAmountIn < sharedTx.localAmountOut || sharedTx.remoteAmountIn < sharedTx.remoteAmountOut) { - return InteractiveTxSessionAction.InvalidTxChangeAmount(fundingParams.channelId, tx.txid) + return Either.Left(InteractiveTxSessionAction.InvalidTxChangeAmount(fundingParams.channelId, tx.txid)) } // The transaction isn't signed yet, and segwit witnesses can be arbitrarily low (e.g. when using an OP_1 script), // so we use empty witnesses to provide a lower bound on the transaction weight. if (tx.weight() > Transactions.MAX_STANDARD_TX_WEIGHT) { - return InteractiveTxSessionAction.InvalidTxWeight(fundingParams.channelId, tx.txid) + return Either.Left(InteractiveTxSessionAction.InvalidTxWeight(fundingParams.channelId, tx.txid)) } if (previousTxs.isNotEmpty()) { @@ -961,14 +1078,14 @@ data class InteractiveTxSession( val previousFeerate = Transactions.fee2rate(previousTxs.first().tx.fees, previousUnsignedTx.weight()) val nextFeerate = Transactions.fee2rate(sharedTx.fees, tx.weight()) if (nextFeerate <= previousFeerate) { - return InteractiveTxSessionAction.InvalidTxFeerate(fundingParams.channelId, tx.txid, fundingParams.targetFeerate, nextFeerate) + return Either.Left(InteractiveTxSessionAction.InvalidTxFeerate(fundingParams.channelId, tx.txid, fundingParams.targetFeerate, nextFeerate)) } } else { // We allow the feerate to be lower than requested: when using on-the-fly liquidity, we may not be able to contribute // as much as we expected, but that's fine because we instead overshoot the feerate and pays liquidity fees accordingly. val minimumFee = Transactions.weight2fee(fundingParams.targetFeerate, tx.weight()) if (sharedTx.fees < minimumFee * 0.5) { - return InteractiveTxSessionAction.InvalidTxFeerate(fundingParams.channelId, tx.txid, fundingParams.targetFeerate, Transactions.fee2rate(sharedTx.fees, tx.weight())) + return Either.Left(InteractiveTxSessionAction.InvalidTxFeerate(fundingParams.channelId, tx.txid, fundingParams.targetFeerate, Transactions.fee2rate(sharedTx.fees, tx.weight()))) } } @@ -979,11 +1096,32 @@ data class InteractiveTxSession( val previousTx = previousSharedTx.tx.buildUnsignedTx() val previousInputs = previousTx.txIn.map { i -> i.outPoint } if (previousInputs.find { i -> currentInputs.contains(i) } == null) { - return InteractiveTxSessionAction.InvalidTxDoesNotDoubleSpendPreviousTx(fundingParams.channelId, tx.txid, previousTx.txid) + return Either.Left(InteractiveTxSessionAction.InvalidTxDoesNotDoubleSpendPreviousTx(fundingParams.channelId, tx.txid, previousTx.txid)) } } - return InteractiveTxSessionAction.SignSharedTx(sharedTx, txComplete) + return Either.Right(sharedTx) + } + + private fun validateTx(txComplete: TxComplete?): InteractiveTxSessionAction { + // tx_complete MUST have been sent and received for us to reach this state, require is used here to tell the compiler that txCompleteSent and txCompleteReceived are not null + require(txCompleteSent != null) + require(txCompleteReceived != null) + + return when (val result = buildSharedTx()) { + is Either.Left -> result.value + is Either.Right -> { + val swapInputsCount = localInputs.count { it is InteractiveTxInput.LocalSwapIn } + remoteInputs.count { it is InteractiveTxInput.RemoteSwapIn } + val sharedInputsCount = localInputs.count { it is InteractiveTxInput.Shared && this.useTaproot } + remoteInputs.count { it is InteractiveTxInput.Shared && this.useTaproot } + if (txCompleteReceived.swapInNonces.size != swapInputsCount) { + return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, swapInputsCount, txCompleteReceived.swapInNonces.size) + } + if (txCompleteReceived.fundingNonces.size != sharedInputsCount) { + return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, sharedInputsCount, txCompleteReceived.fundingNonces.size) + } + InteractiveTxSessionAction.SignSharedTx(result.value, txComplete) + } + } } companion object { @@ -997,7 +1135,7 @@ sealed class InteractiveTxSigningSessionAction { data object WaitForTxSigs : InteractiveTxSigningSessionAction() /** Send our tx_signatures: we cannot forget the channel until it has been spent or double-spent. */ - data class SendTxSigs(val fundingTx: LocalFundingStatus.UnconfirmedFundingTx, val commitment: Commitment, val localSigs: TxSignatures) : InteractiveTxSigningSessionAction() + data class SendTxSigs(val fundingTx: LocalFundingStatus.UnconfirmedFundingTx, val commitment: Commitment, val localSigs: TxSignatures, val nextRemoteNonce: IndividualNonce?) : InteractiveTxSigningSessionAction() data class AbortFundingAttempt(val reason: ChannelException) : InteractiveTxSigningSessionAction() { override fun toString(): String = reason.message } @@ -1014,6 +1152,7 @@ data class InteractiveTxSigningSession( val fundingTx: PartiallySignedSharedTransaction, val localCommit: Either, val remoteCommit: RemoteCommit, + val nextRemoteNonce: IndividualNonce? ) { // Example flow: @@ -1039,7 +1178,18 @@ 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)) { + when (val signedLocalCommit = LocalCommit.fromCommitSig( + channelKeys, + channelParams, + fundingTxIndex, + fundingParams.remoteFundingPubkey, + commitInput, + remoteCommitSig, + localCommitIndex, + localCommit.value.spec, + localPerCommitmentPoint, + logger + )) { is Either.Left -> { val fundingKey = channelKeys.fundingKey(fundingTxIndex) val localSigOfLocalTx = Transactions.sign(localCommit.value.commitTx, fundingKey) @@ -1055,7 +1205,7 @@ data class InteractiveTxSigningSession( if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) { val fundingStatus = LocalFundingStatus.UnconfirmedFundingTx(fundingTx, fundingParams, currentBlockHeight) val commitment = Commitment(fundingTxIndex, fundingParams.remoteFundingPubkey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit.value, remoteCommit, nextRemoteCommit = null) - val action = InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs) + val action = InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs, nextRemoteNonce) Pair(this.copy(localCommit = Either.Right(signedLocalCommit.value)), action) } else { Pair(this.copy(localCommit = Either.Right(signedLocalCommit.value)), InteractiveTxSigningSessionAction.WaitForTxSigs) @@ -1075,7 +1225,7 @@ data class InteractiveTxSigningSession( else -> { val fundingStatus = LocalFundingStatus.UnconfirmedFundingTx(fullySignedTx, fundingParams, currentBlockHeight) val commitment = Commitment(fundingTxIndex, fundingParams.remoteFundingPubkey, fundingStatus, RemoteFundingStatus.NotLocked, localCommit.value, remoteCommit, nextRemoteCommit = null) - Either.Right(InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs)) + Either.Right(InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs, this.nextRemoteNonce)) } } } @@ -1101,7 +1251,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.useTaproot) } val liquidityFees = fundingParams.liquidityFees(liquidityPurchase) return Helpers.Funding.makeCommitTxs( channelKeys, @@ -1118,36 +1268,63 @@ 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 alternativeSigs = if (firstCommitTx.remoteHtlcTxs.isEmpty()) { - val commitSigTlvs = Commitments.alternativeFeerates.map { feerate -> - val alternativeSpec = firstCommitTx.remoteSpec.copy(feerate = feerate) - val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( - channelKeys, - remoteCommitmentIndex, - channelParams.localParams, - channelParams.remoteParams, - fundingTxIndex, - fundingParams.remoteFundingPubkey, - firstCommitTx.remoteCommitTx.input, - remotePerCommitmentPoint, - alternativeSpec - ) - val sig = Transactions.sign(alternativeRemoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) - CommitSigTlv.AlternativeFeerateSig(feerate, sig) + val localSigOfRemoteCommitTx = firstCommitTx.remoteCommitTx.sign(channelKeys.fundingKey(fundingTxIndex)) + val localPartialSigOfRemoteTx = when (session.useTaproot) { + false -> 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.txCompleteReceived?.commitNonces?.first()!! + ).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 = when { + firstCommitTx.remoteHtlcTxs.isNotEmpty() -> null + else -> { + val alts = Commitments.alternativeFeerates.map { feerate -> + val alternativeSpec = firstCommitTx.remoteSpec.copy(feerate = feerate) + val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( + channelKeys, + remoteCommitmentIndex, + channelParams.localParams, + channelParams.remoteParams, + fundingTxIndex, + fundingParams.remoteFundingPubkey, + firstCommitTx.remoteCommitTx.input, + remotePerCommitmentPoint, + alternativeSpec + ) + feerate to alternativeRemoteCommitTx + } + when (session.useTaproot) { + false -> CommitSigTlv.AlternativeFeerateSigs(alts.map { + val sig = Transactions.sign(it.second, channelKeys.fundingKey(fundingTxIndex)) + CommitSigTlv.AlternativeFeerateSig(it.first, sig) + }) + + else -> CommitSigTlv.AlternativeFeeratePartialSigs(alts.map { + val localNonce = channelKeys.signingNonce(fundingTxIndex) + val psig = Transactions.partialSign( + it.second, channelKeys.fundingKey(fundingTxIndex), + channelKeys.fundingKey(fundingTxIndex).publicKey(), session.fundingParams.remoteFundingPubkey, + localNonce, session.txCompleteReceived?.commitNonces?.first()!! + ).right!! + CommitSigTlv.AlternativeFeeratePartialSig(it.first, PartialSignatureWithNonce(psig, localNonce.second)) + }) + } } - TlvStream(CommitSigTlv.AlternativeFeerateSigs(commitSigTlvs) as CommitSigTlv) - } else { - TlvStream.empty() } - 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) val signedFundingTx = sharedTx.sign(session, keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) - Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) + Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit, session.txCompleteReceived?.commitNonces?.elementAtOrNull(1)), commitSig) } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt index 00f67d055..4b81cd117 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt @@ -2,10 +2,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* import fr.acinq.bitcoin.utils.Either -import fr.acinq.lightning.CltvExpiryDelta -import fr.acinq.lightning.Feature -import fr.acinq.lightning.NodeParams -import fr.acinq.lightning.SensitiveTaskEvents +import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.* import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.channel.* @@ -306,16 +303,22 @@ sealed class PersistedChannelState : ChannelState() { internal fun ChannelContext.createChannelReestablish(): HasEncryptedChannelData = when (val state = this@PersistedChannelState) { is WaitForFundingSigned -> { val myFirstPerCommitmentPoint = keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).commitmentPoint(0) + val myNextLocalNonce = when (state.channelParams.isTaprootChannel) { + true -> keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).verificationNonce(0, 1).second + else -> null + } ChannelReestablish( channelId = channelId, nextLocalCommitmentNumber = state.signingSession.reconnectNextLocalCommitmentNumber, nextRemoteRevocationNumber = 0, yourLastCommitmentSecret = PrivateKey(ByteVector32.Zeroes), myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint, - TlvStream(ChannelReestablishTlv.NextFunding(state.signingSession.fundingTx.txId)) + TlvStream(setOfNotNull(ChannelReestablishTlv.NextFunding(state.signingSession.fundingTx.txId), myNextLocalNonce?.let { ChannelReestablishTlv.NextLocalNoncesTlv(listOf(it)) })) ).withChannelData(state.remoteChannelData, logger) } + is ChannelStateWithCommitments -> { + val channelKeys = keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath) val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes val myCurrentPerCommitmentPoint = keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath).commitmentPoint(state.commitments.localCommitIndex) // If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig. @@ -330,13 +333,43 @@ sealed class PersistedChannelState : ChannelState() { } else -> state.commitments.localCommitIndex + 1 } - // If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures. + val myNextLocalNonces = when (state.commitments.isTaprootChannel) { + true -> state.commitments.active.map { channelKeys.verificationNonce(it.fundingTxIndex, state.commitments.localCommitIndex + 1).second } + else -> null + } + val spliceNonces = when { + !state.commitments.isTaprootChannel -> null + state is Normal && state.spliceStatus is SpliceStatus.WaitingForSigs -> { + logger.info { "splice in progress, re-sending splice nonces" } + val localCommitIndex = when (state.spliceStatus.session.localCommit) { + is Either.Left -> state.spliceStatus.session.localCommit.value.index + is Either.Right -> state.spliceStatus.session.localCommit.value.index + } + listOf( + channelKeys.verificationNonce(state.spliceStatus.session.fundingTxIndex, localCommitIndex).second, + channelKeys.verificationNonce(state.spliceStatus.session.fundingTxIndex, localCommitIndex + 1).second + ) + } + + else -> { + logger.info { "splice may not have confirmed yet, re-sending splice nonces" } + listOf( + channelKeys.verificationNonce(state.commitments.latest.fundingTxIndex, state.commitments.localCommitIndex).second, + channelKeys.verificationNonce(state.commitments.latest.fundingTxIndex, state.commitments.localCommitIndex + 1).second + ) + } + } val unsignedFundingTxId = when (state) { is WaitForFundingConfirmed -> state.getUnsignedFundingTxId() is Normal -> state.getUnsignedFundingTxId() else -> null } - val tlvs: TlvStream = unsignedFundingTxId?.let { TlvStream(ChannelReestablishTlv.NextFunding(it)) } ?: TlvStream.empty() + val tlvs: TlvStream = TlvStream( + setOfNotNull( + unsignedFundingTxId?.let { ChannelReestablishTlv.NextFunding(it) }, + myNextLocalNonces?.let { ChannelReestablishTlv.NextLocalNoncesTlv(it) }, + spliceNonces?.let { ChannelReestablishTlv.SpliceNoncesTlv(it) } + )) ChannelReestablish( channelId = channelId, nextLocalCommitmentNumber = nextLocalCommitmentNumber, @@ -348,6 +381,37 @@ sealed class PersistedChannelState : ChannelState() { } } + internal fun ChannelContext.createChannelReady(): ChannelReady = when (val state = this@PersistedChannelState) { + is WaitForFundingSigned -> { + val nextPerCommitmentPoint = keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).commitmentPoint(1) + val nextLocalNonce = when (state.channelParams.isTaprootChannel) { + true -> keyManager.channelKeys(state.channelParams.localParams.fundingKeyPath).verificationNonce(0, 1).second + false -> null + } + val tlvStream = TlvStream( + setOfNotNull( + ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)), + nextLocalNonce?.let { ChannelReadyTlv.NextLocalNonceTlv(it) }) + ) + ChannelReady(channelId, nextPerCommitmentPoint, tlvStream) + } + + is ChannelStateWithCommitments -> { + val channelKeys = keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath) + val nextPerCommitmentPoint = keyManager.channelKeys(state.commitments.params.localParams.fundingKeyPath).commitmentPoint(1) + val nextLocalNonce = when (state.commitments.isTaprootChannel) { + true -> channelKeys.verificationNonce(0, 1).second + else -> null + } + val tlvStream = TlvStream( + setOfNotNull( + ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)), + nextLocalNonce?.let { ChannelReadyTlv.NextLocalNonceTlv(it) }) + ) + ChannelReady(channelId, nextPerCommitmentPoint, tlvStream) + } + } + companion object { // this companion object is used by static extended function `fun PersistedChannelState.Companion.from` in Encryption.kt } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt index 3b25c4da4..760c80128 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt @@ -316,7 +316,7 @@ data class Closing( // note spendingTx != Nil (that's a requirement of DATA_CLOSING) val exc = FundingTxSpent(channelId, spendingTxs.first().txid) val error = Error(channelId, exc.message) - Pair(this@Closing, listOf(ChannelAction.Message.Send(error))) + Pair(this@Closing, listOf(ChannelAction.Message.Send(error))) // README: we don't need to update nonces, we're already in CLOSING state } is Error -> { logger.error { "peer sent error: ascii=${cmd.message.toAscii()} bin=${cmd.message.data.toHex()}" } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt index ca4866310..191f6acfb 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Negotiating.kt @@ -33,8 +33,29 @@ data class Negotiating( override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> { return when (cmd) { - is ChannelCommand.MessageReceived -> when (cmd.message) { - is ClosingSigned -> { + is ChannelCommand.MessageReceived -> when { + cmd.message is ClosingSigned && commitments.isTaprootChannel -> { + val remoteClosingFee = cmd.message.feeSatoshis + logger.info { "received closing fee=$remoteClosingFee" } + when (val result = Helpers.Closing.checkClosingSignature( + channelKeys(), + commitments.latest, + localShutdown.scriptPubKey.toByteArray(), + remoteShutdown.scriptPubKey.toByteArray(), + cmd.message.feeSatoshis, + commitments.closingNonce!!, + this@Negotiating.remoteShutdown.shutdownNonce!!, + cmd.message.partialSignature!! + )) { + is Either.Left -> handleLocalError(cmd, result.value) + is Either.Right -> { + // with simple taproot channels there is no fee negotiation + completeMutualClose(result.value.first, result.value.second) + } + } + } + + cmd.message is ClosingSigned -> { val remoteClosingFee = cmd.message.feeSatoshis logger.info { "received closing fee=$remoteClosingFee" } when (val result = @@ -145,7 +166,8 @@ data class Negotiating( } } } - is Error -> handleRemoteError(cmd.message) + + cmd.message is Error -> handleRemoteError(cmd.message) else -> unhandled(cmd) } is ChannelCommand.WatchReceived -> when (val watch = cmd.watch) { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 13588a1a6..92e501955 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -9,6 +9,7 @@ import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchConfirmedTriggered import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.* +import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.* @@ -95,8 +96,12 @@ data class Normal( commitments.changes.localHasUnsignedOutgoingUpdateFee() -> handleCommandError(cmd, CannotCloseWithUnsignedOutgoingUpdateFee(channelId), channelUpdate) !Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit) -> handleCommandError(cmd, InvalidFinalScript(channelId), channelUpdate) else -> { - val shutdown = Shutdown(channelId, localScriptPubkey) - val newState = this@Normal.copy(localShutdown = shutdown, closingFeerates = cmd.feerates) + val closingNonce = when (commitments.isTaprootChannel) { + true -> channelKeys().signingNonce(commitments.latest.fundingTxIndex) + false -> null + } + val shutdown = Shutdown(channelId, localScriptPubkey, TlvStream(setOfNotNull(closingNonce?.let { ShutdownTlv.ShutdownNonce(it.second) }))) + val newState = this@Normal.copy(localShutdown = shutdown, closingFeerates = cmd.feerates).updateCommitments(this@Normal.commitments.copy(closingNonce = closingNonce)) val actions = listOf(ChannelAction.Storage.StoreState(newState), ChannelAction.Message.Send(shutdown)) Pair(newState, actions) } @@ -180,7 +185,7 @@ data class Normal( is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityPurchase, cmd.message.channelData) } } - ignoreRetransmittedCommitSig(cmd.message) -> { + ignoreRetransmittedCommitSig(cmd.message, channelKeys()) -> { // We haven't received our peer's tx_signatures for the latest funding transaction and asked them to resend it on reconnection. // They also resend their corresponding commit_sig, but we have already received it so we should ignore it. // Note that the funding transaction may have confirmed while we were offline. @@ -231,11 +236,15 @@ data class Normal( } val nextState = if (remoteShutdown != null && !commitments1.changes.localHasUnsignedOutgoingHtlcs()) { // we were waiting for our pending htlcs to be signed before replying with our local shutdown - val localShutdown = Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey) + val closingNonce = when (commitments.isTaprootChannel) { + true -> channelKeys().signingNonce(commitments.latest.fundingTxIndex) + else -> null + } + val localShutdown = Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey, TlvStream(setOfNotNull(closingNonce?.let { ShutdownTlv.ShutdownNonce(it.second) }))) actions.add(ChannelAction.Message.Send(localShutdown)) if (commitments1.latest.remoteCommit.spec.htlcs.isNotEmpty()) { // we just signed htlcs that need to be resolved now - ShuttingDown(commitments1, localShutdown, remoteShutdown, closingFeerates) + ShuttingDown(commitments1.copy(closingNonce = closingNonce), localShutdown, remoteShutdown, closingFeerates) } else { logger.warning { "we have no htlcs but have not replied with our Shutdown yet, this should never happen" } val closingTxProposed = if (paysClosingFees) { @@ -285,6 +294,7 @@ data class Normal( // there are no changes => go to NEGOTIATING when { !Helpers.Closing.isValidFinalScriptPubkey(cmd.message.scriptPubKey, allowAnySegwit) -> handleLocalError(cmd, InvalidFinalScript(channelId)) + commitments.isTaprootChannel && cmd.message.shutdownNonce == null -> handleLocalError(cmd, MissingNextLocalNonces(channelId)) commitments.changes.remoteHasUnsignedOutgoingHtlcs() -> handleLocalError(cmd, CannotCloseWithUnsignedOutgoingHtlcs(channelId)) commitments.changes.remoteHasUnsignedOutgoingUpdateFee() -> handleLocalError(cmd, CannotCloseWithUnsignedOutgoingUpdateFee(channelId)) commitments.changes.localHasUnsignedOutgoingHtlcs() -> { @@ -305,9 +315,23 @@ data class Normal( else -> { // so we don't have any unsigned outgoing changes val actions = mutableListOf() - val localShutdown = this@Normal.localShutdown ?: Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey) + val (commitments0, localShutdown) = when (val s = this@Normal.localShutdown) { + null -> when (commitments.isTaprootChannel) { + true -> { + val closingNonce = channelKeys().signingNonce(commitments.latest.fundingTxIndex) + Pair( + commitments.copy(closingNonce = closingNonce), + Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey, TlvStream(ShutdownTlv.ShutdownNonce(closingNonce.second))) + ) + } + + else -> commitments to Shutdown(channelId, commitments.params.localParams.defaultFinalScriptPubKey) + } + + else -> commitments to s + } if (this@Normal.localShutdown == null) actions.add(ChannelAction.Message.Send(localShutdown)) - val commitments1 = commitments.copy(remoteChannelData = cmd.message.channelData) + val commitments1 = commitments0.copy(remoteChannelData = cmd.message.channelData) when { commitments1.hasNoPendingHtlcsOrFeeUpdate() && paysClosingFees -> { val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx( @@ -316,6 +340,8 @@ data class Normal( localShutdown.scriptPubKey.toByteArray(), cmd.message.scriptPubKey.toByteArray(), closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate), + commitments1.closingNonce, + cmd.message.shutdownNonce ) val nextState = Negotiating( commitments1, @@ -386,7 +412,7 @@ data class Normal( targetFeerate = spliceStatus.command.feerate ) val commitTxFees = when { - paysCommitTxFees -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec) + paysCommitTxFees -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec, commitments.isTaprootChannel) else -> 0.sat } val liquidityFees = when (val requestRemoteFunding = spliceStatus.command.requestRemoteFunding) { @@ -460,12 +486,16 @@ data class Normal( fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), willFund = null, ) + val sharedInput = when (commitments.isTaprootChannel) { + true -> SharedFundingInput.Musig2Input(parentCommitment) + else -> SharedFundingInput.Multisig2of2(parentCommitment) + } val fundingParams = InteractiveTxParams( channelId = channelId, isInitiator = false, localContribution = spliceAck.fundingContribution, remoteContribution = cmd.message.fundingContribution, - sharedInput = SharedFundingInput.Multisig2of2(parentCommitment), + sharedInput = sharedInput, remoteFundingPubkey = cmd.message.fundingPubkey, localOutputs = emptyList(), lockTime = cmd.message.lockTime, @@ -481,7 +511,11 @@ data class Normal( previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, localHtlcs = parentCommitment.localCommit.spec.htlcs, fundingContributions = FundingContributions(emptyList(), emptyList()), // as non-initiator we don't contribute to this splice for now - previousTxs = emptyList() + previousTxs = emptyList(), + parentCommitment.fundingTxIndex + 1, + commitments.localCommitIndex, + commitments.remoteCommitIndex, + useTaproot = commitments.isTaprootChannel, ) val nextState = this@Normal.copy( spliceStatus = SpliceStatus.InProgress( @@ -512,7 +546,7 @@ data class Normal( spliceStatus.command.requestRemoteFunding, remoteNodeId, channelId, - Helpers.Funding.makeFundingPubKeyScript(spliceStatus.spliceInit.fundingPubkey, cmd.message.fundingPubkey), + Helpers.Funding.makeFundingPubKeyScript(spliceStatus.spliceInit.fundingPubkey, cmd.message.fundingPubkey, isTaprootChannel = commitments.isTaprootChannel), cmd.message.fundingContribution, spliceStatus.spliceInit.feerate, isChannelCreation = false, @@ -526,7 +560,10 @@ data class Normal( } is Either.Right -> { val parentCommitment = commitments.active.first() - val sharedInput = SharedFundingInput.Multisig2of2(parentCommitment) + val sharedInput = when (commitments.isTaprootChannel) { + true -> SharedFundingInput.Musig2Input(parentCommitment) + else -> SharedFundingInput.Multisig2of2(parentCommitment) + } val fundingParams = InteractiveTxParams( channelId = channelId, isInitiator = true, @@ -554,7 +591,8 @@ data class Normal( walletInputs = spliceStatus.command.spliceIn?.walletInputs ?: emptyList(), localOutputs = spliceStatus.command.spliceOutputs, liquidityPurchase = liquidityPurchase.value, - changePubKey = null // we don't want a change output: we're spending every funds available + changePubKey = null, // we don't want a change output: we're spending every funds available, + isTaprootChannel = commitments.isTaprootChannel )) { is Either.Left -> { logger.error { "could not create splice contributions: ${fundingContributions.value}" } @@ -572,7 +610,11 @@ data class Normal( previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, localHtlcs = parentCommitment.localCommit.spec.htlcs, fundingContributions = fundingContributions.value, - previousTxs = emptyList() + previousTxs = emptyList(), + fundingTxIndex = parentCommitment.fundingTxIndex + 1, + localCommitmentIndex = commitments.localCommitIndex, + remoteCommitmentIndex = commitments.remoteCommitIndex, + useTaproot = commitments.isTaprootChannel ).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { @@ -677,7 +719,10 @@ data class Normal( } is Either.Right -> { val action: InteractiveTxSigningSessionAction.SendTxSigs = res.value - sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityPurchase, cmd.message.channelData) + val (nextState, actions) = sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityPurchase, cmd.message.channelData) + val nextRemoteNonces = res.value.nextRemoteNonce?.let { listOf(it) + nextState.commitments.nextRemoteNonces } ?: nextState.commitments.nextRemoteNonces + val commitments1 = nextState.commitments.copy(nextRemoteNonces = nextRemoteNonces) + Pair(nextState.updateCommitments(commitments1), actions) } } } @@ -696,7 +741,10 @@ data class Normal( is Either.Left -> Pair(this@Normal, listOf()) is Either.Right -> { logger.info { "received remote funding signatures, publishing txId=${fullySignedTx.signedTx.txid} fundingTxIndex=${commitments.latest.fundingTxIndex}" } - val nextState = this@Normal.copy(commitments = res.value.first) + val commitments = res.value.first + val nextRemoteNonces = commitments.pendingRemoteNextLocalNonce?.let { listOf(it) + commitments.nextRemoteNonces } ?: commitments.nextRemoteNonces + val commitments1 = commitments.copy(nextRemoteNonces = nextRemoteNonces) + val nextState = this@Normal.copy(commitments = commitments1) val actions = buildList { add(ChannelAction.Blockchain.PublishTx(fullySignedTx.signedTx, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) add(ChannelAction.Storage.StoreState(nextState)) @@ -864,7 +912,7 @@ data class Normal( // We watch for confirmation in all cases, to allow pruning outdated commitments when transactions confirm. val fundingMinDepth = staticParams.nodeParams.minDepth(action.fundingTx.fundingParams.fundingAmount) val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, action.commitment.commitInput.txOut.publicKeyScript, fundingMinDepth, WatchConfirmed.ChannelFundingDepthOk) - val commitments = commitments.add(action.commitment).copy(remoteChannelData = remoteChannelData) + val commitments = commitments.add(action.commitment).copy(remoteChannelData = remoteChannelData, pendingRemoteNextLocalNonce = action.nextRemoteNonce) val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) @@ -919,12 +967,28 @@ data class Normal( } /** This function should be used to ignore a commit_sig that we've already received. */ - private fun ignoreRetransmittedCommitSig(commit: CommitSig): Boolean { + private fun ignoreRetransmittedCommitSig(commit: CommitSig, channelKeys: KeyManager.ChannelKeys): Boolean { // If we already have a signed commitment transaction containing their signature, we must have previously received that commit_sig. val commitTx = commitments.latest.localCommit.publishableTxs.commitTx.tx - return commitments.params.channelFeatures.hasFeature(Feature.DualFunding) && - commit.batchSize == 1 && - commitTx.txIn.first().witness.stack.contains(Scripts.der(commit.signature, SigHash.SIGHASH_ALL)) + return when { + !commitments.params.channelFeatures.hasFeature(Feature.DualFunding) -> false + commit.batchSize != 1 -> false + commit.sigOrPartialSig.isLeft -> commitTx.txIn.first().witness.stack.contains(Scripts.der(commit.signature, SigHash.SIGHASH_ALL)) + this.commitments.active.size > 1 -> { + // we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed) + // => instead we simply check that the provided partial signature is valid for our latest commit tx + val localFundingKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex).publicKey() + val localNonce = channelKeys.verificationNonce(commitments.latest.fundingTxIndex, commitments.latest.localCommit.index).second + commitments.latest.localCommit.publishableTxs.commitTx.checkPartialSignature(commit.partialSig!!, localFundingKey, localNonce, commitments.latest.remoteFundingPubkey) + } + else -> { + // we cannot compare partial signatures directly as they are not deterministic (a new signing nonce is used every time a signature is computed) + // => instead we simply check that the provided partial signature is valid for our latest commit tx + val localFundingKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex).publicKey() + val localNonce = channelKeys.verificationNonce(commitments.latest.fundingTxIndex, commitments.latest.localCommit.index).second + commitments.latest.localCommit.publishableTxs.commitTx.checkPartialSignature(commit.partialSig!!, localFundingKey, localNonce, commitments.latest.remoteFundingPubkey) + } + } } /** If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. */ diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt index b39f8e753..51566b2c6 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Offline.kt @@ -67,8 +67,7 @@ data class Offline(val state: PersistedChannelState) : ChannelState() { val nextState = when (state) { is WaitForFundingConfirmed -> { logger.info { "was confirmed while offline at blockHeight=${watch.blockHeight} txIndex=${watch.txIndex} with funding txid=${watch.tx.txid}" } - val nextPerCommitmentPoint = commitments1.params.localParams.channelKeys(keyManager).commitmentPoint(1) - val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) + val channelReady = state.run { createChannelReady() } val shortChannelId = ShortChannelId(watch.blockHeight, watch.txIndex, commitments1.latest.commitInput.outPoint.index.toInt()) WaitForChannelReady(commitments1, shortChannelId, channelReady) } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt index 5ee0ba46a..7aaa08627 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.PrivateKey +import fr.acinq.bitcoin.Script.tail import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.WatchConfirmed @@ -32,22 +33,23 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: when (cmd.message.nextFundingTxId) { // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. state.signingSession.fundingTx.txId -> { - val commitSig = state.signingSession.remoteCommit.sign(channelKeys(), state.channelParams, state.signingSession) + val commitSig = state.signingSession.remoteCommit.sign(channelKeys(), state.channelParams, state.signingSession, cmd.message.nextLocalNonces.firstOrNull()) Pair(state, listOf(ChannelAction.Message.Send(commitSig))) } - else -> Pair(state, listOf()) + + else -> Pair(state/*.copy(secondRemoteNonce = cmd.message.nextLocalNonces.firstOrNull())*/, listOf()) } } is WaitForFundingConfirmed -> { when (cmd.message.nextFundingTxId) { - null -> Pair(state, listOf()) + null -> Pair(state.copy(commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), listOf()) else -> { if (state.rbfStatus is RbfStatus.WaitingForSigs && state.rbfStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) { // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. logger.info { "re-sending commit_sig for rbf attempt with fundingTxId=${cmd.message.nextFundingTxId}" } - val commitSig = state.rbfStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.rbfStatus.session) + val commitSig = state.rbfStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.rbfStatus.session, cmd.message.nextLocalNonces.firstOrNull()) val actions = listOf(ChannelAction.Message.Send(commitSig)) - Pair(state, actions) + Pair(state.copy(commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), actions) } else if (state.latestFundingTx.txId == cmd.message.nextFundingTxId) { val actions = buildList { if (state.latestFundingTx.sharedTx is PartiallySignedSharedTransaction) { @@ -58,19 +60,23 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.params, fundingTxIndex = 0, state.commitments.latest.remoteFundingPubkey, - state.commitments.latest.commitInput + state.commitments.latest.commitInput, + cmd.message.nextLocalNonces.firstOrNull() ) add(ChannelAction.Message.Send(commitSig)) } logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" } add(ChannelAction.Message.Send(state.latestFundingTx.sharedTx.localSigs)) } - Pair(state, actions) + Pair(state.copy(commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), actions) } else { // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving their tx_complete). // We tell them to abort that RBF attempt. logger.info { "aborting obsolete rbf attempt for fundingTxId=${cmd.message.nextFundingTxId}" } - Pair(state.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(state.channelId, RbfAttemptAborted(state.channelId).message)))) + Pair( + state.copy(rbfStatus = RbfStatus.RbfAborted, commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), + listOf(ChannelAction.Message.Send(TxAbort(state.channelId, RbfAttemptAborted(state.channelId).message))) + ) } } } @@ -88,7 +94,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.params, fundingTxIndex = state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, - state.commitments.latest.commitInput + state.commitments.latest.commitInput, + cmd.message.nextLocalNonces.firstOrNull() ) actions.add(ChannelAction.Message.Send(commitSig)) } @@ -103,11 +110,10 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: } logger.debug { "re-sending channel_ready" } - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) - val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint) + val channelReady = state.run { createChannelReady() } actions.add(ChannelAction.Message.Send(channelReady)) - Pair(state, actions) + Pair(state.copy(commitments = state.commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonces)), actions) } is LegacyWaitForFundingLocked -> { logger.debug { "re-sending channel_ready" } @@ -120,6 +126,25 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: when (val syncResult = handleSync(state.commitments, cmd.message)) { is SyncResult.Failure -> handleSyncFailure(state.commitments, cmd.message, syncResult) is SyncResult.Success -> { + val (pendingRemoteNextLocalNonce, nextRemoteNonces) = when { + !state.commitments.isTaprootChannel -> Pair(null, listOf()) + state.spliceStatus is SpliceStatus.WaitingForSigs && cmd.message.nextLocalNonces.size == state.commitments.active.size -> { + Pair(cmd.message.secondSpliceNonce, cmd.message.nextLocalNonces) + } + + state.spliceStatus is SpliceStatus.WaitingForSigs && cmd.message.nextLocalNonces.size == state.commitments.active.size + 1 -> { + Pair(cmd.message.nextLocalNonces.firstOrNull(), cmd.message.nextLocalNonces.tail()) + } + + cmd.message.nextLocalNonces.size == state.commitments.active.size - 1 -> { + Pair(null, listOf(cmd.message.secondSpliceNonce!!) + cmd.message.nextLocalNonces) + } + + else -> { + Pair(null, cmd.message.nextLocalNonces) + } + } + // normal case, our data is up-to-date val actions = ArrayList() @@ -127,16 +152,23 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (state.commitments.latest.fundingTxIndex == 0L && cmd.message.nextLocalCommitmentNumber == 1L && state.commitments.localCommitIndex == 0L) { // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT logger.debug { "re-sending channel_ready" } - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) - val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint) + val channelReady = state.run { createChannelReady() } actions.add(ChannelAction.Message.Send(channelReady)) } // resume splice signing session if any val spliceStatus1 = if (state.spliceStatus is SpliceStatus.WaitingForSigs && state.spliceStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) { // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. - logger.info { "re-sending commit_sig for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" } - val commitSig = state.spliceStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.spliceStatus.session) + val spliceNonce = when { + state.spliceStatus.session.remoteCommit.index == cmd.message.nextLocalCommitmentNumber -> cmd.message.firstSpliceNonce + state.spliceStatus.session.remoteCommit.index == cmd.message.nextLocalCommitmentNumber - 1 -> cmd.message.firstSpliceNonce + else -> { + // we should never end up here, it would have been handled in handleSync() + error("invalid nextLocalCommitmentNumber in ChannelReestablish") + } + } + val commitSig = state.spliceStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.spliceStatus.session, spliceNonce) + logger.info { "re-sending commit_sig ${commitSig.partialSig} for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" } actions.add(ChannelAction.Message.Send(commitSig)) state.spliceStatus } else if (state.commitments.latest.fundingTxId == cmd.message.nextFundingTxId) { @@ -144,14 +176,16 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: is LocalFundingStatus.UnconfirmedFundingTx -> { if (localFundingStatus.sharedTx is PartiallySignedSharedTransaction) { // If we have not received their tx_signatures, we can't tell whether they had received our commit_sig, so we need to retransmit it - logger.info { "re-sending commit_sig for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" } + logger.info { "re-sending commit_sig and tx_signatures for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" } val commitSig = state.commitments.latest.remoteCommit.sign( channelKeys(), state.commitments.params, fundingTxIndex = state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, - state.commitments.latest.commitInput + state.commitments.latest.commitInput, + cmd.message.firstSpliceNonce ) + logger.info { "computed $commitSig with remote nonce = ${nextRemoteNonces.firstOrNull()}" } actions.add(ChannelAction.Message.Send(commitSig)) } logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" } @@ -192,7 +226,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: actions.addAll(syncResult.retransmit.map { ChannelAction.Message.Send(it) }) // then we clean up unsigned updates - val commitments1 = discardUnsignedUpdates(state.commitments) + val commitments1 = discardUnsignedUpdates(state.commitments).copy(pendingRemoteNextLocalNonce = pendingRemoteNextLocalNonce, nextRemoteNonces = nextRemoteNonces) if (commitments1.changes.localHasChanges()) { actions.add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign)) @@ -307,8 +341,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: val nextState = when (state) { is WaitForFundingConfirmed -> { logger.info { "was confirmed while syncing at blockHeight=${watch.blockHeight} txIndex=${watch.txIndex} with funding txid=${watch.tx.txid}" } - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) - val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) + val channelReady = state.run { createChannelReady() } val shortChannelId = ShortChannelId(watch.blockHeight, watch.txIndex, commitments1.latest.commitInput.outPoint.index.toInt()) WaitForChannelReady(commitments1, shortChannelId, channelReady) } @@ -483,10 +516,15 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // they just sent a new commit_sig, we have received it but they didn't receive our revocation val localPerCommitmentSecret = channelKeys.commitmentSecret(commitments.localCommitIndex - 1) val localNextPerCommitmentPoint = channelKeys.commitmentPoint(commitments.localCommitIndex + 1) + val nextLocalNonces = when (commitments.isTaprootChannel) { + true -> commitments.active.map { channelKeys.verificationNonce(it.fundingTxIndex, commitments.localCommitIndex + 1).second } + false -> null + } val revocation = RevokeAndAck( channelId = commitments.channelId, perCommitmentSecret = localPerCommitmentSecret, - nextPerCommitmentPoint = localNextPerCommitmentPoint + nextPerCommitmentPoint = localNextPerCommitmentPoint, + tlvStream = TlvStream(setOfNotNull(nextLocalNonces?.let { RevokeAndAckTlv.NextLocalNoncesTlv(it) })) ) checkRemoteCommit(remoteChannelReestablish, retransmitRevocation = revocation) } else if (commitments.localCommitIndex > remoteChannelReestablish.nextRemoteRevocationNumber + 1) { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt index 007e519c0..d204eaead 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ChannelEvents +import fr.acinq.lightning.Feature import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.Helpers.Funding.computeChannelId import fr.acinq.lightning.utils.msat @@ -55,7 +56,7 @@ data class WaitForAcceptChannel( lastSent.requestFunding, staticParams.remoteNodeId, channelId, - fundingParams.fundingPubkeyScript(channelKeys), + fundingParams.fundingPubkeyScript(channelKeys, isTaprootChannel = channelFeatures.hasFeature(Feature.SimpleTaprootStaging)), accept.fundingAmount, lastSent.fundingFeerate, isChannelCreation = true, @@ -72,7 +73,8 @@ data class WaitForAcceptChannel( keyManager.swapInOnChainWallet, fundingParams, init.walletInputs, - liquidityPurchase.value + liquidityPurchase.value, + isTaprootChannel = channelFeatures.hasFeature(Feature.SimpleTaprootStaging) )) { is Either.Left -> { logger.error { "could not fund channel: ${fundingContributions.value}" } @@ -89,7 +91,11 @@ data class WaitForAcceptChannel( 0.msat, 0.msat, emptySet(), - fundingContributions.value + fundingContributions.value, + fundingTxIndex = 0, + localCommitmentIndex = 0, + remoteCommitmentIndex = 0, + useTaproot = init.channelType.features.contains(Feature.SimpleTaprootStaging) ).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { @@ -105,7 +111,7 @@ data class WaitForAcceptChannel( init.channelConfig, channelFeatures, liquidityPurchase.value, - channelOrigin + channelOrigin, ) val actions = listOf( ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId), diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt index 0d1c882fc..84f718b9f 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReady.kt @@ -59,6 +59,9 @@ data class WaitForChannelReady( Pair(this@WaitForChannelReady, listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidRbfTxConfirmed(channelId, commitments.latest.fundingTxId).message)))) } is ChannelReady -> { + if (commitments.isTaprootChannel) { + require(cmd.message.nextLocalNonce != null) { "missing next local nonce" } + } // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced val initialChannelUpdate = Announcements.makeChannelUpdate( staticParams.nodeParams.chainHash, @@ -73,7 +76,7 @@ data class WaitForChannelReady( enable = Helpers.aboveReserve(commitments) ) val nextState = Normal( - commitments, + commitments.copy(nextRemoteNonces = cmd.message.nextLocalNonce?.let { listOf(it) } ?: listOf()), shortChannelId, initialChannelUpdate, null, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt index 2032224b6..d3b18db9a 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -104,7 +104,10 @@ data class WaitForFundingConfirmed( SharedFundingInputBalances(0.msat, 0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx }, - commitments.latest.localCommit.spec.htlcs + commitments.latest.localCommit.spec.htlcs, + fundingTxIndex = commitments.latest.fundingTxIndex, + localCommitmentIndex = commitments.localCommitIndex, + remoteCommitmentIndex = commitments.remoteCommitIndex, ) val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session)) Pair(nextState, listOf(ChannelAction.Message.Send(TxAckRbf(channelId, fundingParams.localContribution)))) @@ -134,7 +137,7 @@ data class WaitForFundingConfirmed( latestFundingTx.fundingParams.dustLimit, rbfStatus.command.targetFeerate ) - when (val contributions = FundingContributions.create(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, rbfStatus.command.walletInputs, null)) { + when (val contributions = FundingContributions.create(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, rbfStatus.command.walletInputs, null, isTaprootChannel = false)) { // FIXME is Either.Left -> { logger.warning { "error creating funding contributions: ${contributions.value}" } Pair(this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message)))) @@ -149,7 +152,11 @@ data class WaitForFundingConfirmed( 0.msat, emptySet(), contributions.value, - previousFundingTxs.map { it.sharedTx }).send() + previousFundingTxs.map { it.sharedTx }, + fundingTxIndex = commitments.latest.fundingTxIndex, + localCommitmentIndex = commitments.localCommitIndex, + remoteCommitmentIndex = commitments.remoteCommitIndex + ).send() when (action) { is InteractiveTxSessionAction.SendMessage -> { val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session)) @@ -259,8 +266,8 @@ data class WaitForFundingConfirmed( is Either.Left -> Pair(this@WaitForFundingConfirmed, listOf()) is Either.Right -> { val (commitments1, commitment, actions) = res.value - val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) - val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) + val channelReady = createChannelReady() + //ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) // this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel // as soon as it reaches NORMAL state, and before it is announced on the network // (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt index 535543b39..6ce78f442 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PublicKey +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* @@ -41,7 +42,7 @@ data class WaitForFundingCreated( val channelConfig: ChannelConfig, val channelFeatures: ChannelFeatures, val liquidityPurchase: LiquidityAds.Purchase?, - val channelOrigin: Origin? + val channelOrigin: Origin?, ) : ChannelState() { val channelId: ByteVector32 = interactiveTxSession.fundingParams.channelId @@ -94,7 +95,7 @@ data class WaitForFundingCreated( session, remoteSecondPerCommitmentPoint, liquidityPurchase, - channelOrigin + channelOrigin, ) val actions = buildList { interactiveTxAction.txComplete?.let { add(ChannelAction.Message.Send(it)) } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt index 1a3bb70c9..ad5ba6c7b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSigned.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PublicKey import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ChannelEvents import fr.acinq.lightning.LiquidityEvents @@ -114,7 +115,8 @@ data class WaitForFundingSigned( payments = mapOf(), remoteNextCommitInfo = Either.Right(remoteSecondPerCommitmentPoint), remotePerCommitmentSecrets = ShaChain.init, - remoteChannelData = remoteChannelData + remoteChannelData = remoteChannelData, + nextRemoteNonces = action.nextRemoteNonce?.let { listOf(it) } ?: listOf() ) val commonActions = buildList { action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) } @@ -160,8 +162,7 @@ data class WaitForFundingSigned( } return if (staticParams.useZeroConf) { logger.info { "channel is using 0-conf, we won't wait for the funding tx to confirm" } - val nextPerCommitmentPoint = channelParams.localParams.channelKeys(keyManager).commitmentPoint(1) - val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId)))) + val channelReady = createChannelReady() // We use part of the funding txid to create a dummy short channel id. // This gives us a probability of collisions of 0.1% for 5 0-conf channels and 1% for 20 // Collisions mean that users may temporarily see incorrect numbers for their 0-conf channels (until they've been confirmed). diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt index 47d4b4276..51014dff6 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt @@ -1,5 +1,7 @@ package fr.acinq.lightning.channel.states +import fr.acinq.lightning.Feature +import fr.acinq.lightning.Features import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchSpent @@ -52,6 +54,14 @@ data object WaitForInit : ChannelState() { buildSet { add(ChannelTlv.ChannelTypeTlv(cmd.channelType)) cmd.requestRemoteFunding?.let { add(ChannelTlv.RequestFundingTlv(it)) } + if (Features.canUseFeature(cmd.localParams.features, cmd.remoteInit.features, Feature.SimpleTaprootStaging)) add( + ChannelTlv.NextLocalNoncesTlv( + listOf( + channelKeys.verificationNonce(0, 0).second, + channelKeys.verificationNonce(0, 1).second, + ) + ) + ) } ) ) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt index 74b895a15..7f6b7655b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt @@ -4,6 +4,7 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Satoshi import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ChannelEvents +import fr.acinq.lightning.Feature import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.Helpers.Funding.computeChannelId @@ -42,7 +43,8 @@ data class WaitForOpenChannel( val minimumDepth = if (staticParams.useZeroConf) 0 else staticParams.nodeParams.minDepth(open.fundingAmount) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) val localFundingPubkey = channelKeys.fundingPubKey(0) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey) + val isTaprootChannel = channelFeatures.hasFeature(Feature.SimpleTaprootStaging) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey, isTaprootChannel) val requestFunding = open.requestFunding val willFund = when { fundingRates == null -> null @@ -70,6 +72,14 @@ data class WaitForOpenChannel( buildSet { add(ChannelTlv.ChannelTypeTlv(channelType)) willFund?.let { add(ChannelTlv.ProvideFundingTlv(it.willFund)) } + if (isTaprootChannel) add( + ChannelTlv.NextLocalNoncesTlv( + listOf( + channelKeys.verificationNonce(0, 0).second, + channelKeys.verificationNonce(0, 1).second, + ) + ) + ) } ), ) @@ -90,14 +100,27 @@ data class WaitForOpenChannel( val remoteFundingPubkey = open.fundingPubkey val dustLimit = open.dustLimit.max(localParams.dustLimit) val fundingParams = InteractiveTxParams(channelId, false, fundingAmount, open.fundingAmount, remoteFundingPubkey, open.lockTime, dustLimit, open.fundingFeerate) - when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, walletInputs, null)) { + when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, walletInputs, null, isTaprootChannel)) { is Either.Left -> { logger.error { "could not fund channel: ${fundingContributions.value}" } replyTo.complete(ChannelFundingResponse.Failure.FundingFailure(fundingContributions.value)) Pair(Aborted, listOf(ChannelAction.Message.Send(Error(temporaryChannelId, ChannelFundingError(temporaryChannelId).message)))) } is Either.Right -> { - val interactiveTxSession = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value) + val interactiveTxSession = InteractiveTxSession( + staticParams.remoteNodeId, + channelKeys, + keyManager.swapInOnChainWallet, + fundingParams, + 0.msat, + 0.msat, + emptySet(), + fundingContributions.value, + fundingTxIndex = 0, + localCommitmentIndex = 0, + remoteCommitmentIndex = 0, + useTaproot = isTaprootChannel + ) val nextState = WaitForFundingCreated( replyTo, // If our peer asks us to pay the commit tx fees, we accept (only used in tests, as we're otherwise always the channel opener). diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 0a1767f8d..2c2f07edd 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/modules/core/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/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 346365403..22d7e6e7c 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -1404,6 +1404,11 @@ class Peer( // We ask our peer to pay the commit tx fees. val localParams = LocalParams(nodeParams, isChannelOpener = true, payCommitTxFees = false) val channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = true) + val channelType = if (Features.canUseFeature(localParams.features, theirInit!!.features, Feature.SimpleTaprootStaging)) { + ChannelType.SupportedChannelType.SimpleTaprootStaging + } else { + ChannelType.SupportedChannelType.AnchorOutputsZeroReserve + } val initCommand = ChannelCommand.Init.Initiator( replyTo = CompletableDeferred(), fundingAmount = localFundingAmount, @@ -1414,7 +1419,7 @@ class Peer( remoteInit = theirInit!!, channelFlags = channelFlags, channelConfig = ChannelConfig.standard, - channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, + channelType = channelType, requestRemoteFunding = requestRemoteFunding, channelOrigin = Origin.OnChainWallet(cmd.walletInputs.map { it.outPoint }.toSet(), cmd.totalAmount.toMilliSatoshi(), fees), ) @@ -1519,6 +1524,11 @@ class Peer( } else -> { logger.info { "requesting on-the-fly channel for paymentHash=${cmd.paymentHash} feerate=$fundingFeerate fee=${totalFees.total} paymentType=${paymentDetails.paymentType}" } + val channelType = if (Features.canUseFeature(localParams.features, theirInit!!.features, Feature.SimpleTaprootStaging)) { + ChannelType.SupportedChannelType.SimpleTaprootStaging + } else { + ChannelType.SupportedChannelType.AnchorOutputsZeroReserve + } val (state, actions) = WaitForInit.process( ChannelCommand.Init.Initiator( replyTo = CompletableDeferred(), @@ -1530,7 +1540,7 @@ class Peer( remoteInit = theirInit!!, channelFlags = channelFlags, channelConfig = ChannelConfig.standard, - channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, + channelType = channelType, requestRemoteFunding = LiquidityAds.RequestFunding(cmd.requestedAmount, cmd.fundingRate, paymentDetails), channelOrigin = Origin.OffChainPayment(cmd.preimage, cmd.paymentAmount, totalFees), ) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt index 00f3ac999..b7c6f68a7 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt @@ -22,11 +22,13 @@ JsonSerializers.ByteVector64Serializer::class, JsonSerializers.BlockHashSerializer::class, JsonSerializers.PublicKeySerializer::class, + JsonSerializers.XonlyPublicKeySerializer::class, JsonSerializers.PrivateKeySerializer::class, JsonSerializers.TxIdSerializer::class, JsonSerializers.KeyPathSerializer::class, JsonSerializers.SatoshiSerializer::class, JsonSerializers.MilliSatoshiSerializer::class, + JsonSerializers.PartialSignatureWithNonceSerializer::class, JsonSerializers.CltvExpirySerializer::class, JsonSerializers.CltvExpiryDeltaSerializer::class, JsonSerializers.FeeratePerKwSerializer::class, @@ -41,6 +43,8 @@ JsonSerializers.TransactionSerializer::class, JsonSerializers.OutPointSerializer::class, JsonSerializers.TxOutSerializer::class, + JsonSerializers.IndividualNonceSerializer::class, + JsonSerializers.SecretNonceSerializer::class, JsonSerializers.ClosingTxProposedSerializer::class, JsonSerializers.LocalCommitPublishedSerializer::class, JsonSerializers.RemoteCommitPublishedSerializer::class, @@ -81,6 +85,7 @@ JsonSerializers.ChannelReadySerializer::class, JsonSerializers.ChannelReadyTlvShortChannelIdTlvSerializer::class, JsonSerializers.ClosingSignedTlvFeeRangeSerializer::class, + JsonSerializers.ClosingSignedTlvPartialSignatureSerializer::class, JsonSerializers.ShutdownTlvChannelDataSerializer::class, JsonSerializers.GenericTlvSerializer::class, JsonSerializers.TlvStreamSerializer::class, @@ -90,7 +95,10 @@ JsonSerializers.ChannelReadyTlvSerializer::class, JsonSerializers.CommitSigTlvAlternativeFeerateSigSerializer::class, JsonSerializers.CommitSigTlvAlternativeFeerateSigsSerializer::class, + JsonSerializers.CommitSigTlvAlternativeFeeratePartialSigSerializer::class, + JsonSerializers.CommitSigTlvAlternativeFeeratePartialSigsSerializer::class, JsonSerializers.CommitSigTlvBatchSerializer::class, + JsonSerializers.CommitSigTlvPartialSignatureWithNonceSerializer::class, JsonSerializers.CommitSigTlvSerializer::class, JsonSerializers.UUIDSerializer::class, JsonSerializers.ClosingSerializer::class, @@ -100,14 +108,23 @@ JsonSerializers.EncodedNodeIdSerializer::class, JsonSerializers.BlindedHopSerializer::class, JsonSerializers.BlindedRouteSerializer::class, + JsonSerializers.ScriptTreeSerializer::class, + JsonSerializers.SegwitInputSerializer::class, + JsonSerializers.RedeemPathScriptPathSerializer::class, + JsonSerializers.RedeemPathKeyPathSerializer::class, + JsonSerializers.RedeemPathSerializer::class, + JsonSerializers.TaprootInputSerializer::class, ) @file:UseContextualSerialization( - PersistedChannelState::class + PersistedChannelState::class, + Transactions.InputInfo::class ) package fr.acinq.lightning.json import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -124,6 +141,7 @@ import fr.acinq.lightning.payment.Bolt11Invoice.TaggedField import fr.acinq.lightning.transactions.CommitmentSpec import fr.acinq.lightning.transactions.IncomingHtlc import fr.acinq.lightning.transactions.OutgoingHtlc +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.wire.* import fr.acinq.lightning.wire.OfferTypes.OfferChains @@ -134,10 +152,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json -import kotlinx.serialization.modules.PolymorphicModuleBuilder -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.contextual -import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.* /** * Json support for [ChannelState] based on `kotlinx-serialization`. @@ -205,10 +220,15 @@ object JsonSerializers { } polymorphic(Tlv::class) { subclass(ChannelReadyTlv.ShortChannelIdTlv::class, ChannelReadyTlvShortChannelIdTlvSerializer) + subclass(ChannelReadyTlv.NextLocalNonceTlv::class, ChannelReadyTlvNextLocalNonceTlvSerializer) subclass(CommitSigTlv.AlternativeFeerateSigs::class, CommitSigTlvAlternativeFeerateSigsSerializer) + subclass(CommitSigTlv.AlternativeFeeratePartialSigs::class, CommitSigTlvAlternativeFeeratePartialSigsSerializer) subclass(CommitSigTlv.Batch::class, CommitSigTlvBatchSerializer) + subclass(CommitSigTlv.PartialSignatureWithNonceTlv::class, CommitSigTlvPartialSignatureWithNonceSerializer) subclass(ShutdownTlv.ChannelData::class, ShutdownTlvChannelDataSerializer) + subclass(ShutdownTlv.ShutdownNonce::class, ShutdownTlvShutdownNonceSerializer) subclass(ClosingSignedTlv.FeeRange::class, ClosingSignedTlvFeeRangeSerializer) + subclass(ClosingSignedTlv.PartialSignature::class, ClosingSignedTlvPartialSignatureSerializer) subclass(UpdateAddHtlcTlv.PathKey::class, UpdateAddHtlcTlvPathKeySerializer) } // TODO The following declarations are required because serializers for [TransactionWithInputInfo] @@ -220,9 +240,27 @@ object JsonSerializers { contextual(TransactionSerializer) contextual(ByteVectorSerializer) contextual(ByteVector32Serializer) + contextual(IndividualNonceSerializer) + contextual(SecretNonceSerializer) + contextual(ScriptTreeSerializer) + contextual(XonlyPublicKeySerializer) contextual(Bolt11InvoiceSerializer) contextual(OfferSerializer) + + contextual(PolymorphicSerializer(Transactions.InputInfo.RedeemPath::class)) + polymorphic(Transactions.InputInfo.RedeemPath::class) + { + subclass(Transactions.InputInfo.RedeemPath.KeyPath::class, RedeemPathKeyPathSerializer) + subclass(Transactions.InputInfo.RedeemPath.ScriptPath::class, RedeemPathScriptPathSerializer) + } + + contextual(PolymorphicSerializer(Transactions.InputInfo::class)) + polymorphic(Transactions.InputInfo::class) + { + subclass(Transactions.InputInfo.SegwitInput::class, SegwitInputSerializer) + subclass(Transactions.InputInfo.TaprootInput::class, TaprootInputSerializer) + } } } @@ -278,6 +316,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() @@ -398,15 +437,37 @@ object JsonSerializers { object ByteVector64Serializer : StringSerializer() object BlockHashSerializer : StringSerializer() object PublicKeySerializer : StringSerializer() + object XonlyPublicKeySerializer : StringSerializer() object TxIdSerializer : StringSerializer() object KeyPathSerializer : StringSerializer() object ShortChannelIdSerializer : StringSerializer() object OutPointSerializer : StringSerializer({ "${it.txid}:${it.index}" }) object TransactionSerializer : StringSerializer() + object IndividualNonceSerializer : StringSerializer({ "${it.data} " }) + object SecretNonceSerializer : StringSerializer({ "" }) + object ScriptTreeSerializer : StringSerializer({ it.write().toHexString()}) @Serializer(forClass = PublishableTxs::class) object PublishableTxsSerializer + @Serializer(forClass = Transactions.InputInfo.SegwitInput::class) + object SegwitInputSerializer + + @Serializer(forClass = Transactions.InputInfo.RedeemPath.KeyPath::class) + object RedeemPathKeyPathSerializer + + @Serializer(forClass = Transactions.InputInfo.RedeemPath.ScriptPath::class) + object RedeemPathScriptPathSerializer + + @Serializer(forClass = Transactions.InputInfo.RedeemPath::class) + object RedeemPathSerializer + + @Serializer(forClass = Transactions.InputInfo.TaprootInput::class) + object TaprootInputSerializer + + @Serializer(forClass = Transactions.InputInfo::class) + object InputInfoSerializer + @Serializable data class CommitmentsSpecSurrogate(val htlcsIn: List, val htlcsOut: List, val feerate: FeeratePerKw, val toLocal: MilliSatoshi, val toRemote: MilliSatoshi) object CommitmentSpecSerializer : SurrogateSerializer( @@ -524,12 +585,21 @@ object JsonSerializers { @Serializer(forClass = ChannelReadyTlv.ShortChannelIdTlv::class) object ChannelReadyTlvShortChannelIdTlvSerializer + @Serializer(forClass = ChannelReadyTlv.NextLocalNonceTlv::class) + object ChannelReadyTlvNextLocalNonceTlvSerializer + @Serializer(forClass = ClosingSignedTlv.FeeRange::class) object ClosingSignedTlvFeeRangeSerializer + @Serializer(forClass = ClosingSignedTlv.PartialSignature::class) + object ClosingSignedTlvPartialSignatureSerializer + @Serializer(forClass = ShutdownTlv.ChannelData::class) object ShutdownTlvChannelDataSerializer + @Serializer(forClass = ShutdownTlv.ShutdownNonce::class) + object ShutdownTlvShutdownNonceSerializer + @Serializer(forClass = ShutdownTlv::class) object ShutdownTlvSerializer @@ -539,9 +609,21 @@ object JsonSerializers { @Serializer(forClass = CommitSigTlv.AlternativeFeerateSigs::class) object CommitSigTlvAlternativeFeerateSigsSerializer + @Serializer(forClass = CommitSigTlv.AlternativeFeeratePartialSig::class) + object CommitSigTlvAlternativeFeeratePartialSigSerializer + + @Serializer(forClass = CommitSigTlv.AlternativeFeeratePartialSigs::class) + object CommitSigTlvAlternativeFeeratePartialSigsSerializer + @Serializer(forClass = CommitSigTlv.Batch::class) object CommitSigTlvBatchSerializer + @Serializer(forClass = PartialSignatureWithNonce::class) + object PartialSignatureWithNonceSerializer + + @Serializer(forClass = CommitSigTlv.PartialSignatureWithNonceTlv::class) + object CommitSigTlvPartialSignatureWithNonceSerializer + @Serializer(forClass = CommitSigTlv::class) object CommitSigTlvSerializer diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/InputExtensions.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/InputExtensions.kt index 64faf78d0..803b760bd 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/InputExtensions.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/InputExtensions.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.serialization import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.utils.UUID @@ -25,6 +26,8 @@ object InputExtensions { fun Input.readTxId(): TxId = TxId(readByteVector32()) + fun Input.readPublicNonce() = IndividualNonce(ByteArray(66).also { read(it, 0, it.size) }) + fun Input.readUuid(): UUID = UUID.fromBytes(ByteArray(16).also { read(it, 0, it.size) }) fun Input.readDelimitedByteArray(): ByteArray { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/OutputExtensions.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/OutputExtensions.kt index ba583b538..d1c05d7dd 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/OutputExtensions.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/OutputExtensions.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.serialization import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.Output import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.utils.UUID @@ -25,6 +26,8 @@ object OutputExtensions { fun Output.writeTxId(o: TxId) = write(o.value.toByteArray()) + fun Output.writePublicNonce(o: IndividualNonce) = write(o.toByteArray()) + fun Output.writeUuid(o: UUID) = o.run { // NB: copied from kotlin source code (https://github.com/JetBrains/kotlin/blob/v2.1.0/libraries/stdlib/src/kotlin/uuid/Uuid.kt) in order to be forward compatible fun Long.toByteArray(dst: ByteArray, dstOffset: Int) { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/ChannelState.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/ChannelState.kt index 1ea499568..4ff03a295 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/ChannelState.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/ChannelState.kt @@ -1,5 +1,7 @@ @file:OptIn(ExperimentalSerializationApi::class) @file:UseSerializers( + OutPointKSerializer::class, + InputInfoKSerializer::class, KeyPathKSerializer::class, EitherSerializer::class, ShaChainSerializer::class, @@ -32,7 +34,6 @@ FeeratePerKwSerializer::class, MilliSatoshiSerializer::class, UUIDSerializer::class, - OutPointKSerializer::class, TxOutKSerializer::class, TransactionKSerializer::class, ) @@ -62,6 +63,27 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +object InputInfoKSerializer : KSerializer { + @Serializable + @SerialName("fr.acinq.lightning.transactions.InputInfo") + private data class InputInfoSurrogate(@Contextual val outPoint: OutPoint, @Contextual val txOut: TxOut, @Contextual val redeemScript: ByteVector) { + + constructor(input: Transactions.InputInfo.SegwitInput) : this(input.outPoint, input.txOut, input.redeemScript) + } + + override val descriptor: SerialDescriptor = InputInfoSurrogate.serializer().descriptor + + override fun serialize(encoder: Encoder, value: Transactions.InputInfo) { + val surrogate = InputInfoSurrogate(value as Transactions.InputInfo.SegwitInput) + return encoder.encodeSerializableValue(InputInfoSurrogate.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): Transactions.InputInfo { + val surrogate = decoder.decodeSerializableValue(InputInfoSurrogate.serializer()) + return Transactions.InputInfo.SegwitInput(surrogate.outPoint, surrogate.txOut, surrogate.redeemScript) + } +} + @Serializer(forClass = FundingSigned::class) object FundingSignedSerializer diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/Serialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/Serialization.kt index 7099fc9c3..ca8bdb827 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/Serialization.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v2/Serialization.kt @@ -65,6 +65,7 @@ object Serialization { contextual(TxOutKSerializer) contextual(TransactionKSerializer) contextual(BlockHeaderKSerializer) + contextual(InputInfoKSerializer) }) } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/ChannelState.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/ChannelState.kt index 1dc862ed1..aad39bd8a 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/ChannelState.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/ChannelState.kt @@ -35,6 +35,7 @@ OutPointKSerializer::class, TxOutKSerializer::class, TransactionKSerializer::class, + InputInfoKSerializer::class ) package fr.acinq.lightning.serialization.v3 @@ -62,6 +63,27 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +object InputInfoKSerializer : KSerializer { + @Serializable + @SerialName("fr.acinq.lightning.transactions.InputInfo") + private data class InputInfoSurrogate(@Contextual val outPoint: OutPoint, @Contextual val txOut: TxOut, @Contextual val redeemScript: ByteVector) { + + constructor(input: Transactions.InputInfo.SegwitInput) : this(input.outPoint, input.txOut, input.redeemScript) + } + + override val descriptor: SerialDescriptor = InputInfoSurrogate.serializer().descriptor + + override fun serialize(encoder: Encoder, value: Transactions.InputInfo) { + val surrogate = InputInfoSurrogate(value as Transactions.InputInfo.SegwitInput) + return encoder.encodeSerializableValue(InputInfoSurrogate.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): Transactions.InputInfo { + val surrogate = decoder.decodeSerializableValue(InputInfoSurrogate.serializer()) + return Transactions.InputInfo.SegwitInput(surrogate.outPoint, surrogate.txOut, surrogate.redeemScript) + } +} + @Serializer(forClass = FundingSigned::class) object FundingSignedSerializer diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/Serialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/Serialization.kt index 6df2513f3..ee67e9a32 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/Serialization.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v3/Serialization.kt @@ -67,6 +67,7 @@ object Serialization { contextual(TransactionKSerializer) contextual(BlockHeaderKSerializer) contextual(EncryptedChannelDataSerializer) + contextual(InputInfoKSerializer) }) } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Deserialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Deserialization.kt index 8f5a20d1f..0f3854f33 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Deserialization.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Deserialization.kt @@ -22,6 +22,7 @@ import fr.acinq.lightning.serialization.InputExtensions.readLightningMessage import fr.acinq.lightning.serialization.InputExtensions.readNullable import fr.acinq.lightning.serialization.InputExtensions.readNumber import fr.acinq.lightning.serialization.InputExtensions.readPublicKey +import fr.acinq.lightning.serialization.InputExtensions.readPublicNonce import fr.acinq.lightning.serialization.InputExtensions.readString import fr.acinq.lightning.serialization.InputExtensions.readTxId import fr.acinq.lightning.serialization.common.liquidityads.Deserialization.readLiquidityPurchase @@ -83,7 +84,7 @@ object Deserialization { signingSession = readInteractiveTxSigningSession(), remoteSecondPerCommitmentPoint = readPublicKey(), liquidityPurchase = readNullable { readLiquidityPurchase() }, - channelOrigin = readNullable { readChannelOrigin() } + channelOrigin = readNullable { readChannelOrigin() }, ) private fun Input.readWaitForFundingSignedWithPushAmount(): WaitForFundingSigned { @@ -252,6 +253,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}") } @@ -482,7 +488,7 @@ object Deserialization { txid = readTxId(), remotePerCommitmentPoint = readPublicKey() ) - return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, localCommit, remoteCommit) + return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, localCommit, remoteCommit, null) } private fun Input.readChannelOrigin(): Origin = when (val discriminator = read()) { @@ -669,7 +675,10 @@ object Deserialization { lastIndex = readNullable { readNumber() } ) val remoteChannelData = EncryptedChannelData(readDelimitedByteArray().toByteVector()) - return Commitments(params, changes, active, inactive, payments, remoteNextCommitInfo, remotePerCommitmentSecrets, remoteChannelData) + val nextRemoteNonces = if (params.isTaprootChannel) { + readCollection { readPublicNonce() } + } else listOf() + return Commitments(params, changes, active, inactive, payments, remoteNextCommitInfo, remotePerCommitmentSecrets, remoteChannelData, nextRemoteNonces.toList()) } private fun Input.readDirectedHtlc(): DirectedHtlc = when (val discriminator = read()) { @@ -704,11 +713,23 @@ object Deserialization { toRemote = readNumber().msat ) - private fun Input.readInputInfo(): Transactions.InputInfo = Transactions.InputInfo( - outPoint = readOutPoint(), - txOut = TxOut.read(readDelimitedByteArray()), - redeemScript = readDelimitedByteArray().toByteVector() - ) + private fun Input.readScriptTree(): ScriptTree = ScriptTree.read(ByteArrayInput(readDelimitedByteArray())) + + private fun Input.readRedeemPath(): Transactions.InputInfo.RedeemPath = when (val discriminator = read()) { + 0x01 -> Transactions.InputInfo.RedeemPath.KeyPath(readNullable { readScriptTree() }) + 0x02 -> Transactions.InputInfo.RedeemPath.ScriptPath(readScriptTree(), readByteVector32()) + else -> error("unknown discriminator $discriminator for class ${Transactions.InputInfo.RedeemPath::class}") + } + + private fun Input.readInputInfo(): Transactions.InputInfo { + val outPoint = readOutPoint() + val txOut = TxOut.read(readDelimitedByteArray()) + val redeemScript = readDelimitedByteArray().toByteVector() + return when (redeemScript.isEmpty()) { + true -> Transactions.InputInfo.TaprootInput(outPoint, txOut, XonlyPublicKey(readByteVector32()), readRedeemPath()) + else -> Transactions.InputInfo.SegwitInput(outPoint, txOut, redeemScript) + } + } private fun Input.readOutPoint(): OutPoint = OutPoint.read(readDelimitedByteArray()) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Serialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Serialization.kt index 14cbc015b..e53a1004e 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Serialization.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Serialization.kt @@ -1,9 +1,6 @@ package fr.acinq.lightning.serialization.channel.v4 -import fr.acinq.bitcoin.BtcSerializer -import fr.acinq.bitcoin.OutPoint -import fr.acinq.bitcoin.ScriptWitness -import fr.acinq.bitcoin.Transaction +import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Output import fr.acinq.lightning.FeatureSupport @@ -21,13 +18,13 @@ import fr.acinq.lightning.serialization.OutputExtensions.writeLightningMessage import fr.acinq.lightning.serialization.OutputExtensions.writeNullable import fr.acinq.lightning.serialization.OutputExtensions.writeNumber import fr.acinq.lightning.serialization.OutputExtensions.writePublicKey +import fr.acinq.lightning.serialization.OutputExtensions.writePublicNonce import fr.acinq.lightning.serialization.OutputExtensions.writeString import fr.acinq.lightning.serialization.OutputExtensions.writeTxId import fr.acinq.lightning.serialization.common.liquidityads.Serialization.writeLiquidityPurchase import fr.acinq.lightning.transactions.* import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* import fr.acinq.lightning.wire.LightningCodecs -import fr.acinq.lightning.wire.LiquidityAds /** * Serialization for [ChannelStateWithCommitments]. @@ -249,6 +246,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 { @@ -586,6 +589,9 @@ object Serialization { writeNullable(lastIndex) { writeNumber(it) } } writeDelimited(remoteChannelData.data.toByteArray()) + if (o.isTaprootChannel) { + writeCollection(o.nextRemoteNonces) { writePublicNonce(it) } + } } private fun Output.writeDirectedHtlc(htlc: DirectedHtlc) = htlc.run { @@ -617,10 +623,31 @@ object Serialization { writeNumber(toRemote.toLong()) } + private fun Output.writeScriptTree(tree: ScriptTree): Unit = tree.run { + writeDelimited(this.write()) + } + + private fun Output.writeRedeemPath(redeemPath: Transactions.InputInfo.RedeemPath): Unit = when (redeemPath) { + is Transactions.InputInfo.RedeemPath.KeyPath -> { + write(0x01); writeNullable(redeemPath.scriptTree) { writeScriptTree(it) } + } + + is Transactions.InputInfo.RedeemPath.ScriptPath -> { + write(0x02); writeScriptTree(redeemPath.scriptTree); writeByteVector32(redeemPath.leafHash) + } + } + private fun Output.writeInputInfo(o: Transactions.InputInfo): Unit = o.run { writeBtcObject(outPoint) writeBtcObject(txOut) - writeDelimited(redeemScript.toByteArray()) + when (o) { + is Transactions.InputInfo.SegwitInput -> writeDelimited(o.redeemScript.toByteArray()) + is Transactions.InputInfo.TaprootInput -> { + writeDelimited(ByteArray(0)) + writeByteVector32(o.internalKey.value) + writeRedeemPath(o.redeemPath) + } + } } private fun Output.writeTransactionWithInputInfo(o: Transactions.TransactionWithInputInfo) { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt index 985d09dec..56ee0f13b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt @@ -2,6 +2,7 @@ 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.utils.sat @@ -30,6 +31,8 @@ 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) } + /** * minimal encoding of a number into a script element: * - OP_0 to OP_16 if 0 <= n <= 16 @@ -241,4 +244,155 @@ 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 NUMS_POINT = PublicKey.fromHex("02dca094751109d0bd055d03565874e8276dd53e926b44e3bd1bb6bf4bc130a279") + + 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?) + + val anchorScript: List = listOf(OP_16, OP_CHECKSEQUENCEVERIFY) + + val anchorScriptTree = ScriptTree.Leaf(anchorScript) + + /** + * Script that can be spent with the revocation key and reveals the delayed payment key to allow observers to claim + * unused anchor outputs. + * + * miniscript: this is not miniscript compatible + * + * @param localDelayedPaymentPubkey local delayed key + * @param revocationPubkey revocation key + * @return a script that will be used to add a "revocation" leaf to a script tree + */ + fun toRevocationKey(revocationPubkey: PublicKey, localDelayedPaymentPubkey: PublicKey) = + listOf(OP_PUSHDATA(localDelayedPaymentPubkey.xOnly()), OP_DROP, OP_PUSHDATA(revocationPubkey.xOnly()), OP_CHECKSIG) + + /** + * Script that can be spent by the owner of the commitment transaction after a delay. + * + * miniscript: and_v(v:pk(delayed_key),older(delay)) + * + * @param localDelayedPaymentPubkey delayed payment key + * @param toSelfDelay to-self CSV delay + * @return a script that will be used to add a "to local key" leaf to a script tree + */ + fun toLocalDelayed(localDelayedPaymentPubkey: PublicKey, toLocalDelay: CltvExpiryDelta) = + listOf(OP_PUSHDATA(localDelayedPaymentPubkey.xOnly()), OP_CHECKSIGVERIFY, encodeNumber(toLocalDelay.toLong()), OP_CHECKSEQUENCEVERIFY) + + /** + * + * @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(toLocalDelayed(localDelayedPaymentPubkey, toSelfDelay)), + ScriptTree.Leaf(toRevocationKey(revocationPubkey, localDelayedPaymentPubkey)), + ) + } + + /** + * Script that can be spent by the channel counterparty after a 1-block delay. + * + * miniscript: and_v(v:pk(remote_key),older(1)) + * + * @param remotePaymentPubkey remote payment key + * @return a script that will be used to add a "to remote key" leaf to a script tree + */ + fun toRemoteDelayed(remotePaymentPubkey: PublicKey) = listOf(OP_PUSHDATA(remotePaymentPubkey.xOnly()), OP_CHECKSIGVERIFY, OP_1, OP_CHECKSEQUENCEVERIFY) + + /** + * + * @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(toRemoteDelayed(remotePaymentPubkey)) + + /** + * Script that can be spent when an offered (outgoing) HTLC times out. + * It is spent using a pre-signed HTLC transaction signed with both keys. + * + * miniscript: and_v(v:pk(local_htlc_key),pk(remote_htlc_key)) + * + * @param localHtlcPubkey local HTLC key + * @param remoteHtlcPubkey remote HTLC key + * @return a script used to create a "HTLC timeout" leaf in a script tree + */ + fun offeredHtlcTimeout(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey) = + listOf(OP_PUSHDATA(localHtlcPubkey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(remoteHtlcPubkey.xOnly()), OP_CHECKSIG) + + /** + * Script that can be spent when an offered (outgoing) HTLC is fulfilled. + * It is spent using a signature from the receiving node and the preimage, with a 1-block delay. + * + * miniscript: and_v(v:hash160(H),and_v(v:pk(remote_htlc_key),older(1))) + * + * @param remoteHtlcPubkey remote HTLC key + * @param paymentHash payment hash + * @return a script used to create a "spend offered HTLC" leaf in a script tree + */ + 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_CHECKSIGVERIFY, + OP_1, OP_CHECKSEQUENCEVERIFY + // @formatter:on + ) + + fun offeredHtlcTree(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32) = + ScriptTree.Branch( + ScriptTree.Leaf(offeredHtlcTimeout(localHtlcPubkey, remoteHtlcPubkey)), + ScriptTree.Leaf(offeredHtlcSuccessScript(remoteHtlcPubkey, paymentHash)) + ) + + /** + * Script that can be spent when a received (incoming) HTLC times out. + * It is spent using a signature from the receiving node after an absolute delay and a 1-block relative delay. + * + * miniscript: and_v(v:pk(remote_htlc_key),and_v(v:older(1),after(delay))) + * + * @param remoteHtlcPubkey remote HTLC key + * @param lockTime HTLC expiry + */ + fun receivedHtlcTimeout(remoteHtlcPubkey: PublicKey, lockTime: CltvExpiry) = listOf( + // @formatter:off + OP_PUSHDATA(remoteHtlcPubkey.xOnly()), OP_CHECKSIGVERIFY, + OP_1, OP_CHECKSEQUENCEVERIFY, OP_VERIFY, + encodeNumber(lockTime.toLong()), OP_CHECKLOCKTIMEVERIFY + // @formatter:on + ) + + /** + * Script that can be spent when a received (incoming) HTLC is fulfilled. + * It is spent using a pre-signed HTLC transaction signed with both keys and the preimage. + * + * miniscript: and_v(v:hash160(H),and_v(v:pk(local_key),pk(remote_key))) + * + * @param localHtlcPubkey local HTLC key + * @param remoteHtlcPubkey remote HTLC key + * @param paymentHash payment hash + */ + 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(receivedHtlcTimeout(remoteHtlcPubkey, lockTime)), + ScriptTree.Leaf(receivedHtlcSuccessScript(localHtlcPubkey, remoteHtlcPubkey, paymentHash)), + ) + } + } } \ No newline at end of file diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index 2415ae2c4..52d6ee33b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -18,14 +18,22 @@ 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.getOrElse import fr.acinq.bitcoin.utils.runTrying import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.Commitments +import fr.acinq.lightning.channel.PartialSignatureWithNonce 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.Taproot.NUMS_POINT import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.UpdateAddHtlc import kotlinx.serialization.Contextual @@ -41,27 +49,109 @@ object Transactions { const val MAX_STANDARD_TX_WEIGHT = 400_000 - @Serializable - data class InputInfo( - @Contextual val outPoint: OutPoint, - @Contextual val txOut: TxOut, - @Contextual val redeemScript: ByteVector - ) { - constructor(outPoint: OutPoint, txOut: TxOut, redeemScript: List) : this(outPoint, txOut, ByteVector(Script.write(redeemScript))) + sealed class InputInfo { + abstract val outPoint: OutPoint + abstract val txOut: TxOut + + data class SegwitInput(override val outPoint: OutPoint, override val txOut: TxOut, val redeemScript: ByteVector) : InputInfo() { + constructor(outPoint: OutPoint, txOut: TxOut, redeemScript: List) : this(outPoint, txOut, ByteVector(Script.write(redeemScript))) + } + + sealed class RedeemPath { + data class KeyPath(val scriptTree: ScriptTree?) : RedeemPath() + data class ScriptPath(val scriptTree: ScriptTree, val leafHash: ByteVector32) : RedeemPath() { + init { + require(findScript(scriptTree, leafHash) != null) { "script tree must contain the provided leaf" } + } + + companion object { + fun findScript(scriptTree: ScriptTree, leafHash: ByteVector32): ScriptTree.Leaf? = when (scriptTree) { + is ScriptTree.Leaf -> if (scriptTree.hash() == leafHash) scriptTree else null + is ScriptTree.Branch -> findScript(scriptTree.left, leafHash) ?: findScript(scriptTree.right, leafHash) + } + } + } + } + + data class TaprootInput(override val outPoint: OutPoint, override val txOut: TxOut, val internalKey: XonlyPublicKey, val redeemPath: RedeemPath) : InputInfo() } @Serializable sealed class TransactionWithInputInfo { + @Contextual abstract val input: InputInfo abstract val tx: Transaction val amountIn: Satoshi get() = input.txOut.amount val fee: Satoshi get() = input.txOut.amount - tx.txOut.map { it.amount }.sum() + fun sign(key: PrivateKey): ByteVector64 { + val sigHash = when (input) { + is InputInfo.SegwitInput -> SigHash.SIGHASH_ALL + is InputInfo.TaprootInput -> SigHash.SIGHASH_DEFAULT + } + return sign(key, sigHash) + } + + open fun sign(key: PrivateKey, sigHash: Int): ByteVector64 { + val inputIndex = tx.txIn.indexOfFirst { it.outPoint == input.outPoint } + require(inputIndex >= 0) { "transaction doesn't spend the input to sign" } + return when (input) { + is InputInfo.SegwitInput -> sign(tx, inputIndex, (input as InputInfo.SegwitInput).redeemScript.toByteArray(), input.txOut.amount, key, sigHash) + is InputInfo.TaprootInput -> { + when (val redeemPath = (input as InputInfo.TaprootInput).redeemPath) { + is InputInfo.RedeemPath.KeyPath -> { + Transaction.signInputTaprootKeyPath(key, tx, inputIndex, listOf(input.txOut), sigHash, redeemPath.scriptTree) + } + + is InputInfo.RedeemPath.ScriptPath -> { + Transaction.signInputTaprootScriptPath(key, tx, inputIndex, listOf(input.txOut), sigHash, redeemPath.leafHash) + } + } + } + } + } + + open fun checkSig(sig: ByteVector64, pubKey: PublicKey, sigHash: Int = SigHash.SIGHASH_ALL): Boolean { + return when (input) { + is InputInfo.SegwitInput -> { + val data = Transaction.hashForSigning(tx, 0, (input as InputInfo.SegwitInput).redeemScript.toByteArray(), sigHash, input.txOut.amount, SigVersion.SIGVERSION_WITNESS_V0) + Crypto.verifySignature(data, sig, pubKey) + } + + is InputInfo.TaprootInput -> { + val data = when (val redeemPath = (input as InputInfo.TaprootInput).redeemPath) { + is InputInfo.RedeemPath.KeyPath -> { + Transaction.hashForSigningTaprootKeyPath(tx, 0, listOf(input.txOut), sigHash) + } + + is InputInfo.RedeemPath.ScriptPath -> { + Transaction.hashForSigningTaprootScriptPath(tx, 0, listOf(input.txOut), sigHash, redeemPath.leafHash) + } + } + Crypto.verifySignatureSchnorr(data, sig, pubKey.xOnly()) + } + } + } + @Serializable - data class SpliceTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class SpliceTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() @Serializable - data class CommitTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class CommitTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + fun checkPartialSignature(psig: PartialSignatureWithNonce, localPubKey: PublicKey, localNonce: IndividualNonce, remotePubKey: PublicKey): Boolean { + val inputIndex = this.tx.txIn.indexOfFirst { it.outPoint == input.outPoint } + val session = Musig2.taprootSession( + this.tx, + inputIndex, + listOf(this.input.txOut), + Scripts.sort(listOf(localPubKey, remotePubKey)), + listOf(localNonce, psig.nonce), + null + ) + val result = session.map { it.verify(psig.partialSig, psig.nonce, remotePubKey) }.getOrElse { false } + return result + } + } @Serializable sealed class HtlcTx : TransactionWithInputInfo() { @@ -69,14 +159,16 @@ object Transactions { @Serializable data class HtlcSuccessTx( - override val input: InputInfo, + @Contextual override val input: InputInfo, @Contextual override val tx: Transaction, @Contextual val paymentHash: ByteVector32, override val htlcId: Long - ) : HtlcTx() + ) : HtlcTx() { + } @Serializable - data class HtlcTimeoutTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : HtlcTx() + data class HtlcTimeoutTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : HtlcTx() { + } } @Serializable @@ -84,42 +176,49 @@ 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(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : ClaimHtlcTx() { + } @Serializable - data class ClaimHtlcTimeoutTx(override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : ClaimHtlcTx() + data class ClaimHtlcTimeoutTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction, override val htlcId: Long) : ClaimHtlcTx() { + } } @Serializable sealed class ClaimAnchorOutputTx : TransactionWithInputInfo() { @Serializable - data class ClaimLocalAnchorOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimAnchorOutputTx() + data class ClaimLocalAnchorOutputTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimAnchorOutputTx() @Serializable - data class ClaimRemoteAnchorOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimAnchorOutputTx() + data class ClaimRemoteAnchorOutputTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimAnchorOutputTx() } @Serializable - data class ClaimLocalDelayedOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class ClaimLocalDelayedOutputTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + } @Serializable sealed class ClaimRemoteCommitMainOutputTx : TransactionWithInputInfo() { // TODO: once we deprecate v2/v3 serialization, we can remove the class nesting. @Serializable - data class ClaimRemoteDelayedOutputTx(override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimRemoteCommitMainOutputTx() + data class ClaimRemoteDelayedOutputTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction) : ClaimRemoteCommitMainOutputTx() { + } } @Serializable - data class MainPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class MainPenaltyTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + } @Serializable - data class HtlcPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class HtlcPenaltyTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + } @Serializable - data class ClaimHtlcDelayedOutputPenaltyTx(override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() + data class ClaimHtlcDelayedOutputPenaltyTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction) : TransactionWithInputInfo() { + } @Serializable - data class ClosingTx(override val input: InputInfo, @Contextual override val tx: Transaction, val toLocalIndex: Int?) : TransactionWithInputInfo() { + data class ClosingTx(@Contextual override val input: InputInfo, @Contextual override val tx: Transaction, val toLocalIndex: Int?) : TransactionWithInputInfo() { val toLocalOutput: TxOut? get() = toLocalIndex?.let { tx.txOut[it] } } } @@ -158,6 +257,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 @@ -223,14 +323,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 @@ -278,12 +382,17 @@ object Transactions { /** * Represent a link between a commitment spec item (to-local, to-remote, htlc) and the actual output in the commit tx - * - * @param output transaction output - * @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> { + sealed class CommitmentOutputLink : Comparable> { + abstract val output: TxOut + abstract val commitmentOutput: T + + @Suppress("UNCHECKED_CAST") + inline fun filter(): CommitmentOutputLink? = when (commitmentOutput) { + is R -> this as CommitmentOutputLink + else -> null + } + /** * 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. @@ -297,6 +406,10 @@ object Transactions { else -> LexicographicalOrdering.compare(this.output, other.output) } } + + data class SegwitOutput(override val output: TxOut, val redeemScript: List, override val commitmentOutput: T) : CommitmentOutputLink() + + data class TaprootOutput(override val output: TxOut, val internalKey: XonlyPublicKey, val scriptTree: ScriptTree?, override val commitmentOutput: T) : CommitmentOutputLink() } fun makeCommitTxOutputs( @@ -310,9 +423,10 @@ object Transactions { remotePaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, - spec: CommitmentSpec + spec: CommitmentSpec, + isTaprootChannel: Boolean ): TransactionsCommitmentOutputs { - val commitFee = commitTxFee(localDustLimit, spec) + val commitFee = commitTxFee(localDustLimit, spec, isTaprootChannel) val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localPaysCommitTxFees) { Pair(spec.toLocal.truncateToSatoshi() - commitFee, spec.toRemote.truncateToSatoshi()) @@ -322,50 +436,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 = Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + outputs.add( + CommitmentOutputLink.TaprootOutput( + TxOut(toLocalAmount, Script.pay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree)), + NUMS_POINT.xOnly(), toLocalScriptTree, + CommitmentOutput.ToLocal + ) + ) + } + + else -> outputs.add( + CommitmentOutputLink.SegwitOutput( + 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.TaprootOutput( + TxOut(toRemoteAmount, Script.pay2tr(XonlyPublicKey(NUMS_POINT), toRemoteScriptTree)), + NUMS_POINT.xOnly(), toRemoteScriptTree, + CommitmentOutput.ToRemote + ) + ) + } + + else -> outputs.add( + CommitmentOutputLink.SegwitOutput( + 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.TaprootOutput( + TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2tr(localDelayedPaymentPubkey.xOnly(), Taproot.anchorScriptTree)), + localDelayedPaymentPubkey.xOnly(), Taproot.anchorScriptTree, + CommitmentOutput.ToLocalAnchor(localFundingPubkey) + ) + ) + } + + else -> outputs.add( + CommitmentOutputLink.SegwitOutput( + 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.TaprootOutput( + TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2tr(remotePaymentPubkey.xOnly(), Taproot.anchorScriptTree)), + remotePaymentPubkey.xOnly(), Taproot.anchorScriptTree, + CommitmentOutput.ToLocalAnchor(remoteFundingPubkey) + ) ) - ) + + else -> outputs.add( + CommitmentOutputLink.SegwitOutput( + 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 = Taproot.offeredHtlcTree(localHtlcPubkey, remoteHtlcPubkey, htlc.add.paymentHash) + outputs.add( + CommitmentOutputLink.TaprootOutput( + TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2tr(localRevocationPubkey.xOnly(), offeredHtlcTree)), localRevocationPubkey.xOnly(), offeredHtlcTree, OutHtlc(htlc) + ) + ) + } + + else -> { + val redeemScript = Scripts.htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray())) + outputs.add(CommitmentOutputLink.SegwitOutput(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 = Taproot.receivedHtlcTree(localHtlcPubkey, remoteHtlcPubkey, htlc.add.paymentHash, htlc.add.cltvExpiry) + outputs.add( + CommitmentOutputLink.TaprootOutput( + TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2tr(localRevocationPubkey.xOnly(), receivedHtlcTree)), localRevocationPubkey.xOnly(), receivedHtlcTree, InHtlc(htlc) + ) + ) + } + + else -> { + val redeemScript = Scripts.htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray()), htlc.add.cltvExpiry) + outputs.add(CommitmentOutputLink.SegwitOutput(TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) + } + } } return outputs.apply { sort() } @@ -393,8 +587,23 @@ 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 + + abstract fun flatMap(f: (T) -> TxResult): TxResult + + abstract fun orElse(or: TxResult): TxResult + + data class Skipped(val why: TxGenerationSkipped) : TxResult() { + override fun map(f: (T) -> R): TxResult = Skipped(why) + override fun flatMap(f: (T) -> TxResult) = Skipped(why) + override fun orElse(or: TxResult): TxResult = or + } + + data class Success(val result: T) : TxResult() { + override fun map(f: (T) -> R): TxResult = Success(f(result)) + override fun flatMap(f: (T) -> TxResult): TxResult = f(result) + override fun orElse(or: TxResult): TxResult = this + } } private fun makeHtlcTimeoutTx( @@ -405,23 +614,39 @@ object Transactions { localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, - feerate: FeeratePerKw + feerate: FeeratePerKw, ): TxResult { val fee = weight2fee(feerate, Commitments.HTLC_TIMEOUT_WEIGHT) - val redeemScript = output.redeemScript val htlc = output.commitmentOutput.outgoingHtlc.add val amount = htlc.amountMsat.truncateToSatoshi() - fee 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 (output) { + is CommitmentOutputLink.TaprootOutput -> { + val scriptTree: ScriptTree.Branch = (output.scriptTree as ScriptTree.Branch?)!! + val input = InputInfo.TaprootInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], output.internalKey, InputInfo.RedeemPath.ScriptPath(scriptTree, scriptTree.left.hash())) + val tree = ScriptTree.Leaf(Taproot.toLocalDelayed(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)) + } + + is CommitmentOutputLink.SegwitOutput -> { + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], output.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)) + } + } } } @@ -433,23 +658,39 @@ object Transactions { localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, - feerate: FeeratePerKw + feerate: FeeratePerKw, ): TxResult { val fee = weight2fee(feerate, Commitments.HTLC_SUCCESS_WEIGHT) - val redeemScript = output.redeemScript val htlc = output.commitmentOutput.incomingHtlc.add val amount = htlc.amountMsat.truncateToSatoshi() - fee 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 (output) { + is CommitmentOutputLink.TaprootOutput -> { + val scriptTree: ScriptTree.Branch = (output.scriptTree as ScriptTree.Branch?)!! + val input = InputInfo.TaprootInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], output.internalKey, InputInfo.RedeemPath.ScriptPath(scriptTree, scriptTree.right.hash())) + val tree = ScriptTree.Leaf(Taproot.toLocalDelayed(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)) + } + + is CommitmentOutputLink.SegwitOutput -> { + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], output.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)) + } + } } } @@ -463,20 +704,18 @@ object Transactions { outputs: TransactionsCommitmentOutputs ): 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) + .mapIndexed { outputIndex, link -> + link.filter()?.let { makeHtlcTimeoutTx(commitTx, it, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feerate) } } - .mapNotNull { (it as? TxResult.Success)?.result } + .filterIsInstance>() + .map { it.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) + .mapIndexed { outputIndex, link -> + link.filter()?.let { makeHtlcSuccessTx(commitTx, it, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feerate) } } - .mapNotNull { (it as? TxResult.Success)?.result } + .filterIsInstance>() + .map { it.result } return (htlcTimeoutTxs + htlcSuccessTxs).sortedBy { it.input.outPoint.index } } @@ -490,13 +729,19 @@ 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 output = outputs[outputIndex]) { + is CommitmentOutputLink.SegwitOutput -> InputInfo.SegwitInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], redeemScript) + is CommitmentOutputLink.TaprootOutput -> { + val scriptTree: ScriptTree.Branch = output.scriptTree as ScriptTree.Branch + InputInfo.TaprootInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], output.internalKey, InputInfo.RedeemPath.ScriptPath(scriptTree, scriptTree.right.hash())) + } + } val tx = Transaction( version = 2, txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), @@ -525,13 +770,19 @@ 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 output = outputs[outputIndex]) { + is CommitmentOutputLink.SegwitOutput -> InputInfo.SegwitInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], redeemScript) + is CommitmentOutputLink.TaprootOutput -> { + val scriptTree: ScriptTree.Branch = output.scriptTree as ScriptTree.Branch + InputInfo.TaprootInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], output.internalKey, InputInfo.RedeemPath.ScriptPath(scriptTree, scriptTree.left.hash())) + } + } // unsigned tx val tx = Transaction( version = 2, @@ -556,35 +807,88 @@ object Transactions { commitTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, - feerate: FeeratePerKw + feerate: FeeratePerKw, ): TxResult { - val redeemScript = Scripts.toRemoteDelayed(localPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) - - return when (val pubkeyScriptIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)) { - is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) - is TxResult.Success -> { - val outputIndex = pubkeyScriptIndex.result - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1)), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - lockTime = 0 - ) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feerate, 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.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx(input, tx1)) + + fun makeUnsignedTx(input: InputInfo): TxResult { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1)), + txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), + lockTime = 0 + ) + // compute weight with a dummy 73 bytes signature (the largest you can get) + val weight = addSigs(TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() + val fee = weight2fee(feerate, 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.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx(input, tx1)) + } + } + + fun makeClaimRemoteDelayedOutputTxTaproot(): TxResult { + val pubkeyScript = Script.pay2tr(XonlyPublicKey(NUMS_POINT), Taproot.toRemoteScriptTree(localPaymentPubkey)) + return findPubKeyScriptIndex(commitTx, pubkeyScript).flatMap { outputIndex -> + val scriptTree = Taproot.toRemoteScriptTree(localPaymentPubkey) + val input = InputInfo.TaprootInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], NUMS_POINT.xOnly(), InputInfo.RedeemPath.ScriptPath(scriptTree, scriptTree.hash())) + makeUnsignedTx(input) + } + } + + fun makeClaimRemoteDelayedOutputTxSegwit(): TxResult { + val pubkeyScript = Script.pay2wsh(Scripts.toRemoteDelayed(localPaymentPubkey)) + return findPubKeyScriptIndex(commitTx, pubkeyScript).flatMap { outputIndex -> + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], Scripts.toRemoteDelayed(localPaymentPubkey)) + makeUnsignedTx(input) + } + } + + return makeClaimRemoteDelayedOutputTxTaproot().orElse(makeClaimRemoteDelayedOutputTxSegwit()) + } + + fun makeHtlcDelayedTx( + htlcTx: Transaction, + localDustLimit: Satoshi, + localRevocationPubkey: PublicKey, + toLocalDelay: CltvExpiryDelta, + localDelayedPaymentPubkey: PublicKey, + localFinalScriptPubKey: ByteArray, + feeratePerKw: FeeratePerKw, + ): TxResult { + + fun makeHtlcDelayedTxTaproot(): TxResult { + val htlcTxTree = ScriptTree.Leaf(Taproot.toLocalDelayed(localDelayedPaymentPubkey, toLocalDelay)) + return when (val pubkeyScriptIndex = findPubKeyScriptIndex(htlcTx, Script.pay2tr(localRevocationPubkey.xOnly(), htlcTxTree))) { + is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) + is TxResult.Success -> { + val outputIndex = pubkeyScriptIndex.result + val input = InputInfo.TaprootInput(OutPoint(htlcTx, outputIndex.toLong()), htlcTx.txOut[outputIndex], localRevocationPubkey.xOnly(), InputInfo.RedeemPath.ScriptPath(htlcTxTree, htlcTxTree.hash())) + // unsigned transaction + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toLong())), + txOut = listOf(TxOut(Satoshi(0), localFinalScriptPubKey)), + lockTime = 0 + ) + val weight = run { + val witness = Script.witnessScriptPathPay2tr(localRevocationPubkey.xOnly(), htlcTxTree, ScriptWitness(listOf(ByteVector64.Zeroes)), htlcTxTree) + tx.updateWitness(0, witness).weight() + } + val fee = weight2fee(feeratePerKw, weight) + val amount = input.txOut.amount - fee + if (amount < localDustLimit) { + TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) + TxResult.Success(TransactionWithInputInfo.ClaimLocalDelayedOutputTx(input, tx1)) + } } } } + return makeHtlcDelayedTxTaproot().orElse(makeClaimLocalDelayedOutputTx(htlcTx, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localFinalScriptPubKey, feeratePerKw)) } fun makeClaimLocalDelayedOutputTx( @@ -594,34 +898,59 @@ object Transactions { toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteArray, - feerate: FeeratePerKw + feerate: FeeratePerKw, ): TxResult { - val redeemScript = Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) - 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))) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toLong())), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - 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 fee = weight2fee(feerate, weight) - val amount = input.txOut.amount - fee - if (amount < localDustLimit) { - TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) - } else { - val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) - TxResult.Success(TransactionWithInputInfo.ClaimLocalDelayedOutputTx(input, tx1)) + + fun makeUnsignedTx(input: InputInfo): TxResult { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toLong())), + txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), + lockTime = 0 + ) + val weight = when (input) { + is InputInfo.TaprootInput -> { + val toLocalScriptTree = Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree.left as ScriptTree.Leaf, ScriptWitness(listOf(ByteVector64.Zeroes)), toLocalScriptTree) + tx.updateWitness(0, witness).weight() } + + else -> addSigs(TransactionWithInputInfo.ClaimLocalDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() + } + val fee = weight2fee(feerate, weight) + val amount = input.txOut.amount - fee + return if (amount < localDustLimit) { + TxResult.Skipped(TxGenerationSkipped.AmountBelowDustLimit) + } else { + val tx1 = tx.copy(txOut = listOf(tx.txOut.first().copy(amount = amount))) + TxResult.Success(TransactionWithInputInfo.ClaimLocalDelayedOutputTx(input, tx1)) } } + + fun makeClaimLocalDelayedOutputTxTaproot(): TxResult { + val toLocalScriptTree = Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + val pubkeyScript = Script.pay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree) + return findPubKeyScriptIndex(delayedOutputTx, pubkeyScript).flatMap { outputIndex -> + val input = InputInfo.TaprootInput( + OutPoint(delayedOutputTx, outputIndex.toLong()), + delayedOutputTx.txOut[outputIndex], + NUMS_POINT.xOnly(), + InputInfo.RedeemPath.ScriptPath(toLocalScriptTree, toLocalScriptTree.left.hash()) + ) + makeUnsignedTx(input) + } + } + + fun makeClaimLocalDelayedOutputTxSegwit(): TxResult { + val redeemScript = Script.write(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)).byteVector() + val pubkeyScript = Script.pay2wsh(redeemScript) + return findPubKeyScriptIndex(delayedOutputTx, pubkeyScript).flatMap { outputIndex -> + val input = InputInfo.SegwitInput(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)) + makeUnsignedTx(input) + } + } + + return makeClaimLocalDelayedOutputTxTaproot().orElse(makeClaimLocalDelayedOutputTxSegwit()) } fun makeClaimDelayedOutputPenaltyTxs( @@ -631,33 +960,55 @@ object Transactions { toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteArray, - feerate: FeeratePerKw + feerate: FeeratePerKw, ): List> { - val redeemScript = Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) - 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))) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), - txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), - lockTime = 0 - ) - // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx(input, tx), PlaceHolderSig).tx.weight() - val fee = weight2fee(feerate, 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.ClaimHtlcDelayedOutputPenaltyTx(input, tx1)) + + fun makeUnsignedTx(input: InputInfo): TxResult { + // unsigned transaction + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), + txOut = listOf(TxOut(0.sat, localFinalScriptPubKey)), + lockTime = 0 + ) + // compute weight with a dummy 73 bytes signature (the largest you can get) + val weight = addSigs(TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx(input, tx), PlaceHolderSig).tx.weight() + val fee = weight2fee(feerate, 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.ClaimHtlcDelayedOutputPenaltyTx(input, tx1)) + } + } + + fun makeClaimHtlcDelayedOutputPenaltyTxsTaproot(): TxResult>> { + val tree = Taproot.toLocalScriptTree(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + val pubkeyScript = Script.pay2tr(localRevocationPubkey.xOnly(), tree.left) + return findPubKeyScriptIndexes(delayedOutputTx, pubkeyScript).map { outputIndexes -> + outputIndexes.map { outputIndex -> + val input = InputInfo.TaprootInput(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], localRevocationPubkey.xOnly(), InputInfo.RedeemPath.KeyPath(tree.left)) + makeUnsignedTx(input) + } + } + } + + fun makeClaimHtlcDelayedOutputPenaltyTxsSegwit(): TxResult>> { + val pubkeyScript = Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)) + return findPubKeyScriptIndexes(delayedOutputTx, pubkeyScript).map { outputIndexes -> + outputIndexes.map { outputIndex -> + val redeemScript = Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) + val input = InputInfo.SegwitInput(OutPoint(delayedOutputTx, outputIndex.toLong()), delayedOutputTx.txOut[outputIndex], redeemScript) + makeUnsignedTx(input) } } } + + return when (val result = makeClaimHtlcDelayedOutputPenaltyTxsTaproot().orElse(makeClaimHtlcDelayedOutputPenaltyTxsSegwit())) { + is TxResult.Skipped -> listOf(TxResult.Skipped(result.why)) + is TxResult.Success -> result.result + } } fun makeMainPenaltyTx( @@ -667,34 +1018,47 @@ object Transactions { localFinalScriptPubKey: ByteArray, toRemoteDelay: CltvExpiryDelta, remoteDelayedPaymentPubkey: PublicKey, - feerate: FeeratePerKw + feerate: FeeratePerKw, ): TxResult { - val redeemScript = Scripts.toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) - val pubkeyScript = Script.write(Script.pay2wsh(redeemScript)) - return when (val pubkeyScriptIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)) { - is TxResult.Skipped -> TxResult.Skipped(pubkeyScriptIndex.why) - is TxResult.Success -> { - val outputIndex = pubkeyScriptIndex.result - val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], ByteVector(Script.write(redeemScript))) - // unsigned transaction - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), - txOut = listOf(TxOut(0.sat, 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(feerate, 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.MainPenaltyTx(input, tx1)) - } + + fun nmakeUnsignedTx(input: InputInfo): TxResult { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 0xffffffffL)), + txOut = listOf(TxOut(0.sat, 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(feerate, 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.MainPenaltyTx(input, tx1)) + } + } + + fun makeMainPenaltyTxTaproot(): TxResult { + val tree = Taproot.toLocalScriptTree(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) + val pubkeyScript = Script.pay2tr(XonlyPublicKey(NUMS_POINT), tree) + return findPubKeyScriptIndex(commitTx, pubkeyScript).flatMap { outputIndex -> + val input = InputInfo.TaprootInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], NUMS_POINT.xOnly(), InputInfo.RedeemPath.ScriptPath(tree, tree.right.hash())) + nmakeUnsignedTx(input) } } + + fun makeMainPenaltyTxSegwit(): TxResult { + val redeemScript = Scripts.toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) + val pubkeyScript = Script.pay2wsh(redeemScript) + return findPubKeyScriptIndex(commitTx, pubkeyScript).flatMap { outputIndex -> + val input = InputInfo.SegwitInput(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex], redeemScript) + nmakeUnsignedTx(input) + } + } + + return makeMainPenaltyTxTaproot().orElse(makeMainPenaltyTxSegwit()) } /** @@ -708,7 +1072,7 @@ object Transactions { localFinalScriptPubKey: ByteArray, feerate: FeeratePerKw ): TxResult { - val input = InputInfo(OutPoint(commitTx, htlcOutputIndex.toLong()), commitTx.txOut[htlcOutputIndex], ByteVector(redeemScript)) + val input = InputInfo.SegwitInput(OutPoint(commitTx, htlcOutputIndex.toLong()), commitTx.txOut[htlcOutputIndex], ByteVector(redeemScript)) // unsigned transaction val tx = Transaction( version = 2, @@ -728,6 +1092,36 @@ object Transactions { } } + fun makeHtlcPenaltyTx( + commitTx: Transaction, + htlcOutputIndex: Int, + internalKey: XonlyPublicKey, + scriptTree: ScriptTree?, + localDustLimit: Satoshi, + localFinalScriptPubKey: ByteArray, + feeratePerKw: FeeratePerKw + ): TxResult { + val input = InputInfo.TaprootInput(OutPoint(commitTx, htlcOutputIndex.toLong()), commitTx.txOut[htlcOutputIndex], internalKey, InputInfo.RedeemPath.KeyPath(scriptTree)) + // 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.HtlcPenaltyTx(input, tx), PlaceHolderSig, PlaceHolderPubKey).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, @@ -772,6 +1166,8 @@ object Transactions { } } + private fun findPubKeyScriptIndex(tx: Transaction, pubkeyScript: List): TxResult = findPubKeyScriptIndex(tx, Script.write(pubkeyScript)) + private fun findPubKeyScriptIndexes(tx: Transaction, pubkeyScript: ByteArray): TxResult> { val outputIndexes = tx.txOut.withIndex().filter { it.value.publicKeyScript.contentEquals(pubkeyScript) }.map { it.index } return if (outputIndexes.isNotEmpty()) { @@ -781,6 +1177,8 @@ object Transactions { } } + private fun findPubKeyScriptIndexes(tx: Transaction, pubkeyScript: List): TxResult> = findPubKeyScriptIndexes(tx, Script.write(pubkeyScript)) + /** * Default public key used for fee estimation */ @@ -798,10 +1196,45 @@ object Transactions { return Crypto.der2compact(sigDER) } - fun sign(txInfo: TransactionWithInputInfo, key: PrivateKey, sigHash: Int = SigHash.SIGHASH_ALL): ByteVector64 { - val inputIndex = txInfo.tx.txIn.indexOfFirst { it.outPoint == txInfo.input.outPoint } - require(inputIndex >= 0) { "transaction doesn't spend the input to sign" } - return sign(txInfo.tx, inputIndex, txInfo.input.redeemScript.toByteArray(), txInfo.amountIn, key, sigHash) + fun sign(txInfo: TransactionWithInputInfo, key: PrivateKey, sigHash: Int): ByteVector64 { + return txInfo.sign(key, sigHash) + } + + fun sign(txInfo: TransactionWithInputInfo, key: PrivateKey): ByteVector64 { + return txInfo.sign(key) + } + + 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( @@ -816,47 +1249,158 @@ object Transactions { } fun addSigs(mainPenaltyTx: TransactionWithInputInfo.MainPenaltyTx, revocationSig: ByteVector64): TransactionWithInputInfo.MainPenaltyTx { - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScript) + val witness = when (mainPenaltyTx.input) { + is InputInfo.SegwitInput -> { + Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScript) + } + + is InputInfo.TaprootInput -> { + when (val redeemPath = mainPenaltyTx.input.redeemPath) { + is InputInfo.RedeemPath.ScriptPath -> { + Script.witnessScriptPathPay2tr( + mainPenaltyTx.input.internalKey, + (redeemPath.scriptTree as ScriptTree.Branch).right as ScriptTree.Leaf, + ScriptWitness(listOf(revocationSig)), + redeemPath.scriptTree + ) + } + + is InputInfo.RedeemPath.KeyPath -> error("unexpected key path redeem path when building main penalty tx") + } + } + } 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) { + is InputInfo.SegwitInput -> { + 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) { + is InputInfo.SegwitInput -> Scripts.witnessHtlcSuccess(localSig, remoteSig, paymentPreimage, htlcSuccessTx.input.redeemScript) + is InputInfo.TaprootInput -> { + when (val redeemPath = htlcSuccessTx.input.redeemPath) { + is InputInfo.RedeemPath.ScriptPath -> { + val branch = redeemPath.scriptTree as ScriptTree.Branch + val sigHash = (SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY).toByte() + Script.witnessScriptPathPay2tr(htlcSuccessTx.input.internalKey, branch.right as ScriptTree.Leaf, ScriptWitness(listOf(remoteSig.concat(sigHash), localSig, paymentPreimage)), branch) + } + + is InputInfo.RedeemPath.KeyPath -> error("unexpected key path when building HTLC success tx") + } + } + } 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) { + is InputInfo.SegwitInput -> Scripts.witnessHtlcTimeout(localSig, remoteSig, htlcTimeoutTx.input.redeemScript) + is InputInfo.TaprootInput -> { + when (val redeemPath = htlcTimeoutTx.input.redeemPath) { + is InputInfo.RedeemPath.ScriptPath -> { + val branch = redeemPath.scriptTree as ScriptTree.Branch + val sigHash = (SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY).toByte() + Script.witnessScriptPathPay2tr(htlcTimeoutTx.input.internalKey, branch.left as ScriptTree.Leaf, ScriptWitness(listOf(remoteSig.concat(sigHash), localSig)), branch) + } + + is InputInfo.RedeemPath.KeyPath -> error("unexpected key path when building HTLC timeout tx") + } + } + } 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 (claimHtlcSuccessTx.input) { + is InputInfo.SegwitInput -> Scripts.witnessClaimHtlcSuccessFromCommitTx(localSig, paymentPreimage, claimHtlcSuccessTx.input.redeemScript) + is InputInfo.TaprootInput -> { + when (val redeemPath = claimHtlcSuccessTx.input.redeemPath) { + is InputInfo.RedeemPath.ScriptPath -> { + val tree = redeemPath.scriptTree + Script.witnessScriptPathPay2tr(claimHtlcSuccessTx.input.internalKey, (tree as ScriptTree.Branch).right as ScriptTree.Leaf, ScriptWitness(listOf(localSig, paymentPreimage)), tree) + } + + is InputInfo.RedeemPath.KeyPath -> error("unexpected key path when building claim HTLC success tx") + } + } + } + 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 (claimHtlcTimeoutTx.input) { + is InputInfo.SegwitInput -> Scripts.witnessClaimHtlcTimeoutFromCommitTx(localSig, claimHtlcTimeoutTx.input.redeemScript) + is InputInfo.TaprootInput -> { + when (val redeemPath = claimHtlcTimeoutTx.input.redeemPath) { + is InputInfo.RedeemPath.ScriptPath -> { + val tree = redeemPath.scriptTree + Script.witnessScriptPathPay2tr(claimHtlcTimeoutTx.input.internalKey, (tree as ScriptTree.Branch).left as ScriptTree.Leaf, ScriptWitness(listOf(localSig)), tree) + } + + is InputInfo.RedeemPath.KeyPath -> error("unexpected key path when building claim HTLC timeout tx") + } + } + + } 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 (claimRemoteDelayed.input) { + is InputInfo.SegwitInput -> Scripts.witnessToRemoteDelayedAfterDelay(localSig, claimRemoteDelayed.input.redeemScript) + is InputInfo.TaprootInput -> { + val leaf = (claimRemoteDelayed.input.redeemPath as InputInfo.RedeemPath.ScriptPath).scriptTree as ScriptTree.Leaf + Script.witnessScriptPathPay2tr(claimRemoteDelayed.input.internalKey, leaf, ScriptWitness(listOf(localSig)), leaf) + } + } return claimRemoteDelayed.copy(tx = claimRemoteDelayed.tx.updateWitness(0, witness)) } fun addSigs(claimLocalDelayed: TransactionWithInputInfo.ClaimLocalDelayedOutputTx, localSig: ByteVector64): TransactionWithInputInfo.ClaimLocalDelayedOutputTx { - val witness = Scripts.witnessToLocalDelayedAfterDelay(localSig, claimLocalDelayed.input.redeemScript) + val witness = when (claimLocalDelayed.input) { + is InputInfo.SegwitInput -> Scripts.witnessToLocalDelayedAfterDelay(localSig, claimLocalDelayed.input.redeemScript) + is InputInfo.TaprootInput -> { + when (val tree = (claimLocalDelayed.input.redeemPath as InputInfo.RedeemPath.ScriptPath).scriptTree) { + is ScriptTree.Branch -> { + // claim a to-local delayed output + Script.witnessScriptPathPay2tr(claimLocalDelayed.input.internalKey, tree.left as ScriptTree.Leaf, ScriptWitness(listOf(localSig)), tree) + } + + is ScriptTree.Leaf -> { + // claim a delayed HTLC output + Script.witnessScriptPathPay2tr(claimLocalDelayed.input.internalKey, tree, ScriptWitness(listOf(localSig)), tree) + } + } + } + } return claimLocalDelayed.copy(tx = claimLocalDelayed.tx.updateWitness(0, witness)) } fun addSigs(claimHtlcDelayedPenalty: TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx, revocationSig: ByteVector64): TransactionWithInputInfo.ClaimHtlcDelayedOutputPenaltyTx { - val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScript) + val witness = when (claimHtlcDelayedPenalty.input) { + is InputInfo.SegwitInput -> Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScript) + is InputInfo.TaprootInput -> Script.witnessKeyPathPay2tr(revocationSig) + } return claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness)) } @@ -865,12 +1409,26 @@ object Transactions { return closingTx.copy(tx = closingTx.tx.updateWitness(0, witness)) } + fun addAggregatedSignature(commitTx: TransactionWithInputInfo.CommitTx, aggregatedSignature: ByteVector64): TransactionWithInputInfo.CommitTx { + return commitTx.copy(tx = commitTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregatedSignature))) + } + + fun addAggregatedSignature(closingTx: TransactionWithInputInfo.ClosingTx, aggregatedSignature: ByteVector64): TransactionWithInputInfo.ClosingTx { + return closingTx.copy(tx = closingTx.tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggregatedSignature))) + } + fun checkSpendable(txinfo: TransactionWithInputInfo): Try = runTrying { txinfo.tx.correctlySpends(mapOf(txinfo.tx.txIn.first().outPoint to txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } fun checkSig(txinfo: TransactionWithInputInfo, sig: ByteVector64, pubKey: PublicKey, sigHash: Int = SigHash.SIGHASH_ALL): Boolean { - val data = txinfo.tx.hashForSigning(0, txinfo.input.redeemScript.toByteArray(), sigHash, txinfo.amountIn, SigVersion.SIGVERSION_WITNESS_V0) - return Crypto.verifySignature(data, sig, pubKey) + return when (txinfo.input) { + is InputInfo.SegwitInput -> { + val data = txinfo.tx.hashForSigning(0, (txinfo.input as InputInfo.SegwitInput).redeemScript.toByteArray(), sigHash, txinfo.amountIn, SigVersion.SIGVERSION_WITNESS_V0) + return Crypto.verifySignature(data, sig, pubKey) + } + + is InputInfo.TaprootInput -> false + } } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt index 227c7c5e5..49b204264 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output import fr.acinq.lightning.Features @@ -8,10 +9,8 @@ import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelType -import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.utils.toByteVector -import fr.acinq.lightning.utils.toByteVector64 +import fr.acinq.lightning.utils.* +import fr.acinq.lightning.channel.PartialSignatureWithNonce sealed class ChannelTlv : Tlv { /** Commitment to where the funds will go in case of a mutual close, which remote node will enforce in case we're compromised. */ @@ -100,6 +99,23 @@ sealed class ChannelTlv : Tlv { override fun read(input: Input): FeeCreditUsedTlv = FeeCreditUsedTlv(LightningCodecs.tu64(input).msat) } } + + data class NextLocalNoncesTlv(val nonces: List) : ChannelTlv() { + override val tag: Long get() = NextLocalNoncesTlv.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): NextLocalNoncesTlv { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return NextLocalNoncesTlv(nonces) + } + } + } } sealed class ChannelReadyTlv : Tlv { @@ -112,6 +128,19 @@ sealed class ChannelReadyTlv : Tlv { override fun read(input: Input): ShortChannelIdTlv = ShortChannelIdTlv(ShortChannelId(LightningCodecs.u64(input))) } } + + data class NextLocalNonceTlv(val nonce: IndividualNonce) : ChannelReadyTlv() { + override val tag: Long get() = NextLocalNonceTlv.tag + + override fun write(out: Output) { + LightningCodecs.writeBytes(nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): NextLocalNonceTlv = NextLocalNonceTlv(IndividualNonce(LightningCodecs.bytes(input, 66))) + } + } } sealed class CommitSigTlv : Tlv { @@ -165,6 +194,63 @@ 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)) + ) + ) + } + } + } + + data class AlternativeFeeratePartialSig(val feerate: FeeratePerKw, val psig: PartialSignatureWithNonce) + + /** + * When there are no pending HTLCs, we provide a list of signatures for the commitment transaction signed at various feerates. + * This gives more options to the remote node to recover their funds if the user disappears without closing channels. + */ + data class AlternativeFeeratePartialSigs(val psigs: List) : CommitSigTlv() { + override val tag: Long get() = AlternativeFeeratePartialSigs.tag + override fun write(out: Output) { + LightningCodecs.writeByte(psigs.size, out) + psigs.forEach { + LightningCodecs.writeU32(it.feerate.toLong().toInt(), out) + LightningCodecs.writeBytes(it.psig.partialSig, out) + LightningCodecs.writeBytes(it.psig.nonce.toByteArray(), out) + } + } + + companion object : TlvValueReader { + const val tag: Long = 0x47010003 + override fun read(input: Input): AlternativeFeeratePartialSigs { + val count = LightningCodecs.byte(input) + val sigs = (0 until count).map { + AlternativeFeeratePartialSig( + FeeratePerKw(LightningCodecs.u32(input).toLong().sat), + PartialSignatureWithNonce( + LightningCodecs.bytes(input, 32).byteVector32(), + IndividualNonce(LightningCodecs.bytes(input, 66)) + ) + ) + } + return AlternativeFeeratePartialSigs(sigs) + } + } + } + } sealed class RevokeAndAckTlv : Tlv { @@ -177,6 +263,23 @@ sealed class RevokeAndAckTlv : Tlv { override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) } } + + data class NextLocalNoncesTlv(val nonces: List) : RevokeAndAckTlv() { + override val tag: Long get() = NextLocalNoncesTlv.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): NextLocalNoncesTlv { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return NextLocalNoncesTlv(nonces) + } + } + } } sealed class ChannelReestablishTlv : Tlv { @@ -199,6 +302,40 @@ sealed class ChannelReestablishTlv : Tlv { override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) } } + + data class NextLocalNoncesTlv(val nonces: List) : ChannelReestablishTlv() { + override val tag: Long get() = NextLocalNoncesTlv.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): NextLocalNoncesTlv { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return NextLocalNoncesTlv(nonces) + } + } + } + + data class SpliceNoncesTlv(val nonces: List) : ChannelReestablishTlv() { + override val tag: Long get() = SpliceNoncesTlv.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 6 + override fun read(input: Input): SpliceNoncesTlv { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return SpliceNoncesTlv(nonces) + } + } + } } sealed class ShutdownTlv : Tlv { @@ -211,6 +348,20 @@ sealed class ShutdownTlv : Tlv { override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) } } + + data class ShutdownNonce(val nonce: IndividualNonce) : ShutdownTlv() { + override val tag: Long get() = ShutdownNonce.tag + + override fun write(out: Output) { + LightningCodecs.writeBytes(nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 8 + + override fun read(input: Input): ShutdownNonce = ShutdownNonce(IndividualNonce(LightningCodecs.bytes(input, 66))) + } + } } sealed class ClosingSignedTlv : Tlv { @@ -237,4 +388,15 @@ sealed class ClosingSignedTlv : Tlv { override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector())) } } + + data class PartialSignature(val partialSignature: ByteVector32) : ClosingSignedTlv() { + override val tag: Long get() = PartialSignature.tag + + override fun write(out: Output) = LightningCodecs.writeBytes(partialSignature, out) + + companion object : TlvValueReader { + const val tag: Long = 6 + override fun read(input: Input): PartialSignature = PartialSignature(LightningCodecs.bytes(input, 32).toByteVector32()) + } + } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index dac94e411..aeee197ed 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt @@ -4,6 +4,7 @@ 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.toByteVector64 @@ -72,19 +73,54 @@ sealed class TxRemoveOutputTlv : Tlv sealed class TxCompleteTlv : Tlv { /** Public nonces for all Musig2 swap-in inputs (local and remote), ordered by serial id. */ - data class Nonces(val nonces: List) : TxCompleteTlv() { - override val tag: Long get() = Nonces.tag + data class SwapInNonces(val nonces: List) : TxCompleteTlv() { + override val tag: Long get() = SwapInNonces.tag override fun write(out: Output) { nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } } - companion object : TlvValueReader { + companion object : TlvValueReader { const val tag: Long = 101 - override fun read(input: Input): Nonces { + override fun read(input: Input): SwapInNonces { val count = input.availableBytes / 66 val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } - return Nonces(nonces) + return SwapInNonces(nonces) + } + } + } + + /** Public nonces for all Musig2 shared inputs (local and remote), ordered by serial id. */ + data class FundingNonces(val nonces: List) : TxCompleteTlv() { + override val tag: Long get() = FundingNonces.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): FundingNonces { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return FundingNonces(nonces) + } + } + } + + data class CommitNonces(val nonces: List) : TxCompleteTlv() { + override val tag: Long get() = CommitNonces.tag + + override fun write(out: Output) { + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } + } + + companion object : TlvValueReader { + const val tag: Long = 6 + override fun read(input: Input): CommitNonces { + val count = input.availableBytes / 66 + val nonces = (0 until count).map { IndividualNonce(LightningCodecs.bytes(input, 66)) } + return CommitNonces(nonces) } } } @@ -102,6 +138,24 @@ sealed class TxSignaturesTlv : Tlv { } } + data class PreviousFundingTxPartialSig(val partialSigWithNonce: PartialSignatureWithNonce) : TxSignaturesTlv() { + override val tag: Long get() = PreviousFundingTxPartialSig.tag + override fun write(out: Output) { + LightningCodecs.writeBytes(partialSigWithNonce.partialSig.toByteArray(), out) + LightningCodecs.writeBytes(partialSigWithNonce.nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 2 + override fun read(input: Input): PreviousFundingTxPartialSig = PreviousFundingTxPartialSig( + PartialSignatureWithNonce( + LightningCodecs.bytes(input, 32).byteVector32(), + IndividualNonce(LightningCodecs.bytes(input, 66)) + ) + ) + } + } + /** Signatures from the swap user for inputs that belong to them. */ data class SwapInUserSigs(val sigs: List) : TxSignaturesTlv() { override val tag: Long get() = SwapInUserSigs.tag diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 0143b9e87..5ae4b5689 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -6,11 +6,14 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelFlags import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.logging.MDCLogger +import fr.acinq.lightning.channel.PartialSignatureWithNonce +import fr.acinq.lightning.logging.* import fr.acinq.lightning.router.Announcements import fr.acinq.lightning.utils.* import fr.acinq.secp256k1.Hex @@ -466,9 +469,14 @@ data class TxComplete( ) : InteractiveTxConstructionMessage(), HasChannelId { override val type: Long get() = TxComplete.type - val publicNonces: List = tlvs.get()?.nonces ?: listOf() + val swapInNonces: List = tlvs.get()?.nonces ?: listOf() + val fundingNonces: List = tlvs.get()?.nonces ?: listOf() + val commitNonces: List = tlvs.get()?.nonces ?: listOf() - constructor(channelId: ByteVector32, publicNonces: List) : this(channelId, TlvStream(TxCompleteTlv.Nonces(publicNonces))) + constructor(channelId: ByteVector32, swapInNonces: List, fundingNonces: List, commitNonces: List) : this( + channelId, + TlvStream(TxCompleteTlv.SwapInNonces(swapInNonces), TxCompleteTlv.FundingNonces(fundingNonces), TxCompleteTlv.CommitNonces(commitNonces)) + ) override fun write(out: Output) { LightningCodecs.writeBytes(channelId.toByteArray(), out) @@ -479,7 +487,11 @@ data class TxComplete( const val type: Long = 70 @Suppress("UNCHECKED_CAST") - val readers = mapOf(TxCompleteTlv.Nonces.tag to TxCompleteTlv.Nonces.Companion as TlvValueReader) + val readers = mapOf( + TxCompleteTlv.SwapInNonces.tag to TxCompleteTlv.SwapInNonces.Companion as TlvValueReader, + TxCompleteTlv.FundingNonces.tag to TxCompleteTlv.FundingNonces.Companion as TlvValueReader, + TxCompleteTlv.CommitNonces.tag to TxCompleteTlv.CommitNonces.Companion as TlvValueReader + ) override fun read(input: Input): TxComplete = TxComplete(LightningCodecs.bytes(input, 32).byteVector32(), TlvStreamSerializer(false, readers).read(input)) } @@ -496,6 +508,7 @@ data class TxSignatures( tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, + previousFundingPartialSig: PartialSignatureWithNonce?, swapInUserSigs: List, swapInServerSigs: List, swapInUserPartialSigs: List, @@ -507,6 +520,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, @@ -518,6 +532,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() @@ -545,6 +560,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, @@ -908,6 +924,7 @@ data class ChannelReady( ) : ChannelMessage, HasChannelId { override val type: Long get() = ChannelReady.type val alias: ShortChannelId? = tlvStream.get()?.alias + val nextLocalNonce: IndividualNonce? = tlvStream.get()?.nonce override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) @@ -919,7 +936,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)), @@ -1023,7 +1042,7 @@ data class SpliceAck( fundingPubkey, TlvStream( setOfNotNull( - willFund?.let { ChannelTlv.ProvideFundingTlv(it) } + willFund?.let { ChannelTlv.ProvideFundingTlv(it) }, )) ) @@ -1237,7 +1256,12 @@ data class CommitSig( override fun withNonEmptyChannelData(ecd: EncryptedChannelData): CommitSig = copy(tlvStream = tlvStream.addOrUpdate(CommitSigTlv.ChannelData(ecd))) val alternativeFeerateSigs: List = tlvStream.get()?.sigs ?: listOf() + val alternativeFeeratePartialSigs: List = tlvStream.get()?.psigs ?: listOf() val batchSize: Int = tlvStream.get()?.size ?: 1 + val partialSig = tlvStream.get()?.psig + val sigOrPartialSig: Either = partialSig?.let { Either.Right(it) } ?: Either.Left(signature) + val alternativFeerateSigsOrPartialSigs: List> = + partialSig?.let { alternativeFeeratePartialSigs.map { Either.Right(it) } } ?: alternativeFeerateSigs.map { Either.Left(it) } override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) @@ -1254,7 +1278,9 @@ data class CommitSig( val readers = mapOf( CommitSigTlv.ChannelData.tag to CommitSigTlv.ChannelData.Companion as TlvValueReader, CommitSigTlv.AlternativeFeerateSigs.tag to CommitSigTlv.AlternativeFeerateSigs.Companion as TlvValueReader, + CommitSigTlv.AlternativeFeeratePartialSigs.tag to CommitSigTlv.AlternativeFeeratePartialSigs.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 { @@ -1281,6 +1307,8 @@ data class RevokeAndAck( override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): RevokeAndAck = copy(tlvStream = tlvStream.addOrUpdate(RevokeAndAckTlv.ChannelData(ecd))) + val nextLocalNonces = tlvStream.get()?.nonces ?: listOf() + override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) LightningCodecs.writeBytes(perCommitmentSecret.value, out) @@ -1292,7 +1320,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( @@ -1339,6 +1370,11 @@ data class ChannelReestablish( override val type: Long get() = ChannelReestablish.type val nextFundingTxId: TxId? = tlvStream.get()?.txId + val nextLocalNonces: List = tlvStream.get()?.nonces ?: listOf() + val spliceNonces: List = tlvStream.get()?.nonces ?: listOf() + val firstSpliceNonce = if (spliceNonces.isNotEmpty()) spliceNonces[0] else null + val secondSpliceNonce = if (spliceNonces.isNotEmpty()) spliceNonces[1] else null + override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): ChannelReestablish = copy(tlvStream = tlvStream.addOrUpdate(ChannelReestablishTlv.ChannelData(ecd))) @@ -1358,6 +1394,8 @@ 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, + ChannelReestablishTlv.SpliceNoncesTlv.tag to ChannelReestablishTlv.SpliceNoncesTlv.Companion as TlvValueReader, ) override fun read(input: Input): ChannelReestablish { @@ -1558,6 +1596,8 @@ data class Shutdown( override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): Shutdown = copy(tlvStream = tlvStream.addOrUpdate(ShutdownTlv.ChannelData(ecd))) + val shutdownNonce: IndividualNonce? = tlvStream.get()?.nonce + override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) LightningCodecs.writeU16(scriptPubKey.size(), out) @@ -1569,7 +1609,10 @@ data class Shutdown( const val type: Long = 38 @Suppress("UNCHECKED_CAST") - val readers = mapOf(ShutdownTlv.ChannelData.tag to ShutdownTlv.ChannelData.Companion as TlvValueReader) + val readers = mapOf( + ShutdownTlv.ChannelData.tag to ShutdownTlv.ChannelData.Companion as TlvValueReader, + ShutdownTlv.ShutdownNonce.tag to ShutdownTlv.ShutdownNonce.Companion as TlvValueReader, + ) override fun read(input: Input): Shutdown { return Shutdown( @@ -1592,6 +1635,8 @@ data class ClosingSigned( override val channelData: EncryptedChannelData get() = tlvStream.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): ClosingSigned = copy(tlvStream = tlvStream.addOrUpdate(ClosingSignedTlv.ChannelData(ecd))) + val partialSignature = tlvStream.get()?.partialSignature + override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) LightningCodecs.writeU64(feeSatoshis.toLong(), out) @@ -1605,7 +1650,8 @@ data class ClosingSigned( @Suppress("UNCHECKED_CAST") val readers = mapOf( ClosingSignedTlv.FeeRange.tag to ClosingSignedTlv.FeeRange.Companion as TlvValueReader, - ClosingSignedTlv.ChannelData.tag to ClosingSignedTlv.ChannelData.Companion as TlvValueReader + ClosingSignedTlv.ChannelData.tag to ClosingSignedTlv.ChannelData.Companion as TlvValueReader, + ClosingSignedTlv.PartialSignature.tag to ClosingSignedTlv.PartialSignature.Companion as TlvValueReader ) override fun read(input: Input): ClosingSigned { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt index 2a478607f..64f923aa5 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt @@ -318,7 +318,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.SegwitInput(tx.txIn.first().outPoint, TxOut(0.sat, ByteVector.empty), Script.pay2wpkh(randomKey().publicKey())) } private fun createClosingTransactions(): Triple { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt index 36597b9d0..8a4b15430 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt @@ -495,7 +495,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { val fundingAmount = (toLocal + toRemote).truncateToSatoshi() val dummyFundingScript = Scripts.multiSig2of2(randomKey().publicKey(), randomKey().publicKey()) val dummyFundingTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 1), 0)), listOf(TxOut(fundingAmount, Script.pay2wsh(dummyFundingScript))), 0) - val commitmentInput = Transactions.InputInfo(OutPoint(dummyFundingTx, 0), dummyFundingTx.txOut[0], dummyFundingScript) + val commitmentInput = Transactions.InputInfo.SegwitInput(OutPoint(dummyFundingTx, 0), dummyFundingTx.txOut[0], dummyFundingScript) val localCommitTx = Transactions.TransactionWithInputInfo.CommitTx(commitmentInput, Transaction(2, listOf(), listOf(), 0)) return Commitments( ChannelParams( diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt index dae11c3c3..4dd10cb8a 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt +++ b/modules/core/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.SegwitInput(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/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index cb266c319..7232c3ed6 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -29,8 +29,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingB = 100_000.sat val utxosB = listOf(30_000.sat, 100_000.sat) val legacyUtxosB = listOf(25_000.sat, 50_000.sat) + val isTaprootChannel = false val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 42) - assertEquals(f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), f.fundingParamsB.fundingPubkeyScript(f.channelKeysB)) + assertEquals(f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel), f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel)) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) @@ -78,7 +79,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA + fundingB }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == fundingA + fundingB }, 1) assertEquals(sharedTxA.sharedTx.localAmountIn, 215_000_000.msat) assertEquals(sharedTxA.sharedTx.remoteAmountIn, 205_000_000.msat) @@ -153,6 +154,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingB = 50_000.sat val utxosB = listOf(80_000.sat) val legacyUtxosB = listOf(30_000.sat) + val isTaprootChannel = false val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) @@ -183,7 +185,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA + fundingB }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == fundingA + fundingB }, 1) assertEquals(sharedTxA.sharedTx.totalAmountIn, 190_000.sat) assertEquals(sharedTxA.sharedTx.fees, 5130.sat) @@ -224,6 +226,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingB = 50_000.sat val utxosB = listOf(200_000.sat) val legacyUtxosB = listOf(30_000.sat) + val isTaprootChannel = false val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) @@ -253,7 +256,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA + fundingB }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == fundingA + fundingB }, 1) assertEquals(sharedTxA.sharedTx.totalAmountIn, 410_000.sat) assertEquals(sharedTxA.sharedTx.fees, 8550.sat) @@ -281,6 +284,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingA = 150_000.sat val utxosA = listOf(80_000.sat, 120_000.sat) val legacyUtxosA = listOf(30_000.sat) + val isTaprootChannel = false val f = createFixture(fundingA, utxosA, legacyUtxosA, 0.sat, listOf(), listOf(), targetFeerate, 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA) @@ -314,7 +318,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared output. assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == fundingA }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == fundingA }, 1) assertEquals(sharedTxA.sharedTx.totalAmountIn, 230_000.sat) assertEquals(sharedTxA.sharedTx.fees, 2985.sat) @@ -399,6 +403,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val balanceB = 50_000_600.msat val additionalFundingB = 20_000.sat val utxosB = listOf(80_000.sat) + val isTaprootChannel = false val f = createSpliceFixture(balanceA, additionalFundingA, utxosA, listOf(), balanceB, additionalFundingB, utxosB, listOf(), targetFeerate, 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, 200_000.sat) assertNotNull(f.fundingParamsA.sharedInput) @@ -431,7 +436,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared input and the shared output. assertEquals(listOf(inputA1, inputA2).count { it.sharedInput == f.fundingParamsA.sharedInput?.info?.outPoint }, 1) assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 200_000.sat }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == 200_000.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 129_999_400.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 70_000_600.msat) @@ -476,6 +481,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val balanceB = 90_000_300.msat val spliceOutputsB = listOf(TxOut(30_000.sat, Script.pay2wpkh(randomKey().publicKey()))) val subtractedFundingB = 30_500.sat + val isTaprootChannel = false + val f = createSpliceFixture(balanceA, -subtractedFundingA, listOf(), spliceOutputsA, balanceB, -subtractedFundingB, listOf(), spliceOutputsB, FeeratePerKw(1000.sat), 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, 108_500.sat) assertNotNull(f.fundingParamsA.sharedInput) @@ -505,7 +512,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNull(inputA.previousTx) assertEquals(inputA.sharedInput, f.fundingParamsA.sharedInput?.info?.outPoint) assertNotEquals(outputA1.pubkeyScript, outputA2.pubkeyScript) - assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 108_500.sat }, 1) + assertEquals(listOf(outputA1, outputA2).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == 108_500.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 48_999_700.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 59_500_300.msat) @@ -549,6 +556,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val balanceB = 99_999_175.msat val spliceOutputsB = listOf(25_000.sat, 15_000.sat).map { TxOut(it, Script.pay2wpkh(randomKey().publicKey())) } val subtractedFundingB = 40_500.sat + val isTaprootChannel = false + val f = createSpliceFixture(balanceA, -subtractedFundingA, listOf(), spliceOutputsA, balanceB, -subtractedFundingB, listOf(), spliceOutputsB, FeeratePerKw(1000.sat), 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, 158_500.sat) assertNotNull(f.fundingParamsA.sharedInput) @@ -585,7 +594,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared input and the shared output. assertNull(inputA.previousTx) assertEquals(inputA.sharedInput, f.fundingParamsA.sharedInput?.info?.outPoint) - assertEquals(listOf(outputA1, outputA2, outputA3, outputA4).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 158_500.sat }, 1) + assertEquals(listOf(outputA1, outputA2, outputA3, outputA4).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == 158_500.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 99_000_825.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 59_499_175.msat) @@ -627,6 +636,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val additionalFundingB = 15_000.sat val spliceOutputsB = listOf(TxOut(10_000.sat, Script.pay2wpkh(randomKey().publicKey()))) val utxosB = listOf(50_000.sat) + val isTaprootChannel = false + val f = createSpliceFixture(balanceA, additionalFundingA, utxosA, spliceOutputsA, balanceB, additionalFundingB, utxosB, spliceOutputsB, targetFeerate, 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, 290_000.sat) assertNotNull(f.fundingParamsA.sharedInput) @@ -662,7 +673,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice is responsible for adding the shared input and the shared output. assertEquals(listOf(inputA1, inputA2).count { it.sharedInput == f.fundingParamsA.sharedInput?.info?.outPoint }, 1) - assertEquals(listOf(outputA1, outputA2, outputA3).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA) && it.amount == 290_000.sat }, 1) + assertEquals(listOf(outputA1, outputA2, outputA3).count { it.pubkeyScript == f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel) && it.amount == 290_000.sat }, 1) assertEquals(sharedTxA.sharedTx.sharedOutput.localAmount, 174_000_333.msat) assertEquals(sharedTxA.sharedTx.sharedOutput.remoteAmount, 115_999_667.msat) @@ -990,14 +1001,15 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `multiple funding outputs`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob - val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob - val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 4, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 4, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_complete --> Bob val failure = receiveInvalidMessage(bob3, TxComplete(f.channelId)) assertIs(failure) @@ -1008,10 +1020,11 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val balanceA = 100_000_000.msat val spliceOutputA = TxOut(20_000.sat, Script.pay2wpkh(randomKey().publicKey())) val subtractedFundingA = 25_000.sat + val isTaprootChannel = false val f = createSpliceFixture(balanceA, -subtractedFundingA, listOf(), listOf(spliceOutputA), 0.msat, 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_output --> Bob - val (bob1, _) = receiveMessage(bob0, TxAddOutput(f.channelId, 0, 75_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob1, _) = receiveMessage(bob0, TxAddOutput(f.channelId, 0, 75_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, spliceOutputA.amount, spliceOutputA.publicKeyScript)) // Alice --- tx_complete --> Bob @@ -1073,12 +1086,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `invalid funding amount`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob - val failure = receiveInvalidMessage(bob1, TxAddOutput(f.channelId, 2, 100_001.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val failure = receiveInvalidMessage(bob1, TxAddOutput(f.channelId, 2, 100_001.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) assertIs(failure) assertEquals(failure.expected, 100_000.sat) assertEquals(failure.amount, 100_001.sat) @@ -1143,13 +1157,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `total input amount too low`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob - val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 4, 51_000.sat, validScript)) // Alice --- tx_complete --> Bob @@ -1159,13 +1174,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `minimum fee not met`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob - val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 2, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 4, 49_999.sat, validScript)) // Alice --- tx_complete --> Bob @@ -1176,8 +1192,9 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `previous attempts not double-spent`() { + val isTaprootChannel = false val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val sharedOutput = InteractiveTxOutput.Shared(0, f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), 100_000_000.msat, 0.msat, 0.msat) + val sharedOutput = InteractiveTxOutput.Shared(0, f.fundingParamsA.fundingPubkeyScript(f.channelKeysA, isTaprootChannel), 100_000_000.msat, 0.msat, 0.msat) val previousTx1 = Transaction(2, listOf(), listOf(TxOut(150_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) val previousTx2 = Transaction(2, listOf(), listOf(TxOut(160_000.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(200_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() @@ -1195,7 +1212,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, TxAddInput(f.channelId, 4, previousTx2, 1, 0u)) // Alice --- tx_add_output --> Bob - val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 6, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) + val (bob2, _) = receiveMessage(bob1, TxAddOutput(f.channelId, 6, 100_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB, isTaprootChannel))) // Alice --- tx_add_output --> Bob val (bob3, _) = receiveMessage(bob2, TxAddOutput(f.channelId, 8, 25_000.sat, validScript)) // Alice --- tx_complete --> Bob @@ -1245,14 +1262,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87") ) ) - val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, listOf(), listOf(), listOf(), listOf()) + val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, null, listOf(), listOf(), listOf(), listOf()) val nonInitiatorWitness = ScriptWitness( listOf( ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484") ) ) - val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, listOf(), listOf(), listOf(), listOf()) + val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, null, listOf(), listOf(), listOf(), listOf()) val initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs, null) assertEquals(initiatorSignedTx.feerate, FeeratePerKw(262.sat)) val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs, null) @@ -1339,7 +1356,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingScript = Script.write(Script.pay2wsh(redeemScript)).byteVector() val previousFundingAmount = (balanceA + balanceB).truncateToSatoshi() val previousFundingTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(previousFundingAmount, fundingScript)), 0) - val inputInfo = Transactions.InputInfo(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0], redeemScript) + val inputInfo = Transactions.InputInfo.SegwitInput(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0], redeemScript) val sharedInputA = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, channelKeysB.fundingPubKey(fundingTxIndex)) val nextFundingPubkeyB = channelKeysB.fundingPubKey(fundingTxIndex + 1) val fundingParamsA = InteractiveTxParams(channelId, true, fundingContributionA, fundingContributionB, sharedInputA, nextFundingPubkeyB, outputsA, lockTime, dustLimit, targetFeerate) @@ -1374,7 +1391,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingScript = Script.write(Script.pay2wsh(redeemScript)).byteVector() val previousFundingAmount = (balanceA + balanceB).truncateToSatoshi() val previousFundingTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(previousFundingAmount, fundingScript)), 0) - val inputInfo = Transactions.InputInfo(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0], redeemScript) + val inputInfo = Transactions.InputInfo.SegwitInput(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0], redeemScript) val sharedInputA = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, channelKeysB.fundingPubKey(fundingTxIndex)) val sharedInputB = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, channelKeysA.fundingPubKey(fundingTxIndex)) val nextFundingPubkeyA = channelKeysA.fundingPubKey(fundingTxIndex + 1) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index 2fd3056f2..fbf81de3e 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -1,6 +1,8 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.utils.Either +import fr.acinq.bitcoin.utils.flatMap import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.blockchain.Watch @@ -123,16 +125,20 @@ data class LNChannel( else -> state.copy(rbfStatus = RbfStatus.None) } is Normal -> when (state.spliceStatus) { - is SpliceStatus.WaitingForSigs -> state + is SpliceStatus.WaitingForSigs -> state.copy(spliceStatus = state.spliceStatus.copy(session = state.spliceStatus.session.copy(nextRemoteNonce = null))) else -> state.copy(spliceStatus = SpliceStatus.None) - } + }.updateCommitments(state.commitments.copy(closingNonce = null, pendingRemoteNextLocalNonce = null)) + + is WaitForFundingSigned -> state.copy(signingSession = state.signingSession.copy(nextRemoteNonce = null)) + is ChannelStateWithCommitments -> state.updateCommitments(state.commitments.copy(closingNonce = null, pendingRemoteNextLocalNonce = null)) else -> state } val serialized = Serialization.serialize(state) val deserialized = Serialization.deserialize(serialized).value + val filtered = removeTemporaryStatuses(state) - assertEquals(removeTemporaryStatuses(state), deserialized, "serialization error") + assertEquals(filtered, deserialized, "serialization error") } private fun checkSerialization(actions: List) { @@ -155,14 +161,26 @@ object TestsHelper { zeroConf: Boolean = false, channelOrigin: Origin? = null ): Triple, LNChannel, OpenDualFundedChannel> { + val isTaprootChannel = when (channelType) { + is ChannelType.SupportedChannelType.SimpleTaprootStaging -> true + else -> false + } + val aliceFeatures1 = when (isTaprootChannel) { + true -> aliceFeatures.add(Feature.SimpleTaprootStaging to FeatureSupport.Mandatory) + false -> aliceFeatures + } + val bobFeatures1 = when (isTaprootChannel) { + true -> bobFeatures.add(Feature.SimpleTaprootStaging to FeatureSupport.Mandatory) + false -> bobFeatures + } val (aliceNodeParams, bobNodeParams) = when (zeroConf) { true -> Pair( - TestConstants.Alice.nodeParams.copy(features = aliceFeatures, zeroConfPeers = setOf(TestConstants.Bob.nodeParams.nodeId)), - TestConstants.Bob.nodeParams.copy(features = bobFeatures, zeroConfPeers = setOf(TestConstants.Alice.nodeParams.nodeId)) + TestConstants.Alice.nodeParams.copy(features = aliceFeatures1, zeroConfPeers = setOf(TestConstants.Bob.nodeParams.nodeId)), + TestConstants.Bob.nodeParams.copy(features = bobFeatures1, zeroConfPeers = setOf(TestConstants.Alice.nodeParams.nodeId)) ) false -> Pair( - TestConstants.Alice.nodeParams.copy(features = aliceFeatures), - TestConstants.Bob.nodeParams.copy(features = bobFeatures) + TestConstants.Alice.nodeParams.copy(features = aliceFeatures1), + TestConstants.Bob.nodeParams.copy(features = bobFeatures1) ) } val alice = LNChannel( @@ -185,10 +203,10 @@ object TestsHelper { ) val channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = requestRemoteFunding != null) - val aliceChannelParams = TestConstants.Alice.channelParams(payCommitTxFees = !channelFlags.nonInitiatorPaysCommitFees).copy(features = aliceFeatures.initFeatures()) - val bobChannelParams = TestConstants.Bob.channelParams(payCommitTxFees = channelFlags.nonInitiatorPaysCommitFees).copy(features = bobFeatures.initFeatures()) - val aliceInit = Init(aliceFeatures) - val bobInit = Init(bobFeatures) + val aliceChannelParams = TestConstants.Alice.channelParams(payCommitTxFees = !channelFlags.nonInitiatorPaysCommitFees).copy(features = aliceFeatures1.initFeatures()) + val bobChannelParams = TestConstants.Bob.channelParams(payCommitTxFees = channelFlags.nonInitiatorPaysCommitFees).copy(features = bobFeatures1.initFeatures()) + val aliceInit = Init(aliceFeatures1) + val bobInit = Init(bobFeatures1) val (alice1, actionsAlice1) = alice.process( ChannelCommand.Init.Initiator( CompletableDeferred(), @@ -398,9 +416,13 @@ object TestsHelper { return s1 to remoteCommitPublished } - fun useAlternativeCommitSig(s: LNChannel, commitment: Commitment, alternative: CommitSigTlv.AlternativeFeerateSig): Transaction { + fun useAlternativeCommitSig(s: LNChannel, commitment: Commitment, alternative: Either): Transaction { val channelKeys = s.commitments.params.localParams.channelKeys(s.ctx.keyManager) - val alternativeSpec = commitment.localCommit.spec.copy(feerate = alternative.feerate) + val feerate = when (alternative) { + is Either.Left -> alternative.value.feerate + is Either.Right -> alternative.value.feerate + } + val alternativeSpec = commitment.localCommit.spec.copy(feerate = feerate) val fundingTxIndex = commitment.fundingTxIndex val commitInput = commitment.commitInput val remoteFundingPubKey = commitment.remoteFundingPubkey @@ -416,10 +438,23 @@ object TestsHelper { localPerCommitmentPoint, alternativeSpec ) - val localSig = Transactions.sign(localCommitTx, channelKeys.fundingKey(fundingTxIndex)) - val signedCommitTx = Transactions.addSigs(localCommitTx, channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, localSig, alternative.sig) - assertTrue(Transactions.checkSpendable(signedCommitTx).isSuccess) - return signedCommitTx.tx + return when (alternative) { + is Either.Left -> { + val localSig = Transactions.sign(localCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val signedCommitTx = Transactions.addSigs(localCommitTx, channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, localSig, alternative.value.sig) + assertTrue(Transactions.checkSpendable(signedCommitTx).isSuccess) + signedCommitTx.tx + } + + is Either.Right -> { + val remoteSig = alternative.value.psig + val localNonce = channelKeys.verificationNonce(fundingTxIndex, commitment.localCommit.index) + val signed = Transactions.partialSign(localCommitTx, channelKeys.fundingKey(fundingTxIndex), channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, localNonce, alternative.value.psig.nonce) + .flatMap { localSig -> Transactions.aggregatePartialSignatures(localCommitTx, localSig, remoteSig.partialSig, channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, localNonce.second, remoteSig.nonce) } + .map { aggSig -> Transactions.addAggregatedSignature(localCommitTx, aggSig) } + signed.right!!.tx + } + } } fun signAndRevack(alice: LNChannel, bob: LNChannel): Pair, LNChannel> { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt index 3ab0a0f84..ff24c29ed 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt @@ -110,6 +110,17 @@ class ClosingTestsCommon : LightningTestSuite() { assertContains(actions1, ChannelAction.Storage.SetLocked(mutualCloseTx.tx.txid)) } + @Test + fun `recv BITCOIN_TX_CONFIRMED -- mutual close -- simple taproot channel`() { + val (alice0, _, _) = initMutualClose(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val mutualCloseTx = alice0.state.mutualClosePublished.last() + + // actual test starts here + val (alice1, actions1) = alice0.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(ByteVector32.Zeroes, WatchConfirmed.ClosingTxConfirmed, 0, 0, mutualCloseTx.tx))) + assertIs(alice1.state) + assertContains(actions1, ChannelAction.Storage.SetLocked(mutualCloseTx.tx.txid)) + } + @Test fun `recv ClosingTxConfirmed -- mutual close with external btc address`() { val pubKey = Lightning.randomKey().publicKey() @@ -215,6 +226,18 @@ class ClosingTestsCommon : LightningTestSuite() { assertTrue(actions1.isEmpty()) } + @Test + fun `recv ChannelSpent -- local commit -- simple taproot channels`() { + val (aliceNormal, _) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (aliceClosing, localCommitPublished) = localClose(aliceNormal) + + // actual test starts here + // we are notified afterwards from our watcher about the tx that we just published + val (alice1, actions1) = aliceClosing.process(ChannelCommand.WatchReceived(WatchSpentTriggered(aliceNormal.state.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), localCommitPublished.commitTx))) + assertEquals(aliceClosing, alice1) + assertTrue(actions1.isEmpty()) + } + @Test fun `recv ClosingTxConfirmed -- local commit`() { val (alice0, bob0) = reachNormal() @@ -286,9 +309,8 @@ class ClosingTestsCommon : LightningTestSuite() { actions3.find().also { assertEquals(localCommitPublished.commitTx.txid, it.txId) } } - @Test - fun `recv ClosingTxConfirmed -- local commit with multiple htlcs for the same payment`() { - val (alice0, bob0) = reachNormal() + fun `recv ClosingTxConfirmed -- local commit with multiple htlcs for the same payment -- internal`(channelType: ChannelType.SupportedChannelType) { + val (alice0, bob0) = reachNormal(channelType) // alice sends an htlc to bob val (aliceClosing, localCommitPublished) = run { val (nodes1, preimage, _) = addHtlc(30_000_000.msat, alice0, bob0) @@ -329,6 +351,16 @@ class ClosingTestsCommon : LightningTestSuite() { confirmWatchedTxs(aliceClosing, watchConfirmed) } + @Test + fun `recv ClosingTxConfirmed -- local commit with multiple htlcs for the same payment`() { + `recv ClosingTxConfirmed -- local commit with multiple htlcs for the same payment -- internal`(ChannelType.SupportedChannelType.SimpleTaprootStaging) + } + + @Test + fun `recv ClosingTxConfirmed -- local commit with multiple htlcs for the same payment -- simple taproot channels`() { + `recv ClosingTxConfirmed -- local commit with multiple htlcs for the same payment -- internal`(ChannelType.SupportedChannelType.SimpleTaprootStaging) + } + @Test fun `recv ClosingTxConfirmed -- local commit with htlcs only signed by local`() { val (alice0, bob0) = reachNormal() @@ -760,7 +792,7 @@ class ClosingTestsCommon : LightningTestSuite() { val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(commitSigAlice)) val revBob = actionsBob6.hasOutgoingMessage() val (alice6, _) = alice5.process(ChannelCommand.MessageReceived(revBob)) - val alternativeCommitTx = useAlternativeCommitSig(alice6, alice6.commitments.active.first(), commitSigBob.alternativeFeerateSigs.first()) + val alternativeCommitTx = useAlternativeCommitSig(alice6, alice6.commitments.active.first(), commitSigBob.alternativFeerateSigsOrPartialSigs.first()) remoteClose(alternativeCommitTx, bob6) } @@ -1026,7 +1058,7 @@ class ClosingTestsCommon : LightningTestSuite() { val (bob4, actionsBob4) = bob3.process(ChannelCommand.Commitment.Sign) val commitSigBob = actionsBob4.hasOutgoingMessage() val (alice4, _) = alice3.process(ChannelCommand.MessageReceived(commitSigBob)) - val alternativeCommitTx = useAlternativeCommitSig(alice4, alice4.commitments.active.first(), commitSigBob.alternativeFeerateSigs.first()) + val alternativeCommitTx = useAlternativeCommitSig(alice4, alice4.commitments.active.first(), commitSigBob.alternativFeerateSigsOrPartialSigs.first()) remoteClose(alternativeCommitTx, bob4) } @@ -1542,9 +1574,8 @@ class ClosingTestsCommon : LightningTestSuite() { assertTrue(addSettledFails.all { it.result is ChannelAction.HtlcResult.Fail.OnChainFail }) } - @Test - fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx`() { - val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose() + fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx -- internal`(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs) { + val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose(channelType) // bob publishes one of his revoked txs val bobRevokedTx = bobCommitTxs[2] @@ -1649,6 +1680,16 @@ class ClosingTestsCommon : LightningTestSuite() { } } + @Test + fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx`() { + `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx -- internal`() + } + + @Test + fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx -- simple taproot channels`() { + `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published HtlcSuccess tx -- internal`(ChannelType.SupportedChannelType.SimpleTaprootStaging) + } + @Test fun `recv BITCOIN_OUTPUT_SPENT -- one revoked tx + counterparty published aggregated htlc tx`() { val (alice0, _, bobCommitTxs, htlcsAlice, htlcsBob) = prepareRevokedClose() @@ -1766,8 +1807,8 @@ class ClosingTestsCommon : LightningTestSuite() { } companion object { - fun initMutualClose(withPayments: Boolean = false): Triple, LNChannel, List> { - val (aliceInit, bobInit) = reachNormal() + fun initMutualClose(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, withPayments: Boolean = false): Triple, LNChannel, List> { + val (aliceInit, bobInit) = reachNormal(channelType = channelType) var mutableAlice: LNChannel = aliceInit var mutableBob: LNChannel = bobInit @@ -1808,8 +1849,8 @@ class ClosingTestsCommon : LightningTestSuite() { data class RevokedCloseFixture(val alice: LNChannel, val bob: LNChannel, val bobRevokedTxs: List, val htlcsAlice: List, val htlcsBob: List) - fun prepareRevokedClose(): RevokedCloseFixture { - val (aliceInit, bobInit) = reachNormal() + fun prepareRevokedClose(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs): RevokedCloseFixture { + val (aliceInit, bobInit) = reachNormal(channelType) var mutableAlice: LNChannel = aliceInit var mutableBob: LNChannel = bobInit @@ -1910,5 +1951,4 @@ class ClosingTestsCommon : LightningTestSuite() { actions.has() } } - } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt index a5c32083a..cd2c41749 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt +++ b/modules/core/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) } @@ -408,6 +408,18 @@ class NormalTestsCommon : LightningTestSuite() { ) } + @Test + fun `recv UpdateAddHtlc -- simple taproot channels`() { + val (_, bob0) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val add = UpdateAddHtlc(bob0.channelId, 0, 15_000.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(bob0.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket) + val (bob1, actions1) = bob0.process(ChannelCommand.MessageReceived(add)) + assertTrue(actions1.isEmpty()) + assertEquals( + bob0.copy(state = bob0.state.copy(commitments = bob0.commitments.copy(changes = bob0.commitments.changes.copy(remoteNextHtlcId = 1, remoteChanges = bob0.commitments.changes.remoteChanges.copy(proposed = listOf(add)))))), + bob1 + ) + } + @Test fun `recv UpdateAddHtlc -- zero-reserve`() { val (alice0, _) = reachNormal(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, bobFundingAmount = 10_000.sat) @@ -806,6 +818,33 @@ class NormalTestsCommon : LightningTestSuite() { assertEquals(3, alice9.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) } + @Test + fun `recv CommitSig -- multiple htlcs in both directions -- simple taproot channels`() { + val (alice0, bob0) = reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (nodes1, _, _) = addHtlc(50_000_000.msat, alice0, bob0) // a->b (regular) + val (alice1, bob1) = nodes1 + val (nodes2, _, _) = addHtlc(8_000_000.msat, alice1, bob1) // a->b (regular) + val (alice2, bob2) = nodes2 + val (nodes3, _, _) = addHtlc(300_000.msat, bob2, alice2) // b->a (dust) + val (bob3, alice3) = nodes3 + val (nodes4, _, _) = addHtlc(1_000_000.msat, alice3, bob3) // a->b (regular) + val (alice4, bob4) = nodes4 + val (nodes5, _, _) = addHtlc(50_000_000.msat, bob4, alice4) // b->a (regular) + val (bob5, alice5) = nodes5 + val (nodes6, _, _) = addHtlc(500_000.msat, alice5, bob5) // a->b (dust) + val (alice6, bob6) = nodes6 + val (nodes7, _, _) = addHtlc(4_000_000.msat, bob6, alice6) // b->a (regular) + val (bob7, alice7) = nodes7 + + val (alice8, bob8) = signAndRevack(alice7, bob7) + val (_, actionsBob9) = bob8.process(ChannelCommand.Commitment.Sign) + val commitSig = actionsBob9.findOutgoingMessage() + val (alice9, _) = alice8.process(ChannelCommand.MessageReceived(commitSig)) + assertIs>(alice9) + assertEquals(1, alice9.commitments.latest.localCommit.index) + assertEquals(3, alice9.commitments.latest.localCommit.publishableTxs.htlcTxsAndSigs.size) + } + @Test fun `recv CommitSig -- multiple htlcs in both directions -- non-initiator pays commit fees`() { val (alice0, bob0) = reachNormal(requestRemoteFunding = TestConstants.bobFundingAmount) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt index db62531c9..c773ea1f4 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt @@ -221,10 +221,12 @@ class OfflineTestsCommon : LightningTestSuite() { assertEquals(1, bob4.commitments.changes.remoteNextHtlcId) } - @Test - fun `resume htlc settlement`() { + fun resumeHtlcSettlement(isTaprootChannel: Boolean) { val (alice0, bob0, revB) = run { - val (alice0, bob0) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) + val (alice0, bob0) = when (isTaprootChannel) { + true -> TestsHelper.reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging, bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) + else -> TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) + } val (nodes1, r1, htlc1) = TestsHelper.addHtlc(15_000_000.msat, bob0, alice0) val (bob1, alice1) = TestsHelper.crossSign(nodes1.first, nodes1.second) val (bob2, alice2) = TestsHelper.fulfillHtlc(htlc1.id, r1, bob1, alice1) @@ -280,6 +282,16 @@ class OfflineTestsCommon : LightningTestSuite() { assertEquals(4, bob5.commitments.localCommitIndex) } + @Test + fun `resume htlc settlement`() { + resumeHtlcSettlement(isTaprootChannel = false) + } + + @Test + fun `resume htlc settlement -- simple taproot channels`() { + resumeHtlcSettlement(isTaprootChannel = true) + } + @Test fun `discover that we have a revoked commitment`() { val (alice, aliceOld, bob) = run { diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index 11cb63698..e37081818 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* +import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.WatchConfirmed @@ -31,23 +32,24 @@ import kotlinx.coroutines.runBlocking import kotlin.math.abs import kotlin.test.* -class SpliceTestsCommon : LightningTestSuite() { +open class SpliceTestsCommon : LightningTestSuite() { + open val defaultChannelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootStaging // ChannelType.SupportedChannelType.AnchorOutputs @Test fun `splice funds out`() { - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) spliceOut(alice, bob, 50_000.sat) } @Test fun `splice funds in`() { - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) spliceIn(alice, bob, listOf(50_000.sat)) } @Test fun `splice funds in and out with pending htlcs`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) val (alice2, bob2) = spliceInAndOut(alice1, bob1, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) @@ -82,7 +84,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice funds in and out with pending htlcs resolved after splice locked`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) val (alice2, bob2) = spliceInAndOut(alice1, bob1, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) val spliceTx = alice2.commitments.latest.localFundingStatus.signedTx!! @@ -97,13 +99,13 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice funds in -- non-initiator`() { - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) spliceIn(bob, alice, listOf(50_000.sat)) } @Test fun `splice funds in -- many utxos`() { - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) spliceIn(alice, bob, listOf(30_000.sat, 40_000.sat, 25_000.sat)) } @@ -111,7 +113,7 @@ class SpliceTestsCommon : LightningTestSuite() { fun `splice funds in -- local and remote commit index mismatch`() { // Alice and Bob asynchronously exchange HTLCs, which makes their commit indices diverge. val (nodes, preimages) = run { - val (alice0, bob0) = reachNormal() + val (alice0, bob0) = reachNormal(defaultChannelType) // Alice sends an HTLC to Bob and signs it. val (nodes1, preimage1, _) = addHtlc(15_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 @@ -156,7 +158,10 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice funds out -- would go below reserve`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + if (defaultChannelType.toFeatures().hasFeature(Feature.ZeroReserveChannels)) { + return + } + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice1, bob1, _) = setupHtlcs(alice, bob) val cmd = createSpliceOutRequest(810_000.sat) val (alice2, actionsAlice2) = alice1.process(cmd) @@ -174,7 +179,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice cpfp`() { - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) val (nodes, preimage, _) = addHtlc(15_000_000.msat, alice, bob) val (alice0, bob0) = crossSign(nodes.first, nodes.second) val (alice1, bob1) = spliceIn(alice0, bob0, listOf(50_000.sat)) @@ -219,7 +224,11 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice to purchase inbound liquidity`() { - val (alice, bob) = reachNormal() + val isTaprootChannel = when (defaultChannelType) { + is ChannelType.SupportedChannelType.SimpleTaprootStaging -> true + else -> false + } + val (alice, bob) = reachNormal(defaultChannelType) val fundingRates = LiquidityAds.WillFundRates( fundingRates = listOf(LiquidityAds.FundingRate(100_000.sat, 500_000.sat, 0, 250 /* 2.5% */, 0.sat, 1000.sat)), paymentTypes = setOf(LiquidityAds.PaymentType.FromChannelBalance), @@ -234,7 +243,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) val defaultSpliceAck = actionsBob2.findOutgoingMessage() assertNull(defaultSpliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey, isTaprootChannel) run { val willFund = fundingRates.validateRequest(bob.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)?.willFund assertNotNull(willFund) @@ -314,7 +323,7 @@ class SpliceTestsCommon : LightningTestSuite() { val spliceAck = actionsAlice2.hasOutgoingMessage() // We don't implement the liquidity provider side, so we must fake it. assertNull(spliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, isTaprootChannel = false) val willFund = fundingRates.validateRequest(alice.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)!!.willFund val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceAck.copy(fundingContribution = liquidityRequest.requestedAmount, tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))))) assertEquals(1, actionsBob3.size) @@ -335,7 +344,7 @@ class SpliceTestsCommon : LightningTestSuite() { val spliceAck = actionsAlice2.hasOutgoingMessage() // We don't implement the liquidity provider side, so we must fake it. assertNull(spliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, isTaprootChannel = false) val willFund = fundingRates.validateRequest(alice.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)!!.willFund val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceAck.copy(fundingContribution = liquidityRequest.requestedAmount, tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))))) assertEquals(1, actionsBob3.size) @@ -401,7 +410,7 @@ class SpliceTestsCommon : LightningTestSuite() { val spliceAck = actionsAlice2.hasOutgoingMessage() // We don't implement the liquidity provider side, so we must fake it. assertNull(spliceAck.willFund) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, isTaprootChannel = false) val willFund = fundingRates.validateRequest(alice.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)!!.willFund val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceAck.copy(fundingContribution = fundingRequest.requestedAmount, tlvStream = TlvStream(ChannelTlv.ProvideFundingTlv(willFund))))) actionsBob3.hasOutgoingMessage() @@ -411,7 +420,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `reject splice_init`() { val cmd = createSpliceOutRequest(25_000.sat) - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) val (alice0, bob0) = crossSign(nodes.first, nodes.second) val (alice1, _, _) = reachQuiescent(cmd, alice0, bob0) @@ -425,7 +434,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `reject splice_init -- cancel on-the-fly funding`() { val cmd = createSpliceOutRequest(50_000.sat) - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) val (alice1, _, _) = reachQuiescent(cmd, alice, bob) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(CancelOnTheFlyFunding(alice.channelId, listOf(randomBytes32()), "cancelling on-the-fly funding"))) assertIs(alice2.state) @@ -436,7 +445,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `reject splice_ack`() { val cmd = createSpliceOutRequest(25_000.sat) - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) val (alice0, bob0) = crossSign(nodes.first, nodes.second) val (_, bob1, spliceInit) = reachQuiescent(cmd, alice0, bob0) @@ -453,7 +462,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `abort before tx_complete`() { val cmd = createSpliceOutRequest(20_000.sat) - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) val (alice0, bob0) = crossSign(nodes.first, nodes.second) val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice0, bob0) @@ -482,7 +491,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `abort after tx_complete`() { val cmd = createSpliceOutRequest(31_000.sat) - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) val (alice0, bob0) = crossSign(nodes.first, nodes.second) val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice0, bob0) @@ -519,7 +528,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `abort after tx_complete then receive commit_sig`() { val cmd = createSpliceOutRequest(50_000.sat) - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) val (alice0, bob0) = crossSign(nodes.first, nodes.second) val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice0, bob0) @@ -554,7 +563,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `exchange splice_locked`() { - val (alice, bob) = reachNormal() + val (alice, bob) = reachNormal(defaultChannelType) val (alice1, bob1) = spliceOut(alice, bob, 60_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! @@ -588,7 +597,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `exchange splice_locked -- zero-conf`() { - val (alice, bob) = reachNormal(zeroConf = true) + val (alice, bob) = reachNormal(defaultChannelType, zeroConf = true) val (alice1, bob1) = spliceOut(alice, bob, 60_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! @@ -611,7 +620,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `remote splice_locked applies to previous splices`() { - val (alice, bob) = reachNormal(zeroConf = true) + val (alice, bob) = reachNormal(defaultChannelType, zeroConf = true) val (alice1, bob1) = spliceOut(alice, bob, 60_000.sat) val spliceTx1 = alice1.commitments.latest.localFundingStatus.signedTx!! val (alice2, _) = spliceOut(alice1, bob1, 60_000.sat) @@ -626,7 +635,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `use channel before splice_locked -- zero-conf`() { - val (alice, bob) = reachNormal(zeroConf = true) + val (alice, bob) = reachNormal(defaultChannelType, zeroConf = true) val (alice1, bob1) = spliceOut(alice, bob, 50_000.sat) assertEquals(alice1.commitments.active.size, 2) assertEquals(bob1.commitments.active.size, 2) @@ -658,7 +667,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `use channel during splice_locked -- zero-conf`() { - val (alice, bob) = reachNormal(zeroConf = true) + val (alice, bob) = reachNormal(defaultChannelType, zeroConf = true) val (alice1, bob1) = spliceOut(alice, bob, 30_000.sat) val (alice2, bob2) = spliceOut(alice1, bob1, 20_000.sat) assertEquals(alice2.commitments.active.size, 3) @@ -740,7 +749,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- commit_sig not received`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) val aliceCommitIndex = alice0.commitments.localCommitIndex val bobCommitIndex = bob0.commitments.localCommitIndex @@ -770,7 +779,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- commit_sig received by alice`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) val aliceCommitIndex = alice1.commitments.localCommitIndex val bobCommitIndex = bob1.commitments.localCommitIndex @@ -825,7 +834,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- commit_sig received by bob`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) val aliceCommitIndex = alice0.commitments.localCommitIndex val bobCommitIndex = bob0.commitments.localCommitIndex @@ -879,7 +888,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- commit_sig received by bob -- zero-conf`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType, zeroConf = true) val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) val (alice1, commitSigAlice1, bob1, _) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(75_000.sat), outAmount = 120_000.sat) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSigAlice1)) @@ -946,7 +955,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- tx_signatures received by alice -- confirms while bob is offline`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) val (alice1, commitSigAlice1, bob1, commitSigBob1) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(70_000.sat, 60_000.sat), outAmount = 150_000.sat) @@ -1000,7 +1009,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- tx_signatures received by alice`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) val (alice1, commitSigAlice, bob1, commitSigBob) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(315_000.sat), outAmount = 25_000.sat) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(commitSigBob)) @@ -1039,7 +1048,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- new changes before splice_locked`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice1, bob1) = spliceOut(alice, bob, 70_000.sat) val (nodes2, _, htlc) = addHtlc(50_000_000.msat, alice1, bob1) val (alice3, actionsAlice3) = nodes2.first.process(ChannelCommand.Commitment.Sign) @@ -1062,7 +1071,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- splice_locked sent`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceInAndOut(alice0, bob0, inAmounts = listOf(150_000.sat, 25_000.sat, 15_000.sat), outAmount = 250_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! @@ -1104,7 +1113,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- latest commitment locked remotely and locally -- zero-conf`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType, zeroConf = true) val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceIn(alice0, bob0, listOf(50_000.sat)) val (alice2, bob2) = spliceOut(alice1, bob1, 100_000.sat) @@ -1151,7 +1160,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- latest commitment locked remotely but not locally`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceIn(alice0, bob0, listOf(50_000.sat)) val spliceTx1 = alice1.commitments.latest.localFundingStatus.signedTx!! @@ -1214,7 +1223,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- splice tx published`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, _) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceOut(alice0, bob0, 40_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! @@ -1234,7 +1243,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- latest active commitment`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, _) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceOut(alice0, bob0, 75_000.sat) @@ -1261,12 +1270,12 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- latest active commitment -- alternative feerate`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice1, commitSigAlice, bob1, commitSigBob) = spliceOutWithoutSigs(alice, bob, 75_000.sat) val (alice2, bob2) = exchangeSpliceSigs(alice1, commitSigAlice, bob1, commitSigBob) // Bob force-closes using the latest active commitment and an optional feerate. - val bobCommitTx = useAlternativeCommitSig(bob2, bob2.commitments.active.first(), commitSigAlice.alternativeFeerateSigs.last()) + val bobCommitTx = useAlternativeCommitSig(bob2, bob2.commitments.active.first(), commitSigAlice.alternativFeerateSigsOrPartialSigs.last()) val commitment = alice1.commitments.active.first() val (alice3, actionsAlice3) = alice2.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTx))) assertIs>(alice3) @@ -1276,7 +1285,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- previous active commitment`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, _) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceOut(alice0, bob0, 75_000.sat) @@ -1288,7 +1297,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- previous active commitment -- alternative feerate`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice1, commitSigAlice1, bob1, commitSigBob1) = spliceOutWithoutSigs(alice, bob, 75_000.sat) val (alice2, bob2) = exchangeSpliceSigs(alice1, commitSigAlice1, bob1, commitSigBob1) val (alice3, commitSigAlice3, bob3, commitSigBob3) = spliceOutWithoutSigs(alice2, bob2, 75_000.sat) @@ -1296,13 +1305,13 @@ class SpliceTestsCommon : LightningTestSuite() { // Bob force-closes using an older active commitment with an alternative feerate. assertEquals(bob4.commitments.active.map { it.localCommit.publishableTxs.commitTx.tx }.toSet().size, 3) - val bobCommitTx = useAlternativeCommitSig(bob4, bob4.commitments.active[1], commitSigAlice1.alternativeFeerateSigs.first()) + val bobCommitTx = useAlternativeCommitSig(bob4, bob4.commitments.active[1], commitSigAlice1.alternativFeerateSigsOrPartialSigs.first()) handlePreviousRemoteClose(alice4, bobCommitTx) } @Test fun `force-close -- previous inactive commitment`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType, zeroConf = true) val (alice0, bob0, _) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! @@ -1321,7 +1330,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- revoked latest active commitment`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, _) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val bobCommitTx = bob1.commitments.active.first().localCommit.publishableTxs.commitTx.tx @@ -1338,10 +1347,10 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- revoked latest active commitment -- alternative feerate`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice1, commitSigAlice, bob1, commitSigBob) = spliceOutWithoutSigs(alice, bob, 50_000.sat) val (alice2, bob2) = exchangeSpliceSigs(alice1, commitSigAlice, bob1, commitSigBob) - val bobCommitTx = useAlternativeCommitSig(bob2, bob2.commitments.active.first(), commitSigAlice.alternativeFeerateSigs.first()) + val bobCommitTx = useAlternativeCommitSig(bob2, bob2.commitments.active.first(), commitSigAlice.alternativFeerateSigsOrPartialSigs.first()) // Alice sends an HTLC to Bob, which revokes the previous commitment. val (nodes3, _, _) = addHtlc(25_000_000.msat, alice2, bob2) @@ -1355,7 +1364,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- revoked previous active commitment`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice0, bob0, _) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val bobCommitTx = bob1.commitments.active.last().localCommit.publishableTxs.commitTx.tx @@ -1374,7 +1383,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- revoked previous inactive commitment`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType, zeroConf = true) val (alice0, bob0, _) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! @@ -1400,7 +1409,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- revoked previous inactive commitment after two splices`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType, zeroConf = true) val (alice0, bob0, _) = setupHtlcs(alice, bob) val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! @@ -1443,7 +1452,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `recv invalid htlc signatures during splice`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(defaultChannelType) val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) val (alice2, commitSigAlice, bob2, commitSigBob) = spliceInAndOutWithoutSigs(alice1, bob1, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) assertEquals(commitSigAlice.htlcSignatures.size, 4) @@ -1464,8 +1473,8 @@ class SpliceTestsCommon : LightningTestSuite() { companion object { private val spliceFeerate = FeeratePerKw(253.sat) - private fun reachNormalWithConfirmedFundingTx(zeroConf: Boolean = false): Pair, LNChannel> { - val (alice, bob) = reachNormal(zeroConf = zeroConf) + private fun reachNormalWithConfirmedFundingTx(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, zeroConf: Boolean = false): Pair, LNChannel> { + val (alice, bob) = reachNormal(channelType = channelType, zeroConf = zeroConf) val fundingTx = alice.commitments.latest.localFundingStatus.signedTx!! val (alice1, _) = alice.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice.channelId, WatchConfirmed.ChannelFundingDepthOk, 42, 3, fundingTx))) val (bob1, _) = bob.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob.channelId, WatchConfirmed.ChannelFundingDepthOk, 42, 3, fundingTx))) @@ -1961,3 +1970,7 @@ class SpliceTestsCommon : LightningTestSuite() { } } + +class SpliceWithTaprootChannelsTestsCommon : SpliceTestsCommon() { + override val defaultChannelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootStaging +} diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt index 08d49781a..db5bf44dc 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForChannelReadyTestsCommon.kt +++ b/modules/core/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() @@ -77,6 +90,25 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { assertIs(actionsBob1.find().event) } + @Test + fun `recv ChannelReady -- simple taproot channels`() { + val (alice, channelReadyAlice, bob, channelReadyBob) = init(ChannelType.SupportedChannelType.SimpleTaprootStaging) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(channelReadyBob)) + assertIs(alice1.state) + actionsAlice1.find().also { + assertEquals(alice.commitments.latest.fundingTxId, it.txId) + } + actionsAlice1.has() + assertIs(actionsAlice1.find().event) + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(channelReadyAlice)) + assertIs(bob1.state) + actionsBob1.find().also { + assertEquals(bob.commitments.latest.fundingTxId, it.txId) + } + actionsBob1.has() + assertIs(actionsBob1.find().event) + } + @Test fun `recv ChannelSpent -- remote commit`() { val (alice, _, bob, _) = init() diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt index f0e069a46..40a5c9dfc 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* import fr.acinq.lightning.Feature +import fr.acinq.lightning.FeatureSupport import fr.acinq.lightning.Features import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey @@ -69,6 +70,40 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { verifyCommits(alice2.state.signingSession, bob3.state.signingSession, TestConstants.aliceFundingAmount.toMilliSatoshi(), 0.msat) } + @Test + fun `complete interactive-tx protocol -- simple taproot channels`() { + val (alice, bob, inputAlice) = init( + ChannelType.SupportedChannelType.SimpleTaprootStaging, + aliceFeatures = TestConstants.Alice.nodeParams.features.initFeatures().add(Feature.SimpleTaprootStaging to FeatureSupport.Mandatory), + bobFeatures = TestConstants.Bob.nodeParams.features.initFeatures().add(Feature.SimpleTaprootStaging to FeatureSupport.Mandatory), + bobFundingAmount = 0.sat + ) + // Alice ---- tx_add_input ----> Bob + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(inputAlice)) + // Alice <--- tx_complete ----- Bob + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(actionsBob1.findOutgoingMessage())) + // Alice ---- tx_add_output ----> Bob + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(actionsAlice1.findOutgoingMessage())) + // Alice <--- tx_complete ----- Bob + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) + // Alice ---- tx_complete ----> Bob + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) + val commitSigAlice = actionsAlice2.findOutgoingMessage() + val commitSigBob = actionsBob3.findOutgoingMessage() + assertEquals(commitSigAlice.channelId, commitSigBob.channelId) + assertTrue(commitSigAlice.htlcSignatures.isEmpty()) + assertTrue(commitSigAlice.channelData.isEmpty()) + assertTrue(commitSigBob.htlcSignatures.isEmpty()) + assertFalse(commitSigBob.channelData.isEmpty()) + actionsAlice2.has() + actionsBob3.has() + assertIs(alice2.state) + assertIs(bob3.state) + assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels, Feature.DualFunding, Feature.SimpleTaprootStaging))) + assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels, Feature.DualFunding, Feature.SimpleTaprootStaging))) + verifyCommits(alice2.state.signingSession, bob3.state.signingSession, TestConstants.aliceFundingAmount.toMilliSatoshi(), 0.msat) + } + @Test fun `complete interactive-tx protocol -- with non-initiator contributions`() { val (alice, bob, inputAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt index 8f7c51c5d..382eee13d 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt @@ -42,6 +42,31 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { } } + @Test + fun `recv CommitSig -- simple taproot channels`() { + val (alice, commitSigAlice, bob, commitSigBob) = init(channelType = ChannelType.SupportedChannelType.SimpleTaprootStaging) + val commitInput = alice.state.signingSession.commitInput + run { + val (_, _) = alice.process(ChannelCommand.MessageReceived(commitSigBob)) + .also { (state, actions) -> + assertIs(state.state) + assertTrue(actions.isEmpty()) + } + } + run { + val (_, _) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) + .also { (state, actions) -> + assertIs(state.state) + assertEquals(actions.size, 5) + actions.hasOutgoingMessage().also { assertFalse(it.channelData.isEmpty()) } + actions.findWatch().also { assertEquals(WatchConfirmed(state.channelId, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, 3, WatchConfirmed.ChannelFundingDepthOk), it) } + actions.find().also { assertEquals(TestConstants.bobFundingAmount.toMilliSatoshi(), it.amountReceived) } + actions.has() + actions.find().also { assertEquals(ChannelEvents.Created(state.state), it.event) } + } + } + } + @Test fun `recv CommitSig -- zero conf`() { val (alice, commitSigAlice, bob, commitSigBob) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/serialization/channel/StateSerializationTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/serialization/channel/StateSerializationTestsCommon.kt index f839fe1e4..a93c4bffb 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/serialization/channel/StateSerializationTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/serialization/channel/StateSerializationTestsCommon.kt @@ -1,6 +1,10 @@ package fr.acinq.lightning.serialization.channel +import fr.acinq.bitcoin.* import fr.acinq.lightning.Feature +import fr.acinq.lightning.Lightning.randomBytes +import fr.acinq.lightning.Lightning.randomBytes32 +import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.channel.* @@ -10,6 +14,7 @@ import fr.acinq.lightning.channel.states.PersistedChannelState import fr.acinq.lightning.channel.states.SpliceTestsCommon import fr.acinq.lightning.serialization.channel.Encryption.from import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.value @@ -161,4 +166,33 @@ class StateSerializationTestsCommon : LightningTestSuite() { } } + @Test + fun `encode taproot specific fields`() { + val (alice, _) = TestsHelper.reachNormal() + val bytes = Serialization.serialize(alice.state) + val check = Serialization.deserialize(bytes).value + assertEquals(alice.state, check) + val input = alice.commitments.active[0].localCommit.publishableTxs.commitTx.input + val scriptTree = ScriptTree.Branch(ScriptTree.Leaf(Script.pay2tr(randomKey().xOnlyPublicKey())), ScriptTree.Leaf(Script.pay2wpkh(randomKey().publicKey()))) + val input1 = Transactions.InputInfo.TaprootInput(input.outPoint, input.txOut, randomKey().xOnlyPublicKey(), Transactions.InputInfo.RedeemPath.ScriptPath(scriptTree, scriptTree.left.hash())) + 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/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt index aeed6f94d..aeb85c4f6 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt @@ -60,7 +60,7 @@ class AnchorOutputsTestsCommon { val funding_tx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") - val commitTxInput = Transactions.InputInfo( + val commitTxInput = Transactions.InputInfo.SegwitInput( OutPoint(funding_tx, 0), funding_tx.txOut[0], Scripts.multiSig2of2(local_funding_pubkey, remote_funding_pubkey) @@ -136,7 +136,7 @@ class AnchorOutputsTestsCommon { 42, localParams, remoteParams, fundingTxIndex = 0, remote_funding_pubkey, - Transactions.InputInfo(OutPoint(funding_tx, 0), funding_tx.txOut[0], Scripts.multiSig2of2(local_funding_pubkey, remote_funding_pubkey)), + Transactions.InputInfo.SegwitInput(OutPoint(funding_tx, 0), funding_tx.txOut[0], Scripts.multiSig2of2(local_funding_pubkey, remote_funding_pubkey)), local_per_commitment_point, spec ) @@ -149,7 +149,7 @@ class AnchorOutputsTestsCommon { val remoteHtlcSigs = testCase.HtlcDescs.map { Transaction.read(it.ResolutionTxHex).txid to ByteVector(it.RemoteSigHex) }.toMap() assertTrue { remoteHtlcSigs.keys.containsAll(htlcTxs.map { it.tx.txid }) } htlcTxs.forEach { htlcTx -> - val localHtlcSig = Transactions.sign(htlcTx, local_htlc_privkey, SigHash.SIGHASH_ALL) + val localHtlcSig = Transactions.sign(htlcTx, local_htlc_privkey) val remoteHtlcSig = Crypto.der2compact(remoteHtlcSigs[htlcTx.tx.txid]!!.toByteArray()) val expectedTx = txs[htlcTx.tx.txid] val signed = when (htlcTx) { @@ -179,7 +179,8 @@ class AnchorOutputsTestsCommon { remote_payment_privkey.publicKey(), local_htlc_privkey.publicKey(), remote_htlc_privkey.publicKey(), - spec + spec, + false ) val commitTx = Transactions.makeCommitTx( commitTxInput, diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index 0af96c1e9..db3639580 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -22,8 +22,10 @@ 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.Taproot.musig2Aggregate import fr.acinq.lightning.transactions.Scripts.toLocalDelayed import fr.acinq.lightning.transactions.Transactions.PlaceHolderPubKey +import fr.acinq.lightning.transactions.Scripts.Taproot.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 @@ -50,6 +52,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 @@ -72,7 +75,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) @@ -109,7 +112,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) } @@ -120,6 +123,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val toLocalDelay = CltvExpiryDelta(144) val feeratePerKw = FeeratePerKw.MinimumFeeratePerKw val blockHeight = 400_000 + val isTaprootChannel = false run { // ClaimHtlcDelayedTx @@ -178,7 +182,8 @@ class TransactionsTestsCommon : LightningTestSuite() { remotePaymentPriv.publicKey(), localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), - spec + spec, + isTaprootChannel ) val commitTx = Transaction(version = 2, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = outputs.map { it.output }, lockTime = 0) val claimHtlcSuccessTx = @@ -207,7 +212,8 @@ class TransactionsTestsCommon : LightningTestSuite() { remotePaymentPriv.publicKey(), localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), - spec + spec, + isTaprootChannel ) val commitTx = Transaction(version = 2, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = outputs.map { it.output }, lockTime = 0) val claimHtlcTimeoutTx = @@ -220,10 +226,211 @@ class TransactionsTestsCommon : LightningTestSuite() { } } + @Test + fun `build taproot transactions`() { + + // funding tx sends to musig2 aggregate of local and remote funding keys + val fundingTxOutpoint = OutPoint(TxId(randomBytes32()), 0) + val fundingOutput = TxOut(Satoshi(100000), Script.pay2tr(musig2Aggregate(localFundingPriv.publicKey(), remoteFundingPriv.publicKey()), null as ByteVector32?)) + + // to-local output script tree, with 2 leaves + val toLocalScriptTree = ScriptTree.Branch( + ScriptTree.Leaf(Scripts.Taproot.toLocalDelayed(localDelayedPaymentPriv.publicKey(), toLocalDelay)), + ScriptTree.Leaf(Scripts.Taproot.toRevocationKey(localRevocationPriv.publicKey(), localDelayedPaymentPriv.publicKey())), + ) + + // to-remote output script tree, with a single leaf + val toRemoteScriptTree = ScriptTree.Leaf(Scripts.Taproot.toRemoteDelayed(remotePaymentPriv.publicKey())) + + // offered HTLC + val preimage = ByteVector32.fromValidHex("0101010101010101010101010101010101010101010101010101010101010101") + val paymentHash = sha256(preimage).byteVector32() + val offeredHtlcTree = Scripts.Taproot.offeredHtlcTree(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), paymentHash) + val receivedHtlcTree = Scripts.Taproot.receivedHtlcTree(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), paymentHash, CltvExpiry(300)) + + val txNumber = 0x404142434445L + val (sequence, lockTime) = encodeTxNumber(txNumber) + val commitTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(fundingTxOutpoint, sequence)), + txOut = listOf( + TxOut(30000000.sat, Script.pay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree)), + TxOut(40000000.sat, Script.pay2tr(XonlyPublicKey(NUMS_POINT), toRemoteScriptTree)), + TxOut(330.sat, Script.pay2tr(localDelayedPaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree)), + TxOut(330.sat, Script.pay2tr(remotePaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree)), + TxOut(100.sat, Script.pay2tr(localRevocationPriv.xOnlyPublicKey(), offeredHtlcTree)), + TxOut(150.sat, Script.pay2tr(localRevocationPriv.xOnlyPublicKey(), receivedHtlcTree)) + ), + lockTime + ) + + val localNonce = Musig2.generateNonce(randomBytes32(), localFundingPriv, listOf(localFundingPriv.publicKey())) + val remoteNonce = Musig2.generateNonce(randomBytes32(), remoteFundingPriv, listOf(remoteFundingPriv.publicKey())) + + val localPartialSig = Musig2.signTaprootInput( + localFundingPriv, + tx, 0, listOf(fundingOutput), + Scripts.sort(listOf(localFundingPriv.publicKey(), remoteFundingPriv.publicKey())), + localNonce.first, listOf(localNonce.second, remoteNonce.second), + null + ).right!! + + val remotePartialSig = Musig2.signTaprootInput( + remoteFundingPriv, + tx, 0, listOf(fundingOutput), + Scripts.sort(listOf(localFundingPriv.publicKey(), remoteFundingPriv.publicKey())), + remoteNonce.first, listOf(localNonce.second, remoteNonce.second), + null + ).right!! + + val aggSig = Musig2.aggregateTaprootSignatures( + listOf(localPartialSig, remotePartialSig), tx, 0, + listOf(fundingOutput), + Scripts.sort(listOf(localFundingPriv.publicKey(), remoteFundingPriv.publicKey())), + listOf(localNonce.second, remoteNonce.second), + null + ).right!! + + tx.updateWitness(0, Script.witnessKeyPathPay2tr(aggSig)) + } + Transaction.correctlySpends(commitTx, mapOf(fundingTxOutpoint to fundingOutput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey())) + + val spendToLocalOutputTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 0), sequence = toLocalDelay.toLong())), + txOut = listOf(TxOut(30000000.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, listOf(commitTx.txOut[0]), SigHash.SIGHASH_DEFAULT, toLocalScriptTree.left.hash()) + val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree.left as ScriptTree.Leaf, ScriptWitness(listOf(sig)), toLocalScriptTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendToLocalOutputTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + + val spendToRemoteOutputTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 1), sequence = 1)), + txOut = listOf(TxOut(40000000.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootScriptPath(remotePaymentPriv, tx, 0, listOf(commitTx.txOut[1]), SigHash.SIGHASH_DEFAULT, toRemoteScriptTree.hash()) + val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(NUMS_POINT), toRemoteScriptTree, ScriptWitness(listOf(sig)), toRemoteScriptTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendToRemoteOutputTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val spendLocalAnchorTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 2), sequence = TxIn.SEQUENCE_FINAL)), + txOut = listOf(TxOut(330.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootKeyPath(localDelayedPaymentPriv, tx, 0, listOf(commitTx.txOut[2]), SigHash.SIGHASH_DEFAULT, Scripts.Taproot.anchorScriptTree) + val witness = Script.witnessKeyPathPay2tr(sig) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendLocalAnchorTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val spendRemoteAnchorTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 3), listOf(), sequence = TxIn.SEQUENCE_FINAL)), + txOut = listOf(TxOut(330.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootKeyPath(remotePaymentPriv, tx, 0, listOf(commitTx.txOut[3]), SigHash.SIGHASH_DEFAULT, Scripts.Taproot.anchorScriptTree) + val witness = Script.witnessKeyPathPay2tr(sig) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(spendRemoteAnchorTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + val mainPenaltyTx = run { + val tx = Transaction( + version = 2, + txIn = listOf(TxIn(OutPoint(commitTx, 0), sequence = TxIn.SEQUENCE_FINAL)), + txOut = listOf(TxOut(330.sat, finalPubKeyScript)), + lockTime = 0 + ) + val sig = Transaction.signInputTaprootScriptPath(localRevocationPriv, tx, 0, listOf(commitTx.txOut[0]), SigHash.SIGHASH_DEFAULT, toLocalScriptTree.right.hash()) + val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(NUMS_POINT), toLocalScriptTree.right as ScriptTree.Leaf, ScriptWitness(listOf(sig)), toLocalScriptTree) + tx.updateWitness(0, witness) + } + Transaction.correctlySpends(mainPenaltyTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + + // sign and spend received HTLC with HTLC-Success tx + val htlcSuccessTree = ScriptTree.Leaf(Scripts.Taproot.toLocalDelayed(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)) @@ -272,7 +479,8 @@ class TransactionsTestsCommon : LightningTestSuite() { remotePaymentPriv.publicKey(), localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), - spec + spec, + isTaprootChannel ) val commitTxNumber = 0x404142434445L @@ -367,7 +575,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) assertEquals(1, claimHtlcDelayedPenaltyTxs.size) val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") @@ -376,7 +585,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) assertEquals(listOf(Skipped(AmountBelowDustLimit)), claimHtlcDelayedPenaltyTxsSkipped) } run { @@ -391,7 +601,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) assertEquals(1, claimHtlcDelayedPenaltyTxs.size) val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") @@ -400,7 +611,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) assertEquals(listOf(Skipped(AmountBelowDustLimit)), claimHtlcDelayedPenaltyTxsSkipped) } run { @@ -408,7 +620,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) assertEquals(4, claimHtlcDelayedPenaltyTxs.size) val skipped = claimHtlcDelayedPenaltyTxs.filterIsInstance>() assertEquals(2, skipped.size) @@ -446,6 +659,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) + 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, SigHash.SIGHASH_DEFAULT) + 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) + assertTrue(claimHtlcDelayed is Success, "is $claimHtlcDelayed") + val localSig = claimHtlcDelayed.result.sign(localDelayedPaymentPriv, SigHash.SIGHASH_DEFAULT) + 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) + 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, SigHash.SIGHASH_DEFAULT) + 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, SigHash.SIGHASH_DEFAULT) + 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) + assertTrue(claimHtlcDelayed is Success, "is $claimHtlcDelayed") + val localSig = claimHtlcDelayed.result.sign(localDelayedPaymentPriv, SigHash.SIGHASH_DEFAULT) + 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) + assertEquals(Skipped(OutputNotFound), claimHtlcDelayed1) + } + run { + // remote spends main output + val claimP2WPKHOutputTx = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey(), finalPubKeyScript.toByteVector(), feerate) + assertTrue(claimP2WPKHOutputTx is Success, "is $claimP2WPKHOutputTx") + val localSig = claimP2WPKHOutputTx.result.sign(remotePaymentPriv, SigHash.SIGHASH_DEFAULT) + 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) + assertEquals(1, claimHtlcDelayedPenaltyTxs.size) + val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() + assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") + val sig = claimHtlcDelayedPenaltyTx.result.sign(localRevocationPriv, SigHash.SIGHASH_DEFAULT) + 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) + 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, SigHash.SIGHASH_DEFAULT) + 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) + assertEquals(1, claimHtlcDelayedPenaltyTxs.size) + val claimHtlcDelayedPenaltyTx = claimHtlcDelayedPenaltyTxs.first() + assertTrue(claimHtlcDelayedPenaltyTx is Success, "is $claimHtlcDelayedPenaltyTx") + val sig = claimHtlcDelayedPenaltyTx.result.sign(localRevocationPriv, SigHash.SIGHASH_DEFAULT) + 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) + 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) + 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, localRevocationPriv.publicKey().xOnly(), scriptTree, localDustLimit, finalPubKeyScript, feerate) + } + + else -> { + val script = write(htlcOffered(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), localRevocationPriv.publicKey(), ripemd160(htlc1.paymentHash))) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feerate) + } + } + assertTrue(htlcPenaltyTx is Success, "is $htlcPenaltyTx") + val sig = htlcPenaltyTx.result.sign(localRevocationPriv, SigHash.SIGHASH_DEFAULT) + 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, localRevocationPriv.publicKey().xOnly(), scriptTree, localDustLimit, finalPubKeyScript, feerate) + } + + else -> { + val script = write(htlcReceived(localHtlcPriv.publicKey(), remoteHtlcPriv.publicKey(), localRevocationPriv.publicKey(), ripemd160(htlc2.paymentHash), htlc2.cltvExpiry)) + makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feerate) + } + } + + assertTrue(htlcPenaltyTx is Success, "is $htlcPenaltyTx") + val sig = htlcPenaltyTx.result.sign(localRevocationPriv, SigHash.SIGHASH_DEFAULT) + 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 @@ -660,7 +1137,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 @@ -688,6 +1165,7 @@ class TransactionsTestsCommon : LightningTestSuite() { ) val commitTxNumber = 0x404142434446L + val isTaprootChannel = false val (commitTx, outputs, htlcTxs) = run { val outputs = makeCommitTxOutputs( @@ -701,7 +1179,8 @@ 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) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index e35ea9bc3..27cd2ab8e 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -379,7 +379,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val nodeKey = PrivateKey.fromHex("57ac961f1b80ebfb610037bf9c96c6333699bde42257919a53974811c34649e3") val fundingLease = LiquidityAds.FundingRate(500_000.sat, 5_000_000.sat, 1100, 75, 0.sat, 1_500.sat) val requestFunds = LiquidityAds.RequestFunding(750_000.sat, fundingLease, LiquidityAds.PaymentDetails.FromChannelBalance) - val fundingScript = Helpers.Funding.makeFundingPubKeyScript(publicKey(1), publicKey(1)) + val fundingScript = Helpers.Funding.makeFundingPubKeyScript(publicKey(1), publicKey(1), isTaprootChannel = false) val willFund = LiquidityAds.WillFundRates(listOf(fundingLease), setOf(LiquidityAds.PaymentType.FromChannelBalance)).validateRequest(nodeKey, fundingScript, FeeratePerKw(5000.sat), requestFunds, isChannelCreation = true, 0.msat)!!.willFund // @formatter:off val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000.sat, 473.sat, 100_000_000, 1.msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), publicKey(7)) @@ -489,19 +489,19 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TxRemoveInput(channelId2, 561) to ByteVector("0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231"), TxRemoveOutput(channelId1, 1) to ByteVector("0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001"), TxComplete(channelId1) to ByteVector("0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - TxSignatures(channelId1, tx2, listOf(ScriptWitness(listOf(ByteVector("68656c6c6f2074686572652c2074686973206973206120626974636f6e212121"), ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), ScriptWitness(listOf(ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), - TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), - TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5 fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId1, tx2, listOf(ScriptWitness(listOf(ByteVector("68656c6c6f2074686572652c2074686973206973206120626974636f6e212121"), ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), ScriptWitness(listOf(ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))), null, null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), + TxSignatures(channelId2, tx1, listOf(), null, null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), + TxSignatures(channelId2, tx1, listOf(), null, null, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, null, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + TxSignatures(channelId2, tx1, listOf(), signature, null, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, null, listOf(), listOf(), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5 fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), TxInitRbf(channelId1, 8388607, FeeratePerKw(4000.sat)) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(1_500_000.sat), TxInitRbfTlv.RequireConfirmedInputsTlv)) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(0.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000"), diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Closing_0ba41d17/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Closing_0ba41d17/data.json index 4a95ea4b8..1e19b1aed 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Closing_0ba41d17/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Closing_0ba41d17/data.json @@ -148,6 +148,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -162,6 +163,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:3", "txOut": { "amount": 95000, @@ -179,6 +181,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:4", "txOut": { "amount": 110000, @@ -247,6 +250,7 @@ "commitTx": "020000000001012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd000000000000b77080064a0100000000000022002046672803839d10e21d43eafb532bc37cd21bd97dec7f9b50d45380cd1cce69654a01000000000000220020bbd7f17366848d8f624c28438128ce4dc8c72ea21deb0ccf25bee110920da032905f010000000000220020cb93e3adb8d213704e9a07ea2a6ce91dca51a9b76fcb6bef3cc9b5b7e10a799018730100000000002200202228f1681cff3adf3524363fad7481c1aad6aa344b223cd97b4fb09a667f5e2db0ad01000000000022002028eee5bc7a7219b098d1f27ee035b83a6870d2be541097068097ab8a8d2568fba8a20a00000000002200206aae960776a0a9f526c5ea6841c28baa16a7ae997313ed417dbc55e38fe1c1730400473044022069b2ce78c6eb6728db4296f6a88b65af682c81dc613985ecf6c23d14cfad3b6602205af6b39cf883448ac1769495c029f230c18d1c4b70292bec6cafad220b74bb5801473044022043cd8250bf3125c454dbe9d7389a3000d6198e35ad82ddd767d2fb770d4e00000220740dd54ac489715d33c6730ee78ee3df4e00f875f67c93b005235d5d637334e40147522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452aedf99dc20", "claimMainDelayedOutputTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:5", "txOut": { "amount": 697000, @@ -260,6 +264,7 @@ "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:3": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:3", "txOut": { "amount": 95000, @@ -273,6 +278,7 @@ "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:4": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:4", "txOut": { "amount": 110000, @@ -288,6 +294,7 @@ "claimHtlcDelayedTxs": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "65e1efd0ab984bc9fadaa395864df708584c8939af593bfe77b262673fb8fa04:0", "txOut": { "amount": 91670, @@ -299,6 +306,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "4030163c4fb5c9aa83454c1fa8dc5cf9ed9f6fbbb7e1be85dbc90e40ef1f6474:0", "txOut": { "amount": 106470, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Closing_0ed6ff68/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Closing_0ed6ff68/data.json index 5d3bbb100..592dde1bd 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Closing_0ed6ff68/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Closing_0ed6ff68/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -162,6 +163,7 @@ "mutualClosePublished": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Closing_0efffae3/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Closing_0efffae3/data.json index 88f4ea5e0..9b40ac55c 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Closing_0efffae3/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Closing_0efffae3/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -169,6 +170,7 @@ "claimMainOutputTx": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:7", "txOut": { "amount": 730280, @@ -180,6 +182,7 @@ }, "mainPenaltyTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:6", "txOut": { "amount": 162000, @@ -192,6 +195,7 @@ "htlcPenaltyTxs": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:2", "txOut": { "amount": 18000, @@ -203,6 +207,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:3", "txOut": { "amount": 20000, @@ -214,6 +219,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:4", "txOut": { "amount": 25000, @@ -225,6 +231,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:5", "txOut": { "amount": 35000, @@ -238,6 +245,7 @@ "claimHtlcDelayedPenaltyTxs": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "34d00149cc8115b9e222e7bafcda6ebbcfcda8e2df8b5afe9c81f8950ac21785:0", "txOut": { "amount": 31470, @@ -249,6 +257,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "cae12bb3c504d6a627b7ca1d0b7e88cb74bb6d82e216adf71be70df2057fdab3:0", "txOut": { "amount": 14670, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Closing_2fd2a3fa/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Closing_2fd2a3fa/data.json index 26f1cbbbd..0cd2988c0 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Closing_2fd2a3fa/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Closing_2fd2a3fa/data.json @@ -180,6 +180,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -194,6 +195,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "5def32053e7f13610e4f8ada9cbfd6212f2aeb6d44cf85098586d9e1a7a8ef2d:2", "txOut": { "amount": 10000, @@ -212,6 +214,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "5def32053e7f13610e4f8ada9cbfd6212f2aeb6d44cf85098586d9e1a7a8ef2d:3", "txOut": { "amount": 12000, @@ -230,6 +233,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "5def32053e7f13610e4f8ada9cbfd6212f2aeb6d44cf85098586d9e1a7a8ef2d:4", "txOut": { "amount": 15000, @@ -247,6 +251,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "5def32053e7f13610e4f8ada9cbfd6212f2aeb6d44cf85098586d9e1a7a8ef2d:5", "txOut": { "amount": 20000, @@ -401,6 +406,7 @@ "claimMainOutputTx": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:8", "txOut": { "amount": 734420, @@ -414,6 +420,7 @@ "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:5": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:5", "txOut": { "amount": 20000, @@ -427,6 +434,7 @@ "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:4": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:4", "txOut": { "amount": 15000, @@ -440,6 +448,7 @@ "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:2": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:2", "txOut": { "amount": 10000, @@ -453,6 +462,7 @@ "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:3": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:3", "txOut": { "amount": 12000, @@ -466,6 +476,7 @@ "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:6": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9269a688c278f4d86b79a545287703dd31e28f2099ad99f54a38f2292f8cc82c:6", "txOut": { "amount": 20000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Closing_3bb07fb6/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Closing_3bb07fb6/data.json index e45f340f5..29a8886e7 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Closing_3bb07fb6/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Closing_3bb07fb6/data.json @@ -148,6 +148,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -162,6 +163,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "735ca47f5592a678bb13ba5ef0c27b9acf0181dbeef2073f5668022077e05d38:3", "txOut": { "amount": 95000, @@ -179,6 +181,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "735ca47f5592a678bb13ba5ef0c27b9acf0181dbeef2073f5668022077e05d38:4", "txOut": { "amount": 110000, @@ -248,6 +251,7 @@ "claimMainOutputTx": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:5", "txOut": { "amount": 697000, @@ -261,6 +265,7 @@ "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:3": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:3", "txOut": { "amount": 95000, @@ -274,6 +279,7 @@ "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:4": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:4", "txOut": { "amount": 110000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Closing_8f1a524e/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Closing_8f1a524e/data.json index 6c47ac089..1022a8b48 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Closing_8f1a524e/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Closing_8f1a524e/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -165,6 +166,7 @@ "mutualCloseProposed": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -179,6 +181,7 @@ "mutualClosePublished": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Closing_ef682e2e/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Closing_ef682e2e/data.json index c4afcbc46..24e96e933 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Closing_ef682e2e/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Closing_ef682e2e/data.json @@ -147,6 +147,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -161,6 +162,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "007ffe03e47cb696972af3e747c5f0ff813953d1d127b844aa8e66d4eedd6d48:2", "txOut": { "amount": 100000, @@ -178,6 +180,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "007ffe03e47cb696972af3e747c5f0ff813953d1d127b844aa8e66d4eedd6d48:4", "txOut": { "amount": 250000, @@ -252,6 +255,7 @@ "claimMainOutputTx": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "478e652c74babe1ffabfefe85f7afaa4ceee6b6c3f7abedacc7b56429071c91f:2", "txOut": { "amount": 443710, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json index d11f3451c..4f5d5b57f 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_c8d15808/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -181,6 +182,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -209,6 +211,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -237,6 +240,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -265,6 +269,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -293,6 +298,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -321,6 +327,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -349,6 +356,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -377,6 +385,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -405,6 +414,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -433,6 +443,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -461,6 +472,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -489,6 +501,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -517,6 +530,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -545,6 +559,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -573,6 +588,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -601,6 +617,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -630,6 +647,7 @@ ], "bestUnpublishedClosingTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json index 93b457720..8a57dcffb 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_d9b4cd96/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json index f6569bd10..2debb9947 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_ee10091c/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -171,6 +172,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -200,6 +202,7 @@ ], "bestUnpublishedClosingTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json index 80d2aeba6..c97bdd54a 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Negotiating_f52b19b8/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -184,6 +185,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json index 602f95563..bf9d20572 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_748a735b/data.json @@ -122,6 +122,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json index e72eeab7f..4409eeda1 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_e2253ddd/data.json @@ -209,6 +209,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -223,6 +224,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9cc4da8281ede5c3eb2f4d06752524a2b4483e61931deb8c88bdf9120a925534:2", "txOut": { "amount": 8000, @@ -240,6 +242,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9cc4da8281ede5c3eb2f4d06752524a2b4483e61931deb8c88bdf9120a925534:3", "txOut": { "amount": 50000, @@ -258,6 +261,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9cc4da8281ede5c3eb2f4d06752524a2b4483e61931deb8c88bdf9120a925534:4", "txOut": { "amount": 50000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json index c11a9729d..f8fa18419 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff248f8d/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json index 632438fee..e36899137 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ff4a71b6/data.json @@ -176,6 +176,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -190,6 +191,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "2ba9027cf8e58bb8298b03b361336d6453ebc9c03135afc8741c0871b167d491:2", "txOut": { "amount": 4540, @@ -207,6 +209,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "2ba9027cf8e58bb8298b03b361336d6453ebc9c03135afc8741c0871b167d491:3", "txOut": { "amount": 4550, @@ -224,6 +227,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "2ba9027cf8e58bb8298b03b361336d6453ebc9c03135afc8741c0871b167d491:4", "txOut": { "amount": 4640, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json index 8ab396eb4..62cf85a1a 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/Normal_ffd9f5db/data.json @@ -129,6 +129,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json b/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json index 7af3c7ae3..f403f3d35 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_c321b947/data.json @@ -140,6 +140,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -154,6 +155,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "24237f2dafcc072f634bc4ffc74b34157a827567ed32e8adc85bf72b54b9a07a:2", "txOut": { "amount": 200000, @@ -171,6 +173,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "24237f2dafcc072f634bc4ffc74b34157a827567ed32e8adc85bf72b54b9a07a:5", "txOut": { "amount": 300000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json b/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json index 679424d87..8bc77951d 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/ShuttingDown_f89ecd50/data.json @@ -147,6 +147,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -161,6 +162,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "86441d0d05c27ecef1fd4bd850172890c8cee07eb0aedb272034eb36f93221be:3", "txOut": { "amount": 200000, @@ -179,6 +181,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "86441d0d05c27ecef1fd4bd850172890c8cee07eb0aedb272034eb36f93221be:5", "txOut": { "amount": 300000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_f7421b49/data.json b/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_f7421b49/data.json index 53698523e..88c54bc56 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_f7421b49/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_f7421b49/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_fe3c5978/data.json b/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_fe3c5978/data.json index a86556ee2..f7b80a98e 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_fe3c5978/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_fe3c5978/data.json @@ -124,6 +124,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_ff74dd33/data.json b/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_ff74dd33/data.json index bb25ba1b6..c341ca96e 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_ff74dd33/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingConfirmed_ff74dd33/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingLocked_f3437082/data.json b/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingLocked_f3437082/data.json index 0dee1662d..4868f8dce 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingLocked_f3437082/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/WaitForFundingLocked_f3437082/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_ae47fde9/data.json b/modules/core/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_ae47fde9/data.json index 25fb5c97f..c72fb51aa 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_ae47fde9/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_ae47fde9/data.json @@ -122,6 +122,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_d803549f/data.json b/modules/core/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_d803549f/data.json index b2209f2a9..ba2328a06 100644 --- a/modules/core/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_d803549f/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v2/WaitForRemotePublishFutureCommitment_d803549f/data.json @@ -147,6 +147,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -161,6 +162,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "007ffe03e47cb696972af3e747c5f0ff813953d1d127b844aa8e66d4eedd6d48:2", "txOut": { "amount": 100000, @@ -178,6 +180,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "007ffe03e47cb696972af3e747c5f0ff813953d1d127b844aa8e66d4eedd6d48:4", "txOut": { "amount": 250000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Closing_029bf8f3/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Closing_029bf8f3/data.json index 2d57e4eeb..1311fb322 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Closing_029bf8f3/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Closing_029bf8f3/data.json @@ -177,6 +177,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -191,6 +192,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "7372fda43b27856d434c954465abbff64d4f9967d1f76fe3f140281fa19d690f:2", "txOut": { "amount": 50000, @@ -209,6 +211,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "7372fda43b27856d434c954465abbff64d4f9967d1f76fe3f140281fa19d690f:3", "txOut": { "amount": 55000, @@ -227,6 +230,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "7372fda43b27856d434c954465abbff64d4f9967d1f76fe3f140281fa19d690f:5", "txOut": { "amount": 100000, @@ -244,6 +248,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "7372fda43b27856d434c954465abbff64d4f9967d1f76fe3f140281fa19d690f:6", "txOut": { "amount": 250000, @@ -396,6 +401,7 @@ "claimMainOutputTx": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "6fa734b6dd68a4ec5374924405d3f359c29c31df3e5775afc33b8cab86302be3:6", "txOut": { "amount": 491130, @@ -409,6 +415,7 @@ "6fa734b6dd68a4ec5374924405d3f359c29c31df3e5775afc33b8cab86302be3:5": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "6fa734b6dd68a4ec5374924405d3f359c29c31df3e5775afc33b8cab86302be3:5", "txOut": { "amount": 250000, @@ -422,6 +429,7 @@ "6fa734b6dd68a4ec5374924405d3f359c29c31df3e5775afc33b8cab86302be3:4": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "6fa734b6dd68a4ec5374924405d3f359c29c31df3e5775afc33b8cab86302be3:4", "txOut": { "amount": 100000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Closing_0ba41d17/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Closing_0ba41d17/data.json index c9c73f51c..18f9b8b25 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Closing_0ba41d17/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Closing_0ba41d17/data.json @@ -146,6 +146,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -160,6 +161,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:3", "txOut": { "amount": 95000, @@ -177,6 +179,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:4", "txOut": { "amount": 110000, @@ -245,6 +248,7 @@ "commitTx": "020000000001012f7c825b61cbbf0e685aef20f8923a8cd28943346e59301b629cc89b4e27cdbd000000000000b77080064a0100000000000022002046672803839d10e21d43eafb532bc37cd21bd97dec7f9b50d45380cd1cce69654a01000000000000220020bbd7f17366848d8f624c28438128ce4dc8c72ea21deb0ccf25bee110920da032905f010000000000220020cb93e3adb8d213704e9a07ea2a6ce91dca51a9b76fcb6bef3cc9b5b7e10a799018730100000000002200202228f1681cff3adf3524363fad7481c1aad6aa344b223cd97b4fb09a667f5e2db0ad01000000000022002028eee5bc7a7219b098d1f27ee035b83a6870d2be541097068097ab8a8d2568fba8a20a00000000002200206aae960776a0a9f526c5ea6841c28baa16a7ae997313ed417dbc55e38fe1c1730400473044022069b2ce78c6eb6728db4296f6a88b65af682c81dc613985ecf6c23d14cfad3b6602205af6b39cf883448ac1769495c029f230c18d1c4b70292bec6cafad220b74bb5801473044022043cd8250bf3125c454dbe9d7389a3000d6198e35ad82ddd767d2fb770d4e00000220740dd54ac489715d33c6730ee78ee3df4e00f875f67c93b005235d5d637334e40147522102b6eaf304d966a6df90f3b3df7af7be6b1625854bbc096cb8b3507b2a37c2bf9c210385cfd7d8850e4cb8fcbed57310911218e5d5e1fd34f92ef5d9db14d56418caa452aedf99dc20", "claimMainDelayedOutputTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:5", "txOut": { "amount": 697000, @@ -258,6 +262,7 @@ "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:3": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:3", "txOut": { "amount": 95000, @@ -271,6 +276,7 @@ "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:4": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "9601ac0b3a2c26b11effb0690a37b4f86b10b144f1751ad6d64f5e58d40c78b8:4", "txOut": { "amount": 110000, @@ -286,6 +292,7 @@ "claimHtlcDelayedTxs": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "65e1efd0ab984bc9fadaa395864df708584c8939af593bfe77b262673fb8fa04:0", "txOut": { "amount": 91670, @@ -297,6 +304,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "4030163c4fb5c9aa83454c1fa8dc5cf9ed9f6fbbb7e1be85dbc90e40ef1f6474:0", "txOut": { "amount": 106470, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Closing_0ed6ff68/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Closing_0ed6ff68/data.json index b44ecb4f7..1365efac1 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Closing_0ed6ff68/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Closing_0ed6ff68/data.json @@ -121,6 +121,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -160,6 +161,7 @@ "mutualClosePublished": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Closing_0efffae3/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Closing_0efffae3/data.json index 9fe8fd38d..91fd78ec9 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Closing_0efffae3/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Closing_0efffae3/data.json @@ -121,6 +121,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -167,6 +168,7 @@ "claimMainOutputTx": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:7", "txOut": { "amount": 730280, @@ -178,6 +180,7 @@ }, "mainPenaltyTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:6", "txOut": { "amount": 162000, @@ -190,6 +193,7 @@ "htlcPenaltyTxs": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:2", "txOut": { "amount": 18000, @@ -201,6 +205,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:3", "txOut": { "amount": 20000, @@ -212,6 +217,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:4", "txOut": { "amount": 25000, @@ -223,6 +229,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "3cdaac5fd4ca78ec2efca165400efb5078f9f57d221a733d372f0213136b92eb:5", "txOut": { "amount": 35000, @@ -236,6 +243,7 @@ "claimHtlcDelayedPenaltyTxs": [ { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "34d00149cc8115b9e222e7bafcda6ebbcfcda8e2df8b5afe9c81f8950ac21785:0", "txOut": { "amount": 31470, @@ -247,6 +255,7 @@ }, { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "cae12bb3c504d6a627b7ca1d0b7e88cb74bb6d82e216adf71be70df2057fdab3:0", "txOut": { "amount": 14670, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Closing_ebbd24bc/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Closing_ebbd24bc/data.json index 2ff5cbc8c..fd91f8862 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Closing_ebbd24bc/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Closing_ebbd24bc/data.json @@ -146,6 +146,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -160,6 +161,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "735ca47f5592a678bb13ba5ef0c27b9acf0181dbeef2073f5668022077e05d38:3", "txOut": { "amount": 95000, @@ -177,6 +179,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "735ca47f5592a678bb13ba5ef0c27b9acf0181dbeef2073f5668022077e05d38:4", "txOut": { "amount": 110000, @@ -246,6 +249,7 @@ "claimMainOutputTx": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:5", "txOut": { "amount": 697000, @@ -259,6 +263,7 @@ "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:3": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:3", "txOut": { "amount": 95000, @@ -272,6 +277,7 @@ "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:4": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimHtlcTx.ClaimHtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "d4756e4280b3178aa6e35eb9937681a6dded4098af1f93d055c9a693686f922c:4", "txOut": { "amount": 110000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Closing_f137669f/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Closing_f137669f/data.json index 9b3aa9aaa..b55298089 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Closing_f137669f/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Closing_f137669f/data.json @@ -120,6 +120,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -161,6 +162,7 @@ "claimMainOutputTx": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx.ClaimRemoteDelayedOutputTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "b43849d22c5216e7a6c30d2fc5ff6d8cd3a791e4d4bf64f41362236dfe198559:5", "txOut": { "amount": 734000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json index 6b9cdd587..8a7d7d92b 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_da44c6e2/data.json @@ -121,6 +121,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -179,6 +180,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -207,6 +209,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -235,6 +238,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -264,6 +268,7 @@ ], "bestUnpublishedClosingTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json index 72e70b027..f76088498 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_dabbed55/data.json @@ -121,6 +121,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -179,6 +180,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -207,6 +209,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -235,6 +238,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -263,6 +267,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -291,6 +296,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -319,6 +325,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -347,6 +354,7 @@ { "unsignedTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -376,6 +384,7 @@ ], "bestUnpublishedClosingTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json index cf146d3bc..531ee9ddb 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Negotiating_fadb50c1/data.json @@ -123,6 +123,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json index 512ac704c..52d162b9f 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Normal_fd10d3cc/data.json @@ -138,6 +138,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -152,6 +153,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "24237f2dafcc072f634bc4ffc74b34157a827567ed32e8adc85bf72b54b9a07a:2", "txOut": { "amount": 200000, @@ -169,6 +171,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "24237f2dafcc072f634bc4ffc74b34157a827567ed32e8adc85bf72b54b9a07a:5", "txOut": { "amount": 300000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json index 2b92004d7..f5ab48a11 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Normal_fe897b64/data.json @@ -131,6 +131,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json index e1ee17f9b..fb79ac0f0 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff248f8d/data.json @@ -121,6 +121,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json b/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json index c544a46fc..eccb18677 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/Normal_ff4a71b6/data.json @@ -174,6 +174,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -188,6 +189,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "2ba9027cf8e58bb8298b03b361336d6453ebc9c03135afc8741c0871b167d491:2", "txOut": { "amount": 4540, @@ -205,6 +207,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "2ba9027cf8e58bb8298b03b361336d6453ebc9c03135afc8741c0871b167d491:3", "txOut": { "amount": 4550, @@ -222,6 +225,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "2ba9027cf8e58bb8298b03b361336d6453ebc9c03135afc8741c0871b167d491:4", "txOut": { "amount": 4640, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json b/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json index e9cc3eb21..1a07c4a27 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef41a1a5/data.json @@ -145,6 +145,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -159,6 +160,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "86441d0d05c27ecef1fd4bd850172890c8cee07eb0aedb272034eb36f93221be:3", "txOut": { "amount": 200000, @@ -177,6 +179,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "86441d0d05c27ecef1fd4bd850172890c8cee07eb0aedb272034eb36f93221be:5", "txOut": { "amount": 300000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json b/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json index 83e5f8439..83de20a1a 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/ShuttingDown_ef7081a1/data.json @@ -140,6 +140,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -154,6 +155,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "05322cf6e79cbb9b6155390ecffe61d01751fe6a0d6f1be4d8f6574eebdb9e47:2", "txOut": { "amount": 25000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_fe3c5978/data.json b/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_fe3c5978/data.json index 771038e28..758fb3730 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_fe3c5978/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_fe3c5978/data.json @@ -124,6 +124,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_ff74dd33/data.json b/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_ff74dd33/data.json index e9b8d4857..0c81ff51f 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_ff74dd33/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingConfirmed_ff74dd33/data.json @@ -121,6 +121,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingLocked_f3437082/data.json b/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingLocked_f3437082/data.json index 6663b2af0..3944504b0 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingLocked_f3437082/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/WaitForFundingLocked_f3437082/data.json @@ -121,6 +121,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_ae47fde9/data.json b/modules/core/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_ae47fde9/data.json index 8b6b7b1b6..9e309b35a 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_ae47fde9/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_ae47fde9/data.json @@ -120,6 +120,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, diff --git a/modules/core/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_d803549f/data.json b/modules/core/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_d803549f/data.json index 5162faaed..a0041b739 100644 --- a/modules/core/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_d803549f/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v3/WaitForRemotePublishFutureCommitment_d803549f/data.json @@ -145,6 +145,7 @@ "publishableTxs": { "commitTx": { "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "bdcd274e9bc89c621b30596e344389d28c3a92f820ef5a680ebfcb615b827c2f:0", "txOut": { "amount": 1000000, @@ -159,6 +160,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "007ffe03e47cb696972af3e747c5f0ff813953d1d127b844aa8e66d4eedd6d48:2", "txOut": { "amount": 100000, @@ -176,6 +178,7 @@ "txinfo": { "type": "fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx", "input": { + "type": "fr.acinq.lightning.transactions.Transactions.InputInfo.SegwitInput", "outPoint": "007ffe03e47cb696972af3e747c5f0ff813953d1d127b844aa8e66d4eedd6d48:4", "txOut": { "amount": 250000,