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 7ae127229..50b239b74 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt @@ -217,6 +217,13 @@ sealed class Feature { override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) } + // README: this is not the feature bit specified in the BOLT, this one is specific to Phoenix + @Serializable + object SimpleTaprootChannels : Feature() { + override val rfcName get() = "simple_taproot_channels" + override val mandatory get() = 564 + override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) + } } @Serializable @@ -294,7 +301,8 @@ data class Features(val activated: Map, val unknown: Se Feature.WakeUpNotificationProvider, Feature.ExperimentalSplice, Feature.OnTheFlyFunding, - Feature.FundingFeeCredit + Feature.FundingFeeCredit, + Feature.SimpleTaprootChannels ) operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray()) 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 e3afb6de5..24d2f1d37 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -200,6 +200,7 @@ data class NodeParams( Feature.Wumbo to FeatureSupport.Optional, Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Optional, // can't set Mandatory because peers prefers AnchorOutputsZeroFeeHtlcTx + Feature.SimpleTaprootChannels to FeatureSupport.Optional, Feature.RouteBlinding to FeatureSupport.Optional, Feature.DualFunding to FeatureSupport.Mandatory, Feature.ShutdownAnySegwit to FeatureSupport.Mandatory, 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 67b94e093..41f5ff547 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 @@ -92,4 +92,9 @@ data class PleasePublishYourCommitment (override val channelId: Byte data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state") data class ForbiddenDuringSplice (override val channelId: ByteVector32, val command: String?) : ChannelException(channelId, "cannot process $command while splicing") data class InvalidSpliceRequest (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice request") +data class MissingCommitNonce (override val channelId: ByteVector32, val fundingTxId: TxId, val commitmentNumber: Long) : ChannelException(channelId, "missing commit nonce for funding tx $fundingTxId commitmentNumber $commitmentNumber") +data class InvalidCommitNonce (override val channelId: ByteVector32, val fundingTxId: TxId, val commitmentNumber: Long) : ChannelException(channelId, "invalid commit nonce for funding tx $fundingTxId commitmentNumber $commitmentNumber") +data class MissingFundingNonce (override val channelId: ByteVector32, val fundingTxId: TxId) : ChannelException(channelId, "missing funding nonce for funding tx $fundingTxId") +data class InvalidFundingNonce (override val channelId: ByteVector32, val fundingTxId: TxId) : ChannelException(channelId, "invalid funding nonce for funding tx $fundingTxId") +data class MissingClosingNonce (override val channelId: ByteVector32) : ChannelException(channelId, "missing closing nonce") // @formatter:on 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 3e4ad727d..9f260a1a0 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 @@ -32,7 +32,7 @@ data class ChannelFeatures(val features: Set) { * In addition to channel types features, the following features will be added to the permanent channel features if they * are supported by both peers. */ - private val permanentChannelFeatures = setOf(Feature.DualFunding) + private val permanentChannelFeatures: Set = setOf(Feature.DualFunding) } } @@ -65,6 +65,12 @@ sealed class ChannelType { override val commitmentFormat: Transactions.CommitmentFormat get() = Transactions.CommitmentFormat.AnchorOutputs } + object SimpleTaprootChannels : SupportedChannelType() { + override val name: String get() = "simple_taproot_channel" + override val features: Set get() = setOf(Feature.SimpleTaprootChannels, Feature.ZeroReserveChannels) + override val permanentChannelFeatures: Set get() = setOf(Feature.ZeroReserveChannels) + override val commitmentFormat: Transactions.CommitmentFormat get() = Transactions.CommitmentFormat.SimpleTaprootChannels + } } data class UnsupportedChannelType(val featureBits: Features) : ChannelType() { @@ -79,6 +85,7 @@ sealed class ChannelType { // NB: Bolt 2: features must exactly match in order to identify a channel type. fun fromFeatures(features: Features): ChannelType = when (features) { // @formatter:off + Features(Feature.SimpleTaprootChannels to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory) -> SupportedChannelType.SimpleTaprootChannels Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory) -> SupportedChannelType.AnchorOutputsZeroReserve Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory) -> SupportedChannelType.AnchorOutputs else -> UnsupportedChannelType(features) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt index c6b21fa19..6160c28c3 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,6 +2,7 @@ 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.utils.Either import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Feature @@ -12,11 +13,7 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.blockchain.fee.FeerateTolerance import fr.acinq.lightning.channel.states.Channel import fr.acinq.lightning.channel.states.ChannelContext -import fr.acinq.lightning.crypto.ChannelKeys -import fr.acinq.lightning.crypto.KeyManager -import fr.acinq.lightning.crypto.LocalCommitmentKeys -import fr.acinq.lightning.crypto.RemoteCommitmentKeys -import fr.acinq.lightning.crypto.ShaChain +import fr.acinq.lightning.crypto.* import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.payment.OutgoingPaymentPacket import fr.acinq.lightning.transactions.CommitmentSpec @@ -62,7 +59,7 @@ data class RemoteChanges(val proposed: List, val acked: List get() = proposed + signed + acked } -/** Changes are applied to all commitments, and must be be valid for all commitments. */ +/** Changes are applied to all commitments, and must be valid for all commitments. */ data class CommitmentChanges(val localChanges: LocalChanges, val remoteChanges: RemoteChanges, val localNextHtlcId: Long, val remoteNextHtlcId: Long) { fun addLocalProposal(proposal: UpdateMessage): CommitmentChanges = copy(localChanges = localChanges.copy(proposed = localChanges.proposed + proposal)) @@ -97,6 +94,9 @@ data class CommitmentChanges(val localChanges: LocalChanges, val remoteChanges: sealed class ChannelSpendSignature { /** When using a 2-of-2 multisig, we need two individual ECDSA signatures. */ data class IndividualSignature(val sig: ByteVector64) : ChannelSpendSignature() + + /** When using Musig2, we need two partial signatures and the signer's nonce. */ + data class PartialSignatureWithNonce(val partialSig: ByteVector32, val nonce: IndividualNonce) : ChannelSpendSignature() } /** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */ @@ -126,7 +126,17 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val txId: TxId commitmentFormat = commitmentFormat, spec = spec, ) - if (!localCommitTx.checkRemoteSig(fundingKey.publicKey(), remoteFundingPubKey, commit.signature)) { + val remoteSigOk = when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> localCommitTx.checkRemoteSig(fundingKey.publicKey(), remoteFundingPubKey, commit.signature) + Transactions.CommitmentFormat.SimpleTaprootChannels -> when (val remoteSig = commit.sigOrPartialSig) { + is ChannelSpendSignature.IndividualSignature -> false + is ChannelSpendSignature.PartialSignatureWithNonce -> { + val localNonce = NonceGenerator.verificationNonce(commitInput.outPoint.txid, fundingKey, remoteFundingPubKey, localCommitIndex) + localCommitTx.checkRemotePartialSignature(fundingKey.publicKey(), remoteFundingPubKey, remoteSig, localNonce.publicNonce) + } + } + } + if (!remoteSigOk) { log.error { "remote signature $commit is invalid" } return Either.Left(InvalidCommitmentSignature(channelParams.channelId, localCommitTx.tx.txid)) } @@ -138,7 +148,7 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val txId: TxId return Either.Left(InvalidHtlcSignature(channelParams.channelId, htlcTx.tx.txid)) } } - return Either.Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commit.signature, commit.htlcSignatures)) + return Either.Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commit.sigOrPartialSig, commit.htlcSignatures)) } } } @@ -153,11 +163,12 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, commitmentFormat: Transactions.CommitmentFormat, - batchSize: Int - ): CommitSig { + batchSize: Int, + remoteNonce: IndividualNonce?, + ): Either { val fundingKey = channelKeys.fundingKey(fundingTxIndex) val commitKeys = channelKeys.remoteCommitmentKeys(channelParams, remotePerCommitmentPoint) - val (remoteCommitTx, sortedHtlcsTxs) = Commitments.makeRemoteTxs( + val (remoteCommitTx, sortedHtlcTxs) = Commitments.makeRemoteTxs( channelParams = channelParams, commitParams = commitParams, commitKeys = commitKeys, @@ -168,24 +179,36 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI commitmentFormat = commitmentFormat, spec = spec ) - val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey) - val htlcSigs = sortedHtlcsTxs.map { it.localSig(commitKeys) } - val tlvs = buildSet { - if (batchSize > 1) add(CommitSigTlv.Batch(batchSize)) + val htlcSigs = sortedHtlcTxs.map { it.localSig(commitKeys) } + return when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> { + val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey) + Either.Right(CommitSig(channelParams.channelId, sig, htlcSigs, batchSize)) + } + Transactions.CommitmentFormat.SimpleTaprootChannels -> when (remoteNonce) { + null -> Either.Left(MissingCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index)) + else -> { + val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey(), remoteFundingPubKey, commitInput.outPoint.txid) + when (val psig = remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteNonce))) { + is Either.Left -> Either.Left(InvalidCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index)) + is Either.Right -> Either.Right(CommitSig(channelParams.channelId, psig.value, htlcSigs, batchSize)) + } + } + } } - return CommitSig(channelParams.channelId, sig, htlcSigs.toList(), TlvStream(tlvs)) } - fun sign(channelParams: ChannelParams, channelKeys: ChannelKeys, signingSession: InteractiveTxSigningSession): CommitSig { + fun sign(channelParams: ChannelParams, channelKeys: ChannelKeys, signingSession: InteractiveTxSigningSession, remoteNonce: IndividualNonce?): Either { return sign( channelParams, signingSession.remoteCommitParams, channelKeys, - signingSession.fundingTxIndex, + signingSession.fundingParams.fundingTxIndex, signingSession.fundingParams.remoteFundingPubkey, signingSession.commitInput(channelKeys), signingSession.fundingParams.commitmentFormat, - batchSize = 1 + batchSize = 1, + remoteNonce ) } } @@ -252,6 +275,14 @@ data class Commitment( val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubkey) unsignedCommitTx.aggregateSigs(fundingKey.publicKey(), remoteFundingPubkey, localSig, remoteSig) } + is ChannelSpendSignature.PartialSignatureWithNonce -> { + val localNonce = NonceGenerator.verificationNonce(fundingTxId, fundingKey, remoteFundingPubkey, localCommit.index) + // We have already validated the remote nonce and partial signature when we received it, so we're guaranteed + // that the following code cannot produce an error. + val localSig = unsignedCommitTx.partialSign(fundingKey, remoteFundingPubkey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteSig.nonce)).right!! + val signedTx = unsignedCommitTx.aggregateSigs(fundingKey.publicKey(), remoteFundingPubkey, localSig, remoteSig, mapOf()).right!! + signedTx + } } } @@ -514,7 +545,16 @@ data class Commitment( } } - fun sendCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, log: MDCLogger): Pair { + fun sendCommit( + params: ChannelParams, + channelKeys: ChannelKeys, + commitKeys: RemoteCommitmentKeys, + changes: CommitmentChanges, + remoteNextPerCommitmentPoint: PublicKey, + batchSize: Int, + nextRemoteNonce: IndividualNonce?, + log: MDCLogger + ): Either> { val fundingKey = localFundingKey(channelKeys) // remote commitment will include all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) @@ -529,43 +569,29 @@ data class Commitment( commitmentFormat = commitmentFormat, spec = spec ) - val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubkey) + val sig = when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> remoteCommitTx.sign(fundingKey, remoteFundingPubkey) + Transactions.CommitmentFormat.SimpleTaprootChannels -> when (nextRemoteNonce) { + null -> return Either.Left(MissingCommitNonce(params.channelId, fundingTxId, remoteCommit.index + 1)) + else -> { + val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey(), remoteFundingPubkey, fundingTxId) + when (val psig = remoteCommitTx.partialSign(fundingKey, remoteFundingPubkey, mapOf(), localNonce, listOf(localNonce.publicNonce, nextRemoteNonce))) { + is Either.Left -> return Either.Left(InvalidCommitNonce(params.channelId, fundingTxId, remoteCommit.index + 1)) + is Either.Right -> psig.value + } + } + } + } val htlcSigs = sortedHtlcTxs.map { it.localSig(commitKeys) } - // NB: IN/OUT htlcs are inverted because this is the remote commit log.info { val htlcsIn = spec.htlcs.outgoings().map { it.id }.joinToString(",") val htlcsOut = spec.htlcs.incomings().map { it.id }.joinToString(",") "built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txId=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId" } - - val tlvs = buildSet { - if (spec.htlcs.isEmpty()) { - val alternativeSigs = Commitments.alternativeFeerates.map { feerate -> - val alternativeSpec = spec.copy(feerate = feerate) - val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( - channelParams = params, - commitParams = remoteCommitParams, - commitKeys = commitKeys, - commitTxNumber = remoteCommit.index + 1, - localFundingKey = fundingKey, - remoteFundingPubKey = remoteFundingPubkey, - commitmentInput = commitInput(fundingKey), - commitmentFormat = commitmentFormat, - spec = alternativeSpec - ) - val alternativeSig = alternativeRemoteCommitTx.sign(fundingKey, remoteFundingPubkey).sig - CommitSigTlv.AlternativeFeerateSig(feerate, alternativeSig) - } - add(CommitSigTlv.AlternativeFeerateSigs(alternativeSigs)) - } - if (batchSize > 1) { - add(CommitSigTlv.Batch(batchSize)) - } - } - val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), TlvStream(tlvs)) + val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), batchSize) val commitment1 = copy(nextRemoteCommit = RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)) - return Pair(commitment1, commitSig) + return Either.Right(Pair(commitment1, commitSig)) } fun receiveCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, changes: CommitmentChanges, commit: CommitSig, log: MDCLogger): Either { @@ -639,7 +665,7 @@ data class Commitments( val inactive: List, 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 remotePerCommitmentSecrets: ShaChain ) { init { require(active.isNotEmpty()) { "there must be at least one active commitment" } @@ -830,11 +856,17 @@ data class Commitments( return failure?.let { Either.Left(it) } ?: Either.Right(copy(changes = changes1)) } - fun sendCommit(channelKeys: ChannelKeys, log: MDCLogger): Either> { + fun sendCommit(channelKeys: ChannelKeys, remoteCommitNonces: Map, log: MDCLogger): Either> { val remoteNextPerCommitmentPoint = remoteNextCommitInfo.right ?: return Either.Left(CannotSignBeforeRevocation(channelId)) - val commitKeys = channelKeys.remoteCommitmentKeys(channelParams, remoteNextPerCommitmentPoint) if (!changes.localHasChanges()) return Either.Left(CannotSignWithoutChanges(channelId)) - val (active1, sigs) = active.map { it.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, log) }.unzip() + val (active1, sigs) = active.map { c -> + val commitKeys = channelKeys.remoteCommitmentKeys(channelParams, remoteNextPerCommitmentPoint) + val remoteNonce = remoteCommitNonces[c.fundingTxId] + when (val res = c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, remoteNonce, log)) { + is Either.Left -> return Either.Left(res.left) + is Either.Right -> res.value + } + }.unzip() val commitments1 = copy( active = active1, remoteNextCommitInfo = Either.Left(WaitingForRevocation(localCommitIndex)), @@ -868,7 +900,16 @@ 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 localCommitNonces = active.mapNotNull { c -> + when (c.commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> null + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + val localNonce = NonceGenerator.verificationNonce(c.fundingTxId, c.localFundingKey(channelKeys), c.remoteFundingPubkey, localCommitIndex + 2) + c.fundingTxId to localNonce.publicNonce + } + } + } + val revocation = RevokeAndAck(channelId, localPerCommitmentSecret, localNextPerCommitmentPoint, localCommitNonces) val commitments1 = copy( active = active1, changes = changes.copy( 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 9c5208603..215e70a81 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 @@ -1,8 +1,10 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce 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.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi @@ -14,10 +16,7 @@ import fr.acinq.lightning.blockchain.fee.FeerateTolerance import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.channel.Helpers.Closing.inputsAlreadySpent import fr.acinq.lightning.channel.states.Channel -import fr.acinq.lightning.crypto.ChannelKeys -import fr.acinq.lightning.crypto.LocalCommitmentKeys -import fr.acinq.lightning.crypto.RemoteCommitmentKeys -import fr.acinq.lightning.crypto.ShaChain +import fr.acinq.lightning.crypto.* import fr.acinq.lightning.logging.LoggingContext import fr.acinq.lightning.transactions.* import fr.acinq.lightning.transactions.Transactions.commitTxFee @@ -33,7 +32,7 @@ object Helpers { // NB: we only accept channels from peers who support explicit channel type negotiation. val channelType = open.channelType ?: return Either.Left(MissingChannelType(open.temporaryChannelId)) if (channelType is ChannelType.UnsupportedChannelType) { - return Either.Left(InvalidChannelType(open.temporaryChannelId, ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, channelType)) + return Either.Left(InvalidChannelType(open.temporaryChannelId, ChannelType.SupportedChannelType.SimpleTaprootChannels, channelType)) } // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: @@ -282,6 +281,19 @@ object Helpers { fun isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean, allowOpReturn: Boolean): Boolean = isValidFinalScriptPubkey(scriptPubKey.toByteArray(), allowAnySegwit, allowOpReturn) + fun createShutdown(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptOverride: ByteVector? = null): Pair { + val localScript = localScriptOverride ?: commitment.channelParams.localParams.defaultFinalScriptPubKey + return when (commitment.commitmentFormat) { + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + // We create a fresh local closee nonce every time we send shutdown. + val localFundingPubKey = channelKeys.fundingKey(commitment.fundingTxIndex).publicKey() + val localCloseeNonce = NonceGenerator.signingNonce(localFundingPubKey, commitment.remoteFundingPubkey, commitment.fundingTxId) + Pair(localCloseeNonce, Shutdown(commitment.channelId, localScript, localCloseeNonce.publicNonce)) + } + Transactions.CommitmentFormat.AnchorOutputs -> Pair(null, Shutdown(commitment.channelId, localScript)) + } + } + /** We are the closer: we sign closing transactions for which we pay the fees. */ fun makeClosingTxs( channelKeys: ChannelKeys, @@ -290,7 +302,8 @@ object Helpers { remoteScriptPubkey: ByteVector, feerate: FeeratePerKw, lockTime: Long, - ): Either> { + remoteNonce: IndividualNonce? + ): Either> { val commitInput = commitment.commitInput(channelKeys) // We must convert the feerate to a fee: we must build dummy transactions to compute their weight. val closingFee = run { @@ -311,15 +324,37 @@ object Helpers { return Either.Left(CannotGenerateClosingTx(commitment.channelId)) } val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - val tlvs = TlvStream( - setOfNotNull( - closingTxs.localAndRemote?.let { tx -> ClosingCompleteTlv.CloserAndCloseeOutputs(tx.sign(localFundingKey, commitment.remoteFundingPubkey).sig) }, - closingTxs.localOnly?.let { tx -> ClosingCompleteTlv.CloserOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubkey).sig) }, - closingTxs.remoteOnly?.let { tx -> ClosingCompleteTlv.CloseeOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubkey).sig) }, + val localNonces = Transactions.CloserNonces.generate(localFundingKey.publicKey(), commitment.remoteFundingPubkey, commitment.fundingTxId) + val tlvs = when (commitment.commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> TlvStream( + setOfNotNull( + closingTxs.localAndRemote?.let { tx -> ClosingCompleteTlv.CloserAndCloseeOutputs(tx.sign(localFundingKey, commitment.remoteFundingPubkey).sig) }, + closingTxs.localOnly?.let { tx -> ClosingCompleteTlv.CloserOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubkey).sig) }, + closingTxs.remoteOnly?.let { tx -> ClosingCompleteTlv.CloseeOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubkey).sig) }, + ) ) - ) + Transactions.CommitmentFormat.SimpleTaprootChannels -> when (remoteNonce) { + null -> return Either.Left(MissingClosingNonce(commitment.channelId)) + else -> { + // If we cannot create our partial signature for one of our closing txs, we just skip it. + // It will only happen if our peer sent an invalid nonce, in which case we cannot do anything anyway + // apart from eventually force-closing. + fun localSig(tx: Transactions.ClosingTx, localNonce: Transactions.LocalNonce): ChannelSpendSignature.PartialSignatureWithNonce? { + return tx.partialSign(localFundingKey, commitment.remoteFundingPubkey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteNonce)).right + } + + TlvStream( + setOfNotNull( + closingTxs.localAndRemote?.let { tx -> localSig(tx, localNonces.localAndRemote)?.let { ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature(it) } }, + closingTxs.localOnly?.let { tx -> localSig(tx, localNonces.localOnly)?.let { ClosingCompleteTlv.CloserOutputOnlyPartialSignature(it) } }, + closingTxs.remoteOnly?.let { tx -> localSig(tx, localNonces.remoteOnly)?.let { ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(it) } } + ) + ) + } + } + } val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, lockTime, tlvs) - return Either.Right(Pair(closingTxs, closingComplete)) + return Either.Right(Triple(closingTxs, closingComplete, localNonces)) } /** @@ -333,39 +368,83 @@ object Helpers { commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, - closingComplete: ClosingComplete - ): Either> { + closingComplete: ClosingComplete, + localNonce: Transactions.LocalNonce? + ): Either> { val closingFee = Transactions.ClosingTxFee.PaidByThem(closingComplete.fees) val closingTxs = Transactions.makeClosingTxs(commitment.commitInput(channelKeys), commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) // If our output isn't dust, they must provide a signature for a transaction that includes it. // Note that we're the closee, so we look for signatures including the closee output. - if (closingTxs.localAndRemote != null && closingTxs.localOnly != null && closingComplete.closerAndCloseeOutputsSig == null && closingComplete.closeeOutputOnlySig == null) { - return Either.Left(MissingCloseSignature(commitment.channelId)) - } - if (closingTxs.localAndRemote != null && closingTxs.localOnly == null && closingComplete.closerAndCloseeOutputsSig == null) { - return Either.Left(MissingCloseSignature(commitment.channelId)) - } - if (closingTxs.localAndRemote == null && closingTxs.localOnly != null && closingComplete.closeeOutputOnlySig == null) { - return Either.Left(MissingCloseSignature(commitment.channelId)) - } - // We choose the closing signature that matches our preferred closing transaction. - val closingTxsWithSigs = listOfNotNull ClosingSigTlv>>( - closingComplete.closerAndCloseeOutputsSig?.let { remoteSig -> closingTxs.localAndRemote?.let { tx -> Triple(tx, remoteSig) { localSig: ByteVector64 -> ClosingSigTlv.CloserAndCloseeOutputs(localSig) } } }, - closingComplete.closeeOutputOnlySig?.let { remoteSig -> closingTxs.localOnly?.let { tx -> Triple(tx, remoteSig) { localSig -> ClosingSigTlv.CloseeOutputOnly(localSig) } } }, - closingComplete.closerOutputOnlySig?.let { remoteSig -> closingTxs.remoteOnly?.let { tx -> Triple(tx, remoteSig) { localSig -> ClosingSigTlv.CloserOutputOnly(localSig) } } }, - ) - return when (val preferred = closingTxsWithSigs.firstOrNull()) { - null -> Either.Left(MissingCloseSignature(commitment.channelId)) - else -> { - val (closingTx, remoteSig, sigToTlv) = preferred - val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubkey) - val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey(), commitment.remoteFundingPubkey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig)) - val signedClosingTx = closingTx.copy(tx = signedTx) - if (!signedClosingTx.validate(extraUtxos = mapOf())) { - Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) - } else { - Either.Right(Pair(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig.sig))))) + when (commitment.commitmentFormat) { + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + if (localNonce == null) { + return Either.Left(MissingClosingNonce(commitment.channelId)) + } + if (closingTxs.localAndRemote != null && closingTxs.localOnly != null && closingComplete.closerAndCloseeOutputsPartialSig == null && closingComplete.closeeOutputOnlyPartialSig == null) { + return Either.Left(MissingCloseSignature(commitment.channelId)) + } + if (closingTxs.localAndRemote != null && closingTxs.localOnly == null && closingComplete.closerAndCloseeOutputsPartialSig == null) { + return Either.Left(MissingCloseSignature(commitment.channelId)) + } + if (closingTxs.localAndRemote == null && closingTxs.localOnly != null && closingComplete.closeeOutputOnlyPartialSig == null) { + return Either.Left(MissingCloseSignature(commitment.channelId)) + } + // We choose the closing signature that matches our preferred closing transaction. + val closingTxsWithSigs = listOfNotNull ClosingSigTlv>>( + closingComplete.closerAndCloseeOutputsPartialSig?.let { remoteSig -> closingTxs.localAndRemote?.let { tx -> Triple(tx, remoteSig) { localSig -> ClosingSigTlv.CloserAndCloseeOutputsPartialSignature(localSig) } } }, + closingComplete.closeeOutputOnlyPartialSig?.let { remoteSig -> closingTxs.localOnly?.let { tx -> Triple(tx, remoteSig) { localSig -> ClosingSigTlv.CloseeOutputOnlyPartialSignature(localSig) } } }, + closingComplete.closerOutputOnlyPartialSig?.let { remoteSig -> closingTxs.remoteOnly?.let { tx -> Triple(tx, remoteSig) { localSig -> ClosingSigTlv.CloserOutputOnlyPartialSignature(localSig) } } }, + ) + return when (val preferred = closingTxsWithSigs.firstOrNull()) { + null -> Either.Left(MissingCloseSignature(commitment.channelId)) + else -> { + val (closingTx, remoteSig, sigToTlv) = preferred + val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val localSig = closingTx.partialSign(localFundingKey, commitment.remoteFundingPubkey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteSig.nonce)).right + val signedTx = localSig?.let { closingTx.aggregateSigs(localFundingKey.publicKey(), commitment.remoteFundingPubkey, it, remoteSig, mapOf()).right } + if (localSig == null || signedTx == null) { + return Either.Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + } + val signedClosingTx = closingTx.copy(tx = signedTx) + if (!signedClosingTx.validate(mapOf())) { + return Either.Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + } + val nextLocalNonce = NonceGenerator.signingNonce(localFundingKey.publicKey(), commitment.remoteFundingPubkey, commitment.fundingTxId) + val tlvs = TlvStream(sigToTlv(localSig.partialSig), ClosingSigTlv.NextCloseeNonce(nextLocalNonce.publicNonce)) + Either.Right(Triple(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, tlvs), nextLocalNonce)) + } + } + } + Transactions.CommitmentFormat.AnchorOutputs -> { + if (closingTxs.localAndRemote != null && closingTxs.localOnly != null && closingComplete.closerAndCloseeOutputsSig == null && closingComplete.closeeOutputOnlySig == null) { + return Either.Left(MissingCloseSignature(commitment.channelId)) + } + if (closingTxs.localAndRemote != null && closingTxs.localOnly == null && closingComplete.closerAndCloseeOutputsSig == null) { + return Either.Left(MissingCloseSignature(commitment.channelId)) + } + if (closingTxs.localAndRemote == null && closingTxs.localOnly != null && closingComplete.closeeOutputOnlySig == null) { + return Either.Left(MissingCloseSignature(commitment.channelId)) + } + // We choose the closing signature that matches our preferred closing transaction. + val closingTxsWithSigs = listOfNotNull ClosingSigTlv>>( + closingComplete.closerAndCloseeOutputsSig?.let { remoteSig -> closingTxs.localAndRemote?.let { tx -> Triple(tx, remoteSig) { localSig: ByteVector64 -> ClosingSigTlv.CloserAndCloseeOutputs(localSig) } } }, + closingComplete.closeeOutputOnlySig?.let { remoteSig -> closingTxs.localOnly?.let { tx -> Triple(tx, remoteSig) { localSig -> ClosingSigTlv.CloseeOutputOnly(localSig) } } }, + closingComplete.closerOutputOnlySig?.let { remoteSig -> closingTxs.remoteOnly?.let { tx -> Triple(tx, remoteSig) { localSig -> ClosingSigTlv.CloserOutputOnly(localSig) } } }, + ) + return when (val preferred = closingTxsWithSigs.firstOrNull()) { + null -> Either.Left(MissingCloseSignature(commitment.channelId)) + else -> { + val (closingTx, remoteSig, sigToTlv) = preferred + val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubkey) + val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey(), commitment.remoteFundingPubkey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig)) + val signedClosingTx = closingTx.copy(tx = signedTx) + if (!signedClosingTx.validate(extraUtxos = mapOf())) { + Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + } else { + Either.Right(Triple(signedClosingTx, ClosingSig(commitment.channelId, remoteScriptPubkey, localScriptPubkey, closingComplete.fees, closingComplete.lockTime, TlvStream(sigToTlv(localSig.sig))), null)) + } + } } } } @@ -382,25 +461,51 @@ object Helpers { channelKeys: ChannelKeys, commitment: FullCommitment, closingTxs: Transactions.ClosingTxs, - closingSig: ClosingSig + closingSig: ClosingSig, + localNonces: Transactions.CloserNonces?, + remoteNonce: IndividualNonce? ): Either { val closingTxsWithSig = listOfNotNull( - closingSig.closerAndCloseeOutputsSig?.let { sig -> closingTxs.localAndRemote?.let { tx -> Pair(tx, sig) } }, - closingSig.closerOutputOnlySig?.let { sig -> closingTxs.localOnly?.let { tx -> Pair(tx, sig) } }, - closingSig.closeeOutputOnlySig?.let { sig -> closingTxs.remoteOnly?.let { tx -> Pair(tx, sig) } }, + closingSig.closerAndCloseeOutputsSig?.let { sig -> closingTxs.localAndRemote?.let { tx -> Pair(tx, ChannelSpendSignature.IndividualSignature(sig)) } }, + closingSig.closerAndCloseeOutputsPartialSig?.let { sig -> remoteNonce?.let { nonce -> closingTxs.localAndRemote?.let { tx -> Pair(tx, ChannelSpendSignature.PartialSignatureWithNonce(sig, nonce)) } } }, + closingSig.closerOutputOnlySig?.let { sig -> closingTxs.localOnly?.let { tx -> Pair(tx, ChannelSpendSignature.IndividualSignature(sig)) } }, + closingSig.closerOutputOnlyPartialSig?.let { sig -> remoteNonce?.let { nonce -> closingTxs.localOnly?.let { tx -> Pair(tx, ChannelSpendSignature.PartialSignatureWithNonce(sig, nonce)) } } }, + closingSig.closeeOutputOnlySig?.let { sig -> closingTxs.remoteOnly?.let { tx -> Pair(tx, ChannelSpendSignature.IndividualSignature(sig)) } }, + closingSig.closeeOutputOnlyPartialSig?.let { sig -> remoteNonce?.let { nonce -> closingTxs.remoteOnly?.let { tx -> Pair(tx, ChannelSpendSignature.PartialSignatureWithNonce(sig, nonce)) } } }, ) return when (val preferred = closingTxsWithSig.firstOrNull()) { null -> Either.Left(MissingCloseSignature(commitment.channelId)) else -> { val (closingTx, remoteSig) = preferred val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) - val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubkey) - val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey(), commitment.remoteFundingPubkey, localSig, ChannelSpendSignature.IndividualSignature(remoteSig)) - val signedClosingTx = closingTx.copy(tx = signedTx) - if (!signedClosingTx.validate(extraUtxos = mapOf())) { - Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) - } else { - Either.Right(signedClosingTx) + when (remoteSig) { + is ChannelSpendSignature.IndividualSignature -> { + val localSig = closingTx.sign(localFundingKey, commitment.remoteFundingPubkey) + val signedTx = closingTx.aggregateSigs(localFundingKey.publicKey(), commitment.remoteFundingPubkey, localSig, remoteSig) + val signedClosingTx = closingTx.copy(tx = signedTx) + if (!signedClosingTx.validate(extraUtxos = mapOf())) { + Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + } else { + Either.Right(signedClosingTx) + } + } + is ChannelSpendSignature.PartialSignatureWithNonce -> { + val localNonce = when { + localNonces == null -> return Either.Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + closingTx.tx.txOut.size == 2 -> localNonces.localAndRemote + closingTx.toLocalOutput != null -> localNonces.localOnly + else -> localNonces.remoteOnly + } + val signedClosingTx = closingTx.partialSign(localFundingKey, commitment.remoteFundingPubkey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteSig.nonce)) + .flatMap { closingTx.aggregateSigs(localFundingKey.publicKey(), commitment.remoteFundingPubkey, it, remoteSig, mapOf()) } + .map { closingTx.copy(tx = it) } + .right ?: return Either.Left(InvalidCloseSignature(commitment.channelId, closingTx.tx.txid)) + if (!signedClosingTx.validate(mapOf())) { + Either.Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) + } else { + Either.Right(signedClosingTx) + } + } } } } 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 cbf9fb342..bf2fb5ff2 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 @@ -5,16 +5,14 @@ import fr.acinq.bitcoin.Script.tail 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.getOrDefault -import fr.acinq.bitcoin.utils.runTrying +import fr.acinq.bitcoin.utils.* import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.NonceGenerator import fr.acinq.lightning.crypto.SwapInOnChainKeys import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.transactions.* @@ -42,9 +40,20 @@ data class SharedFundingInput( val weight: Int = commitmentFormat.fundingInputWeight - fun sign(channelKeys: ChannelKeys, tx: Transaction, spentUtxos: Map): ChannelSpendSignature.IndividualSignature { + fun sign(channelId: ByteVector32, channelKeys: ChannelKeys, tx: Transaction, localNonce: Transactions.LocalNonce?, remoteNonce: IndividualNonce?, spentUtxos: Map): Either { val fundingKey = channelKeys.fundingKey(fundingTxIndex) - return Transactions.SpliceTx(info, tx).sign(fundingKey, remoteFundingPubkey, spentUtxos) + val spliceTx = Transactions.SpliceTx(info, tx) + return when (commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> Either.Right(spliceTx.sign(fundingKey, remoteFundingPubkey, spentUtxos)) + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + val localNonce = localNonce ?: return Either.Left(MissingFundingNonce(channelId, tx.txid)) + val remoteNonce = remoteNonce ?: return Either.Left(MissingFundingNonce(channelId, tx.txid)) + when (val psig = spliceTx.partialSign(fundingKey, remoteFundingPubkey, spentUtxos, localNonce, listOf(localNonce.publicNonce, remoteNonce))) { + is Either.Left -> Either.Left(InvalidFundingNonce(channelId, tx.txid)) + is Either.Right -> Either.Right(psig.value) + } + } + } } } @@ -92,12 +101,12 @@ data class InteractiveTxParams( /** Amount of the new funding output, which is the sum of the shared input, if any, and both sides' contributions. */ val fundingAmount: Satoshi = (sharedInput?.info?.txOut?.amount ?: 0.sat) + localContribution + remoteContribution - // BOLT 2: MUST set `feerate` greater than or equal to 25/24 times the `feerate` of the previously constructed transaction, rounded down. val minNextFeerate: FeeratePerKw = targetFeerate * 25 / 24 - // BOLT 2: the initiator's serial IDs MUST use even values and the non-initiator odd values. val serialIdParity = if (isInitiator) 0 else 1 + // If we don't have a shared input, this isn't a splice: it is the initial channel funding transaction. + val fundingTxIndex = sharedInput?.let { it.fundingTxIndex + 1 } ?: 0 fun fundingPubkeyScript(channelKeys: ChannelKeys): ByteVector { val fundingTxIndex = sharedInput?.let { it.fundingTxIndex + 1 } ?: 0 @@ -471,23 +480,24 @@ data class SharedTransaction( return Transaction(2, inputs, outputs, lockTime) } - fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalChannelParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { + fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, remoteNodeId: PublicKey): Either { val unsignedTx = buildUnsignedTx() - val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) - val sharedSig = fundingParams.sharedInput?.sign(channelKeys, unsignedTx, spentOutputs) + val sharedSig = when (val sig = fundingParams.sharedInput?.sign(session.fundingParams.channelId, session.channelKeys, unsignedTx, session.localFundingNonce, session.remoteFundingNonce, spentOutputs)) { + is Either.Left -> return Either.Left(sig.value) + is Either.Right -> sig.value + null -> null + } // 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) + .zip(session.txCompleteReceived.swapInNonces) .associate { it.first.serialId to it.second } } - // If we are swapping funds in, we provide our partial signatures to the corresponding inputs. val legacySwapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs @@ -501,14 +511,13 @@ data class SharedTransaction( .find { txIn.outPoint == it.outPoint } ?.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 userNonce = session.swapInSecretNonces[input.serialId]!! val serverNonce = 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) } }.filterNotNull() - // If the remote is swapping funds in, they'll need our partial signatures to finalize their witness. val legacySwapServerSigs = unsignedTx.txIn.mapIndexed { i, txIn -> remoteInputs @@ -528,15 +537,15 @@ data class SharedTransaction( val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) 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 serverNonce = session.swapInSecretNonces[input.serialId]!! val userNonce = 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?.sig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) + val txSigs = TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs) + return Either.Right(PartiallySignedSharedTransaction(this, txSigs)) } } @@ -561,13 +570,25 @@ 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 - val sharedSigs = fundingParams.sharedInput?.let { - Scripts.witness2of2( - localSigs.previousFundingTxSig ?: return null, - remoteSigs.previousFundingTxSig ?: return null, - channelKeys.fundingKey(it.fundingTxIndex).publicKey(), - it.remoteFundingPubkey, - ) + val sharedSigs = fundingParams.sharedInput?.let { input -> + val localFundingPubkey = channelKeys.fundingKey(input.fundingTxIndex).publicKey() + val spliceTx = Transactions.SpliceTx(input.info, tx.buildUnsignedTx()) + val signedTx = when (input.commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> spliceTx.aggregateSigs( + localFundingPubkey, + input.remoteFundingPubkey, + localSigs.previousFundingTxSig?.let { ChannelSpendSignature.IndividualSignature(it) } ?: return null, + remoteSigs.previousFundingTxSig?.let { ChannelSpendSignature.IndividualSignature(it) } ?: return null + ) + Transactions.CommitmentFormat.SimpleTaprootChannels -> spliceTx.aggregateSigs( + localFundingPubkey, + input.remoteFundingPubkey, + localSigs.previousFundingTxPartialSig ?: return null, + remoteSigs.previousFundingTxPartialSig ?: return null, + extraUtxos = tx.spentOutputs + ).right ?: return null + } + signedTx.txIn[spliceTx.inputIndex].witness } val fullySignedTx = FullySignedSharedTransaction(tx, localSigs, remoteSigs, sharedSigs) return when (runTrying { fullySignedTx.signedTx.correctlySpends(tx.spentOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) { @@ -656,6 +677,7 @@ data class InteractiveTxSession( val channelKeys: ChannelKeys, val swapInKeys: SwapInOnChainKeys, val fundingParams: InteractiveTxParams, + val localCommitIndex: Long, val previousFunding: SharedFundingInputBalances, val toSend: List>, val previousTxs: List = listOf(), @@ -668,7 +690,8 @@ data class InteractiveTxSession( val txCompleteReceived: TxComplete? = null, val inputsReceivedCount: Int = 0, val outputsReceivedCount: Int = 0, - val secretNonces: Map> = mapOf() + val swapInSecretNonces: Map> = mapOf(), + val localFundingNonce: Transactions.LocalNonce? = null, ) { // Example flow: @@ -690,35 +713,76 @@ data class InteractiveTxSession( channelKeys: ChannelKeys, swapInKeys: SwapInOnChainKeys, fundingParams: InteractiveTxParams, + localCommitIndex: Long, previousLocalBalance: MilliSatoshi, previousRemoteBalance: MilliSatoshi, localHtlcs: Set, fundingContributions: FundingContributions, - previousTxs: List = listOf() + previousTxs: List = listOf(), ) : this( remoteNodeId, channelKeys, swapInKeys, fundingParams, + localCommitIndex, 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, + localFundingNonce = fundingParams.sharedInput?.let { + // If we're splicing an existing channel, we create a random local nonce for this interactive-tx session. + val previousFundingKey = channelKeys.fundingKey(it.fundingTxIndex).publicKey() + NonceGenerator.signingNonce(previousFundingKey, it.remoteFundingPubkey, it.info.outPoint.txid) + } ) val isComplete: Boolean = txCompleteSent != null && txCompleteReceived != null + val localFundingKey: PrivateKey = channelKeys.fundingKey(fundingParams.fundingTxIndex) + val remoteFundingNonce: IndividualNonce? = txCompleteReceived?.fundingNonce + val currentRemoteCommitNonce: IndividualNonce? = txCompleteReceived?.commitNonces?.commitNonce + val nextRemoteCommitNonce: IndividualNonce? = txCompleteReceived?.commitNonces?.nextCommitNonce fun send(): Pair { return when (val msg = toSend.firstOrNull()) { null -> { val localSwapIns = localInputs.filterIsInstance() val remoteSwapIns = remoteInputs.filterIsInstance() - val publicNonces = (localSwapIns + remoteSwapIns) + 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) + .map { serialId -> swapInSecretNonces[serialId]!!.second } + val txComplete = when (fundingParams.commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> TxComplete(fundingParams.channelId, TlvStream(TxCompleteTlv.SwapInNonces(swapInNonces))) + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + // We don't have more inputs or outputs to contribute to the shared transaction. + // If our peer doesn't have anything more to contribute either, we will proceed to exchange commitment + // signatures spending this shared transaction, so we need to provide nonces to create those signatures. + // If our peer adds more inputs or outputs, we will simply send a new tx_complete message in response with + // nonces for the updated shared transaction. + // Note that we don't validate the shared transaction at that point: this will be done later once we've + // both sent tx_complete. If the shared transaction is invalid, we will abort and discard our nonces. + val fundingTxId = Transaction( + version = 2, + txIn = (localInputs.filterIsInstance() + remoteInputs.filterIsInstance()) + .map { it.serialId to TxIn(it.outPoint, it.sequence.toLong()) } + .sortedBy { it.first } + .map { it.second }, + txOut = (localOutputs.filterIsInstance() + remoteOutputs.filterIsInstance()) + .map { it.serialId to TxOut(it.amount, it.pubkeyScript) } + .sortedBy { it.first } + .map { it.second }, + lockTime = fundingParams.lockTime + ).txid + TxComplete( + channelId = fundingParams.channelId, + commitNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey, fundingParams.remoteFundingPubkey, localCommitIndex).publicNonce, + nextCommitNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey, fundingParams.remoteFundingPubkey, localCommitIndex + 1).publicNonce, + fundingNonce = localFundingNonce?.publicNonce, + swapInNonces = swapInNonces, + ) + } + } val next = copy(txCompleteSent = txComplete) if (next.isComplete) { Pair(next, next.validateTx(txComplete)) @@ -743,16 +807,16 @@ data class InteractiveTxSession( } val nextSecretNonces = when (inputOutgoing) { // Generate a secret nonce for this input if we don't already have one. - is InteractiveTxInput.LocalSwapIn -> when (secretNonces[inputOutgoing.serialId]) { + is InteractiveTxInput.LocalSwapIn -> when (swapInSecretNonces[inputOutgoing.serialId]) { null -> { val secretNonce = Musig2.generateNonce(randomBytes32(), Either.Left(swapInKeys.userPrivateKey), listOf(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey), null, null) - secretNonces + (inputOutgoing.serialId to secretNonce) + swapInSecretNonces + (inputOutgoing.serialId to secretNonce) } - else -> secretNonces + else -> swapInSecretNonces } - else -> secretNonces + else -> swapInSecretNonces } - val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = null, secretNonces = nextSecretNonces) + val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = null, swapInSecretNonces = nextSecretNonces) Pair(next, InteractiveTxSessionAction.SendMessage(txAddInput)) } is Either.Right -> { @@ -782,7 +846,6 @@ data class InteractiveTxSession( if (expectedSharedOutpoint != receivedSharedOutpoint) return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) InteractiveTxInput.Shared(message.serialId, receivedSharedOutpoint, fundingParams.sharedInput.info.txOut.publicKeyScript, message.sequence, previousFunding.toLocal, previousFunding.toRemote, previousFunding.toHtlcs) } - else -> { if (message.previousTx.txOut.size <= message.previousTxOutput) { return Either.Left(InteractiveTxSessionAction.InputOutOfBounds(message.channelId, message.serialId, message.previousTx.txid, message.previousTxOutput)) @@ -829,16 +892,16 @@ data class InteractiveTxSession( } 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]) { + is InteractiveTxInput.RemoteSwapIn -> when (swapInSecretNonces[input.serialId]) { null -> { val secretNonce = Musig2.generateNonce(randomBytes32(), Either.Right(input.serverKey), listOf(input.userKey, input.serverKey), null, null) - secretNonces + (input.serialId to secretNonce) + swapInSecretNonces + (input.serialId to secretNonce) } - else -> secretNonces + else -> swapInSecretNonces } - else -> secretNonces + else -> swapInSecretNonces } - val session1 = this.copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = null, secretNonces = secretNonces1) + val session1 = this.copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = null, swapInSecretNonces = secretNonces1) return Either.Right(session1) } @@ -948,8 +1011,8 @@ data class InteractiveTxSession( // Our peer must send us one nonce for each swap input (local and remote), ordered by serial_id. val swapInputsCount = localInputs.count { it is InteractiveTxInput.LocalSwapIn } + remoteInputs.count { it is InteractiveTxInput.RemoteSwapIn } - if (txCompleteReceived.publicNonces.size != swapInputsCount) { - return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, swapInputsCount, txCompleteReceived.publicNonces.size) + if (txCompleteReceived.swapInNonces.size != swapInputsCount) { + return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, swapInputsCount, txCompleteReceived.swapInNonces.size) } val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputs, remoteOnlyInputs, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) @@ -1009,7 +1072,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 nextRemoteCommitNonce: IndividualNonce?) : InteractiveTxSigningSessionAction() data class AbortFundingAttempt(val reason: ChannelException) : InteractiveTxSigningSessionAction() { override fun toString(): String = reason.message } @@ -1022,14 +1085,13 @@ sealed class InteractiveTxSigningSessionAction { */ data class InteractiveTxSigningSession( val fundingParams: InteractiveTxParams, - val fundingTxIndex: Long, val fundingTx: PartiallySignedSharedTransaction, val localCommitParams: CommitParams, val localCommit: Either, val remoteCommitParams: CommitParams, val remoteCommit: RemoteCommit, + val nextRemoteCommitNonce: IndividualNonce? ) { - // Example flow: // +-------+ +-------+ // | |-------- commit_sig -------->| | @@ -1037,14 +1099,15 @@ data class InteractiveTxSigningSession( // | |-------- tx_signatures ----->| | // | |<------- tx_signatures ------| | // +-------+ +-------+ - + val fundingTxId: TxId = fundingTx.txId + val localCommitIndex = localCommit.fold({ it.index }, { it.index }) // This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not. - val reconnectNextLocalCommitmentNumber = when (localCommit) { + val nextLocalCommitmentNumber = when (localCommit) { is Either.Left -> localCommit.value.index is Either.Right -> localCommit.value.index + 1 } - fun localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex) + fun localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingParams.fundingTxIndex) fun commitInput(fundingKey: PrivateKey): Transactions.InputInfo { val fundingScript = Transactions.makeFundingScript(fundingKey.publicKey(), fundingParams.remoteFundingPubkey, fundingParams.commitmentFormat).pubkeyScript @@ -1054,6 +1117,15 @@ data class InteractiveTxSigningSession( fun commitInput(channelKeys: ChannelKeys): Transactions.InputInfo = commitInput(localFundingKey(channelKeys)) + /** Nonce for the current commitment, which our peer will need if they must re-send their commit_sig for our current commitment transaction. */ + fun currentCommitNonce(channelKeys: ChannelKeys): Transactions.LocalNonce? = when (localCommit) { + is Either.Left -> NonceGenerator.verificationNonce(fundingTxId, localFundingKey(channelKeys), fundingParams.remoteFundingPubkey, localCommitIndex) + is Either.Right -> null + } + + /** Nonce for the next commitment, which our peer will need to sign our next commitment transaction. */ + fun nextCommitNonce(channelKeys: ChannelKeys): Transactions.LocalNonce = NonceGenerator.verificationNonce(fundingTxId, localFundingKey(channelKeys), fundingParams.remoteFundingPubkey, localCommitIndex + 1) + fun receiveCommitSig(channelKeys: ChannelKeys, channelParams: ChannelParams, remoteCommitSig: CommitSig, currentBlockHeight: Long, logger: MDCLogger): Pair { return when (localCommit) { is Either.Left -> { @@ -1079,7 +1151,7 @@ data class InteractiveTxSigningSession( if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) { val fundingStatus = LocalFundingStatus.UnconfirmedFundingTx(fundingTx, fundingParams, currentBlockHeight) val commitment = Commitment( - fundingTxIndex, + fundingParams.fundingTxIndex, fundingInput.outPoint, fundingParams.fundingAmount, fundingParams.remoteFundingPubkey, @@ -1092,7 +1164,7 @@ data class InteractiveTxSigningSession( remoteCommit, nextRemoteCommit = null ) - val action = InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs) + val action = InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs, nextRemoteCommitNonce) Pair(this.copy(localCommit = Either.Right(signedLocalCommit.value)), action) } else { Pair(this.copy(localCommit = Either.Right(signedLocalCommit.value)), InteractiveTxSigningSessionAction.WaitForTxSigs) @@ -1113,7 +1185,7 @@ data class InteractiveTxSigningSession( val fundingInput = commitInput(channelKeys) val fundingStatus = LocalFundingStatus.UnconfirmedFundingTx(fullySignedTx, fundingParams, currentBlockHeight) val commitment = Commitment( - fundingTxIndex, + fundingParams.fundingTxIndex, fundingInput.outPoint, fundingParams.fundingAmount, fundingParams.remoteFundingPubkey, @@ -1126,7 +1198,7 @@ data class InteractiveTxSigningSession( remoteCommit, nextRemoteCommit = null ) - Either.Right(InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs)) + Either.Right(InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs, nextRemoteCommitNonce)) } } } @@ -1143,7 +1215,6 @@ data class InteractiveTxSigningSession( localCommitParams: CommitParams, remoteCommitParams: CommitParams, fundingParams: InteractiveTxParams, - fundingTxIndex: Long, sharedTx: SharedTransaction, liquidityPurchase: LiquidityAds.Purchase?, localCommitmentIndex: Long, @@ -1152,12 +1223,11 @@ data class InteractiveTxSigningSession( remotePerCommitmentPoint: PublicKey, localHtlcs: Set ): Either> { - val channelKeys = channelParams.localParams.channelKeys(keyManager) - val fundingKey = channelKeys.fundingKey(fundingTxIndex) - val localCommitKeys = channelKeys.localCommitmentKeys(channelParams, localCommitmentIndex) - val remoteCommitKeys = channelKeys.remoteCommitmentKeys(channelParams, remotePerCommitmentPoint) + val fundingKey = session.localFundingKey + val localCommitKeys = session.channelKeys.localCommitmentKeys(channelParams, localCommitmentIndex) + val remoteCommitKeys = session.channelKeys.remoteCommitmentKeys(channelParams, remotePerCommitmentPoint) val unsignedTx = sharedTx.buildUnsignedTx() - val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) }.toLong() + val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(session.channelKeys) }.toLong() val liquidityFees = fundingParams.liquidityFees(liquidityPurchase) return Helpers.Funding.makeCommitTxs( channelParams = channelParams, @@ -1177,36 +1247,35 @@ data class InteractiveTxSigningSession( remoteFundingPubkey = fundingParams.remoteFundingPubkey, localCommitKeys = localCommitKeys, remoteCommitKeys = remoteCommitKeys, - ).map { firstCommitTx -> - val localSigOfRemoteCommitTx = firstCommitTx.remoteCommitTx.sign(fundingKey, fundingParams.remoteFundingPubkey) - val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { it.localSig(remoteCommitKeys) } - val alternativeSigs = if (firstCommitTx.remoteHtlcTxs.isEmpty()) { - val commitSigTlvs = Commitments.alternativeFeerates.map { feerate -> - val alternativeSpec = firstCommitTx.remoteSpec.copy(feerate = feerate) - val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( - channelParams = channelParams, - commitParams = remoteCommitParams, - commitKeys = remoteCommitKeys, - commitTxNumber = remoteCommitmentIndex, - localFundingKey = fundingKey, - remoteFundingPubKey = fundingParams.remoteFundingPubkey, - commitmentInput = firstCommitTx.remoteCommitTx.input, - commitmentFormat = fundingParams.commitmentFormat, - spec = alternativeSpec - ) - val sig = alternativeRemoteCommitTx.sign(fundingKey, fundingParams.remoteFundingPubkey).sig - CommitSigTlv.AlternativeFeerateSig(feerate, sig) + ).flatMap { firstCommitTx -> + val localSigOfRemoteCommitTx = when (fundingParams.commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> firstCommitTx.remoteCommitTx.sign(fundingKey, fundingParams.remoteFundingPubkey) + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + val remoteNonce = session.currentRemoteCommitNonce ?: return Either.Left(MissingCommitNonce(channelParams.channelId, unsignedTx.txid, remoteCommitmentIndex)) + val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey(), fundingParams.remoteFundingPubkey, unsignedTx.txid) + when (val psig = firstCommitTx.remoteCommitTx.partialSign(fundingKey, fundingParams.remoteFundingPubkey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteNonce))) { + is Either.Left -> return Either.Left(InvalidCommitNonce(channelParams.channelId, unsignedTx.txid, remoteCommitmentIndex)) + is Either.Right -> psig.value + } } - TlvStream(CommitSigTlv.AlternativeFeerateSigs(commitSigTlvs) as CommitSigTlv) - } else { - TlvStream.empty() } - val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, alternativeSigs) + val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { it.localSig(remoteCommitKeys) } + val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, batchSize = 1) // We haven't received the remote commit_sig: we don't have local htlc txs yet. val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx.tx.txid) 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, localCommitParams, Either.Left(unsignedLocalCommit), remoteCommitParams, remoteCommit), commitSig) + sharedTx.sign(session, keyManager, fundingParams, channelParams.remoteParams.nodeId).map { signedFundingTx -> + val signingSession = InteractiveTxSigningSession( + fundingParams, + signedFundingTx, + localCommitParams, + Either.Left(unsignedLocalCommit), + remoteCommitParams, + remoteCommit, + session.nextRemoteCommitNonce + ) + Pair(signingSession, 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 dc50a9637..d3538438e 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 @@ -1,10 +1,12 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.NodeParams import fr.acinq.lightning.SensitiveTaskEvents +import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchConfirmedTriggered import fr.acinq.lightning.blockchain.WatchSpent @@ -13,13 +15,18 @@ import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.channel.* import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.NonceGenerator import fr.acinq.lightning.db.ChannelCloseOutgoingPayment.ChannelClosingType import fr.acinq.lightning.logging.LoggingContext import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.wire.* +import fr.acinq.lightning.wire.ChannelReady +import fr.acinq.lightning.wire.ChannelReestablish +import fr.acinq.lightning.wire.ChannelUpdate +import fr.acinq.lightning.wire.Error +import fr.acinq.lightning.wire.Shutdown import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -292,26 +299,43 @@ sealed class PersistedChannelState : ChannelState() { internal fun ChannelContext.createChannelReestablish(): ChannelReestablish = when (val state = this@PersistedChannelState) { is WaitForFundingSigned -> { val myFirstPerCommitmentPoint = channelKeys().commitmentPoint(0) + val nextFundingTxId = state.signingSession.fundingTxId + val (currentCommitNonce, nextCommitNonce) = when (state.signingSession.fundingParams.commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> Pair(null, null) + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + val localFundingKey = channelKeys().fundingKey(0) + val remoteFundingPubKey = state.signingSession.fundingParams.remoteFundingPubkey + val currentCommitNonce = when (state.signingSession.localCommit) { + is Either.Left -> NonceGenerator.verificationNonce(nextFundingTxId, localFundingKey, remoteFundingPubKey, 0) + is Either.Right -> null + } + val nextCommitNonce = NonceGenerator.verificationNonce(nextFundingTxId, localFundingKey, remoteFundingPubKey, 1) + Pair(currentCommitNonce?.publicNonce, nextCommitNonce.publicNonce) + } + } ChannelReestablish( channelId = channelId, - nextLocalCommitmentNumber = state.signingSession.reconnectNextLocalCommitmentNumber, + nextLocalCommitmentNumber = state.signingSession.nextLocalCommitmentNumber, nextRemoteRevocationNumber = 0, yourLastCommitmentSecret = PrivateKey(ByteVector32.Zeroes), myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint, - TlvStream(ChannelReestablishTlv.NextFunding(state.signingSession.fundingTx.txId)) + nextCommitNonces = nextCommitNonce?.let { listOf(nextFundingTxId to it) } ?: listOf(), + nextFundingTxId = nextFundingTxId, + currentCommitNonce = currentCommitNonce ) } is ChannelStateWithCommitments -> { + val channelKeys = channelKeys() val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes - val myCurrentPerCommitmentPoint = channelKeys().commitmentPoint(state.commitments.localCommitIndex) + val myCurrentPerCommitmentPoint = channelKeys.commitmentPoint(state.commitments.localCommitIndex) // If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig. val nextLocalCommitmentNumber = when (state) { is WaitForFundingConfirmed -> when (state.rbfStatus) { - is RbfStatus.WaitingForSigs -> state.rbfStatus.session.reconnectNextLocalCommitmentNumber + is RbfStatus.WaitingForSigs -> state.rbfStatus.session.nextLocalCommitmentNumber else -> state.commitments.localCommitIndex + 1 } is Normal -> when (state.spliceStatus) { - is SpliceStatus.WaitingForSigs -> state.spliceStatus.session.reconnectNextLocalCommitmentNumber + is SpliceStatus.WaitingForSigs -> state.spliceStatus.session.nextLocalCommitmentNumber else -> state.commitments.localCommitIndex + 1 } else -> state.commitments.localCommitIndex + 1 @@ -322,14 +346,43 @@ sealed class PersistedChannelState : ChannelState() { is Normal -> state.getUnsignedFundingTxId() else -> null } - val tlvs: TlvStream = unsignedFundingTxId?.let { TlvStream(ChannelReestablishTlv.NextFunding(it)) } ?: TlvStream.empty() + // We send our verification nonces for all active commitments. + val nextCommitNonces = state.commitments.active.mapNotNull { c -> + when (c.commitmentFormat) { + Transactions.CommitmentFormat.AnchorOutputs -> null + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + val localFundingKey = channelKeys.fundingKey(c.fundingTxIndex) + val localCommitNonce = NonceGenerator.verificationNonce(c.fundingTxId, localFundingKey, c.remoteFundingPubkey, c.localCommit.index + 1) + c.fundingTxId to localCommitNonce.publicNonce + } + } + } + // If an interactive-tx session hasn't been fully signed, we also need to include the corresponding nonces. + val (interactiveTxCurrentCommitNonce, interactiveTxNextCommitNonce) = run { + val signingSession = when { + state is WaitForFundingConfirmed && state.rbfStatus is RbfStatus.WaitingForSigs -> state.rbfStatus.session + state is Normal && state.spliceStatus is SpliceStatus.WaitingForSigs -> state.spliceStatus.session + else -> null + } + when (signingSession?.fundingParams?.commitmentFormat) { + null -> Pair(null, null) + Transactions.CommitmentFormat.AnchorOutputs -> Pair(null, null) + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + val currentCommitNonce = signingSession.currentCommitNonce(channelKeys)?.publicNonce + val nextCommitNonce = signingSession.nextCommitNonce(channelKeys).publicNonce + Pair(currentCommitNonce, signingSession.fundingTxId to nextCommitNonce) + } + } + } ChannelReestablish( channelId = channelId, nextLocalCommitmentNumber = nextLocalCommitmentNumber, nextRemoteRevocationNumber = state.commitments.remoteCommitIndex, yourLastCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret), myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint, - tlvStream = tlvs + nextCommitNonces = nextCommitNonces + listOfNotNull(interactiveTxNextCommitNonce), + nextFundingTxId = unsignedFundingTxId, + currentCommitNonce = interactiveTxCurrentCommitNonce, ) } } @@ -341,6 +394,8 @@ sealed class PersistedChannelState : ChannelState() { sealed class ChannelStateWithCommitments : PersistedChannelState() { abstract val commitments: Commitments + // Remote nonces that must be used when signing the next remote commitment transaction (one per active commitment). + abstract val remoteNextCommitNonces: Map override val channelId: ByteVector32 get() = commitments.channelId val isChannelOpener: Boolean get() = commitments.channelParams.localParams.isChannelOpener val paysCommitTxFees: Boolean get() = commitments.channelParams.localParams.paysCommitTxFees @@ -367,6 +422,14 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { } } + internal fun ChannelContext.createChannelReady(): ChannelReady { + val localFundingKey = channelKeys().fundingKey(fundingTxIndex = 0) + val remoteFundingKey = commitments.latest.remoteFundingPubkey + val nextPerCommitmentPoint = channelKeys().commitmentPoint(1) + val nextCommitNonce = NonceGenerator.verificationNonce(commitments.latest.fundingTxId, localFundingKey, remoteFundingKey, commitIndex = 1) + return ChannelReady(channelId, nextPerCommitmentPoint, ShortChannelId.peerId(staticParams.nodeParams.nodeId), nextCommitNonce.publicNonce) + } + /** * Default handler when a funding transaction confirms. */ @@ -395,32 +458,72 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { internal fun ChannelContext.startClosingNegotiation( cmd: ChannelCommand.Close.MutualClose?, commitments: Commitments, + remoteNextCommitNonces: Map, localShutdown: Shutdown, + localCloseeNonce: Transactions.LocalNonce?, remoteShutdown: Shutdown, - actions: List + actions: List, ): Pair> { val localScript = localShutdown.scriptPubKey val remoteScript = remoteShutdown.scriptPubKey + val remoteCloseeNonce = remoteShutdown.closeeNonce val currentHeight = currentBlockHeight.toLong() return when (cmd) { null -> { logger.info { "mutual close was initiated by our peer, waiting for remote closing_complete" } - val nextState = Negotiating(commitments, localScript, remoteScript, listOf(), listOf(), currentHeight, cmd) + val nextState = Negotiating( + commitments, + remoteNextCommitNonces, + localScript, + remoteScript, + listOf(), + listOf(), + currentHeight, + cmd, + localCloseeNonce, + remoteCloseeNonce, + localCloserNonces = null + ) val actions1 = listOf(ChannelAction.Storage.StoreState(nextState)) Pair(nextState, actions + actions1) } else -> { - when (val closingResult = Helpers.Closing.makeClosingTxs(channelKeys(), commitments.latest, localScript, remoteScript, cmd.feerate, currentHeight)) { + when (val closingResult = Helpers.Closing.makeClosingTxs(channelKeys(), commitments.latest, localScript, remoteScript, cmd.feerate, currentHeight, remoteCloseeNonce)) { is Either.Left -> { logger.warning { "cannot create local closing txs, waiting for remote closing_complete: ${closingResult.value.message}" } cmd.replyTo.complete(ChannelCloseResponse.Failure.Unknown(closingResult.value)) - val nextState = Negotiating(commitments, localScript, remoteScript, listOf(), listOf(), currentHeight, cmd) + val nextState = Negotiating( + commitments, + remoteNextCommitNonces, + localScript, + remoteScript, + listOf(), + listOf(), + currentHeight, + cmd, + localCloseeNonce, + remoteCloseeNonce, + localCloserNonces = null + ) val actions1 = listOf(ChannelAction.Storage.StoreState(nextState)) Pair(nextState, actions + actions1) } is Either.Right -> { - val (closingTxs, closingComplete) = closingResult.value - val nextState = Negotiating(commitments, localScript, remoteScript, listOf(closingTxs), listOf(), currentHeight, cmd) + val (closingTxs, closingComplete, localCloserNonces) = closingResult.value + val nextState = + Negotiating( + commitments, + remoteNextCommitNonces, + localScript, + remoteScript, + listOf(closingTxs), + listOf(), + currentHeight, + cmd, + localCloseeNonce, + remoteCloseeNonce, + localCloserNonces + ) val actions1 = listOf( ChannelAction.Storage.StoreState(nextState), ChannelAction.Message.Send(closingComplete), diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closed.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closed.kt index 1bf83e3e1..eae0629cd 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closed.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closed.kt @@ -1,5 +1,7 @@ package fr.acinq.lightning.channel.states +import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.lightning.channel.ChannelAction import fr.acinq.lightning.channel.ChannelCommand import fr.acinq.lightning.channel.Commitments @@ -9,6 +11,7 @@ import fr.acinq.lightning.channel.Commitments */ data class Closed(val state: Closing) : ChannelStateWithCommitments() { override val commitments: Commitments get() = state.commitments + override val remoteNextCommitNonces: Map get() = mapOf() override fun updateCommitments(input: Commitments): ChannelStateWithCommitments { return this.copy(state = state.updateCommitments(input) as Closing) 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 47520064b..c3a0c9d50 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 @@ -1,6 +1,8 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.Transaction +import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.updated import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.blockchain.WatchConfirmed @@ -43,6 +45,8 @@ data class Closing( val revokedCommitPublished: List = emptyList() ) : ChannelStateWithCommitments() { + override val remoteNextCommitNonces: Map = mapOf() + private val spendingTxs: List by lazy { mutualClosePublished.map { it.tx } + revokedCommitPublished.map { it.commitTx } + listOfNotNull( localCommitPublished?.commitTx, 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 af837b4a5..e9cd9a954 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 @@ -2,6 +2,8 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.Transaction +import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchConfirmedTriggered @@ -13,6 +15,7 @@ import fr.acinq.lightning.wire.* data class Negotiating( override val commitments: Commitments, + override val remoteNextCommitNonces: Map, val localScript: ByteVector, val remoteScript: ByteVector, // Closing transactions we created, where we pay the fees (unsigned). @@ -22,6 +25,9 @@ data class Negotiating( val publishedClosingTxs: List, val waitingSinceBlock: Long, // how many blocks since we initiated the closing val closeCommand: ChannelCommand.Close.MutualClose?, + val localCloseeNonce: Transactions.LocalNonce?, + val remoteCloseeNonce: IndividualNonce?, + val localCloserNonces: Transactions.CloserNonces?, ) : ChannelStateWithCommitments() { override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) @@ -32,11 +38,11 @@ data class Negotiating( if (cmd.message.scriptPubKey != remoteScript) { // This may lead to a signature mismatch: peers must use closing_complete to update their closing script. logger.warning { "received shutdown changing remote script, this may lead to a signature mismatch (previous=$remoteScript, current=${cmd.message.scriptPubKey})" } - val nextState = this@Negotiating.copy(remoteScript = cmd.message.scriptPubKey) + val nextState = this@Negotiating.copy(remoteScript = cmd.message.scriptPubKey, remoteCloseeNonce = cmd.message.closeeNonce) Pair(nextState, listOf(ChannelAction.Storage.StoreState(nextState))) } else { // This is a retransmission of their previous shutdown, we can ignore it. - Pair(this@Negotiating, listOf()) + Pair(this@Negotiating.copy(remoteCloseeNonce = cmd.message.closeeNonce), listOf()) } } is ClosingComplete -> { @@ -48,15 +54,19 @@ data class Negotiating( val nextState = this@Negotiating.copy(remoteScript = cmd.message.closerScriptPubKey) Pair(nextState, listOf(ChannelAction.Message.Send(Warning(channelId, InvalidCloseeScript(channelId, cmd.message.closeeScriptPubKey, localScript).message)))) } else { - when (val result = Helpers.Closing.signClosingTx(channelKeys(), commitments.latest, cmd.message.closeeScriptPubKey, cmd.message.closerScriptPubKey, cmd.message)) { + when (val result = Helpers.Closing.signClosingTx(channelKeys(), commitments.latest, cmd.message.closeeScriptPubKey, cmd.message.closerScriptPubKey, cmd.message, localCloseeNonce)) { is Either.Left -> { logger.warning { "invalid closing_complete: ${result.value.message}" } Pair(this@Negotiating, listOf(ChannelAction.Message.Send(Warning(channelId, result.value.message)))) } is Either.Right -> { - val (signedClosingTx, closingSig) = result.value + val (signedClosingTx, closingSig, nextLocalNonce) = result.value logger.debug { "signing remote mutual close transaction: ${signedClosingTx.tx}" } - val nextState = this@Negotiating.copy(remoteScript = cmd.message.closerScriptPubKey, publishedClosingTxs = publishedClosingTxs + signedClosingTx) + val nextState = this@Negotiating.copy( + remoteScript = cmd.message.closerScriptPubKey, + publishedClosingTxs = publishedClosingTxs + signedClosingTx, + localCloseeNonce = nextLocalNonce + ) val actions = listOf( ChannelAction.Storage.StoreState(nextState), ChannelAction.Blockchain.PublishTx(signedClosingTx), @@ -69,16 +79,16 @@ data class Negotiating( } } is ClosingSig -> { - when (val result = Helpers.Closing.receiveClosingSig(channelKeys(), commitments.latest, proposedClosingTxs.last(), cmd.message)) { + when (val result = Helpers.Closing.receiveClosingSig(channelKeys(), commitments.latest, proposedClosingTxs.last(), cmd.message, localCloserNonces, remoteCloseeNonce)) { is Either.Left -> { logger.warning { "invalid closing_sig: ${result.value.message}" } - Pair(this@Negotiating, listOf(ChannelAction.Message.Send(Warning(channelId, result.value.message)))) + Pair(this@Negotiating.copy(remoteCloseeNonce = cmd.message.nextCloseeNonce), listOf(ChannelAction.Message.Send(Warning(channelId, result.value.message)))) } is Either.Right -> { val signedClosingTx = result.value logger.debug { "received signatures for local mutual close transaction: ${signedClosingTx.tx}" } closeCommand?.replyTo?.complete(ChannelCloseResponse.Success(signedClosingTx.tx.txid, signedClosingTx.fee)) - val nextState = this@Negotiating.copy(publishedClosingTxs = publishedClosingTxs + signedClosingTx) + val nextState = this@Negotiating.copy(publishedClosingTxs = publishedClosingTxs + signedClosingTx, remoteCloseeNonce = cmd.message.nextCloseeNonce) val actions = listOf( ChannelAction.Storage.StoreState(nextState), ChannelAction.Blockchain.PublishTx(signedClosingTx), @@ -139,18 +149,23 @@ data class Negotiating( cmd.replyTo.complete(ChannelCloseResponse.Failure.RbfFeerateTooLow(cmd.feerate, closeCommand.feerate * 1.2)) handleCommandError(cmd, InvalidRbfFeerate(channelId, cmd.feerate, closeCommand.feerate * 1.2)) } else { - when (val result = Helpers.Closing.makeClosingTxs(channelKeys(), commitments.latest, cmd.scriptPubKey ?: localScript, remoteScript, cmd.feerate, currentBlockHeight.toLong())) { + when (val result = Helpers.Closing.makeClosingTxs(channelKeys(), commitments.latest, cmd.scriptPubKey ?: localScript, remoteScript, cmd.feerate, currentBlockHeight.toLong(), remoteCloseeNonce)) { is Either.Left -> { cmd.replyTo.complete(ChannelCloseResponse.Failure.Unknown(result.value)) handleCommandError(cmd, result.value) } is Either.Right -> { - val (closingTxs, closingComplete) = result.value + val (closingTxs, closingComplete, localCloserNonces) = result.value logger.debug { "signing local mutual close transactions: $closingTxs" } // If we never received our peer's closing_sig, the previous command was not completed, so we must complete now. // If it was already completed because we received closing_sig, this will be a no-op. closeCommand?.replyTo?.complete(ChannelCloseResponse.Failure.ClosingUpdated(cmd.feerate, cmd.scriptPubKey)) - val nextState = this@Negotiating.copy(closeCommand = cmd, localScript = closingComplete.closerScriptPubKey, proposedClosingTxs = proposedClosingTxs + closingTxs) + val nextState = this@Negotiating.copy( + closeCommand = cmd, + localScript = closingComplete.closerScriptPubKey, + proposedClosingTxs = proposedClosingTxs + closingTxs, + localCloserNonces = localCloserNonces + ) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) add(ChannelAction.Message.Send(closingComplete)) 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 82daca9a8..d2fa87f56 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 @@ -2,6 +2,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.Bitcoin import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.WatchConfirmed @@ -14,6 +15,7 @@ import fr.acinq.lightning.wire.* data class Normal( override val commitments: Commitments, + override val remoteNextCommitNonces: Map, val shortChannelId: ShortChannelId, val channelUpdate: ChannelUpdate, val remoteChannelUpdate: ChannelUpdate?, @@ -21,6 +23,8 @@ data class Normal( val localShutdown: Shutdown?, val remoteShutdown: Shutdown?, val closeCommand: ChannelCommand.Close.MutualClose?, + val localCloseeNonce: Transactions.LocalNonce?, + val localCloserNonces: Transactions.CloserNonces?, ) : ChannelStateWithCommitments() { override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) @@ -62,7 +66,7 @@ data class Normal( logger.debug { "already in the process of signing, will sign again as soon as possible" } Pair(this@Normal, listOf()) } - else -> when (val result = commitments.sendCommit(channelKeys(), logger)) { + else -> when (val result = commitments.sendCommit(channelKeys(), remoteNextCommitNonces, logger)) { is Either.Left -> handleCommandError(cmd, result.value, channelUpdate) is Either.Right -> { val commitments1 = result.value.first @@ -113,8 +117,8 @@ data class Normal( handleCommandError(cmd, InvalidFinalScript(channelId), channelUpdate) } else -> { - val shutdown = Shutdown(channelId, localScriptPubkey) - val newState = this@Normal.copy(localShutdown = shutdown, closeCommand = cmd) + val (localCloseeNonce, shutdown) = Helpers.Closing.createShutdown(channelKeys(), commitments.latest, localScriptPubkey) + val newState = this@Normal.copy(localCloseeNonce = localCloseeNonce, localShutdown = shutdown, closeCommand = cmd) val actions = listOf(ChannelAction.Storage.StoreState(newState), ChannelAction.Message.Send(shutdown)) Pair(newState, actions) } @@ -237,17 +241,29 @@ 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.channelParams.localParams.defaultFinalScriptPubKey) + val (localCloseeNonce, localShutdown) = Helpers.Closing.createShutdown(channelKeys(), commitments1.latest) 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, closeCommand) + ShuttingDown(commitments, cmd.message.nextCommitNonces, localShutdown, remoteShutdown, closeCommand, localCloseeNonce) } else { logger.warning { "we have no htlcs but have not replied with our shutdown yet, this should never happen" } - Negotiating(commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, listOf(), listOf(), currentBlockHeight.toLong(), closeCommand) + Negotiating( + commitments, + cmd.message.nextCommitNonces, + localShutdown.scriptPubKey, + remoteShutdown.scriptPubKey, + listOf(), + listOf(), + currentBlockHeight.toLong(), + closeCommand, + localCloseeNonce, + remoteShutdown.closeeNonce, + localCloserNonces + ) } } else { - this@Normal.copy(commitments = commitments1) + this@Normal.copy(commitments = commitments1, remoteNextCommitNonces = cmd.message.nextCommitNonces) } actions.add(0, ChannelAction.Storage.StoreState(nextState)) Pair(nextState, actions) @@ -300,13 +316,24 @@ data class Normal( else -> { // so we don't have any unsigned outgoing changes val actions = mutableListOf() - val localShutdown = this@Normal.localShutdown ?: Shutdown(channelId, commitments.channelParams.localParams.defaultFinalScriptPubKey) - if (this@Normal.localShutdown == null) actions.add(ChannelAction.Message.Send(localShutdown)) + val (localCloseeNonce1, localShutdown1) = when (localShutdown) { + null -> Helpers.Closing.createShutdown(channelKeys(), commitments.latest) + else -> localCloseeNonce to localShutdown + } + if (localShutdown == null) actions.add(ChannelAction.Message.Send(localShutdown1)) when { - commitments.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation(closeCommand, commitments, localShutdown, cmd.message, actions) + commitments.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation( + closeCommand, + commitments, + remoteNextCommitNonces, + localShutdown1, + localCloseeNonce1, + cmd.message, + actions, + ) else -> { // there are some pending changes, we need to wait for them to be settled (fail/fulfill htlcs and sign fee updates) - val nextState = ShuttingDown(commitments, localShutdown, cmd.message, closeCommand) + val nextState = ShuttingDown(commitments, remoteNextCommitNonces, localShutdown1, cmd.message, closeCommand, localCloseeNonce1) actions.add(ChannelAction.Storage.StoreState(nextState)) Pair(nextState, actions) } @@ -393,6 +420,7 @@ data class Normal( feerate = spliceStatus.command.feerate, fundingPubkey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey(), requestFunding = spliceStatus.command.requestRemoteFunding, + channelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, ) logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution}" } Pair(this@Normal.copy(spliceStatus = SpliceStatus.Requested(spliceStatus.command, spliceInit)), listOf(ChannelAction.Message.Send(spliceInit))) @@ -428,11 +456,18 @@ data class Normal( val channelKeys = channelKeys() logger.info { "accepting splice with remote.amount=${cmd.message.fundingContribution}" } val parentCommitment = commitments.active.first() + val (nextCommitmentFormat, channelType) = when { + cmd.message.channelType == ChannelType.SupportedChannelType.SimpleTaprootChannels && parentCommitment.commitmentFormat == Transactions.CommitmentFormat.AnchorOutputs -> { + Pair(Transactions.CommitmentFormat.SimpleTaprootChannels, cmd.message.channelType) + } + else -> Pair(parentCommitment.commitmentFormat, null) + } val spliceAck = SpliceAck( channelId, fundingContribution = 0.sat, // only remote contributes to the splice fundingPubkey = channelKeys.fundingKey(parentCommitment.fundingTxIndex + 1).publicKey(), willFund = null, + channelType = channelType ) val fundingParams = InteractiveTxParams( channelId = channelId, @@ -442,7 +477,7 @@ data class Normal( sharedInput = SharedFundingInput(channelKeys, parentCommitment), remoteFundingPubkey = cmd.message.fundingPubkey, localOutputs = emptyList(), - commitmentFormat = commitments.latest.commitmentFormat, + commitmentFormat = nextCommitmentFormat, lockTime = cmd.message.lockTime, dustLimit = commitments.latest.localCommitParams.dustLimit.max(commitments.latest.remoteCommitParams.dustLimit), targetFeerate = cmd.message.feerate @@ -452,11 +487,12 @@ data class Normal( channelKeys, keyManager.swapInOnChainWallet, fundingParams, + parentCommitment.localCommit.index, previousLocalBalance = parentCommitment.localCommit.spec.toLocal, 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(), ) val nextState = this@Normal.copy( spliceStatus = SpliceStatus.InProgress( @@ -511,10 +547,11 @@ data class Normal( sharedInput = sharedInput, remoteFundingPubkey = cmd.message.fundingPubkey, localOutputs = spliceStatus.command.spliceOutputs, - commitmentFormat = commitments.latest.commitmentFormat, + // We always upgrade to taproot whenever initiating a splice. + commitmentFormat = Transactions.CommitmentFormat.SimpleTaprootChannels, lockTime = spliceStatus.spliceInit.lockTime, dustLimit = commitments.latest.localCommitParams.dustLimit.max(commitments.latest.remoteCommitParams.dustLimit), - targetFeerate = spliceStatus.spliceInit.feerate + targetFeerate = spliceStatus.spliceInit.feerate, ) when (val fundingContributions = FundingContributions.create( channelKeys = channelKeys, @@ -545,11 +582,12 @@ data class Normal( channelKeys, keyManager.swapInOnChainWallet, fundingParams, + parentCommitment.localCommit.index, previousLocalBalance = parentCommitment.localCommit.spec.toLocal, previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, localHtlcs = parentCommitment.localCommit.spec.htlcs, fundingContributions = fundingContributions.value, - previousTxs = emptyList() + previousTxs = emptyList(), ).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { @@ -593,7 +631,6 @@ data class Normal( parentCommitment.localCommitParams, parentCommitment.remoteCommitParams, spliceStatus.spliceSession.fundingParams, - fundingTxIndex = parentCommitment.fundingTxIndex + 1, interactiveTxAction.sharedTx, liquidityPurchase = spliceStatus.liquidityPurchase, localCommitmentIndex = parentCommitment.localCommit.index, @@ -617,7 +654,7 @@ data class Normal( spliceStatus.replyTo?.complete( ChannelFundingResponse.Success( channelId = channelId, - fundingTxIndex = session.fundingTxIndex, + fundingTxIndex = session.fundingParams.fundingTxIndex, fundingTxId = session.fundingTx.txId, capacity = session.fundingParams.fundingAmount, balance = session.localCommit.fold({ it.spec }, { it.spec }).toLocal, @@ -841,7 +878,8 @@ data class Normal( val fundingScript = action.commitment.commitInput(channelKeys()).txOut.publicKeyScript val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, fundingScript, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ChannelFundingDepthOk) val commitments = commitments.add(action.commitment) - val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None) + val remoteNextCommitNonces1 = remoteNextCommitNonces + listOfNotNull(action.nextRemoteCommitNonce?.let { action.commitment.fundingTxId to it }).toMap() + val nextState = this@Normal.copy(commitments = commitments, remoteNextCommitNonces = remoteNextCommitNonces1, spliceStatus = SpliceStatus.None) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) } 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 bc64cc9fa..5c2cfc055 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 @@ -6,10 +6,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.wire.ChannelReady -import fr.acinq.lightning.wire.ChannelReadyTlv import fr.acinq.lightning.wire.Error -import fr.acinq.lightning.wire.TlvStream data class Offline(val state: PersistedChannelState) : ChannelState() { @@ -66,10 +63,9 @@ 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.channelParams.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.fundingInput.index.toInt()) - WaitForChannelReady(commitments1, shortChannelId, channelReady) + WaitForChannelReady(commitments1, mapOf(), shortChannelId, channelReady) } else -> state } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt index 438dc6b77..7617fb77f 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/ShuttingDown.kt @@ -1,5 +1,7 @@ package fr.acinq.lightning.channel.states +import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.blockchain.WatchConfirmedTriggered import fr.acinq.lightning.blockchain.WatchSpentTriggered @@ -9,9 +11,11 @@ import fr.acinq.lightning.wire.* data class ShuttingDown( override val commitments: Commitments, + override val remoteNextCommitNonces: Map, val localShutdown: Shutdown, val remoteShutdown: Shutdown, val closeCommand: ChannelCommand.Close.MutualClose?, + val localCloseeNonce: Transactions.LocalNonce? ) : ChannelStateWithCommitments() { override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) @@ -44,7 +48,15 @@ data class ShuttingDown( is Either.Right -> { val (commitments1, revocation) = result.value when { - commitments1.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation(closeCommand, commitments1, localShutdown, remoteShutdown, listOf(ChannelAction.Message.Send(revocation))) + commitments1.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation( + closeCommand, + commitments1, + remoteNextCommitNonces, + localShutdown, + localCloseeNonce, + remoteShutdown, + listOf(ChannelAction.Message.Send(revocation)), + ) else -> { val nextState = this@ShuttingDown.copy(commitments = commitments1) val actions = buildList { @@ -65,9 +77,17 @@ data class ShuttingDown( is Either.Right -> { val (commitments1, actions) = result.value when { - commitments1.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation(closeCommand, commitments1, localShutdown, remoteShutdown, actions) + commitments1.hasNoPendingHtlcsOrFeeUpdate() -> startClosingNegotiation( + closeCommand, + commitments1, + remoteNextCommitNonces, + localShutdown, + localCloseeNonce, + remoteShutdown, + actions, + ) else -> { - val nextState = this@ShuttingDown.copy(commitments = commitments1) + val nextState = this@ShuttingDown.copy(commitments = commitments1, remoteNextCommitNonces = cmd.message.nextCommitNonces) val actions1 = buildList { addAll(actions) add(ChannelAction.Storage.StoreState(nextState)) @@ -87,7 +107,7 @@ data class ShuttingDown( Pair(nextState, listOf(ChannelAction.Storage.StoreState(nextState))) } else { // This is a retransmission of their previous shutdown, we can ignore it. - Pair(this@ShuttingDown, listOf()) + Pair(this@ShuttingDown.copy(remoteShutdown = cmd.message), listOf()) } } is Error -> { @@ -109,7 +129,7 @@ data class ShuttingDown( logger.debug { "already in the process of signing, will sign again as soon as possible" } Pair(this@ShuttingDown, listOf()) } else { - when (val result = commitments.sendCommit(channelKeys(), logger)) { + when (val result = commitments.sendCommit(channelKeys(), remoteNextCommitNonces, logger)) { is Either.Left -> handleCommandError(cmd, result.value) is Either.Right -> { val commitments1 = result.value.first 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 65f3e0036..4997e24c3 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 @@ -7,11 +7,11 @@ 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.NonceGenerator import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* data class Syncing(val state: PersistedChannelState, val channelReestablishSent: Boolean) : ChannelState() { - val channelId = state.channelId override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> { @@ -29,15 +29,18 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received // their commit_sig or their tx_signatures (depending on who must send tx_signatures first). logger.info { "re-sending commit_sig for channel creation with fundingTxId=${state.signingSession.fundingTx.txId}" } - val commitSig = state.signingSession.remoteCommit.sign(state.channelParams, channelKeys, state.signingSession) - add(ChannelAction.Message.Send(commitSig)) + when (val commitSig = state.signingSession.remoteCommit.sign(state.channelParams, channelKeys, state.signingSession, cmd.message.currentCommitNonce)) { + is Either.Left -> logger.warning { "cannot retransmit commit_sig: ${commitSig.value.message}" } + is Either.Right -> add(ChannelAction.Message.Send(commitSig.value)) + } } } Pair(state, actions) } is WaitForFundingConfirmed -> { + val state1 = state.copy(remoteNextCommitNonces = cmd.message.nextCommitNonces) when (cmd.message.nextFundingTxId) { - null -> Pair(state, listOf()) + null -> Pair(state1, listOf()) else -> { if (state.rbfStatus is RbfStatus.WaitingForSigs && state.rbfStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) { val actions = buildList { @@ -45,18 +48,20 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // They haven't received our commit_sig: we retransmit it. // We're waiting for signatures from them, and will send our tx_signatures once we receive them. logger.info { "re-sending commit_sig for rbf attempt with fundingTxId=${cmd.message.nextFundingTxId}" } - val commitSig = state.rbfStatus.session.remoteCommit.sign(state.commitments.channelParams, channelKeys, state.rbfStatus.session) - add(ChannelAction.Message.Send(commitSig)) + when (val commitSig = state.rbfStatus.session.remoteCommit.sign(state.commitments.channelParams, channelKeys, state.rbfStatus.session, cmd.message.currentCommitNonce)) { + is Either.Left -> logger.warning { "cannot retransmit commit_sig: ${commitSig.value.message}" } + is Either.Right -> add(ChannelAction.Message.Send(commitSig.value)) + } } } - Pair(state, actions) + Pair(state1, actions) } else if (state.latestFundingTx.txId == cmd.message.nextFundingTxId) { // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures // and our commit_sig if they haven't received it already. val actions = buildList { if (cmd.message.nextLocalCommitmentNumber == 0L) { logger.info { "re-sending commit_sig for fundingTxId=${cmd.message.nextFundingTxId}" } - val commitSig = state.commitments.latest.remoteCommit.sign( + when (val commitSig = state.commitments.latest.remoteCommit.sign( state.commitments.channelParams, state.commitments.latest.remoteCommitParams, channelKeys, @@ -64,19 +69,22 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput(channelKeys), state.commitments.latest.commitmentFormat, - batchSize = 1 - ) - add(ChannelAction.Message.Send(commitSig)) + batchSize = 1, + remoteNonce = cmd.message.currentCommitNonce + )) { + is Either.Left -> logger.warning { "cannot retransmit commit_sig: ${commitSig.value.message}" } + is Either.Right -> add(ChannelAction.Message.Send(commitSig.value)) + } } logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" } add(ChannelAction.Message.Send(state.latestFundingTx.sharedTx.localSigs)) } - Pair(state, actions) + Pair(state1, 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(state1.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(state.channelId, RbfAttemptAborted(state.channelId).message)))) } } } @@ -89,7 +97,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (state.commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx) { if (cmd.message.nextLocalCommitmentNumber == 0L) { logger.info { "re-sending commit_sig for fundingTxId=${state.commitments.latest.fundingTxId}" } - val commitSig = state.commitments.latest.remoteCommit.sign( + when (val commitSig = state.commitments.latest.remoteCommit.sign( state.commitments.channelParams, state.commitments.latest.remoteCommitParams, channelKeys, @@ -97,9 +105,12 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput(channelKeys), state.commitments.latest.commitmentFormat, - batchSize = 1 - ) - actions.add(ChannelAction.Message.Send(commitSig)) + batchSize = 1, + remoteNonce = cmd.message.currentCommitNonce + )) { + is Either.Left -> logger.warning { "cannot retransmit commit_sig: ${commitSig.value.message}" } + is Either.Right -> actions.add(ChannelAction.Message.Send(commitSig.value)) + } } logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" } actions.add(ChannelAction.Message.Send(state.commitments.latest.localFundingStatus.sharedTx.localSigs)) @@ -111,10 +122,9 @@ 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(remoteNextCommitNonces = cmd.message.nextCommitNonces), actions) } is Normal -> { when (val syncResult = handleSync(state.commitments, cmd.message)) { @@ -127,8 +137,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (state.commitments.latest.fundingTxIndex == 0L && cmd.message.nextLocalCommitmentNumber == 1L && state.commitments.localCommitIndex == 0L) { // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT logger.debug { "re-sending channel_ready" } - val nextPerCommitmentPoint = channelKeys.commitmentPoint(1) - val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint) + val channelReady = state.run { createChannelReady() } actions.add(ChannelAction.Message.Send(channelReady)) } @@ -137,9 +146,11 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (cmd.message.nextLocalCommitmentNumber == state.commitments.remoteCommitIndex) { // They haven't received our commit_sig: we retransmit it. // We're waiting for signatures from them, and will send our tx_signatures once we receive them. - 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(state.commitments.channelParams, channelKeys, state.spliceStatus.session) - actions.add(ChannelAction.Message.Send(commitSig)) + logger.info { "re-sending commit_sig for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingParams.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" } + when (val commitSig = state.spliceStatus.session.remoteCommit.sign(state.commitments.channelParams, channelKeys, state.spliceStatus.session, cmd.message.currentCommitNonce)) { + is Either.Left -> logger.warning { "cannot retransmit commit_sig: ${commitSig.value.message}" } + is Either.Right -> actions.add(ChannelAction.Message.Send(commitSig.value)) + } } state.spliceStatus } else if (state.commitments.latest.fundingTxId == cmd.message.nextFundingTxId) { @@ -149,7 +160,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // and our commit_sig if they haven't received it already. if (cmd.message.nextLocalCommitmentNumber == state.commitments.remoteCommitIndex) { logger.info { "re-sending commit_sig for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" } - val commitSig = state.commitments.latest.remoteCommit.sign( + when (val commitSig = state.commitments.latest.remoteCommit.sign( state.commitments.channelParams, state.commitments.latest.remoteCommitParams, channelKeys, @@ -157,9 +168,12 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput(channelKeys), state.commitments.latest.commitmentFormat, - batchSize = 1 - ) - actions.add(ChannelAction.Message.Send(commitSig)) + batchSize = 1, + remoteNonce = cmd.message.currentCommitNonce + )) { + is Either.Left -> logger.warning { "cannot retransmit commit_sig: ${commitSig.value.message}" } + is Either.Right -> actions.add(ChannelAction.Message.Send(commitSig.value)) + } } logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" } actions.add(ChannelAction.Message.Send(localFundingStatus.sharedTx.localSigs)) @@ -217,7 +231,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: logger.debug { "re-sending local shutdown" } actions.add(ChannelAction.Message.Send(it)) } - Pair(state.copy(commitments = commitments1, spliceStatus = spliceStatus1), actions) + Pair(state.copy(commitments = commitments1, remoteNextCommitNonces = cmd.message.nextCommitNonces, spliceStatus = spliceStatus1), actions) } } } @@ -226,18 +240,21 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: is SyncResult.Failure -> handleSyncFailure(state.commitments, cmd.message, syncResult) is SyncResult.Success -> { val commitments1 = discardUnsignedUpdates(state.commitments) + val (localCloseeNonce, localShutdown) = Helpers.Closing.createShutdown(channelKeys, state.commitments.latest, state.localShutdown.scriptPubKey) val actions = buildList { addAll(syncResult.retransmit) - add(state.localShutdown) + add(localShutdown) }.map { ChannelAction.Message.Send(it) } - Pair(state.copy(commitments = commitments1), actions) + val nextState = state.copy(commitments = commitments1, remoteNextCommitNonces = cmd.message.nextCommitNonces, localShutdown = localShutdown, localCloseeNonce = localCloseeNonce) + Pair(nextState, actions) } } } is Negotiating -> { // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. - val shutdown = Shutdown(channelId, state.localScript) - Pair(state, listOf(ChannelAction.Message.Send(shutdown))) + val (localCloseeNonce, localShutdown) = Helpers.Closing.createShutdown(channelKeys, state.commitments.latest, state.localScript) + val nextState = state.copy(remoteNextCommitNonces = cmd.message.nextCommitNonces, localCloseeNonce = localCloseeNonce) + Pair(nextState, listOf(ChannelAction.Message.Send(localShutdown))) } is Closing, is Closed, is WaitForRemotePublishFutureCommitment -> unhandled(cmd) } @@ -289,10 +306,9 @@ 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.fundingInput.index.toInt()) - WaitForChannelReady(commitments1, shortChannelId, channelReady) + WaitForChannelReady(commitments1, mapOf(), shortChannelId, channelReady) } else -> state } @@ -408,10 +424,11 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: val batchSize = commitments.active.size val commitSigs = CommitSigs.fromSigs(commitments.active.mapNotNull { c -> val commitInput = c.commitInput(channelKeys) + val remoteNonce = remoteChannelReestablish.nextCommitNonces[c.fundingTxId] // Note that we ignore errors and simply skip failures to sign: we've already signed those updates before // the disconnection, so we don't expect any error here unless our peer sends an invalid nonce. In that // case, we simply won't send back our commit_sig until they fix their node. - c.nextRemoteCommit?.sign(commitments.channelParams, c.remoteCommitParams, channelKeys, c.fundingTxIndex, c.remoteFundingPubkey, commitInput, c.commitmentFormat, batchSize) + c.nextRemoteCommit?.sign(commitments.channelParams, c.remoteCommitParams, channelKeys, c.fundingTxIndex, c.remoteFundingPubkey, commitInput, c.commitmentFormat, batchSize, remoteNonce)?.right }) val retransmit = when (retransmitRevocation) { null -> buildList { @@ -486,10 +503,16 @@ 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 localCommitNonces = commitments.active.map { c -> + val fundingKey = channelKeys.fundingKey(c.fundingTxIndex) + val nonce = NonceGenerator.verificationNonce(c.fundingTxId, fundingKey, c.remoteFundingPubkey, commitments.localCommitIndex + 1) + c.fundingTxId to nonce.publicNonce + } val revocation = RevokeAndAck( channelId = commitments.channelId, perCommitmentSecret = localPerCommitmentSecret, - nextPerCommitmentPoint = localNextPerCommitmentPoint + nextPerCommitmentPoint = localNextPerCommitmentPoint, + nextCommitNonces = localCommitNonces ) 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 e2ee80093..2c3d76a26 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 @@ -83,10 +83,11 @@ data class WaitForAcceptChannel( channelKeys, keyManager.swapInOnChainWallet, fundingParams, - 0.msat, - 0.msat, - emptySet(), - fundingContributions.value + localCommitIndex = 0, + previousLocalBalance = 0.msat, + previousRemoteBalance = 0.msat, + localHtlcs = emptySet(), + fundingContributions = fundingContributions.value, ).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { 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 99ba39471..1169511f5 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 @@ -1,5 +1,7 @@ package fr.acinq.lightning.channel.states +import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ChannelEvents import fr.acinq.lightning.ShortChannelId @@ -13,8 +15,9 @@ import fr.acinq.lightning.wire.* /** The channel funding transaction was confirmed, we exchange funding_locked messages. */ data class WaitForChannelReady( override val commitments: Commitments, + override val remoteNextCommitNonces: Map, val shortChannelId: ShortChannelId, - val lastSent: ChannelReady + val lastSent: ChannelReady, ) : ChannelStateWithCommitments() { override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) @@ -72,8 +75,13 @@ data class WaitForChannelReady( commitments.latest.fundingAmount.toMilliSatoshi(), enable = Helpers.aboveReserve(commitments) ) + val remoteNextCommitNonces1 = when (val nextCommitNonce = cmd.message.nextLocalNonce) { + null -> remoteNextCommitNonces + else -> remoteNextCommitNonces + mapOf(commitments.latest.fundingTxId to nextCommitNonce) + } val nextState = Normal( commitments, + remoteNextCommitNonces1, shortChannelId, initialChannelUpdate, null, @@ -81,6 +89,8 @@ data class WaitForChannelReady( null, null, null, + localCloseeNonce = null, + localCloserNonces = null ) val actions = listOf( ChannelAction.Storage.StoreState(nextState), 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 99eecaecd..9109feda4 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 @@ -1,6 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.WatchConfirmed @@ -12,10 +13,11 @@ import fr.acinq.lightning.wire.* /** We wait for the channel funding transaction to confirm. */ data class WaitForFundingConfirmed( override val commitments: Commitments, + override val remoteNextCommitNonces: Map, val waitingSinceBlock: Long, // how many blocks have we been waiting for the funding tx to confirm val deferred: ChannelReady?, // We can have at most one ongoing RBF attempt. - val rbfStatus: RbfStatus + val rbfStatus: RbfStatus, ) : ChannelStateWithCommitments() { val latestFundingTx = commitments.latest.localFundingStatus as LocalFundingStatus.UnconfirmedFundingTx @@ -102,10 +104,11 @@ data class WaitForFundingConfirmed( channelKeys(), keyManager.swapInOnChainWallet, fundingParams, + localCommitIndex = 0, SharedFundingInputBalances(0.msat, 0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx }, - commitments.latest.localCommit.spec.htlcs + commitments.latest.localCommit.spec.htlcs, ) val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session)) Pair(nextState, listOf(ChannelAction.Message.Send(TxAckRbf(channelId, fundingParams.localContribution)))) @@ -147,11 +150,13 @@ data class WaitForFundingConfirmed( channelKeys(), keyManager.swapInOnChainWallet, fundingParams, - 0.msat, - 0.msat, - emptySet(), - contributions.value, - previousFundingTxs.map { it.sharedTx }).send() + localCommitIndex = 0, + previousLocalBalance = 0.msat, + previousRemoteBalance = 0.msat, + localHtlcs = emptySet(), + fundingContributions = contributions.value, + previousTxs = previousFundingTxs.map { it.sharedTx }, + ).send() when (action) { is InteractiveTxSessionAction.SendMessage -> { val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session)) @@ -174,7 +179,7 @@ data class WaitForFundingConfirmed( is RbfStatus.InProgress -> { val (rbfSession1, interactiveTxAction) = rbfStatus.rbfSession.receive(cmd.message) when (interactiveTxAction) { - is InteractiveTxSessionAction.SendMessage -> Pair(this@WaitForFundingConfirmed.copy(rbfStatus = rbfStatus.copy(rbfSession1)), listOf(ChannelAction.Message.Send(interactiveTxAction.msg))) + is InteractiveTxSessionAction.SendMessage -> Pair(this@WaitForFundingConfirmed.copy(rbfStatus = rbfStatus.copy(rbfSession = rbfSession1)), listOf(ChannelAction.Message.Send(interactiveTxAction.msg))) is InteractiveTxSessionAction.SignSharedTx -> { val replacedCommitment = commitments.latest val signingSession = InteractiveTxSigningSession.create( @@ -184,7 +189,6 @@ data class WaitForFundingConfirmed( commitments.latest.localCommitParams, commitments.latest.remoteCommitParams, rbfSession1.fundingParams, - fundingTxIndex = replacedCommitment.fundingTxIndex, interactiveTxAction.sharedTx, liquidityPurchase = null, localCommitmentIndex = replacedCommitment.localCommit.index, @@ -263,13 +267,12 @@ 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 = run { createChannelReady() } // 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) val shortChannelId = ShortChannelId(cmd.watch.blockHeight, cmd.watch.txIndex, commitment.fundingInput.index.toInt()) - val nextState = WaitForChannelReady(commitments1, shortChannelId, channelReady) + val nextState = WaitForChannelReady(commitments1, remoteNextCommitNonces, shortChannelId, channelReady) val actions1 = buildList { if (rbfStatus != RbfStatus.None) add(ChannelAction.Message.Send(TxAbort(channelId, InvalidRbfTxConfirmed(channelId, cmd.watch.tx.txid).message))) add(ChannelAction.Message.Send(channelReady)) @@ -331,11 +334,13 @@ data class WaitForFundingConfirmed( logger.info { "will wait for ${staticParams.nodeParams.minDepthBlocks} confirmations" } val fundingScript = action.commitment.commitInput(channelKeys()).txOut.publicKeyScript val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, fundingScript, staticParams.nodeParams.minDepthBlocks, WatchConfirmed.ChannelFundingDepthOk) + val remoteNextCommitNonces1 = remoteNextCommitNonces + listOfNotNull(action.nextRemoteCommitNonce?.let { action.commitment.fundingTxId to it }).toMap() val nextState = WaitForFundingConfirmed( commitments.add(action.commitment), + remoteNextCommitNonces1, waitingSinceBlock, deferred, - RbfStatus.None + RbfStatus.None, ) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) 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 c1826304c..2344e3b0a 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 @@ -63,7 +63,6 @@ data class WaitForFundingCreated( localCommitParams, remoteCommitParams, interactiveTxSession.fundingParams, - fundingTxIndex = 0, interactiveTxAction.sharedTx, liquidityPurchase, localCommitmentIndex = 0, @@ -98,7 +97,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 a071521ac..00e3c1ab9 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 @@ -10,6 +10,7 @@ import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.SwapInEvents import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.channel.* +import fr.acinq.lightning.crypto.NonceGenerator import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat @@ -116,8 +117,9 @@ data class WaitForFundingSigned( inactive = emptyList(), payments = mapOf(), remoteNextCommitInfo = Either.Right(remoteSecondPerCommitmentPoint), - remotePerCommitmentSecrets = ShaChain.init, + remotePerCommitmentSecrets = ShaChain.init ) + val remoteNextCommitNonce = signingSession.nextRemoteCommitNonce?.let { mapOf(signingSession.fundingTxId to it) } ?: mapOf() val commonActions = buildList { action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) } add(ChannelAction.Blockchain.SendWatch(watchConfirmed)) @@ -162,13 +164,15 @@ 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 channelKeys = channelParams.localParams.channelKeys(keyManager) + val nextPerCommitmentPoint = channelKeys.commitmentPoint(1) + val nextCommitNonce = NonceGenerator.verificationNonce(action.commitment.fundingTxId, channelKeys.fundingKey(action.commitment.fundingTxIndex), action.commitment.remoteFundingPubkey, commitIndex = 1) + val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, ShortChannelId.peerId(staticParams.nodeParams.nodeId), nextCommitNonce.publicNonce) // 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). val shortChannelId = ShortChannelId(0, Pack.int32BE(action.commitment.fundingTxId.value.slice(0, 16).toByteArray()).absoluteValue, fundingInput.outPoint.index.toInt()) - val nextState = WaitForChannelReady(commitments, shortChannelId, channelReady) + val nextState = WaitForChannelReady(commitments, remoteNextCommitNonce, shortChannelId, channelReady) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) add(ChannelAction.EmitEvent(ChannelEvents.Created(nextState))) @@ -180,9 +184,10 @@ data class WaitForFundingSigned( logger.info { "will wait for ${staticParams.nodeParams.minDepthBlocks} confirmations" } val nextState = WaitForFundingConfirmed( commitments, + remoteNextCommitNonce, currentBlockHeight.toLong(), null, - RbfStatus.None + RbfStatus.None, ) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) 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 f567b582d..aea4cad61 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 @@ -102,7 +102,17 @@ data class WaitForOpenChannel( Pair(Aborted, listOf(ChannelAction.Message.Send(Error(temporaryChannelId, ChannelFundingError(temporaryChannelId).message)))) } is Either.Right -> { - val interactiveTxSession = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value) + val interactiveTxSession = InteractiveTxSession( + staticParams.remoteNodeId, + channelKeys, + keyManager.swapInOnChainWallet, + fundingParams, + localCommitIndex = 0, + previousLocalBalance = 0.msat, + previousRemoteBalance = 0.msat, + localHtlcs = emptySet(), + fundingContributions = fundingContributions.value, + ) 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/channel/states/WaitForRemotePublishFutureCommitment.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForRemotePublishFutureCommitment.kt index cf80eb52b..ba6e573b4 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForRemotePublishFutureCommitment.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForRemotePublishFutureCommitment.kt @@ -1,5 +1,7 @@ package fr.acinq.lightning.channel.states +import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.lightning.blockchain.WatchConfirmedTriggered import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.ChannelAction @@ -9,17 +11,19 @@ import fr.acinq.lightning.wire.ChannelReestablish data class WaitForRemotePublishFutureCommitment( override val commitments: Commitments, - val remoteChannelReestablish: ChannelReestablish + val remoteChannelReestablish: ChannelReestablish, ) : ChannelStateWithCommitments() { + override val remoteNextCommitNonces: Map get() = mapOf() + override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> { - return when { - cmd is ChannelCommand.WatchReceived -> when (cmd.watch) { + return when (cmd) { + is ChannelCommand.WatchReceived -> when (cmd.watch) { is WatchSpentTriggered -> handlePotentialForceClose(cmd.watch) is WatchConfirmedTriggered -> Pair(this@WaitForRemotePublishFutureCommitment, listOf()) } - cmd is ChannelCommand.Disconnected -> Pair(Offline(this@WaitForRemotePublishFutureCommitment), listOf()) + is ChannelCommand.Disconnected -> Pair(Offline(this@WaitForRemotePublishFutureCommitment), listOf()) else -> unhandled(cmd) } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/NonceGenerator.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/NonceGenerator.kt new file mode 100644 index 000000000..41508851e --- /dev/null +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/NonceGenerator.kt @@ -0,0 +1,29 @@ +package fr.acinq.lightning.crypto + +import fr.acinq.bitcoin.PrivateKey +import fr.acinq.bitcoin.PublicKey +import fr.acinq.bitcoin.TxId +import fr.acinq.bitcoin.crypto.musig2.Musig2 +import fr.acinq.bitcoin.utils.Either +import fr.acinq.lightning.Lightning.randomBytes32 +import fr.acinq.lightning.transactions.Transactions + +object NonceGenerator { + + /** + * @return a deterministic nonce used to sign our local commit tx: its public part is sent to our peer. + */ + fun verificationNonce(fundingTxId: TxId, fundingPrivKey: PrivateKey, remoteFundingPubKey: PublicKey, commitIndex: Long): Transactions.LocalNonce { + val nonces = Musig2.generateNonceWithCounter(commitIndex, fundingPrivKey, listOf(fundingPrivKey.publicKey(), remoteFundingPubKey), null, fundingTxId.value) + return Transactions.LocalNonce(nonces.first, nonces.second) + } + + /** + * @return a random nonce used to sign our peer's commit tx. + */ + fun signingNonce(localFundingPubKey: PublicKey, remoteFundingPubKey: PublicKey, fundingTxId: TxId): Transactions.LocalNonce { + val sessionId = randomBytes32() + val nonces = Musig2.generateNonce(sessionId, Either.Right(localFundingPubKey), listOf(localFundingPubKey, remoteFundingPubKey), null, fundingTxId.value) + return Transactions.LocalNonce(nonces.first, nonces.second) + } +} \ No newline at end of file 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 8a1aab1e7..ef61dde11 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 @@ -1,6 +1,7 @@ package fr.acinq.lightning.io import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomKey @@ -47,7 +48,6 @@ data class OpenChannel( val walletInputs: List, val commitTxFeerate: FeeratePerKw, val fundingTxFeerate: FeeratePerKw, - val channelType: ChannelType.SupportedChannelType ) : PeerCommand() /** Consume all the spendable utxos in the wallet state provided to open a channel or splice into an existing channel. */ @@ -676,7 +676,7 @@ class Peer( /** * Estimate the actual fee that will be paid when closing the given channel at the target feerate. */ - fun estimateFeeForMutualClose(channelId: ByteVector32, targetFeerate: FeeratePerKw): ChannelManagementFees? { + fun estimateFeeForMutualClose(channelId: ByteVector32, targetFeerate: FeeratePerKw, remoteNonce: IndividualNonce?): ChannelManagementFees? { return channels.values .filterIsInstance() .filter { it is Normal || it is ShuttingDown || it is Negotiating } @@ -689,7 +689,8 @@ class Peer( channel.commitments.channelParams.localParams.defaultFinalScriptPubKey, channel.commitments.channelParams.localParams.defaultFinalScriptPubKey, targetFeerate, - 0 + 0, + remoteNonce ).map { ChannelManagementFees(miningFee = it.second.fees, serviceFee = 0.sat) }.right } } @@ -1415,7 +1416,7 @@ class Peer( remoteInit = theirInit!!, channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), channelConfig = ChannelConfig.standard, - channelType = cmd.channelType, + channelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, requestRemoteFunding = null, channelOrigin = null, ) @@ -1530,7 +1531,7 @@ class Peer( remoteInit = theirInit!!, channelFlags = channelFlags, channelConfig = ChannelConfig.standard, - channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, + channelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, // we always create taproot channels requestRemoteFunding = requestRemoteFunding, channelOrigin = Origin.OnChainWallet(cmd.walletInputs.map { it.outPoint }.toSet(), cmd.totalAmount.toMilliSatoshi(), fees), ) @@ -1657,7 +1658,7 @@ class Peer( remoteInit = theirInit!!, channelFlags = channelFlags, channelConfig = ChannelConfig.standard, - channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, + channelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, 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 f8ecdf04b..6ffb6d22a 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 @@ -23,6 +23,10 @@ JsonSerializers.BlockHashSerializer::class, JsonSerializers.PublicKeySerializer::class, JsonSerializers.PrivateKeySerializer::class, + JsonSerializers.IndividualNonceSerializer::class, + JsonSerializers.PartialSignatureWithNonceSerializer::class, + JsonSerializers.LocalNonceSerializer::class, + JsonSerializers.CloserNoncesSerializer::class, JsonSerializers.TxIdSerializer::class, JsonSerializers.KeyPathSerializer::class, JsonSerializers.SatoshiSerializer::class, @@ -84,16 +88,18 @@ JsonSerializers.FundingCreatedSerializer::class, JsonSerializers.ChannelReadySerializer::class, JsonSerializers.ChannelReadyTlvShortChannelIdTlvSerializer::class, + JsonSerializers.ChannelReadyTlvNextLocalNonceSerializer::class, JsonSerializers.GenericTlvSerializer::class, JsonSerializers.TlvStreamSerializer::class, JsonSerializers.ShutdownTlvSerializer::class, + JsonSerializers.ShutdownTlvShutdownNonceSerializer::class, JsonSerializers.ClosingCompleteTlvSerializer::class, JsonSerializers.ClosingSigTlvSerializer::class, JsonSerializers.ChannelReestablishTlvSerializer::class, + JsonSerializers.ChannelReestablishTlvNextLocalNoncesSerializer::class, JsonSerializers.ChannelReadyTlvSerializer::class, - JsonSerializers.CommitSigTlvAlternativeFeerateSigSerializer::class, - JsonSerializers.CommitSigTlvAlternativeFeerateSigsSerializer::class, JsonSerializers.CommitSigTlvBatchSerializer::class, + JsonSerializers.CommitSigTlvPartialSignatureWithNonceSerializer::class, JsonSerializers.CommitSigTlvSerializer::class, JsonSerializers.UUIDSerializer::class, JsonSerializers.ClosingSerializer::class, @@ -111,6 +117,7 @@ package fr.acinq.lightning.json import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -204,9 +211,12 @@ object JsonSerializers { } polymorphic(Tlv::class) { subclass(ChannelReadyTlv.ShortChannelIdTlv::class, ChannelReadyTlvShortChannelIdTlvSerializer) - subclass(CommitSigTlv.AlternativeFeerateSigs::class, CommitSigTlvAlternativeFeerateSigsSerializer) + subclass(ChannelReadyTlv.NextLocalNonce::class, ChannelReadyTlvNextLocalNonceSerializer) subclass(CommitSigTlv.Batch::class, CommitSigTlvBatchSerializer) + subclass(CommitSigTlv.PartialSignatureWithNonce::class, CommitSigTlvPartialSignatureWithNonceSerializer) subclass(UpdateAddHtlcTlv.PathKey::class, UpdateAddHtlcTlvPathKeySerializer) + subclass(ShutdownTlv.ShutdownNonce::class, ShutdownTlvShutdownNonceSerializer) + subclass(ChannelReestablishTlv.NextLocalNonces::class, ChannelReestablishTlvNextLocalNoncesSerializer) } contextual(Bolt11InvoiceSerializer) contextual(OfferSerializer) @@ -320,6 +330,7 @@ object JsonSerializers { transform = { f -> when (f) { Transactions.CommitmentFormat.AnchorOutputs -> CommitmentFormatSurrogate("anchor_outputs") + Transactions.CommitmentFormat.SimpleTaprootChannels -> CommitmentFormatSurrogate("simple_taproot_channels") } }, delegateSerializer = CommitmentFormatSurrogate.serializer() @@ -332,11 +343,12 @@ object JsonSerializers { object IndividualSignatureSerializer @Serializable - data class ChannelSpendSignatureSurrogate(val sig: ByteVector64) + data class ChannelSpendSignatureSurrogate(val sig: ByteVector, val nonce: IndividualNonce?) object ChannelSpendSignatureSerializer : SurrogateSerializer( transform = { s -> when (s) { - is ChannelSpendSignature.IndividualSignature -> ChannelSpendSignatureSurrogate(s.sig) + is ChannelSpendSignature.IndividualSignature -> ChannelSpendSignatureSurrogate(s.sig, nonce = null) + is ChannelSpendSignature.PartialSignatureWithNonce -> ChannelSpendSignatureSurrogate(s.partialSig, s.nonce) } }, delegateSerializer = ChannelSpendSignatureSurrogate.serializer() @@ -366,6 +378,9 @@ object JsonSerializers { delegateSerializer = RemoteFundingStatusSurrogate.serializer() ) + @Serializer(forClass = Transactions.CloserNonces::class) + object CloserNoncesSerializer + @Serializer(forClass = Transactions.ClosingTx::class) object ClosingTxSerializer @@ -424,6 +439,9 @@ object JsonSerializers { object ByteVector64Serializer : StringSerializer() object BlockHashSerializer : StringSerializer() object PublicKeySerializer : StringSerializer() + object IndividualNonceSerializer : StringSerializer() + object PartialSignatureWithNonceSerializer : StringSerializer() + object LocalNonceSerializer : StringSerializer() object TxIdSerializer : StringSerializer() object KeyPathSerializer : StringSerializer() object ShortChannelIdSerializer : StringSerializer() @@ -536,18 +554,21 @@ object JsonSerializers { @Serializer(forClass = ChannelReadyTlv.ShortChannelIdTlv::class) object ChannelReadyTlvShortChannelIdTlvSerializer + @Serializer(forClass = ChannelReadyTlv.NextLocalNonce::class) + object ChannelReadyTlvNextLocalNonceSerializer + @Serializer(forClass = ShutdownTlv::class) object ShutdownTlvSerializer - @Serializer(forClass = CommitSigTlv.AlternativeFeerateSig::class) - object CommitSigTlvAlternativeFeerateSigSerializer - - @Serializer(forClass = CommitSigTlv.AlternativeFeerateSigs::class) - object CommitSigTlvAlternativeFeerateSigsSerializer + @Serializer(forClass = ShutdownTlv.ShutdownNonce::class) + object ShutdownTlvShutdownNonceSerializer @Serializer(forClass = CommitSigTlv.Batch::class) object CommitSigTlvBatchSerializer + @Serializer(forClass = CommitSigTlv.PartialSignatureWithNonce::class) + object CommitSigTlvPartialSignatureWithNonceSerializer + @Serializer(forClass = CommitSigTlv::class) object CommitSigTlvSerializer @@ -563,6 +584,9 @@ object JsonSerializers { @Serializer(forClass = ChannelReestablishTlv::class) object ChannelReestablishTlvSerializer + @Serializer(forClass = ChannelReestablishTlv.NextLocalNonces::class) + object ChannelReestablishTlvNextLocalNoncesSerializer + @Serializer(forClass = GenericTlv::class) object GenericTlvSerializer 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..0cadabde6 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 @@ -19,10 +20,12 @@ object InputExtensions { fun Input.readByteVector64(): ByteVector64 = ByteVector64(ByteArray(64).also { read(it, 0, it.size) }) - fun Input.readPublicKey() = PublicKey(ByteArray(33).also { read(it, 0, it.size) }) + fun Input.readIndividualNonce() = IndividualNonce(ByteArray(66).also { read(it, 0, it.size) }) fun Input.readPrivateKey() = PrivateKey(ByteArray(32).also { read(it, 0, it.size) }) + fun Input.readPublicKey() = PublicKey(ByteArray(33).also { read(it, 0, it.size) }) + fun Input.readTxId(): TxId = TxId(readByteVector32()) fun Input.readUuid(): UUID = UUID.fromBytes(ByteArray(16).also { read(it, 0, it.size) }) 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 5bfa7430c..421f228cb 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 @@ -116,7 +116,7 @@ object Deserialization { 0x01 -> RbfStatus.WaitingForSigs(readInteractiveTxSigningSession(emptySet(), localCommitParams, remoteCommitParams)) else -> error("unknown discriminator $discriminator for class ${RbfStatus::class}") } - return WaitForFundingConfirmed(commitments, waitingSinceBlock, deferred, rbfStatus) + return WaitForFundingConfirmed(commitments, remoteNextCommitNonces = mapOf(), waitingSinceBlock, deferred, rbfStatus) } private fun Input.readWaitForFundingConfirmed(): WaitForFundingConfirmed { @@ -130,11 +130,12 @@ object Deserialization { 0x01 -> RbfStatus.WaitingForSigs(readInteractiveTxSigningSession(emptySet(), localCommitParams, remoteCommitParams)) else -> error("unknown discriminator $discriminator for class ${RbfStatus::class}") } - return WaitForFundingConfirmed(commitments, waitingSinceBlock, deferred, rbfStatus) + return WaitForFundingConfirmed(commitments, remoteNextCommitNonces = mapOf(), waitingSinceBlock, deferred, rbfStatus) } private fun Input.readWaitForChannelReady() = WaitForChannelReady( commitments = readCommitments(), + remoteNextCommitNonces = mapOf(), shortChannelId = ShortChannelId(readNumber()), lastSent = readLightningMessage() as ChannelReady ) @@ -145,6 +146,7 @@ object Deserialization { val remoteCommitParams = commitments.latest.remoteCommitParams return Normal( commitments = commitments, + remoteNextCommitNonces = mapOf(), shortChannelId = ShortChannelId(readNumber()), channelUpdate = readLightningMessage() as ChannelUpdate, remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate }, @@ -156,6 +158,8 @@ object Deserialization { localShutdown = readNullable { readLightningMessage() as Shutdown }, remoteShutdown = readNullable { readLightningMessage() as Shutdown }, closeCommand = readNullable { readCloseCommand() }, + localCloseeNonce = null, + localCloserNonces = null, ) } @@ -180,7 +184,19 @@ object Deserialization { 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(commitments.allHtlcs, localCommitParams, remoteCommitParams), readNullable { readLiquidityPurchase() }, readCollection { readChannelOrigin() }.toList()) else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") } - return Normal(commitments, shortChannelId, channelUpdate, remoteChannelUpdate, spliceStatus, localShutdown, remoteShutdown, closeCommand) + return Normal( + commitments, + remoteNextCommitNonces = mapOf(), + shortChannelId, + channelUpdate, + remoteChannelUpdate, + spliceStatus, + localShutdown, + remoteShutdown, + closeCommand, + localCloseeNonce = null, + localCloserNonces = null + ) } private fun Input.readNormalLegacy(): Normal { @@ -204,7 +220,19 @@ object Deserialization { 0x01 -> SpliceStatus.WaitingForSigs(readInteractiveTxSigningSession(commitments.allHtlcs, localCommitParams, remoteCommitParams), null, readCollection { readChannelOrigin() }.toList()) else -> error("unknown discriminator $discriminator for class ${SpliceStatus::class}") } - return Normal(commitments, shortChannelId, channelUpdate, remoteChannelUpdate, spliceStatus, localShutdown, remoteShutdown, closeCommand) + return Normal( + commitments, + remoteNextCommitNonces = mapOf(), + shortChannelId, + channelUpdate, + remoteChannelUpdate, + spliceStatus, + localShutdown, + remoteShutdown, + closeCommand, + localCloseeNonce = null, + localCloserNonces = null + ) } private fun Input.readShuttingDownBeforeSimpleClose(): ShuttingDown { @@ -218,14 +246,16 @@ object Deserialization { readNumber() ChannelCommand.Close.MutualClose(CompletableDeferred(), localShutdown.scriptPubKey, preferred) } - return ShuttingDown(commitments, localShutdown, remoteShutdown, closeCommand) + return ShuttingDown(commitments, remoteNextCommitNonces = mapOf(), localShutdown, remoteShutdown, closeCommand, localCloseeNonce = null) } private fun Input.readShuttingDown(): ShuttingDown = ShuttingDown( commitments = readCommitments(), + remoteNextCommitNonces = mapOf(), localShutdown = readLightningMessage() as Shutdown, remoteShutdown = readLightningMessage() as Shutdown, closeCommand = readNullable { readCloseCommand() }, + localCloseeNonce = null ) private fun Input.readNegotiatingBeforeSimpleClose(): Negotiating { @@ -248,11 +278,24 @@ object Deserialization { readNumber() ChannelCommand.Close.MutualClose(CompletableDeferred(), localShutdown.scriptPubKey, preferred) } - return Negotiating(commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, listOf(), listOfNotNull(bestUnpublishedClosingTx), waitingSinceBlock = 0, closeCommand) + return Negotiating( + commitments, + remoteNextCommitNonces = mapOf(), + localShutdown.scriptPubKey, + remoteShutdown.scriptPubKey, + listOf(), + listOfNotNull(bestUnpublishedClosingTx), + waitingSinceBlock = 0, + closeCommand, + localCloseeNonce = null, + remoteCloseeNonce = null, + localCloserNonces = null + ) } private fun Input.readNegotiating(): Negotiating = Negotiating( commitments = readCommitments(), + remoteNextCommitNonces = mapOf(), localScript = readDelimitedByteArray().byteVector(), remoteScript = readDelimitedByteArray().byteVector(), proposedClosingTxs = readCollection { @@ -265,6 +308,9 @@ object Deserialization { publishedClosingTxs = readCollection { readClosingTx() }.toList(), waitingSinceBlock = readNumber(), closeCommand = readNullable { readCloseCommand() }, + localCloseeNonce = null, + remoteCloseeNonce = null, + localCloserNonces = null ) private fun Input.readClosing(): Closing = Closing( @@ -338,7 +384,7 @@ object Deserialization { private fun Input.readWaitForRemotePublishFutureCommitment(): WaitForRemotePublishFutureCommitment = WaitForRemotePublishFutureCommitment( commitments = readCommitments(), - remoteChannelReestablish = readLightningMessage() as ChannelReestablish + remoteChannelReestablish = readLightningMessage() as ChannelReestablish, ) private fun Input.readClosed(): Closed = Closed( @@ -633,7 +679,7 @@ object Deserialization { private fun Input.readInteractiveTxSigningSession(htlcs: Set, localCommitParams: CommitParams, remoteCommitParams: CommitParams): InteractiveTxSigningSession { val fundingParams = readInteractiveTxParams() - val fundingTxIndex = readNumber() + readNumber() // the fundingTxIndex was explicitly encoded, even though it is already in the fundingParams val fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction val (localCommit, remoteCommit) = when (val discriminator = read()) { 0 -> Pair(Either.Left(readUnsignedLocalCommitWithHtlcs()), readRemoteCommitWithHtlcs()) @@ -650,7 +696,7 @@ object Deserialization { 5 -> Pair(Either.Right(readLocalCommitWithoutHtlcs(htlcs, fundingParams.remoteFundingPubkey).second), readRemoteCommitWithoutHtlcs(htlcs)) else -> error("unknown discriminator $discriminator for class ${InteractiveTxSigningSession::class}") } - return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, localCommitParams, localCommit, remoteCommitParams, remoteCommit) + return InteractiveTxSigningSession(fundingParams, fundingTx, localCommitParams, localCommit, remoteCommitParams, remoteCommit, null) } private fun Input.readChannelOrigin(): Origin = when (val discriminator = read()) { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Deserialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Deserialization.kt index 41ee57607..cd71c33ba 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Deserialization.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Deserialization.kt @@ -17,6 +17,7 @@ import fr.acinq.lightning.serialization.InputExtensions.readByteVector64 import fr.acinq.lightning.serialization.InputExtensions.readCollection import fr.acinq.lightning.serialization.InputExtensions.readDelimitedByteArray import fr.acinq.lightning.serialization.InputExtensions.readEither +import fr.acinq.lightning.serialization.InputExtensions.readIndividualNonce import fr.acinq.lightning.serialization.InputExtensions.readLightningMessage import fr.acinq.lightning.serialization.InputExtensions.readNullable import fr.acinq.lightning.serialization.InputExtensions.readNumber @@ -65,30 +66,33 @@ object Deserialization { signingSession = readInteractiveTxSigningSession(emptySet()), remoteSecondPerCommitmentPoint = readPublicKey(), liquidityPurchase = readNullable { readLiquidityPurchase() }, - channelOrigin = readNullable { readChannelOrigin() } + channelOrigin = readNullable { readChannelOrigin() }, ) private fun Input.readWaitForFundingConfirmed() = WaitForFundingConfirmed( commitments = readCommitments(), + remoteNextCommitNonces = mapOf(), waitingSinceBlock = readNumber(), deferred = readNullable { readLightningMessage() as ChannelReady }, rbfStatus = when (val discriminator = read()) { 0x00 -> RbfStatus.None 0x01 -> RbfStatus.WaitingForSigs(readInteractiveTxSigningSession(emptySet())) else -> error("unknown discriminator $discriminator for class ${RbfStatus::class}") - } + }, ) private fun Input.readWaitForChannelReady() = WaitForChannelReady( commitments = readCommitments(), + remoteNextCommitNonces = mapOf(), shortChannelId = ShortChannelId(readNumber()), - lastSent = readLightningMessage() as ChannelReady + lastSent = readLightningMessage() as ChannelReady, ) private fun Input.readNormal(): Normal { val commitments = readCommitments() return Normal( commitments = commitments, + remoteNextCommitNonces = mapOf(), shortChannelId = ShortChannelId(readNumber()), channelUpdate = readLightningMessage() as ChannelUpdate, remoteChannelUpdate = readNullable { readLightningMessage() as ChannelUpdate }, @@ -100,18 +104,23 @@ object Deserialization { localShutdown = readNullable { readLightningMessage() as Shutdown }, remoteShutdown = readNullable { readLightningMessage() as Shutdown }, closeCommand = readNullable { readCloseCommand() }, + localCloseeNonce = null, + localCloserNonces = null, ) } private fun Input.readShuttingDown(): ShuttingDown = ShuttingDown( commitments = readCommitments(), + remoteNextCommitNonces = mapOf(), localShutdown = readLightningMessage() as Shutdown, remoteShutdown = readLightningMessage() as Shutdown, closeCommand = readNullable { readCloseCommand() }, + localCloseeNonce = null ) private fun Input.readNegotiating(): Negotiating = Negotiating( commitments = readCommitments(), + remoteNextCommitNonces = mapOf(), localScript = readDelimitedByteArray().byteVector(), remoteScript = readDelimitedByteArray().byteVector(), proposedClosingTxs = readCollection { @@ -124,6 +133,9 @@ object Deserialization { publishedClosingTxs = readCollection { readClosingTx() }.toList(), waitingSinceBlock = readNumber(), closeCommand = readNullable { readCloseCommand() }, + localCloserNonces = null, + remoteCloseeNonce = null, + localCloseeNonce = null ) private fun Input.readClosing(): Closing = Closing( @@ -182,7 +194,7 @@ object Deserialization { private fun Input.readWaitForRemotePublishFutureCommitment(): WaitForRemotePublishFutureCommitment = WaitForRemotePublishFutureCommitment( commitments = readCommitments(), - remoteChannelReestablish = readLightningMessage() as ChannelReestablish + remoteChannelReestablish = readLightningMessage() as ChannelReestablish, ) private fun Input.readClosed(): Closed = Closed( @@ -368,7 +380,6 @@ object Deserialization { private fun Input.readInteractiveTxSigningSession(htlcs: Set): InteractiveTxSigningSession = InteractiveTxSigningSession( fundingParams = readInteractiveTxParams(), - fundingTxIndex = readNumber(), fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction, localCommitParams = readCommitParams(), localCommit = when (val discriminator = read()) { @@ -377,7 +388,8 @@ object Deserialization { else -> error("unknown discriminator $discriminator for class ${InteractiveTxSigningSession::class}") }, remoteCommitParams = readCommitParams(), - remoteCommit = readRemoteCommitWithoutHtlcs(htlcs) + remoteCommit = readRemoteCommitWithoutHtlcs(htlcs), + nextRemoteCommitNonce = null ) private fun Input.readChannelOrigin(): Origin = when (val discriminator = read()) { @@ -544,11 +556,13 @@ object Deserialization { private fun Input.readCommitmentFormat(): Transactions.CommitmentFormat = when (val discriminator = read()) { 0x00 -> Transactions.CommitmentFormat.AnchorOutputs + 0x01 -> Transactions.CommitmentFormat.SimpleTaprootChannels else -> error("invalid discriminator $discriminator for class ${Transactions.CommitmentFormat::class}") } private fun Input.readChannelSpendSignature(): ChannelSpendSignature = when (val discriminator = read()) { 0x00 -> ChannelSpendSignature.IndividualSignature(readByteVector64()) + 0x01 -> ChannelSpendSignature.PartialSignatureWithNonce(readByteVector32(), readIndividualNonce()) else -> error("invalid discriminator $discriminator for class ${ChannelSpendSignature::class}") } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Serialization.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Serialization.kt index f5a34ab62..0864c5fa6 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Serialization.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v5/Serialization.kt @@ -415,7 +415,6 @@ object Serialization { private fun Output.writeInteractiveTxSigningSession(s: InteractiveTxSigningSession) = s.run { writeInteractiveTxParams(fundingParams) - writeNumber(s.fundingTxIndex) writeSignedSharedTransaction(fundingTx) writeCommitParams(localCommitParams) when (localCommit) { @@ -588,6 +587,7 @@ object Serialization { private fun Output.writeCommitmentFormat(o: Transactions.CommitmentFormat) = when (o) { Transactions.CommitmentFormat.AnchorOutputs -> write(0x00) + Transactions.CommitmentFormat.SimpleTaprootChannels -> write(0x01) } private fun Output.writeChannelSpendSignature(sig: ChannelSpendSignature) = when (sig) { @@ -595,6 +595,11 @@ object Serialization { write(0x00) writeByteVector64(sig.sig) } + is ChannelSpendSignature.PartialSignatureWithNonce -> { + write(0x01) + writeByteVector32(sig.partialSig) + write(sig.nonce.toByteArray()) + } } private fun Output.writeCloseCommand(o: ChannelCommand.Close.MutualClose) = o.run { 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 76854eed0..8d37535cc 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,9 +2,15 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* import fr.acinq.bitcoin.ScriptEltMapping.code2elt +import fr.acinq.bitcoin.SigHash.SIGHASH_ALL +import fr.acinq.bitcoin.SigHash.SIGHASH_ANYONECANPAY +import fr.acinq.bitcoin.SigHash.SIGHASH_DEFAULT +import fr.acinq.bitcoin.SigHash.SIGHASH_SINGLE +import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.crypto.CommitmentPublicKeys +import fr.acinq.lightning.crypto.LocalCommitmentKeys import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.transactions.Scripts.htlcOffered import fr.acinq.lightning.transactions.Scripts.htlcReceived @@ -17,6 +23,13 @@ object Scripts { fun der(sig: ByteVector64, sigHash: Int): ByteVector = Crypto.compact2der(sig).concat(sigHash.toByte()) + fun sort(pubkeys: List): List = pubkeys.sortedWith { p1, p2 -> LexicographicalOrdering.compare(p1, p2) } + + private fun htlcRemoteSighash(commitmentFormat: Transactions.CommitmentFormat): Int = when (commitmentFormat) { + is Transactions.CommitmentFormat.AnchorOutputs -> SIGHASH_SINGLE or SIGHASH_ANYONECANPAY + is Transactions.CommitmentFormat.SimpleTaprootChannels -> SIGHASH_SINGLE or SIGHASH_ANYONECANPAY + } + fun multiSig2of2(pubkey1: PublicKey, pubkey2: PublicKey): List = when { LexicographicalOrdering.isLessThan(pubkey1.value, pubkey2.value) -> Script.createMultiSigMofN(2, listOf(pubkey1, pubkey2)) else -> Script.createMultiSigMofN(2, listOf(pubkey2, pubkey1)) @@ -26,8 +39,8 @@ object Scripts { * @return a script witness that matches the msig 2-of-2 pubkey script for pubkey1 and pubkey2 */ fun witness2of2(sig1: ByteVector64, sig2: ByteVector64, pubkey1: PublicKey, pubkey2: PublicKey): ScriptWitness { - val encodedSig1 = der(sig1, SigHash.SIGHASH_ALL) - val encodedSig2 = der(sig2, SigHash.SIGHASH_ALL) + val encodedSig1 = der(sig1, SIGHASH_ALL) + val encodedSig2 = der(sig2, SIGHASH_ALL) val redeemScript = ByteVector(Script.write(multiSig2of2(pubkey1, pubkey2))) return when { LexicographicalOrdering.isLessThan(pubkey1.value, pubkey2.value) -> ScriptWitness(listOf(ByteVector.empty, encodedSig1, encodedSig2, redeemScript)) @@ -110,20 +123,20 @@ object Scripts { * This witness script spends a [toLocalDelayed] output using a local sig after a delay */ fun witnessToRemoteDelayedAfterDelay(localSig: ByteVector64, toRemoteDelayedScript: ByteVector) = - ScriptWitness(listOf(der(localSig, SigHash.SIGHASH_ALL), toRemoteDelayedScript)) + ScriptWitness(listOf(der(localSig, SIGHASH_ALL), toRemoteDelayedScript)) /** * This witness script spends a [toLocalDelayed] output using a local sig after a delay */ fun witnessToLocalDelayedAfterDelay(localSig: ByteVector64, toLocalDelayedScript: ByteVector) = - ScriptWitness(listOf(der(localSig, SigHash.SIGHASH_ALL), ByteVector.empty, toLocalDelayedScript)) + ScriptWitness(listOf(der(localSig, SIGHASH_ALL), ByteVector.empty, toLocalDelayedScript)) /** * This witness script spends (steals) a [toLocalDelayed] output using a revocation key as a punishment * for having published a revoked transaction */ fun witnessToLocalDelayedWithRevocationSig(revocationSig: ByteVector64, toLocalScript: ByteVector) = - ScriptWitness(listOf(der(revocationSig, SigHash.SIGHASH_ALL), ByteVector(byteArrayOf(1)), toLocalScript)) + ScriptWitness(listOf(der(revocationSig, SIGHASH_ALL), ByteVector(byteArrayOf(1)), toLocalScript)) fun htlcOffered(keys: CommitmentPublicKeys, paymentHash: ByteVector32): List = listOf( // @formatter:off @@ -151,16 +164,18 @@ object Scripts { * remote signature is created with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY */ fun witnessHtlcSuccess(localSig: ByteVector64, remoteSig: ByteVector64, preimage: ByteVector32, htlcOfferedScript: ByteVector) = - ScriptWitness(listOf(ByteVector.empty, der(remoteSig, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY), der(localSig, SigHash.SIGHASH_ALL), preimage, htlcOfferedScript)) + ScriptWitness(listOf(ByteVector.empty, der(remoteSig, SIGHASH_SINGLE or SIGHASH_ANYONECANPAY), der(localSig, SIGHASH_ALL), preimage, htlcOfferedScript)) /** Extract payment preimages from a 2nd-stage HTLC Success transaction's witness script. */ fun extractPreimagesFromHtlcSuccess(tx: Transaction): Set { return tx.txIn.map { it.witness }.mapNotNull { when { - it.stack.size < 5 -> null - !it.stack[0].isEmpty() -> null - it.stack[3].size() != 32 -> null - else -> ByteVector32(it.stack[3]) + it.stack.size != 5 -> null + // anchor-outputs + it.stack[0].isEmpty() && it.stack[3].size() == 32 -> ByteVector32(it.stack[3]) + // taproot + it.stack[2].size() == 32 -> ByteVector32(it.stack[2]) + else -> null } }.toSet() } @@ -170,15 +185,17 @@ object Scripts { * claim its funds using a payment preimage (consumes htlcOffered script from commit tx) */ fun witnessClaimHtlcSuccessFromCommitTx(localSig: ByteVector64, preimage: ByteVector32, htlcOffered: ByteVector) = - ScriptWitness(listOf(der(localSig, SigHash.SIGHASH_ALL), preimage, htlcOffered)) + ScriptWitness(listOf(der(localSig, SIGHASH_ALL), preimage, htlcOffered)) /** Extract payment preimages from a claim-htlc transaction. */ fun extractPreimagesFromClaimHtlcSuccess(tx: Transaction): Set { return tx.txIn.map { it.witness }.mapNotNull { when { - it.stack.size < 3 -> null - it.stack[1].size() != 32 -> null - else -> ByteVector32(it.stack[1]) + // anchor-outputs + it.stack.size == 3 && it.stack[1].size() == 32 -> ByteVector32(it.stack[1]) + // taproot + it.stack.size == 4 && it.stack[1].size() == 32 -> ByteVector32(it.stack[1]) + else -> null } }.toSet() } @@ -211,20 +228,242 @@ object Scripts { * remote signature is created with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY */ fun witnessHtlcTimeout(localSig: ByteVector64, remoteSig: ByteVector64, htlcOfferedScript: ByteVector) = - ScriptWitness(listOf(ByteVector.empty, der(remoteSig, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY), der(localSig, SigHash.SIGHASH_ALL), ByteVector.empty, htlcOfferedScript)) + ScriptWitness(listOf(ByteVector.empty, der(remoteSig, SIGHASH_SINGLE or SIGHASH_ANYONECANPAY), der(localSig, SIGHASH_ALL), ByteVector.empty, htlcOfferedScript)) /** * If remote publishes its commit tx where there was a local->remote htlc, then local uses this script to * claim its funds after timeout (consumes htlcReceived script from commit tx) */ fun witnessClaimHtlcTimeoutFromCommitTx(localSig: ByteVector64, htlcReceivedScript: ByteVector) = - ScriptWitness(listOf(der(localSig, SigHash.SIGHASH_ALL), ByteVector.empty, htlcReceivedScript)) + ScriptWitness(listOf(der(localSig, SIGHASH_ALL), ByteVector.empty, htlcReceivedScript)) /** * This witness script spends (steals) a [[htlcOffered]] or [[htlcReceived]] output using a revocation key as a punishment * for having published a revoked transaction */ fun witnessHtlcWithRevocationSig(commitKeys: RemoteCommitmentKeys, revocationSig: ByteVector64, htlcScript: ByteVector) = - ScriptWitness(listOf(der(revocationSig, SigHash.SIGHASH_ALL), commitKeys.revocationPublicKey.value, htlcScript)) + ScriptWitness(listOf(der(revocationSig, SIGHASH_ALL), commitKeys.revocationPublicKey.value, htlcScript)) + + /** + * Specific scripts for taproot channels + */ + object Taproot { + /** + * Taproot signatures are usually 64 bytes, unless a non-default sighash is used, in which case it is appended. + */ + fun encodeSig(sig: ByteVector64, sighashType: Int = SIGHASH_DEFAULT): ByteVector = when (sighashType) { + SIGHASH_DEFAULT -> sig + else -> sig.concat(sighashType.toByte()) + } + + /** + * Sort and aggregate the public keys of a musig2 session. + * + * @return the aggregated public key + * @see [fr.acinq.bitcoin.crypto.musig2.Musig2.aggregateKeys] + */ + fun musig2Aggregate(pubkey1: PublicKey, pubkey2: PublicKey): XonlyPublicKey = Musig2.aggregateKeys(sort(listOf(pubkey1, pubkey2))) + + /** + * "Nothing Up My Sleeve" point, for which there is no known private key. + */ + val NUMS_POINT: PublicKey = PublicKey(ByteVector.fromHex("02dca094751109d0bd055d03565874e8276dd53e926b44e3bd1bb6bf4bc130a279")) + + // miniscript: older(16) + private val anchorScript: List = listOf(OP_16, OP_CHECKSEQUENCEVERIFY) + val anchorScriptTree = ScriptTree.Leaf(anchorScript) + + /** + * Script used for local or remote anchor outputs. + * The key used matches the key for the matching node's main output. + */ + fun anchor(anchorKey: PublicKey): List = Script.pay2tr(anchorKey.xOnly(), anchorScriptTree) + + /** + * 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 + * + * @return a script that will be used to add a "revocation" leaf to a script tree + */ + private fun toRevocationKey(keys: CommitmentPublicKeys): List = listOf( + OP_PUSHDATA(keys.localDelayedPaymentPublicKey.xOnly()), OP_DROP, OP_PUSHDATA(keys.revocationPublicKey.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)) + * + * @return a script that will be used to add a "to local key" leaf to a script tree + */ + private fun toLocalDelayed(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): List = listOf( + OP_PUSHDATA(keys.localDelayedPaymentPublicKey.xOnly()), OP_CHECKSIGVERIFY, encodeNumber(toSelfDelay.toLong()), OP_CHECKSEQUENCEVERIFY + ) + + data class ToLocalScriptTree(val localDelayed: ScriptTree.Leaf, val revocation: ScriptTree.Leaf) { + val scriptTree: ScriptTree.Branch = ScriptTree.Branch(localDelayed, revocation) + } + + /** + * @return a script tree with two leaves (to self with delay, and to revocation key) + */ + fun toLocalScriptTree(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): ToLocalScriptTree { + return ToLocalScriptTree( + ScriptTree.Leaf(toLocalDelayed(keys, toSelfDelay)), + ScriptTree.Leaf(toRevocationKey(keys)), + ) + } + + /** + * Script used for the main balance of the owner of the commitment transaction. + */ + fun toLocal(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): List { + return Script.pay2tr(NUMS_POINT.xOnly(), toLocalScriptTree(keys, toSelfDelay).scriptTree) + } + + /** + * Script that can be spent by the channel counterparty after a 1-block delay. + * + * miniscript: and_v(v:pk(remote_key),older(1)) + * + * @return a script that will be used to add a "to remote key" leaf to a script tree + */ + private fun toRemoteDelayed(keys: CommitmentPublicKeys): List = listOf( + OP_PUSHDATA(keys.remotePaymentPublicKey.xOnly()), OP_CHECKSIGVERIFY, OP_1, OP_CHECKSEQUENCEVERIFY + ) + + /** + * Script tree used for the main balance of the remote node in our commitment transaction. + * Note that there is no need for a revocation leaf in that case. + * + * @return a script tree with a single leaf (to remote key, with a 1-block CSV delay) + */ + fun toRemoteScriptTree(keys: CommitmentPublicKeys): ScriptTree.Leaf { + return ScriptTree.Leaf(toRemoteDelayed(keys)) + } + + /** + * Script used for the main balance of the remote node in our commitment transaction. + */ + fun toRemote(keys: CommitmentPublicKeys): List { + return Script.pay2tr(NUMS_POINT.xOnly(), toRemoteScriptTree(keys)) + } + + /** + * 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)) + * + * @return a script used to create a "HTLC timeout" leaf in a script tree + */ + private fun offeredHtlcTimeout(keys: CommitmentPublicKeys): List = listOf( + OP_PUSHDATA(keys.localHtlcPublicKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(keys.remoteHtlcPublicKey.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))) + * + * @return a script used to create a "spend offered HTLC" leaf in a script tree + */ + private fun offeredHtlcSuccess(keys: CommitmentPublicKeys, paymentHash: ByteVector32): List = listOf( + // @formatter:off + OP_SIZE, encodeNumber(32), OP_EQUALVERIFY, + OP_HASH160, OP_PUSHDATA(Crypto.ripemd160(paymentHash)), OP_EQUALVERIFY, + OP_PUSHDATA(keys.remoteHtlcPublicKey.xOnly()), OP_CHECKSIGVERIFY, + OP_1, OP_CHECKSEQUENCEVERIFY + // @formatter:on + ) + + data class OfferedHtlcScriptTree(val timeout: ScriptTree.Leaf, val success: ScriptTree.Leaf) { + val scriptTree: ScriptTree.Branch = ScriptTree.Branch(timeout, success) + + fun witnessTimeout(commitKeys: LocalCommitmentKeys, localSig: ByteVector64, remoteSig: ByteVector64): ScriptWitness = + Script.witnessScriptPathPay2tr( + commitKeys.revocationPublicKey.xOnly(), + timeout, + ScriptWitness(listOf(encodeSig(remoteSig, htlcRemoteSighash(Transactions.CommitmentFormat.SimpleTaprootChannels)), localSig)), + scriptTree + ) + + fun witnessSuccess(commitKeys: RemoteCommitmentKeys, localSig: ByteVector64, paymentPreimage: ByteVector32): ScriptWitness = + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly(), success, ScriptWitness(listOf(localSig, paymentPreimage)), scriptTree) + } + + /** + * Script tree used for offered HTLCs. + */ + fun offeredHtlcScriptTree(keys: CommitmentPublicKeys, paymentHash: ByteVector32): OfferedHtlcScriptTree = + OfferedHtlcScriptTree( + ScriptTree.Leaf(offeredHtlcTimeout(keys)), + ScriptTree.Leaf(offeredHtlcSuccess(keys, 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))) + */ + private fun receivedHtlcTimeout(keys: CommitmentPublicKeys, expiry: CltvExpiry): List = listOf( + // @formatter:off + OP_PUSHDATA(keys.remoteHtlcPublicKey.xOnly()), OP_CHECKSIGVERIFY, + OP_1, OP_CHECKSEQUENCEVERIFY, OP_VERIFY, + encodeNumber(expiry.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))) + */ + private fun receivedHtlcSuccess(keys: CommitmentPublicKeys, paymentHash: ByteVector32): List = listOf( + // @formatter:off + OP_SIZE, encodeNumber(32), OP_EQUALVERIFY, + OP_HASH160, OP_PUSHDATA(Crypto.ripemd160(paymentHash)), OP_EQUALVERIFY, + OP_PUSHDATA(keys.localHtlcPublicKey.xOnly()), OP_CHECKSIGVERIFY, + OP_PUSHDATA(keys.remoteHtlcPublicKey.xOnly()), OP_CHECKSIG, + // @formatter:on + ) + + data class ReceivedHtlcScriptTree(val timeout: ScriptTree.Leaf, val success: ScriptTree.Leaf) { + val scriptTree = ScriptTree.Branch(timeout, success) + + fun witnessSuccess(commitKeys: LocalCommitmentKeys, localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32): ScriptWitness = + Script.witnessScriptPathPay2tr( + commitKeys.revocationPublicKey.xOnly(), + success, + ScriptWitness(listOf(encodeSig(remoteSig, htlcRemoteSighash(Transactions.CommitmentFormat.SimpleTaprootChannels)), localSig, paymentPreimage)), + scriptTree + ) + + fun witnessTimeout(commitKeys: RemoteCommitmentKeys, localSig: ByteVector64): ScriptWitness = + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly(), timeout, ScriptWitness(listOf(localSig)), scriptTree) + + } + + /** + * Script tree used for received HTLCs. + */ + fun receivedHtlcScriptTree(keys: CommitmentPublicKeys, paymentHash: ByteVector32, expiry: CltvExpiry): ReceivedHtlcScriptTree = + ReceivedHtlcScriptTree( + ScriptTree.Leaf(receivedHtlcTimeout(keys, expiry)), + ScriptTree.Leaf(receivedHtlcSuccess(keys, paymentHash)), + ) + + /** + * Script tree used for the output of pre-signed HTLC 2nd-stage transactions. + */ + fun htlcDelayedScriptTree(keys: CommitmentPublicKeys, toSelfDelay: CltvExpiryDelta): ScriptTree.Leaf = + ScriptTree.Leaf(toLocalDelayed(keys, toSelfDelay)) + + } } \ 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 507713351..26b29fbde 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,6 +18,9 @@ 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.flatMap import fr.acinq.bitcoin.utils.runTrying @@ -28,9 +31,11 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelSpendSignature import fr.acinq.lightning.crypto.CommitmentPublicKeys import fr.acinq.lightning.crypto.LocalCommitmentKeys +import fr.acinq.lightning.crypto.NonceGenerator import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.transactions.CommitmentOutput.InHtlc import fr.acinq.lightning.transactions.CommitmentOutput.OutHtlc +import fr.acinq.lightning.transactions.Scripts.Taproot.NUMS_POINT import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.UpdateAddHtlc @@ -94,6 +99,26 @@ object Transactions { override val claimHtlcPenaltyWeight: Int = 483 override val anchorAmount: Satoshi = 330.sat } + + object SimpleTaprootChannels : CommitmentFormat() { + // weights for taproot transactions are deterministic since signatures are encoded as 64 bytes and + // not in variable length DER format (around 72 bytes) + override val fundingInputWeight = 230 + override val commitWeight = 968 + override val htlcOutputWeight = 172 + override val htlcTimeoutWeight = 645 + override val htlcSuccessWeight = 705 + override val claimHtlcSuccessWeight = 559 + override val claimHtlcTimeoutWeight = 504 + override val toLocalDelayedWeight = 501 + override val toRemoteWeight = 467 + override val htlcDelayedWeight = 469 + override val mainPenaltyWeight = 531 + override val htlcOfferedPenaltyWeight = 396 + override val htlcReceivedPenaltyWeight = 396 + override val claimHtlcPenaltyWeight = 396 + override val anchorAmount: Satoshi = 330.sat + } } data class InputInfo(val outPoint: OutPoint, val txOut: TxOut) @@ -108,6 +133,28 @@ object Transactions { override val pubkeyScript: ByteVector = Script.write(Script.pay2wsh(redeemScript)).byteVector() } + + /** + * @param internalKey the private key associated with this public key will be used to sign. + * @param scriptTree_opt the script tree must be known if there is one, even when spending via the key path. + */ + data class TaprootKeyPath(val internalKey: XonlyPublicKey, val scriptTree_opt: ScriptTree?) : RedeemInfo() { + override val pubkeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, scriptTree_opt)).byteVector() + } + + /** + * @param internalKey we need the internal key, even if we don't have the private key, to spend via a script path. + * @param scriptTree we need the complete script tree to spend taproot inputs. + * @param leafHash hash of the leaf script we're spending (must belong to the tree). + */ + data class TaprootScriptPath(val internalKey: XonlyPublicKey, val scriptTree: ScriptTree, val leafHash: ByteVector32) : RedeemInfo() { + val leaf: ScriptTree.Leaf = scriptTree.findScript(leafHash) ?: throw IllegalArgumentException("script tree must contain the provided leaf") + override val pubkeyScript: ByteVector = Script.write(Script.pay2tr(internalKey, scriptTree)).byteVector() + } + } + + data class LocalNonce(val secretNonce: SecretNonce, val publicNonce: IndividualNonce) { + override fun toString(): String = publicNonce.toString() } sealed class TransactionWithInputInfo { @@ -117,16 +164,29 @@ object Transactions { val fee: Satoshi get() = input.txOut.amount - tx.txOut.map { it.amount }.sum() val inputIndex: Int get() = tx.txIn.indexOfFirst { it.outPoint == input.outPoint } - fun sign(key: PrivateKey, sigHash: Int, redeemInfo: RedeemInfo, extraUtxos: Map): ByteVector64 { - // Note that we only need to provide details about all transaction inputs when using taproot, but we want to - // test that we're always correctly providing all inputs in all code paths to benefit from our existing test coverage. + protected fun buildSpentOutputs(extraUtxos: Map): List { + // Callers don't except this function to throw. + // But we want to ensure that we're correctly providing input details, otherwise our signature will silently be + // invalid when using taproot. We verify this in all cases, even when using segwit v0, to ensure that we have as + // many tests as possible that exercise this codepath. val inputsMap = extraUtxos + (input.outPoint to input.txOut) - tx.txIn.forEach { require(inputsMap.contains(it.outPoint)) { "cannot sign txId=${tx.txid}: missing input details for ${it.outPoint}" } } + tx.txIn.forEach { require(inputsMap.contains(it.outPoint)) { "cannot sign $this with txId=${tx.txid}: missing input details for ${it.outPoint}" } } + return tx.txIn.map { inputsMap[it.outPoint]!! } + } + + fun sign(key: PrivateKey, sigHash: Int, redeemInfo: RedeemInfo, extraUtxos: Map): ByteVector64 { + val spentOutputs = buildSpentOutputs(extraUtxos) return when (redeemInfo) { is RedeemInfo.P2wsh -> { val sigDER = tx.signInput(inputIndex, redeemInfo.redeemScript, sigHash, amountIn, SigVersion.SIGVERSION_WITNESS_V0, key) Crypto.der2compact(sigDER) } + is RedeemInfo.TaprootKeyPath -> { + tx.signInputTaprootKeyPath(key, inputIndex, spentOutputs, sigHash, redeemInfo.scriptTree_opt) + } + is RedeemInfo.TaprootScriptPath -> { + tx.signInputTaprootScriptPath(key, inputIndex, spentOutputs, sigHash, redeemInfo.leafHash) + } } } @@ -138,6 +198,14 @@ object Transactions { val data = tx.hashForSigning(inputIndex, redeemScript, sigHash, amountIn, SigVersion.SIGVERSION_WITNESS_V0) Crypto.verifySignature(data, sig, publicKey) } + is RedeemInfo.TaprootKeyPath -> { + val data = tx.hashForSigningTaprootKeyPath(inputIndex, listOf(input.txOut), sigHash) + Crypto.verifySignatureSchnorr(data, sig, publicKey.xOnly()) + } + is RedeemInfo.TaprootScriptPath -> { + val data = tx.hashForSigningTaprootScriptPath(inputIndex, listOf(input.txOut), sigHash, redeemInfo.leafHash) + Crypto.verifySignatureSchnorr(data, sig, publicKey.xOnly()) + } } } else { false @@ -165,17 +233,72 @@ object Transactions { return ChannelSpendSignature.IndividualSignature(sig) } + /** Create a partial transaction for the channel's musig2 funding output when using [CommitmentFormat.SimpleTaprootChannels]. */ + fun partialSign( + localFundingKey: PrivateKey, + remoteFundingPubkey: PublicKey, + extraUtxos: Map, + localNonce: LocalNonce, + publicNonces: List + ): Either { + val spentOutputs = buildSpentOutputs(extraUtxos) + return Musig2.signTaprootInput(localFundingKey, tx, inputIndex, spentOutputs, Scripts.sort(listOf(localFundingKey.publicKey(), remoteFundingPubkey)), localNonce.secretNonce, publicNonces, null) + .map { ChannelSpendSignature.PartialSignatureWithNonce(it, localNonce.publicNonce) } + } + /** Aggregate local and remote channel spending signatures when using [CommitmentFormat.AnchorOutputs]. */ fun aggregateSigs(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: ChannelSpendSignature.IndividualSignature, remoteSig: ChannelSpendSignature.IndividualSignature): Transaction { val witness = Scripts.witness2of2(localSig.sig, remoteSig.sig, localFundingPubkey, remoteFundingPubkey) return tx.updateWitness(inputIndex, witness) } + /** Aggregate local and remote channel spending partial signatures when using [CommitmentFormat.SimpleTaprootChannels]. */ + fun aggregateSigs( + localFundingPubkey: PublicKey, + remoteFundingPubkey: PublicKey, + localSig: ChannelSpendSignature.PartialSignatureWithNonce, + remoteSig: ChannelSpendSignature.PartialSignatureWithNonce, + extraUtxos: Map + ): Either { + val spentOutputs = buildSpentOutputs(extraUtxos) + return Musig2.aggregateTaprootSignatures( + listOf(localSig.partialSig, remoteSig.partialSig), + tx, + inputIndex, + spentOutputs, + Scripts.sort(listOf(localFundingPubkey, remoteFundingPubkey)), + listOf(localSig.nonce, remoteSig.nonce), + null + ).map { + val witness = Script.witnessKeyPathPay2tr(it) + tx.updateWitness(inputIndex, witness) + } + } + /** Verify a signature received from the remote channel participant. */ fun checkRemoteSig(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, remoteSig: ChannelSpendSignature.IndividualSignature): Boolean { val redeemScript = Script.write(Scripts.multiSig2of2(localFundingPubkey, remoteFundingPubkey)).byteVector() return checkSig(remoteSig.sig, remoteFundingPubkey, SigHash.SIGHASH_ALL, RedeemInfo.P2wsh(redeemScript)) } + + fun checkRemotePartialSignature( + localFundingPubKey: PublicKey, + remoteFundingPubKey: PublicKey, + remoteSig: ChannelSpendSignature.PartialSignatureWithNonce, + localNonce: IndividualNonce + ): Boolean { + return Musig2.verify( + remoteSig.partialSig, + remoteSig.nonce, + remoteFundingPubKey, + tx, + inputIndex, + listOf(input.txOut), + Scripts.sort(listOf(localFundingPubKey, remoteFundingPubKey)), + listOf(localNonce, remoteSig.nonce), + scriptTree = null + ) + } } /** This transaction collaboratively spends the channel funding output to change its capacity. */ @@ -201,6 +324,7 @@ object Transactions { /** Sighash flags to use when signing the transaction. */ val sigHash: Int get() = when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> SigHash.SIGHASH_ALL + CommitmentFormat.SimpleTaprootChannels -> SigHash.SIGHASH_DEFAULT } abstract fun sign(): ForceCloseTransaction @@ -271,6 +395,10 @@ object Transactions { TxOwner.Local -> SigHash.SIGHASH_ALL TxOwner.Remote -> SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY } + CommitmentFormat.SimpleTaprootChannels -> when (txOwner) { + TxOwner.Local -> SigHash.SIGHASH_DEFAULT + TxOwner.Remote -> SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY + } } /** Sign an HTLC transaction for the remote commitment. */ @@ -301,6 +429,10 @@ object Transactions { val localSig = sign(commitKeys.ourHtlcKey, sigHash(TxOwner.Local), redeemInfo, extraUtxos = mapOf()) val witness = when (redeemInfo) { is RedeemInfo.P2wsh -> Scripts.witnessHtlcSuccess(localSig, remoteSig, preimage, redeemInfo.redeemScript) + is RedeemInfo.TaprootScriptPath, is RedeemInfo.TaprootKeyPath -> { + val receivedHtlcTree = Scripts.Taproot.receivedHtlcScriptTree(commitKeys.publicKeys, paymentHash, htlcExpiry) + receivedHtlcTree.witnessSuccess(commitKeys, localSig, remoteSig, preimage) + } } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -309,6 +441,10 @@ object Transactions { fun redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, htlcExpiry: CltvExpiry, commitmentFormat: CommitmentFormat): RedeemInfo { return when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.htlcReceived(commitKeys, paymentHash, htlcExpiry)) + CommitmentFormat.SimpleTaprootChannels -> { + val receivedHtlcTree = Scripts.Taproot.receivedHtlcScriptTree(commitKeys, paymentHash, htlcExpiry) + RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly(), receivedHtlcTree.scriptTree, receivedHtlcTree.success.hash()) + } } } @@ -342,6 +478,10 @@ object Transactions { val localSig = sign(commitKeys.ourHtlcKey, sigHash(TxOwner.Local), redeemInfo, extraUtxos = mapOf()) val witness = when (redeemInfo) { is RedeemInfo.P2wsh -> Scripts.witnessHtlcTimeout(localSig, remoteSig, redeemInfo.redeemScript) + is RedeemInfo.TaprootKeyPath, is RedeemInfo.TaprootScriptPath -> { + val offeredHtlcTree = Scripts.Taproot.offeredHtlcScriptTree(commitKeys.publicKeys, paymentHash) + offeredHtlcTree.witnessTimeout(commitKeys, localSig, remoteSig) + } } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -350,6 +490,10 @@ object Transactions { fun redeemInfo(commitKeys: CommitmentPublicKeys, paymentHash: ByteVector32, commitmentFormat: CommitmentFormat): RedeemInfo { return when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.htlcOffered(commitKeys, paymentHash)) + CommitmentFormat.SimpleTaprootChannels -> { + val offeredHtlcTree = Scripts.Taproot.offeredHtlcScriptTree(commitKeys, paymentHash) + RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly(), offeredHtlcTree.scriptTree, offeredHtlcTree.timeout.hash()) + } } } @@ -384,6 +528,12 @@ object Transactions { val sig = sign(commitKeys.ourDelayedPaymentKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) Scripts.witnessToLocalDelayedAfterDelay(sig, redeemScript) } + CommitmentFormat.SimpleTaprootChannels -> { + val scriptTree: ScriptTree.Leaf = Scripts.Taproot.htlcDelayedScriptTree(commitKeys.publicKeys, toLocalDelay) + val redeemInfo = RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly(), scriptTree, scriptTree.hash()) + val sig = sign(commitKeys.ourDelayedPaymentKey, sigHash, redeemInfo, extraUtxos = mapOf()) + Script.witnessScriptPathPay2tr(commitKeys.revocationPublicKey.xOnly(), scriptTree, ScriptWitness(listOf(sig)), scriptTree) + } } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -392,6 +542,10 @@ object Transactions { fun redeemInfo(commitKeys: CommitmentPublicKeys, toLocalDelay: CltvExpiryDelta, commitmentFormat: CommitmentFormat): RedeemInfo { return when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys, toLocalDelay)) + CommitmentFormat.SimpleTaprootChannels -> { + val scriptTree: ScriptTree.Leaf = Scripts.Taproot.htlcDelayedScriptTree(commitKeys, toLocalDelay) + RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly(), scriptTree, scriptTree.hash()) + } } } @@ -448,6 +602,12 @@ object Transactions { val sig = sign(commitKeys.ourHtlcKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) Scripts.witnessClaimHtlcSuccessFromCommitTx(sig, preimage, redeemScript) } + CommitmentFormat.SimpleTaprootChannels -> { + val offeredTree = Scripts.Taproot.offeredHtlcScriptTree(commitKeys.publicKeys, paymentHash) + val redeemInfo = RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly(), offeredTree.scriptTree, offeredTree.success.hash()) + val sig = sign(commitKeys.ourHtlcKey, sigHash, redeemInfo, extraUtxos = mapOf()) + offeredTree.witnessSuccess(commitKeys, sig, preimage) + } } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -513,6 +673,12 @@ object Transactions { val sig = sign(commitKeys.ourHtlcKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) Scripts.witnessClaimHtlcTimeoutFromCommitTx(sig, redeemScript) } + CommitmentFormat.SimpleTaprootChannels -> { + val offeredTree = Scripts.Taproot.receivedHtlcScriptTree(commitKeys.publicKeys, paymentHash, htlcExpiry) + val redeemInfo = RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly(), offeredTree.scriptTree, offeredTree.timeout.hash()) + val sig = sign(commitKeys.ourHtlcKey, sigHash, redeemInfo, extraUtxos = mapOf()) + offeredTree.witnessTimeout(commitKeys, sig) + } } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -574,6 +740,12 @@ object Transactions { val sig = sign(commitKeys.ourDelayedPaymentKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) Scripts.witnessToLocalDelayedAfterDelay(sig, redeemScript) } + CommitmentFormat.SimpleTaprootChannels -> { + val toLocalTree = Scripts.Taproot.toLocalScriptTree(commitKeys.publicKeys, toLocalDelay) + val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly(), toLocalTree.scriptTree, toLocalTree.localDelayed.hash()) + val sig = sign(commitKeys.ourDelayedPaymentKey, sigHash, redeemInfo, extraUtxos = mapOf()) + Script.witnessScriptPathPay2tr(redeemInfo.internalKey, redeemInfo.leaf, ScriptWitness(listOf(sig)), toLocalTree.scriptTree) + } } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -590,6 +762,10 @@ object Transactions { ): Either { val redeemInfo = when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys.publicKeys, toLocalDelay)) + CommitmentFormat.SimpleTaprootChannels -> { + val toLocalTree = Scripts.Taproot.toLocalScriptTree(commitKeys.publicKeys, toLocalDelay) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly(), toLocalTree.scriptTree, toLocalTree.localDelayed.hash()) + } } return findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript).flatMap { outputIndex -> val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex]) @@ -623,6 +799,12 @@ object Transactions { val sig = sign(commitKeys.ourPaymentKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) Scripts.witnessToRemoteDelayedAfterDelay(sig, redeemScript) } + CommitmentFormat.SimpleTaprootChannels -> { + val scriptTree: ScriptTree.Leaf = Scripts.Taproot.toRemoteScriptTree(commitKeys.publicKeys) + val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly(), scriptTree, scriptTree.hash()) + val sig = sign(commitKeys.ourPaymentKey, sigHash, redeemInfo, extraUtxos = mapOf()) + Script.witnessScriptPathPay2tr(redeemInfo.internalKey, scriptTree, ScriptWitness(listOf(sig)), scriptTree) + } } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -638,6 +820,10 @@ object Transactions { ): Either { val redeemInfo = when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toRemoteDelayed(commitKeys.publicKeys)) + CommitmentFormat.SimpleTaprootChannels -> { + val scriptTree: ScriptTree.Leaf = Scripts.Taproot.toRemoteScriptTree(commitKeys.publicKeys) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly(), scriptTree, scriptTree.hash()) + } } return findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript).flatMap { outputIndex -> val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex]) @@ -673,6 +859,12 @@ object Transactions { val sig = sign(revocationKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript) } + CommitmentFormat.SimpleTaprootChannels -> { + val toLocalTree = Scripts.Taproot.toLocalScriptTree(commitKeys.publicKeys, toRemoteDelay) + val redeemInfo = RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly(), toLocalTree.scriptTree, toLocalTree.revocation.hash()) + val sig = sign(revocationKey, sigHash, redeemInfo, extraUtxos = mapOf()) + Script.witnessScriptPathPay2tr(redeemInfo.internalKey, redeemInfo.leaf, ScriptWitness(listOf(sig)), toLocalTree.scriptTree) + } } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -690,6 +882,10 @@ object Transactions { ): Either { val redeemInfo = when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) + CommitmentFormat.SimpleTaprootChannels -> { + val toLocalTree = Scripts.Taproot.toLocalScriptTree(commitKeys.publicKeys, toRemoteDelay) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly(), toLocalTree.scriptTree, toLocalTree.revocation.hash()) + } } return findPubKeyScriptIndex(commitTx, redeemInfo.pubkeyScript).flatMap { outputIndex -> val input = InputInfo(OutPoint(commitTx, outputIndex.toLong()), commitTx.txOut[outputIndex]) @@ -727,6 +923,8 @@ object Transactions { val sig = sign(revocationKey, sigHash, redeemInfo, extraUtxos = mapOf()) val witness = when (redeemInfo) { is RedeemInfo.P2wsh -> Scripts.witnessHtlcWithRevocationSig(commitKeys, sig, redeemInfo.redeemScript) + is RedeemInfo.TaprootKeyPath -> Script.witnessKeyPathPay2tr(sig, sigHash) + is RedeemInfo.TaprootScriptPath -> Script.witnessScriptPathPay2tr(redeemInfo.internalKey, redeemInfo.leaf, ScriptWitness(listOf(sig)), redeemInfo.scriptTree) } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -751,6 +949,11 @@ object Transactions { val received = RedeemInfo.P2wsh(Scripts.htlcReceived(commitKeys.publicKeys, paymentHash, htlcExpiry)) Pair(offered, received) } + CommitmentFormat.SimpleTaprootChannels -> { + val offered = RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly(), Scripts.Taproot.offeredHtlcScriptTree(commitKeys.publicKeys, paymentHash).scriptTree) + val received = RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly(), Scripts.Taproot.receivedHtlcScriptTree(commitKeys.publicKeys, paymentHash, htlcExpiry).scriptTree) + Pair(offered, received) + } } listOf( offered.pubkeyScript to HtlcPenaltyRedeemDetails(offered, paymentHash, htlcExpiry, commitmentFormat.htlcOfferedPenaltyWeight), @@ -806,6 +1009,11 @@ object Transactions { val sig = sign(revocationKey, sigHash, RedeemInfo.P2wsh(redeemScript), extraUtxos = mapOf()) Scripts.witnessToLocalDelayedWithRevocationSig(sig, redeemScript) } + CommitmentFormat.SimpleTaprootChannels -> { + val redeemInfo = RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly(), Scripts.Taproot.htlcDelayedScriptTree(commitKeys.publicKeys, toRemoteDelay)) + val sig = sign(revocationKey, sigHash, redeemInfo, extraUtxos = mapOf()) + Script.witnessKeyPathPay2tr(sig) + } } return this.copy(tx = tx.updateWitness(inputIndex, witness)) } @@ -823,6 +1031,7 @@ object Transactions { ): List> { val redeemInfo = when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys.publicKeys, toRemoteDelay)) + CommitmentFormat.SimpleTaprootChannels -> RedeemInfo.TaprootKeyPath(commitKeys.revocationPublicKey.xOnly(), Scripts.Taproot.htlcDelayedScriptTree(commitKeys.publicKeys, toRemoteDelay)) } // Note that we check *all* outputs of the tx, because it could spend a batch of HTLC outputs from the commit tx. return htlcTx.txOut.withIndex().mapNotNull { (outputIndex, txOut) -> @@ -968,6 +1177,7 @@ object Transactions { fun makeFundingScript(localFundingKey: PublicKey, remoteFundingKey: PublicKey, commitmentFormat: CommitmentFormat): RedeemInfo { return when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.multiSig2of2(localFundingKey, remoteFundingKey)) + CommitmentFormat.SimpleTaprootChannels -> RedeemInfo.TaprootKeyPath(Scripts.Taproot.musig2Aggregate(localFundingKey, remoteFundingKey), null) } } @@ -998,12 +1208,20 @@ object Transactions { if (toLocalAmount >= dustLimit) { val redeemInfo = when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toLocalDelayed(commitKeys, toSelfDelay)) + CommitmentFormat.SimpleTaprootChannels -> { + val toLocalTree = Scripts.Taproot.toLocalScriptTree(commitKeys, toSelfDelay) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly(), toLocalTree.scriptTree, toLocalTree.localDelayed.hash()) + } } outputs.add(CommitmentOutput.ToLocal(TxOut(toLocalAmount, redeemInfo.pubkeyScript))) } if (toRemoteAmount >= dustLimit) { val redeemInfo = when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toRemoteDelayed(commitKeys)) + CommitmentFormat.SimpleTaprootChannels -> { + val scripTree = Scripts.Taproot.toRemoteScriptTree(commitKeys) + RedeemInfo.TaprootScriptPath(NUMS_POINT.xOnly(), scripTree, scripTree.hash()) + } } outputs.add(CommitmentOutput.ToRemote(TxOut(toRemoteAmount, redeemInfo.pubkeyScript))) } @@ -1012,12 +1230,14 @@ object Transactions { if (untrimmedHtlcs || toLocalAmount >= dustLimit) { val redeemInfo = when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toAnchor(localFundingPubkey)) + CommitmentFormat.SimpleTaprootChannels -> RedeemInfo.TaprootKeyPath(commitKeys.localDelayedPaymentPublicKey.xOnly(), Scripts.Taproot.anchorScriptTree) } outputs.add(CommitmentOutput.ToLocalAnchor(TxOut(commitmentFormat.anchorAmount, redeemInfo.pubkeyScript))) } if (untrimmedHtlcs || toRemoteAmount >= dustLimit) { val redeemInfo = when (commitmentFormat) { CommitmentFormat.AnchorOutputs -> RedeemInfo.P2wsh(Scripts.toAnchor(remoteFundingPubkey)) + CommitmentFormat.SimpleTaprootChannels -> RedeemInfo.TaprootKeyPath(commitKeys.remotePaymentPublicKey.xOnly(), Scripts.Taproot.anchorScriptTree) } outputs.add(CommitmentOutput.ToRemoteAnchor(TxOut(commitmentFormat.anchorAmount, redeemInfo.pubkeyScript))) } @@ -1078,6 +1298,20 @@ object Transactions { data class PaidByThem(val fee: Satoshi) : ClosingTxFee() } + /** + * When sending [fr.acinq.lightning.wire.ClosingComplete], we use a different nonce for each closing transaction we create. + * We generate nonces for all variants of the closing transaction for simplicity, even though we never use them all. + */ + data class CloserNonces(val localAndRemote: LocalNonce, val localOnly: LocalNonce, val remoteOnly: LocalNonce) { + companion object { + fun generate(localFundingKey: PublicKey, remoteFundingKey: PublicKey, fundingTxId: TxId): CloserNonces = CloserNonces( + NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId), + NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId), + NonceGenerator.signingNonce(localFundingKey, remoteFundingKey, fundingTxId), + ) + } + } + /** Each closing attempt can result in multiple potential closing transactions, depending on which outputs are included. */ data class ClosingTxs(val localAndRemote: ClosingTx?, val localOnly: ClosingTx?, val remoteOnly: ClosingTx?) { val preferred: ClosingTx? = localAndRemote ?: localOnly ?: remoteOnly 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 5346a0cb7..64e31ec49 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,15 +1,16 @@ 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 import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.ShortChannelId -import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.ChannelSpendSignature import fr.acinq.lightning.channel.ChannelType +import fr.acinq.lightning.serialization.InputExtensions.readIndividualNonce import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector64 sealed class ChannelTlv : Tlv { @@ -56,6 +57,34 @@ sealed class ChannelTlv : Tlv { } } + /** + * TLV used to upgrade to [ChannelType.SupportedChannelType.SimpleTaprootChannels] during splices. + * We don't reuse the [ChannelTypeTlv] above because updating the channel type during a splice is a custom + * protocol extension that may not be accepted into the BOLTs. If it is eventually added to the BOLTs, we + * should remove this TLV in favor of [ChannelTypeTlv]. + */ + data class SpliceChannelTypeTlv(val channelType: ChannelType) : ChannelTlv() { + override val tag: Long get() = SpliceChannelTypeTlv.tag + + override fun write(out: Output) { + val features = when (channelType) { + is ChannelType.SupportedChannelType -> channelType.toFeatures() + is ChannelType.UnsupportedChannelType -> channelType.featureBits + } + LightningCodecs.writeBytes(features.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 0x47000011 + + override fun read(input: Input): SpliceChannelTypeTlv { + val len = input.availableBytes + val features = LightningCodecs.bytes(input, len) + return SpliceChannelTypeTlv(ChannelType.fromFeatures(Features(features))) + } + } + } + object RequireConfirmedInputsTlv : ChannelTlv(), TlvValueReader { override val tag: Long get() = 2 @@ -111,36 +140,38 @@ sealed class ChannelReadyTlv : Tlv { override fun read(input: Input): ShortChannelIdTlv = ShortChannelIdTlv(ShortChannelId(LightningCodecs.u64(input))) } } + + /** Verification nonce used for the next commitment transaction that will be signed (when using taproot channels). */ + data class NextLocalNonce(val nonce: IndividualNonce) : ChannelReadyTlv() { + override val tag: Long get() = NextLocalNonce.tag + override fun write(out: Output) = LightningCodecs.writeBytes(nonce.toByteArray(), out) + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): NextLocalNonce = NextLocalNonce(IndividualNonce(LightningCodecs.bytes(input, 66))) + } + } } sealed class CommitSigTlv : Tlv { - data class AlternativeFeerateSig(val feerate: FeeratePerKw, val sig: ByteVector64) + /** Partial signature along with the signer's nonce, which is usually randomly created at signing time (when using taproot channels). */ + data class PartialSignatureWithNonce(val psig: ChannelSpendSignature.PartialSignatureWithNonce) : CommitSigTlv() { + override val tag: Long get() = PartialSignatureWithNonce.tag - /** - * 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 AlternativeFeerateSigs(val sigs: List) : CommitSigTlv() { - override val tag: Long get() = AlternativeFeerateSigs.tag override fun write(out: Output) { - LightningCodecs.writeByte(sigs.size, out) - sigs.forEach { - LightningCodecs.writeU32(it.feerate.toLong().toInt(), out) - LightningCodecs.writeBytes(it.sig, out) - } + LightningCodecs.writeBytes(psig.partialSig, out) + LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) } - companion object : TlvValueReader { - const val tag: Long = 0x47010001 - override fun read(input: Input): AlternativeFeerateSigs { - val count = LightningCodecs.byte(input) - val sigs = (0 until count).map { - AlternativeFeerateSig( - FeeratePerKw(LightningCodecs.u32(input).toLong().sat), - LightningCodecs.bytes(input, 64).toByteVector64() + companion object : TlvValueReader { + const val tag: Long = 2 + override fun read(input: Input): PartialSignatureWithNonce { + return PartialSignatureWithNonce( + ChannelSpendSignature.PartialSignatureWithNonce( + LightningCodecs.bytes(input, 32).byteVector32(), + IndividualNonce(LightningCodecs.bytes(input, 66)) ) - } - return AlternativeFeerateSigs(sigs) + ) } } } @@ -156,7 +187,29 @@ sealed class CommitSigTlv : Tlv { } } -sealed class RevokeAndAckTlv : Tlv +sealed class RevokeAndAckTlv : Tlv { + /** + * Verification nonces used for the next commitment transaction, when using taproot channels. + * There must be a nonce for each active commitment (when there are pending splices or RBF attempts), indexed by the + * corresponding fundingTxId. + */ + data class NextLocalNonces(val nonces: List>) : RevokeAndAckTlv() { + override val tag: Long get() = NextLocalNonces.tag + override fun write(out: Output) = nonces.forEach { + LightningCodecs.writeTxHash(TxHash(it.first), out) + out.write(it.second.toByteArray()) + } + + companion object : TlvValueReader { + const val tag: Long = 22 + override fun read(input: Input): NextLocalNonces { + val count = input.availableBytes / (32 + 66) + val nonces = (0 until count).map { TxId(LightningCodecs.txHash(input)) to input.readIndividualNonce() } + return NextLocalNonces(nonces) + } + } + } +} sealed class ChannelReestablishTlv : Tlv { data class NextFunding(val txId: TxId) : ChannelReestablishTlv() { @@ -168,9 +221,63 @@ sealed class ChannelReestablishTlv : Tlv { override fun read(input: Input): NextFunding = NextFunding(TxId(LightningCodecs.txHash(input))) } } + + /** + * Verification nonces used for the next commitment transaction, when using taproot channels. + * There must be a nonce for each active commitment (when there are pending splices or RBF attempts), indexed by the + * corresponding fundingTxId. + */ + data class NextLocalNonces(val nonces: List>) : ChannelReestablishTlv() { + override val tag: Long get() = NextLocalNonces.tag + override fun write(out: Output) = nonces.forEach { + LightningCodecs.writeTxHash(TxHash(it.first), out) + out.write(it.second.toByteArray()) + } + + companion object : TlvValueReader { + const val tag: Long = 22 + override fun read(input: Input): NextLocalNonces { + val count = input.availableBytes / (32 + 66) + val nonces = (0 until count).map { TxId(LightningCodecs.txHash(input)) to input.readIndividualNonce() } + return NextLocalNonces(nonces) + } + } + } + + /** + * When disconnected during an interactive tx session, we'll include a verification nonce for our *current* commitment + * which our peer will need to re-send a commit sig for our current commitment transaction spending the interactive tx. + */ + data class CurrentCommitNonce(val nonce: IndividualNonce) : ChannelReestablishTlv() { + override val tag: Long get() = CurrentCommitNonce.tag + override fun write(out: Output) { + LightningCodecs.writeBytes(nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 24 + override fun read(input: Input): CurrentCommitNonce { + return CurrentCommitNonce(input.readIndividualNonce()) + } + } + } } -sealed class ShutdownTlv : Tlv +sealed class ShutdownTlv : Tlv { + /** When closing taproot channels, local nonce that will be used to sign the remote closing transaction. */ + 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 { data class FeeRange(val min: Satoshi, val max: Satoshi) : ClosingSignedTlv() { @@ -221,6 +328,63 @@ sealed class ClosingCompleteTlv : Tlv { override fun read(input: Input): CloserAndCloseeOutputs = CloserAndCloseeOutputs(LightningCodecs.bytes(input, 64).toByteVector64()) } } + + /** When closing taproot channels, partial signature for a closing transaction containing only the closer's output. */ + data class CloserOutputOnlyPartialSignature(val psig: ChannelSpendSignature.PartialSignatureWithNonce) : ClosingCompleteTlv() { + override val tag: Long get() = CloserOutputOnlyPartialSignature.tag + override fun write(out: Output) { + LightningCodecs.writeBytes(psig.partialSig, out) + LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 5 + override fun read(input: Input): CloserOutputOnlyPartialSignature = CloserOutputOnlyPartialSignature( + ChannelSpendSignature.PartialSignatureWithNonce( + LightningCodecs.bytes(input, 32).byteVector32(), + IndividualNonce(LightningCodecs.bytes(input, 66)) + ) + ) + } + } + + /** When closing taproot channels, partial signature for a closing transaction containing only the closee's output. */ + data class CloseeOutputOnlyPartialSignature(val psig: ChannelSpendSignature.PartialSignatureWithNonce) : ClosingCompleteTlv() { + override val tag: Long get() = CloseeOutputOnlyPartialSignature.tag + override fun write(out: Output) { + LightningCodecs.writeBytes(psig.partialSig, out) + LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 6 + override fun read(input: Input): CloseeOutputOnlyPartialSignature = CloseeOutputOnlyPartialSignature( + ChannelSpendSignature.PartialSignatureWithNonce( + LightningCodecs.bytes(input, 32).byteVector32(), + IndividualNonce(LightningCodecs.bytes(input, 66)) + ) + ) + } + } + + /** When closing taproot channels, partial signature for a closing transaction containing the closer and closee's outputs. */ + data class CloserAndCloseeOutputsPartialSignature(val psig: ChannelSpendSignature.PartialSignatureWithNonce) : ClosingCompleteTlv() { + override val tag: Long get() = CloserAndCloseeOutputsPartialSignature.tag + override fun write(out: Output) { + LightningCodecs.writeBytes(psig.partialSig, out) + LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 7 + override fun read(input: Input): CloserAndCloseeOutputsPartialSignature = CloserAndCloseeOutputsPartialSignature( + ChannelSpendSignature.PartialSignatureWithNonce( + LightningCodecs.bytes(input, 32).byteVector32(), + IndividualNonce(LightningCodecs.bytes(input, 66)) + ) + ) + } + } } sealed class ClosingSigTlv : Tlv { @@ -256,4 +420,55 @@ sealed class ClosingSigTlv : Tlv { override fun read(input: Input): CloserAndCloseeOutputs = CloserAndCloseeOutputs(LightningCodecs.bytes(input, 64).toByteVector64()) } } + + /** When closing taproot channels, partial signature for a closing transaction containing only the closer's output. */ + data class CloserOutputOnlyPartialSignature(val psig: ByteVector32) : ClosingSigTlv() { + override val tag: Long get() = CloserOutputOnlyPartialSignature.tag + override fun write(out: Output) = LightningCodecs.writeBytes(psig, out) + + companion object : TlvValueReader { + const val tag: Long = 5 + override fun read(input: Input): CloserOutputOnlyPartialSignature = CloserOutputOnlyPartialSignature( + LightningCodecs.bytes(input, 32).byteVector32() + ) + } + } + + /** When closing taproot channels, partial signature for a closing transaction containing only the closee's output. */ + data class CloseeOutputOnlyPartialSignature(val psig: ByteVector32) : ClosingSigTlv() { + override val tag: Long get() = CloseeOutputOnlyPartialSignature.tag + override fun write(out: Output) = LightningCodecs.writeBytes(psig, out) + + companion object : TlvValueReader { + const val tag: Long = 6 + override fun read(input: Input): CloseeOutputOnlyPartialSignature = CloseeOutputOnlyPartialSignature( + LightningCodecs.bytes(input, 32).byteVector32() + ) + } + } + + /** When closing taproot channels, partial signature for a closing transaction containing the closer and closee's outputs. */ + data class CloserAndCloseeOutputsPartialSignature(val psig: ByteVector32) : ClosingSigTlv() { + override val tag: Long get() = CloserAndCloseeOutputsPartialSignature.tag + override fun write(out: Output) = LightningCodecs.writeBytes(psig, out) + + companion object : TlvValueReader { + const val tag: Long = 7 + override fun read(input: Input): CloserAndCloseeOutputsPartialSignature = CloserAndCloseeOutputsPartialSignature( + LightningCodecs.bytes(input, 32).byteVector32() + ) + } + } + + /** When closing taproot channels, local nonce that will be used to sign the next remote closing transaction. */ + data class NextCloseeNonce(val nonce: IndividualNonce) : ClosingSigTlv() { + override val tag: Long get() = NextCloseeNonce.tag + override fun write(out: Output) = LightningCodecs.writeBytes(nonce.toByteArray(), out) + + companion object : TlvValueReader { + const val tag: Long = 22 + override fun read(input: Input): NextCloseeNonce = NextCloseeNonce(IndividualNonce(LightningCodecs.bytes(input, 66))) + } + } + } 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 c94b2700b..dd82ce9bd 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,8 +4,8 @@ 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.ChannelSpendSignature import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.utils.toByteVector64 sealed class TxAddInputTlv : Tlv { @@ -71,20 +71,59 @@ sealed class TxRemoveInputTlv : Tlv sealed class TxRemoveOutputTlv : Tlv sealed class TxCompleteTlv : Tlv { + /** + * Musig2 nonces for the commitment transaction(s), exchanged during an interactive tx session, when using a taproot + * channel or upgrading a channel to use taproot. + * + * @param commitNonce the sender's verification nonce for the current commit tx spending the interactive tx. + * @param nextCommitNonce the sender's verification nonce for the next commit tx spending the interactive tx. + */ + data class CommitNonces(val commitNonce: IndividualNonce, val nextCommitNonce: IndividualNonce) : TxCompleteTlv() { + override val tag: Long get() = CommitNonces.tag + + override fun write(out: Output) { + LightningCodecs.writeBytes(commitNonce.toByteArray(), out) + LightningCodecs.writeBytes(nextCommitNonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 4 + override fun read(input: Input): CommitNonces { + return CommitNonces(IndividualNonce(LightningCodecs.bytes(input, 66)), IndividualNonce(LightningCodecs.bytes(input, 66))) + } + } + } + + /** When splicing a taproot channel, the sender's random signing nonce for the previous funding output. */ + data class FundingInputNonce(val nonce: IndividualNonce) : TxCompleteTlv() { + override val tag: Long get() = FundingInputNonce.tag + + override fun write(out: Output) { + LightningCodecs.writeBytes(nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 6 + override fun read(input: Input): FundingInputNonce { + return FundingInputNonce(IndividualNonce(LightningCodecs.bytes(input, 66))) + } + } + } + /** 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) } } } @@ -102,6 +141,25 @@ sealed class TxSignaturesTlv : Tlv { } } + /** When doing a splice for a taproot channel, each peer must provide their partial signature for the previous musig2 funding output. */ + data class PreviousFundingTxPartialSig(val psig: ChannelSpendSignature.PartialSignatureWithNonce) : TxSignaturesTlv() { + override val tag: Long get() = PreviousFundingTxPartialSig.tag + override fun write(out: Output) { + LightningCodecs.writeBytes(psig.partialSig.toByteArray(), out) + LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 2 + override fun read(input: Input): PreviousFundingTxPartialSig = PreviousFundingTxPartialSig( + ChannelSpendSignature.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 2d4315a64..5e83647a8 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 @@ -451,9 +451,12 @@ 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 commitNonces: TxCompleteTlv.CommitNonces? = tlvs.get() + val fundingNonce: IndividualNonce? = tlvs.get()?.nonce - constructor(channelId: ByteVector32, publicNonces: List) : this(channelId, TlvStream(TxCompleteTlv.Nonces(publicNonces))) + constructor(channelId: ByteVector32, commitNonce: IndividualNonce, nextCommitNonce: IndividualNonce, fundingNonce: IndividualNonce?, swapInNonces: List) + : this(channelId, TlvStream(setOfNotNull(TxCompleteTlv.SwapInNonces(swapInNonces), TxCompleteTlv.CommitNonces(commitNonce, nextCommitNonce), fundingNonce?.let { TxCompleteTlv.FundingInputNonce(it) }))) override fun write(out: Output) { LightningCodecs.writeBytes(channelId.toByteArray(), out) @@ -464,7 +467,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.CommitNonces.tag to TxCompleteTlv.CommitNonces.Companion as TlvValueReader, + TxCompleteTlv.FundingInputNonce.tag to TxCompleteTlv.FundingInputNonce.Companion as TlvValueReader, + ) override fun read(input: Input): TxComplete = TxComplete(LightningCodecs.bytes(input, 32).byteVector32(), TlvStreamSerializer(false, readers).read(input)) } @@ -480,7 +487,7 @@ data class TxSignatures( channelId: ByteVector32, tx: Transaction, witnesses: List, - previousFundingSig: ByteVector64?, + previousFundingSig: ChannelSpendSignature?, swapInUserSigs: List, swapInServerSigs: List, swapInUserPartialSigs: List, @@ -491,7 +498,12 @@ data class TxSignatures( witnesses, TlvStream( setOfNotNull( - previousFundingSig?.let { TxSignaturesTlv.PreviousFundingTxSig(it) }, + previousFundingSig?.let { + when (it) { + is ChannelSpendSignature.IndividualSignature -> TxSignaturesTlv.PreviousFundingTxSig(it.sig) + is ChannelSpendSignature.PartialSignatureWithNonce -> 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, @@ -503,6 +515,7 @@ data class TxSignatures( override val type: Long get() = TxSignatures.type val previousFundingTxSig: ByteVector64? = tlvs.get()?.sig + val previousFundingTxPartialSig: ChannelSpendSignature.PartialSignatureWithNonce? = tlvs.get()?.psig val swapInUserSigs: List = tlvs.get()?.sigs ?: listOf() val swapInServerSigs: List = tlvs.get()?.sigs ?: listOf() val swapInUserPartialSigs: List = tlvs.get()?.psigs ?: listOf() @@ -527,6 +540,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, @@ -887,8 +901,16 @@ data class ChannelReady( val nextPerCommitmentPoint: PublicKey, val tlvStream: TlvStream = TlvStream.empty() ) : ChannelMessage, HasChannelId { + + constructor(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, alias: ShortChannelId, nextLocalNonce: IndividualNonce) : this( + channelId, + nextPerCommitmentPoint, + TlvStream(ChannelReadyTlv.ShortChannelIdTlv(alias), ChannelReadyTlv.NextLocalNonce(nextLocalNonce)) + ) + 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) @@ -900,7 +922,10 @@ 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.NextLocalNonce.tag to ChannelReadyTlv.NextLocalNonce.Companion as TlvValueReader, + ) override fun read(input: Input) = ChannelReady( ByteVector32(LightningCodecs.bytes(input, 32)), @@ -944,8 +969,9 @@ data class SpliceInit( override val type: Long get() = SpliceInit.type val requireConfirmedInputs: Boolean = tlvStream.get()?.let { true } ?: false val requestFunding: LiquidityAds.RequestFunding? = tlvStream.get()?.request + val channelType: ChannelType? = tlvStream.get()?.channelType - constructor(channelId: ByteVector32, fundingContribution: Satoshi, feerate: FeeratePerKw, lockTime: Long, fundingPubkey: PublicKey, requestFunding: LiquidityAds.RequestFunding?) : this( + constructor(channelId: ByteVector32, fundingContribution: Satoshi, feerate: FeeratePerKw, lockTime: Long, fundingPubkey: PublicKey, requestFunding: LiquidityAds.RequestFunding?, channelType: ChannelType?) : this( channelId, fundingContribution, feerate, @@ -954,6 +980,7 @@ data class SpliceInit( TlvStream( setOfNotNull( requestFunding?.let { ChannelTlv.RequestFundingTlv(it) }, + channelType?.let { ChannelTlv.SpliceChannelTypeTlv(it) }, ) ) ) @@ -974,6 +1001,7 @@ data class SpliceInit( private val readers = mapOf( ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, ChannelTlv.RequestFundingTlv.tag to ChannelTlv.RequestFundingTlv as TlvValueReader, + ChannelTlv.SpliceChannelTypeTlv.tag to ChannelTlv.SpliceChannelTypeTlv as TlvValueReader, ) override fun read(input: Input): SpliceInit = SpliceInit( @@ -997,15 +1025,18 @@ data class SpliceAck( val requireConfirmedInputs: Boolean = tlvStream.get()?.let { true } ?: false val willFund: LiquidityAds.WillFund? = tlvStream.get()?.willFund val feeCreditUsed: MilliSatoshi = tlvStream.get()?.amount ?: 0.msat + val channelType: ChannelType? = tlvStream.get()?.channelType - constructor(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubkey: PublicKey, willFund: LiquidityAds.WillFund?) : this( + constructor(channelId: ByteVector32, fundingContribution: Satoshi, fundingPubkey: PublicKey, willFund: LiquidityAds.WillFund?, channelType: ChannelType?) : this( channelId, fundingContribution, fundingPubkey, TlvStream( setOfNotNull( - willFund?.let { ChannelTlv.ProvideFundingTlv(it) } - )) + willFund?.let { ChannelTlv.ProvideFundingTlv(it) }, + channelType?.let { ChannelTlv.SpliceChannelTypeTlv(it) } + ) + ) ) override fun write(out: Output) { @@ -1023,6 +1054,7 @@ data class SpliceAck( ChannelTlv.RequireConfirmedInputsTlv.tag to ChannelTlv.RequireConfirmedInputsTlv as TlvValueReader, ChannelTlv.ProvideFundingTlv.tag to ChannelTlv.ProvideFundingTlv as TlvValueReader, ChannelTlv.FeeCreditUsedTlv.tag to ChannelTlv.FeeCreditUsedTlv.Companion as TlvValueReader, + ChannelTlv.SpliceChannelTypeTlv.tag to ChannelTlv.SpliceChannelTypeTlv as TlvValueReader, ) override fun read(input: Input): SpliceAck = SpliceAck( @@ -1222,9 +1254,29 @@ data class CommitSig( val htlcSignatures: List, val tlvStream: TlvStream = TlvStream.empty() ) : CommitSigs() { + + constructor(channelId: ByteVector32, signature: ChannelSpendSignature, htlcSignatures: List, batchSize: Int) : this( + channelId, + when (signature) { + is ChannelSpendSignature.IndividualSignature -> signature + is ChannelSpendSignature.PartialSignatureWithNonce -> ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes) + }, + htlcSignatures, + TlvStream( + setOfNotNull( + if (batchSize > 1) CommitSigTlv.Batch(batchSize) else null, + when (signature) { + is ChannelSpendSignature.PartialSignatureWithNonce -> CommitSigTlv.PartialSignatureWithNonce(signature) + is ChannelSpendSignature.IndividualSignature -> null + } + ) + ) + ) + override val type: Long get() = CommitSig.type - val alternativeFeerateSigs: List = tlvStream.get()?.sigs ?: listOf() + val partialSignature: ChannelSpendSignature.PartialSignatureWithNonce? = tlvStream.get()?.psig + val sigOrPartialSig: ChannelSpendSignature = partialSignature ?: signature val batchSize: Int = tlvStream.get()?.size ?: 1 override fun write(out: Output) { @@ -1240,7 +1292,7 @@ data class CommitSig( @Suppress("UNCHECKED_CAST") val readers = mapOf( - CommitSigTlv.AlternativeFeerateSigs.tag to CommitSigTlv.AlternativeFeerateSigs.Companion as TlvValueReader, + CommitSigTlv.PartialSignatureWithNonce.tag to CommitSigTlv.PartialSignatureWithNonce.Companion as TlvValueReader, CommitSigTlv.Batch.tag to CommitSigTlv.Batch.Companion as TlvValueReader, ) @@ -1278,6 +1330,20 @@ data class RevokeAndAck( val nextPerCommitmentPoint: PublicKey, val tlvStream: TlvStream = TlvStream.empty() ) : HtlcMessage, HasChannelId, RequirePeerStorageStore { + + constructor(channelId: ByteVector32, perCommitmentSecret: PrivateKey, nextPerCommitmentPoint: PublicKey, nextCommitNonces: List>) : this( + channelId, + perCommitmentSecret, + nextPerCommitmentPoint, + TlvStream( + setOfNotNull( + if (nextCommitNonces.isNotEmpty()) RevokeAndAckTlv.NextLocalNonces(nextCommitNonces) else null + ) + ) + ) + + val nextCommitNonces: Map = tlvStream.get()?.nonces?.toMap() ?: mapOf() + override val type: Long get() = RevokeAndAck.type override fun write(out: Output) { @@ -1290,7 +1356,10 @@ data class RevokeAndAck( companion object : LightningMessageReader { const val type: Long = 133 - val readers: Map> = mapOf() + @Suppress("UNCHECKED_CAST") + val readers = mapOf( + RevokeAndAckTlv.NextLocalNonces.tag to RevokeAndAckTlv.NextLocalNonces.Companion as TlvValueReader + ) override fun read(input: Input): RevokeAndAck { return RevokeAndAck( @@ -1334,9 +1403,36 @@ data class ChannelReestablish( val myCurrentPerCommitmentPoint: PublicKey, val tlvStream: TlvStream = TlvStream.empty() ) : HasChannelId { + + constructor( + channelId: ByteVector32, + nextLocalCommitmentNumber: Long, + nextRemoteRevocationNumber: Long, + yourLastCommitmentSecret: PrivateKey, + myCurrentPerCommitmentPoint: PublicKey, + nextCommitNonces: List>, + nextFundingTxId: TxId? = null, + currentCommitNonce: IndividualNonce? = null + ) : this( + channelId = channelId, + nextLocalCommitmentNumber = nextLocalCommitmentNumber, + nextRemoteRevocationNumber = nextRemoteRevocationNumber, + yourLastCommitmentSecret = yourLastCommitmentSecret, + myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint, + tlvStream = TlvStream( + setOfNotNull( + if (nextCommitNonces.isNotEmpty()) ChannelReestablishTlv.NextLocalNonces(nextCommitNonces) else null, + nextFundingTxId?.let { ChannelReestablishTlv.NextFunding(it) }, + currentCommitNonce?.let { ChannelReestablishTlv.CurrentCommitNonce(it) }, + ) + ) + ) + override val type: Long get() = ChannelReestablish.type val nextFundingTxId: TxId? = tlvStream.get()?.txId + val nextCommitNonces: Map = tlvStream.get()?.nonces?.toMap() ?: mapOf() + val currentCommitNonce: IndividualNonce? = tlvStream.get()?.nonce override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) @@ -1353,6 +1449,8 @@ data class ChannelReestablish( @Suppress("UNCHECKED_CAST") val readers = mapOf( ChannelReestablishTlv.NextFunding.tag to ChannelReestablishTlv.NextFunding.Companion as TlvValueReader, + ChannelReestablishTlv.NextLocalNonces.tag to ChannelReestablishTlv.NextLocalNonces.Companion as TlvValueReader, + ChannelReestablishTlv.CurrentCommitNonce.tag to ChannelReestablishTlv.CurrentCommitNonce.Companion as TlvValueReader, ) override fun read(input: Input): ChannelReestablish { @@ -1548,7 +1646,11 @@ data class Shutdown( val scriptPubKey: ByteVector, val tlvStream: TlvStream = TlvStream.empty() ) : ChannelMessage, HasChannelId, RequirePeerStorageStore, ForbiddenMessageDuringSplice { + + constructor(channelId: ByteVector32, scriptPubKey: ByteVector, localNonce: IndividualNonce) : this(channelId, scriptPubKey, TlvStream(ShutdownTlv.ShutdownNonce(localNonce))) + override val type: Long get() = Shutdown.type + val closeeNonce: IndividualNonce? = tlvStream.get()?.nonce override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) @@ -1560,7 +1662,10 @@ data class Shutdown( companion object : LightningMessageReader { const val type: Long = 38 - val readers: Map> = mapOf() + @Suppress("UNCHECKED_CAST") + val readers: Map> = mapOf( + ShutdownTlv.ShutdownNonce.tag to ShutdownTlv.ShutdownNonce.Companion as TlvValueReader + ) override fun read(input: Input): Shutdown { return Shutdown( @@ -1619,6 +1724,9 @@ data class ClosingComplete( val closerOutputOnlySig: ByteVector64? = tlvStream.get()?.sig val closeeOutputOnlySig: ByteVector64? = tlvStream.get()?.sig val closerAndCloseeOutputsSig: ByteVector64? = tlvStream.get()?.sig + val closerOutputOnlyPartialSig: ChannelSpendSignature.PartialSignatureWithNonce? = tlvStream.get()?.psig + val closeeOutputOnlyPartialSig: ChannelSpendSignature.PartialSignatureWithNonce? = tlvStream.get()?.psig + val closerAndCloseeOutputsPartialSig: ChannelSpendSignature.PartialSignatureWithNonce? = tlvStream.get()?.psig override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) @@ -1639,6 +1747,9 @@ data class ClosingComplete( ClosingCompleteTlv.CloserOutputOnly.tag to ClosingCompleteTlv.CloserOutputOnly.Companion as TlvValueReader, ClosingCompleteTlv.CloseeOutputOnly.tag to ClosingCompleteTlv.CloseeOutputOnly.Companion as TlvValueReader, ClosingCompleteTlv.CloserAndCloseeOutputs.tag to ClosingCompleteTlv.CloserAndCloseeOutputs.Companion as TlvValueReader, + ClosingCompleteTlv.CloserOutputOnlyPartialSignature.tag to ClosingCompleteTlv.CloserOutputOnlyPartialSignature.Companion as TlvValueReader, + ClosingCompleteTlv.CloseeOutputOnlyPartialSignature.tag to ClosingCompleteTlv.CloseeOutputOnlyPartialSignature.Companion as TlvValueReader, + ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature.tag to ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature.Companion as TlvValueReader, ) override fun read(input: Input): ClosingComplete { @@ -1667,6 +1778,10 @@ data class ClosingSig( val closerOutputOnlySig: ByteVector64? = tlvStream.get()?.sig val closeeOutputOnlySig: ByteVector64? = tlvStream.get()?.sig val closerAndCloseeOutputsSig: ByteVector64? = tlvStream.get()?.sig + val closerOutputOnlyPartialSig: ByteVector32? = tlvStream.get()?.psig + val closeeOutputOnlyPartialSig: ByteVector32? = tlvStream.get()?.psig + val closerAndCloseeOutputsPartialSig: ByteVector32? = tlvStream.get()?.psig + val nextCloseeNonce: IndividualNonce? = tlvStream.get()?.nonce override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) @@ -1687,6 +1802,10 @@ data class ClosingSig( ClosingSigTlv.CloserOutputOnly.tag to ClosingSigTlv.CloserOutputOnly.Companion as TlvValueReader, ClosingSigTlv.CloseeOutputOnly.tag to ClosingSigTlv.CloseeOutputOnly.Companion as TlvValueReader, ClosingSigTlv.CloserAndCloseeOutputs.tag to ClosingSigTlv.CloserAndCloseeOutputs.Companion as TlvValueReader, + ClosingSigTlv.CloserOutputOnlyPartialSignature.tag to ClosingSigTlv.CloserOutputOnlyPartialSignature.Companion as TlvValueReader, + ClosingSigTlv.CloseeOutputOnlyPartialSignature.tag to ClosingSigTlv.CloseeOutputOnlyPartialSignature.Companion as TlvValueReader, + ClosingSigTlv.CloserAndCloseeOutputsPartialSignature.tag to ClosingSigTlv.CloserAndCloseeOutputsPartialSignature.Companion as TlvValueReader, + ClosingSigTlv.NextCloseeNonce.tag to ClosingSigTlv.NextCloseeNonce.Companion as TlvValueReader, ) override fun read(input: Input): ClosingSig { 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 e9c05228e..794cc5c62 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 @@ -34,15 +34,15 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { @Test fun `reach normal state`() { - reachNormal() + reachNormal(channelType = ChannelType.SupportedChannelType.SimpleTaprootChannels) } @Test fun `correct values for availableForSend - availableForReceive -- success case`() { - val (alice, bob) = reachNormal(aliceFundingAmount = 800_000.sat, bobFundingAmount = 200_000.sat) + val (alice, bob) = reachNormal(aliceFundingAmount = 800_000.sat, bobFundingAmount = 200_000.sat, channelType = ChannelType.SupportedChannelType.SimpleTaprootChannels) - val a = 774_660_000.msat // initial balance alice - val b = 190_000_000.msat // initial balance bob + val a = 786_220_000.msat // initial balance alice + val b = 200_000_000.msat // initial balance bob val p = 42_000_000.msat // a->b payment val htlcOutputFee = (2 * 860_000).msat // fee due to the additional htlc output; we count it twice because we keep a reserve for a x2 feerate increase @@ -65,7 +65,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc1.availableBalanceForSend(), b) assertEquals(bc1.availableBalanceForReceive(), a - p - htlcOutputFee) - val (ac2, commit1) = ac1.sendCommit(alice.channelKeys, logger).right!! + val (ac2, commit1) = ac1.sendCommit(alice.channelKeys, alice.state.remoteNextCommitNonces, logger).right!! assertEquals(ac2.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac2.availableBalanceForReceive(), b) @@ -77,7 +77,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac3.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac3.availableBalanceForReceive(), b) - val (bc3, commit2) = bc2.sendCommit(bob.channelKeys, logger).right!! + val (bc3, commit2) = bc2.sendCommit(bob.channelKeys, bob.state.remoteNextCommitNonces, logger).right!! assertEquals(bc3.availableBalanceForSend(), b) assertEquals(bc3.availableBalanceForReceive(), a - p - htlcOutputFee) @@ -98,7 +98,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac5.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac5.availableBalanceForReceive(), b + p) - val (bc6, commit3) = bc5.sendCommit(bob.channelKeys, logger).right!! + val (bc6, commit3) = bc5.sendCommit(bob.channelKeys, revocation2.nextCommitNonces, logger).right!! assertEquals(bc6.availableBalanceForSend(), b + p) assertEquals(bc6.availableBalanceForReceive(), a - p - htlcOutputFee) @@ -110,7 +110,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc7.availableBalanceForSend(), b + p) assertEquals(bc7.availableBalanceForReceive(), a - p) - val (ac7, commit4) = ac6.sendCommit(alice.channelKeys, logger).right!! + val (ac7, commit4) = ac6.sendCommit(alice.channelKeys, revocation1.nextCommitNonces, logger).right!! assertEquals(ac7.availableBalanceForSend(), a - p) assertEquals(ac7.availableBalanceForReceive(), b + p) @@ -125,10 +125,10 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { @Test fun `correct values for availableForSend - availableForReceive -- failure case`() { - val (alice, bob) = reachNormal(aliceFundingAmount = 800_000.sat, bobFundingAmount = 200_000.sat) + val (alice, bob) = reachNormal(aliceFundingAmount = 800_000.sat, bobFundingAmount = 200_000.sat, channelType = ChannelType.SupportedChannelType.SimpleTaprootChannels) - val a = 774_660_000.msat // initial balance alice - val b = 190_000_000.msat // initial balance bob + val a = 786_220_000.msat // initial balance alice + val b = 200_000_000.msat // initial balance bob val p = 42_000_000.msat // a->b payment val htlcOutputFee = (2 * 860_000).msat // fee due to the additional htlc output; we count it twice because we keep a reserve for a x2 feerate increase @@ -151,7 +151,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc1.availableBalanceForSend(), b) assertEquals(bc1.availableBalanceForReceive(), a - p - htlcOutputFee) - val (ac2, commit1) = ac1.sendCommit(alice.channelKeys, logger).right!! + val (ac2, commit1) = ac1.sendCommit(alice.channelKeys, alice.state.remoteNextCommitNonces, logger).right!! assertEquals(ac2.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac2.availableBalanceForReceive(), b) @@ -163,7 +163,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac3.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac3.availableBalanceForReceive(), b) - val (bc3, commit2) = bc2.sendCommit(bob.channelKeys, logger).right!! + val (bc3, commit2) = bc2.sendCommit(bob.channelKeys, bob.state.remoteNextCommitNonces, logger).right!! assertEquals(bc3.availableBalanceForSend(), b) assertEquals(bc3.availableBalanceForReceive(), a - p - htlcOutputFee) @@ -184,7 +184,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac5.availableBalanceForSend(), a - p - htlcOutputFee) assertEquals(ac5.availableBalanceForReceive(), b) - val (bc6, commit3) = bc5.sendCommit(bob.channelKeys, logger).right!! + val (bc6, commit3) = bc5.sendCommit(bob.channelKeys, revocation2.nextCommitNonces, logger).right!! assertEquals(bc6.availableBalanceForSend(), b) assertEquals(bc6.availableBalanceForReceive(), a - p - htlcOutputFee) @@ -196,7 +196,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc7.availableBalanceForSend(), b) assertEquals(bc7.availableBalanceForReceive(), a) - val (ac7, commit4) = ac6.sendCommit(alice.channelKeys, logger).right!! + val (ac7, commit4) = ac6.sendCommit(alice.channelKeys, revocation1.nextCommitNonces, logger).right!! assertEquals(ac7.availableBalanceForSend(), a) assertEquals(ac7.availableBalanceForReceive(), b) @@ -211,10 +211,10 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { @Test fun `correct values for availableForSend - availableForReceive -- multiple htlcs`() { - val (alice, bob) = reachNormal(aliceFundingAmount = 800_000.sat, bobFundingAmount = 200_000.sat) + val (alice, bob) = reachNormal(aliceFundingAmount = 800_000.sat, bobFundingAmount = 200_000.sat, channelType = ChannelType.SupportedChannelType.SimpleTaprootChannels) - val a = 774_660_000.msat // initial balance alice - val b = 190_000_000.msat // initial balance bob + val a = 786_220_000.msat // initial balance alice + val b = 200_000_000.msat // initial balance bob val p1 = 18_000_000.msat // a->b payment val p2 = 20_000_000.msat // a->b payment val p3 = 40_000_000.msat // b->a payment @@ -258,7 +258,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac3.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee) assertEquals(ac3.availableBalanceForReceive(), b - p3) - val (ac4, commit1) = ac3.sendCommit(alice.channelKeys, logger).right!! + val (ac4, commit1) = ac3.sendCommit(alice.channelKeys, alice.state.remoteNextCommitNonces, logger).right!! assertEquals(ac4.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee) assertEquals(ac4.availableBalanceForReceive(), b - p3) @@ -270,7 +270,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac5.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee) assertEquals(ac5.availableBalanceForReceive(), b - p3) - val (bc5, commit2) = bc4.sendCommit(bob.channelKeys, logger).right!! + val (bc5, commit2) = bc4.sendCommit(bob.channelKeys, bob.state.remoteNextCommitNonces, logger).right!! assertEquals(bc5.availableBalanceForSend(), b - p3) assertEquals(bc5.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee) @@ -282,7 +282,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc6.availableBalanceForSend(), b - p3) assertEquals(bc6.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) - val (ac7, commit3) = ac6.sendCommit(alice.channelKeys, logger).right!! + val (ac7, commit3) = ac6.sendCommit(alice.channelKeys, revocation1.nextCommitNonces, logger).right!! assertEquals(ac7.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) assertEquals(ac7.availableBalanceForReceive(), b - p3) @@ -321,7 +321,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc10.availableBalanceForSend(), b + p1 - p3) assertEquals(bc10.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) // the fee for p3 disappears - val (ac12, commit4) = ac11.sendCommit(alice.channelKeys, logger).right!! + val (ac12, commit4) = ac11.sendCommit(alice.channelKeys, revocation3.nextCommitNonces, logger).right!! assertEquals(ac12.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) assertEquals(ac12.availableBalanceForReceive(), b + p1 - p3) @@ -333,7 +333,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(ac13.availableBalanceForSend(), a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) assertEquals(ac13.availableBalanceForReceive(), b + p1 - p3) - val (bc12, commit5) = bc11.sendCommit(bob.channelKeys, logger).right!! + val (bc12, commit5) = bc11.sendCommit(bob.channelKeys, revocation2.nextCommitNonces, logger).right!! assertEquals(bc12.availableBalanceForSend(), b + p1 - p3) assertEquals(bc12.availableBalanceForReceive(), a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) @@ -345,7 +345,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { assertEquals(bc13.availableBalanceForSend(), b + p1 - p3) assertEquals(bc13.availableBalanceForReceive(), a - p1 + p3) - val (ac15, commit6) = ac14.sendCommit(alice.channelKeys, logger).right!! + val (ac15, commit6) = ac14.sendCommit(alice.channelKeys, revocation4.nextCommitNonces, logger).right!! assertEquals(ac15.availableBalanceForSend(), a - p1 + p3) assertEquals(ac15.availableBalanceForReceive(), b + p1 - p3) @@ -420,7 +420,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { ChannelParams( channelId = randomBytes32(), channelConfig = ChannelConfig.standard, - channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features), + channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.SimpleTaprootChannels.features), localParams = localChannelParams, remoteParams = remoteChannelParams, channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), @@ -450,7 +450,7 @@ class CommitmentsTestsCommon : LightningTestSuite(), LoggingContext { inactive = emptyList(), payments = mapOf(), remoteNextCommitInfo = Either.Right(randomKey().publicKey()), - remotePerCommitmentSecrets = ShaChain.init, + remotePerCommitmentSecrets = ShaChain.init ) } } 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 2e5aa9997..b2722d3c9 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 @@ -36,8 +36,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // 3 swap-in inputs, 2 legacy swap-in inputs, and 2 outputs from Alice // 2 swap-in inputs, 2 legacy swap-in inputs, and 1 output from Bob @@ -89,7 +89,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertTrue(sharedTxB.sharedTx.localFees < sharedTxA.sharedTx.localFees) // Bob sends signatures first as he contributed less than Alice. - val signedTxB = sharedTxB.sharedTx.sign(bob8, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) + val signedTxB = sharedTxB.sharedTx.sign(bob8, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right!! assertEquals(signedTxB.localSigs.swapInUserSigs.size, 2) assertEquals(signedTxB.localSigs.swapInUserPartialSigs.size, 2) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 2) @@ -97,39 +97,39 @@ class InteractiveTxTestsCommon : LightningTestSuite() { // Alice detects invalid signatures from Bob. val sigsInvalidTxId = signedTxB.localSigs.copy(txId = TxId(randomBytes32())) - assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidTxId)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidTxId)) val sigsMissingUserSigs = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInUserSigs }.toSet())) - assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserSigs)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserSigs)) val sigsMissingUserPartialSigs = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInUserPartialSigs }.toSet())) - assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserPartialSigs)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserPartialSigs)) val sigsMissingServerSigs = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInServerSigs }.toSet())) - assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerSigs)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerSigs)) val sigsMissingServerPartialSigs = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInServerPartialSigs }.toSet())) - assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerPartialSigs)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerPartialSigs)) val invalidUserSigs = signedTxB.localSigs.swapInUserSigs.map { randomBytes64() } val sigsInvalidUserSig = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInUserSigs }.toSet() + TxSignaturesTlv.SwapInUserSigs(invalidUserSigs))) - assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserSig)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserSig)) val invalidPartialUserSigs = signedTxB.localSigs.swapInUserPartialSigs.map { TxSignaturesTlv.PartialSignature(randomBytes32(), it.localNonce, it.remoteNonce) } val sigsInvalidUserPartialSig = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInUserPartialSigs }.toSet() + TxSignaturesTlv.SwapInUserPartialSigs(invalidPartialUserSigs))) - assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserPartialSig)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserPartialSig)) val invalidServerSigs = signedTxB.localSigs.swapInServerSigs.map { randomBytes64() } val sigsInvalidServerSig = signedTxB.localSigs.copy(tlvs = TlvStream(signedTxB.localSigs.tlvs.records.filterNot { it is TxSignaturesTlv.SwapInServerSigs }.toSet() + TxSignaturesTlv.SwapInServerSigs(invalidServerSigs))) - assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerSig)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerSig)) val invalidPartialServerSigs = signedTxB.localSigs.swapInServerPartialSigs.map { TxSignaturesTlv.PartialSignature(randomBytes32(), it.localNonce, it.remoteNonce) } val sigsInvalidServerPartialSig = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserPartialSigs(signedTxB.localSigs.swapInUserPartialSigs), TxSignaturesTlv.SwapInServerPartialSigs(invalidPartialServerSigs))) - assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerPartialSig)) + assertNull(sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerPartialSig)) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice7, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 2) assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 3) @@ -158,8 +158,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Even though the initiator isn't contributing, they're paying the fees for the common parts of the transaction. // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) @@ -192,7 +192,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertTrue(sharedTxB.sharedTx.localFees < sharedTxA.sharedTx.localFees) // Alice sends signatures first as she contributed less than Bob. - val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) @@ -200,7 +200,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(signedTxA.localSigs.swapInServerPartialSigs.size, 1) // The resulting transaction is valid and has the right feerate. - val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right?.addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) @@ -229,8 +229,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createFixture(fundingA, utxosA, legacyUtxosA, fundingB, utxosB, legacyUtxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -262,7 +262,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertTrue(sharedTxA.sharedTx.remoteFees < sharedTxA.sharedTx.localFees) // Alice contributes more than Bob to the funding output, but Bob's inputs are bigger than Alice's, so Alice must sign first. - val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) @@ -270,7 +270,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(signedTxA.localSigs.swapInServerPartialSigs.size, 1) // The resulting transaction is valid and has the right feerate. - val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right?.addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) Transaction.correctlySpends(signedTxB.signedTx, (sharedTxA.sharedTx.localInputs + sharedTxB.sharedTx.localInputs).map { it.previousTx }, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val feerate = Transactions.fee2rate(signedTxB.tx.fees, signedTxB.signedTx.weight()) @@ -286,8 +286,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createFixture(fundingA, utxosA, legacyUtxosA, 0.sat, listOf(), listOf(), targetFeerate, 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -326,7 +326,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 2_985_000.msat) // Bob sends signatures first as he did not contribute at all. - val signedTxB = sharedTxB.sharedTx.sign(bob6, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) + val signedTxB = sharedTxB.sharedTx.sign(bob6, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 0) assertEquals(signedTxB.localSigs.swapInUserPartialSigs.size, 0) @@ -334,7 +334,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(signedTxB.localSigs.swapInServerPartialSigs.size, 2) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 2) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 0) @@ -359,8 +359,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createFixture(0.sat, listOf(), listOf(), fundingB, utxosB, listOf(), targetFeerate, 330.sat, 0, nonInitiatorPaysCommitFees = true) assertEquals(f.fundingParamsA.fundingAmount, fundingB) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_output --> Bob val (alice1, sharedOutput) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -383,8 +383,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(0.msat, sharedTxB.sharedTx.remoteFees) // Alice signs first since she didn't contribute. - val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB) - val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right!! + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right?.addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) Transaction.correctlySpends(signedTxB.signedTx, sharedTxB.sharedTx.localInputs.map { it.previousTx }, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // The feerate is lower than expected since Alice didn't contribute. @@ -406,8 +406,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -446,14 +446,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 953_000.msat) // Bob sends signatures first as he contributed less than Alice. - val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserPartialSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerPartialSigs.size, 1) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerPartialSigs.size, 1) @@ -483,8 +483,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -520,7 +520,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_000_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right assertNotNull(signedTxB) assertTrue(signedTxB.localSigs.witnesses.isEmpty()) assertTrue(signedTxB.localSigs.swapInUserSigs.isEmpty()) @@ -528,7 +528,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertTrue(signedTxA.localSigs.witnesses.isEmpty()) assertTrue(signedTxA.localSigs.swapInUserSigs.isEmpty()) @@ -556,8 +556,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -600,13 +600,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_000_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right assertNotNull(signedTxB) assertTrue(signedTxB.localSigs.swapInUserSigs.isEmpty()) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertTrue(signedTxA.localSigs.swapInUserSigs.isEmpty()) assertNotNull(signedTxA.localSigs.previousFundingTxSig) @@ -634,8 +634,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -677,14 +677,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_077_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(bob6, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) + val signedTxB = sharedTxB.sharedTx.sign(bob6, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserPartialSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerPartialSigs.size, 1) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserPartialSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerPartialSigs.size, 1) @@ -711,8 +711,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createSpliceFixture(balanceA, 0.sat, listOf(), listOf(), balanceB, additionalFundingB, utxosB, listOf(), targetFeerate, 330.sat, 0, nonInitiatorPaysCommitFees = true) assertEquals(f.fundingParamsA.fundingAmount, 125_000.sat) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, sharedInput) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -729,8 +729,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val (alice4, sharedTxA) = receiveFinalMessage(alice3, sharedTxB.txComplete) // Alice signs first since she didn't contribute. - val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB) - val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.nodeIdB).right!! + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.nodeIdA).right?.addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) Transaction.correctlySpends(signedTxB.signedTx, previousOutputs(f.fundingParamsA, sharedTxB.sharedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // The feerate is lower than expected since Alice didn't contribute. @@ -742,8 +742,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { fun `remove input - output`() { val f = createFixture(100_000.sat, listOf(150_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(2500.sat), 330.sat, 0) // In this flow we introduce dummy inputs/outputs from Bob to Alice that are then removed. - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), FundingContributions(listOf(), listOf())) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 0.msat, 0.msat, emptySet(), FundingContributions(listOf(), listOf())) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) @@ -859,7 +859,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxAddInput(f.channelId, 9, previousTx, 2, 0xffffffffU) to InteractiveTxSessionAction.NonReplaceableInput(f.channelId, 9, previousTx.txid, 2, 0xffffffff), ) testCases.forEach { (input, expected) -> - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -879,7 +879,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxAddOutput(f.channelId, 1, 25_000.sat, Script.write(listOf(OP_1)).byteVector()), ) testCases.forEach { output -> - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -899,7 +899,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxAddOutput(f.channelId, 3, 329.sat, validScript) to InteractiveTxSessionAction.OutputBelowDust(f.channelId, 3, 329.sat, 330.sat), ) testCases.forEach { (output, expected) -> - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -920,7 +920,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxRemoveInput(f.channelId, 57) to InteractiveTxSessionAction.UnknownSerialId(f.channelId, 57), ) testCases.forEach { (msg, expected) -> - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_remove_(in|out)put --- Bob @@ -933,7 +933,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { fun `too many protocol rounds`() { val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() - var (alice, _) = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() (1..InteractiveTxSession.MAX_INPUTS_OUTPUTS_RECEIVED).forEach { i -> // Alice --- tx_message --> Bob val (alice1, _) = alice.receive(TxAddOutput(f.channelId, 2 * i.toLong() + 1, 2500.sat, validScript)) @@ -946,7 +946,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `too many inputs`() { val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - var (alice, _) = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() (1..252).forEach { i -> // Alice --- tx_message --> Bob val (alice1, _) = alice.receive(createTxAddInput(f.channelId, 2 * i.toLong() + 1, 5000.sat)) @@ -962,7 +962,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `too many outputs`() { val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - var (alice, _) = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() (1..252).forEach { i -> // Alice --- tx_message --> Bob @@ -980,7 +980,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { fun `missing funding output`() { val f = createFixture(100_000.sat, listOf(120_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() - val bob0 = InteractiveTxSession(f.nodeIdB, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdB, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 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 @@ -993,7 +993,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `multiple funding outputs`() { 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 bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 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 @@ -1011,7 +1011,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val spliceOutputA = TxOut(20_000.sat, Script.pay2wpkh(randomKey().publicKey())) val subtractedFundingA = 25_000.sat 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) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 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))) // Alice --- tx_add_output --> Bob @@ -1024,8 +1024,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `swap-in input missing user key`() { val f = createFixture(100_000.sat, listOf(), listOf(150_000.sat), 0.sat, listOf(), listOf(), FeeratePerKw(2500.sat), 330.sat, 0) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -1045,17 +1045,17 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNull(sharedTxB.txComplete) // Alice didn't send her user key, so Bob thinks there aren't any swap inputs - val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsA.nodeId).right!! assertTrue(signedTxB.localSigs.swapInServerSigs.isEmpty()) // Alice is unable to sign her input since Bob didn't provide his signature. - assertNull(sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs)) + assertNull(sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsB.nodeId).right?.addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs)) } @Test fun `swap-in input missing user nonce`() { val f = createFixture(100_000.sat, listOf(150_000.sat), listOf(), 0.sat, listOf(), listOf(), FeeratePerKw(2500.sat), 330.sat, 0) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -1076,7 +1076,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `invalid funding amount`() { 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 bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 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 @@ -1097,8 +1097,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -1137,7 +1137,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `missing previous tx`() { 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 bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_output --> Bob val failure = receiveInvalidMessage(bob0, TxAddInput(f.channelId, 0, null, 3, 0u)) assertIs(failure) @@ -1146,7 +1146,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `total input amount too low`() { 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 bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 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)) @@ -1162,7 +1162,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `minimum fee not met`() { 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 bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0, 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)) @@ -1193,7 +1193,18 @@ class InteractiveTxTestsCommon : LightningTestSuite() { SharedTransaction(null, sharedOutput, listOf(), firstAttempt.tx.remoteInputs + listOf(InteractiveTxInput.RemoteOnly(4, OutPoint(previousTx2, 1), TxOut(150_000.sat, validScript), 0u)), listOf(), listOf(), 0), TxSignatures(f.channelId, TxId(randomBytes32()), listOf()), ) - val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB, listOf(firstAttempt, secondAttempt)) + val bob0 = InteractiveTxSession( + f.nodeIdA, + f.channelKeysB, + f.keyManagerB.swapInOnChainWallet, + f.fundingParamsB, + 0, + 0.msat, + 0.msat, + emptySet(), + f.fundingContributionsB, + listOf(firstAttempt, secondAttempt), + ) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, TxAddInput(f.channelId, 4, previousTx2, 1, 0u)) // Alice --- tx_add_output --> Bob diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt deleted file mode 100644 index 2e05db717..000000000 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/RecoveryTestsCommon.kt +++ /dev/null @@ -1,91 +0,0 @@ -package fr.acinq.lightning.channel - -import fr.acinq.bitcoin.* -import fr.acinq.lightning.Lightning.randomKey -import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.crypto.CommitmentPublicKeys -import fr.acinq.lightning.crypto.LocalKeyManager -import fr.acinq.lightning.crypto.RemoteCommitmentKeys -import fr.acinq.lightning.tests.TestConstants -import fr.acinq.lightning.transactions.Scripts -import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.utils.toByteVector32 -import kotlin.test.Test -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue - -class RecoveryTestsCommon { - - @Test - fun `use funding pubkeys from published commitment to spend our output`() { - // Alice creates and uses a LN channel to Bob - val (alice, bob) = TestsHelper.reachNormal() - val (alice1, _) = TestsHelper.addHtlc(MilliSatoshi(50000), alice, bob).first - - // Alice force-closes the channel and publishes her commit tx - val (_, actions) = alice1.process(ChannelCommand.Close.ForceClose) - val transactions = actions.findPublishTxs() - val commitTx = transactions[0] - val aliceTx = transactions[1] - - // how can Bob find and spend his output in Alice's published commit tx with just his wallet seed (derived from his mnemonic words) and nothing else? - - // extract funding pubkeys from the commit tx witness, which is a multisig 2-of-2 - val redeemScript = Script.parse(commitTx.txIn[0].witness.last()) - assertTrue(redeemScript.size == 5 && redeemScript[0] == OP_2 && redeemScript[3] == OP_2 && redeemScript[4] == OP_CHECKMULTISIG) - val pub1 = PublicKey((redeemScript[1] as OP_PUSHDATA).data) - val pub2 = PublicKey((redeemScript[2] as OP_PUSHDATA).data) - - // use Bob's mnemonic words to initialise his key manager - val seed = MnemonicCode.toSeed(TestConstants.Bob.mnemonics, "").toByteVector32() - val keyManager = LocalKeyManager(seed, Chain.Regtest, TestConstants.aliceSwapInServerXpub) - - // recompute our channel keys from the extracted funding pubkey and see if we can find and spend our output - // we only need our payment key and basepoint for our main output - fun findAndSpend(fundingKey: PublicKey): Transaction? { - val channelKeys = keyManager.recoverChannelKeys(fundingKey) - val commitKeys = RemoteCommitmentKeys( - ourPaymentKey = channelKeys.paymentKey, - theirDelayedPaymentPublicKey = randomKey().publicKey(), - ourPaymentBasePoint = channelKeys.paymentBasepoint, - ourHtlcKey = randomKey(), - theirHtlcPublicKey = randomKey().publicKey(), - revocationPublicKey = randomKey().publicKey() - ) - val finalScript = Script.write(Script.pay2wpkh(fundingKey)).byteVector() - val mainTx = Transactions.ClaimRemoteDelayedOutputTx.createUnsignedTx( - commitKeys, - commitTx, - TestConstants.Bob.nodeParams.dustLimit, - finalScript, - FeeratePerKw(750.sat()), - Transactions.CommitmentFormat.AnchorOutputs - ).map { it.sign().tx }.right - mainTx?.let { Transaction.correctlySpends(it, commitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - return mainTx - } - - // this is the script of the output that we're spending - val bobTx = findAndSpend(pub1) ?: findAndSpend(pub2)!! - assertNotEquals(aliceTx, bobTx) - - val outputScript = Script.parse(commitTx.txOut[bobTx.txIn[0].outPoint.index.toInt()].publicKeyScript) - - // this is what our main output script should be - fun ourDelayedOutputScript(pub: PublicKey): List { - val channelKeys = keyManager.recoverChannelKeys(pub) - val commitKeys = CommitmentPublicKeys( - localDelayedPaymentPublicKey = randomKey().publicKey(), - remotePaymentPublicKey = channelKeys.paymentBasepoint, - localHtlcPublicKey = randomKey().publicKey(), - remoteHtlcPublicKey = randomKey().publicKey(), - revocationPublicKey = randomKey().publicKey() - ) - return Script.pay2wsh(Scripts.toRemoteDelayed(commitKeys)) - } - - assertTrue(outputScript == ourDelayedOutputScript(pub1) || outputScript == ourDelayedOutputScript(pub2)) - } - -} 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 d26ccc4cb..ad4f1fa16 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 @@ -152,15 +152,20 @@ data class LNChannel( private fun checkSerialization(state: PersistedChannelState) { // We don't persist unsigned funding RBF or splice attempts. + // We don't persist taproot nonces either, they will be retransmitted on reconnection. fun removeTemporaryStatuses(state: PersistedChannelState): PersistedChannelState = when (state) { + is WaitForFundingSigned -> state.copy(signingSession = state.signingSession.copy(nextRemoteCommitNonce = null)) is WaitForFundingConfirmed -> when (state.rbfStatus) { - is RbfStatus.WaitingForSigs -> state - else -> state.copy(rbfStatus = RbfStatus.None) + is RbfStatus.WaitingForSigs -> state.copy(remoteNextCommitNonces = mapOf(), rbfStatus = state.rbfStatus.copy(session = state.rbfStatus.session.copy(nextRemoteCommitNonce = null))) + else -> state.copy(remoteNextCommitNonces = mapOf(), rbfStatus = RbfStatus.None) } + is WaitForChannelReady -> state.copy(remoteNextCommitNonces = mapOf()) is Normal -> when (state.spliceStatus) { - is SpliceStatus.WaitingForSigs -> state - else -> state.copy(spliceStatus = SpliceStatus.None) + is SpliceStatus.WaitingForSigs -> state.copy(remoteNextCommitNonces = mapOf(), localCloseeNonce = null, localCloserNonces = null, spliceStatus = state.spliceStatus.copy(session = state.spliceStatus.session.copy(nextRemoteCommitNonce = null))) + else -> state.copy(remoteNextCommitNonces = mapOf(), localCloseeNonce = null, localCloserNonces = null, spliceStatus = SpliceStatus.None) } + is ShuttingDown -> state.copy(remoteNextCommitNonces = mapOf(), localCloseeNonce = null) + is Negotiating -> state.copy(remoteNextCommitNonces = mapOf(), localCloseeNonce = null, remoteCloseeNonce = null, localCloserNonces = null) else -> state } @@ -174,7 +179,6 @@ data class LNChannel( val serialized = Serialization.serialize(state) val deserialized = Serialization.deserialize(serialized).value - assertEquals(removeTemporaryStatuses(ignoreClosingReplyTo(state)), ignoreClosingReplyTo(deserialized), "serialization error") } @@ -188,7 +192,7 @@ data class LNChannel( object TestsHelper { fun init( - channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, aliceFeatures: Features = TestConstants.Alice.nodeParams.features, bobFeatures: Features = TestConstants.Bob.nodeParams.features, bobUsePeerStorage: Boolean = true, @@ -284,7 +288,7 @@ object TestsHelper { } fun reachNormal( - channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, aliceFeatures: Features = TestConstants.Alice.nodeParams.features.initFeatures(), bobFeatures: Features = TestConstants.Bob.nodeParams.features.initFeatures(), bobUsePeerStorage: Boolean = true, @@ -380,7 +384,7 @@ object TestsHelper { fun localClose(s: LNChannel, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): Triple, LocalCommitPublished, LocalCloseTxs> { assertIs>(s) - assertEquals(Transactions.CommitmentFormat.AnchorOutputs, s.state.commitments.latest.commitmentFormat) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, s.state.commitments.latest.commitmentFormat) // An error occurs and we publish our commit tx. val commitTxId = s.state.commitments.latest.localCommit.txId val (s1, actions1) = s.process(ChannelCommand.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) @@ -442,7 +446,7 @@ object TestsHelper { fun remoteClose(rCommitTx: Transaction, s: LNChannel, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): Triple, RemoteCommitPublished, RemoteCloseTxs> { assertIs>(s) - assertEquals(Transactions.CommitmentFormat.AnchorOutputs, s.state.commitments.latest.commitmentFormat) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, s.state.commitments.latest.commitmentFormat) // Our peer has unilaterally closed the channel. val (s1, actions1) = s.process(ChannelCommand.WatchReceived(WatchSpentTriggered(s.state.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), rCommitTx))) assertIs>(s1) @@ -501,13 +505,13 @@ object TestsHelper { return Triple(closingState, remoteCommitPublished, RemoteCloseTxs(mainTx, htlcSuccessTxs, htlcTimeoutTxs)) } - fun useAlternativeCommitSig(s: LNChannel, commitment: Commitment, alternative: CommitSigTlv.AlternativeFeerateSig): Transaction { + fun useAlternativeCommitSig(s: LNChannel, commitment: Commitment, feerate: FeeratePerKw): Transaction { val channelKeys = s.commitments.channelKeys(s.ctx.keyManager) val fundingKey = commitment.localFundingKey(channelKeys) val commitKeys = channelKeys.localCommitmentKeys(s.commitments.channelParams, commitment.localCommit.index) - val alternativeSpec = commitment.localCommit.spec.copy(feerate = alternative.feerate) - val alternativeSig = ChannelSpendSignature.IndividualSignature(alternative.sig) + val alternativeSpec = commitment.localCommit.spec.copy(feerate = feerate) val remoteFundingPubKey = commitment.remoteFundingPubkey + // This commitment transaction isn't signed, but we don't care, we will make it look like it was confirmed anyway. val (localCommitTx, _) = Commitments.makeLocalTxs( channelParams = s.commitments.channelParams, commitParams = commitment.localCommitParams, @@ -519,10 +523,7 @@ object TestsHelper { commitmentFormat = commitment.commitmentFormat, spec = alternativeSpec, ) - val localSig = localCommitTx.sign(fundingKey, remoteFundingPubKey) - val signedCommitTx = localCommitTx.aggregateSigs(fundingKey.publicKey(), remoteFundingPubKey, localSig, alternativeSig) - Transaction.correctlySpends(signedCommitTx, mapOf(commitment.fundingInput to commitment.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - return signedCommitTx + return localCommitTx.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 1c408ed8b..4f4a8168d 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 @@ -49,7 +49,7 @@ class ClosingTestsCommon : LightningTestSuite() { @Test fun `recv ChannelFundingDepthOk`() { - val (alice, bob, _) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, _) = WaitForFundingConfirmedTestsCommon.init() val fundingTx = alice.state.latestFundingTx.sharedTx.tx.buildUnsignedTx() run { val (aliceClosing, _) = localClose(alice) @@ -74,7 +74,7 @@ class ClosingTestsCommon : LightningTestSuite() { @Test fun `recv ChannelFundingDepthOk -- previous funding tx`() { - val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init() val (alice1, bob1, fundingTx) = WaitForFundingConfirmedTestsCommon.rbf(alice, bob, walletAlice) assertNotEquals(previousFundingTx.txid, fundingTx.txid) run { @@ -644,7 +644,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(), Commitments.alternativeFeerates.first()) remoteClose(alternativeCommitTx, bob6) } @@ -857,7 +857,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(), Commitments.alternativeFeerates.first()) remoteClose(alternativeCommitTx, bob4) } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt index 2194fd0fa..86dcf9f9a 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt @@ -112,15 +112,15 @@ class NegotiatingTestsCommon : LightningTestSuite() { assertNull(actionsBob2.findOutgoingMessageOpt()) // Bob cannot pay mutual close fees. val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) val closingCompleteAlice = actionsAlice2.findOutgoingMessage() - assertNull(closingCompleteAlice.closerAndCloseeOutputsSig) - assertNotNull(closingCompleteAlice.closerOutputOnlySig) + assertNull(closingCompleteAlice.closerAndCloseeOutputsPartialSig) + assertNotNull(closingCompleteAlice.closerOutputOnlyPartialSig) val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(closingCompleteAlice)) assertIs(bob3.state) val closingTxAlice = actionsBob3.findPublishTxs().first() assertEquals(1, closingTxAlice.txOut.size) val closingSigBob = actionsBob3.findOutgoingMessage() - assertNotNull(closingSigBob.closerOutputOnlySig) + assertNotNull(closingSigBob.closerOutputOnlyPartialSig) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(closingSigBob)) assertIs(alice3.state) @@ -157,24 +157,33 @@ class NegotiatingTestsCommon : LightningTestSuite() { val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(closingSigAlice1)) actionsBob3.hasOutgoingMessage() - // Alice handles Bob's updated closing_complete. + // Bob's closing_complete doesn't use Alice's latest closee nonce, so she ignores his closing_complete. val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(closingCompleteBob2)) - assertIs(alice2.state) - assertEquals(4, actionsAlice2.size) - actionsAlice2.has() - val closingTx2 = actionsAlice2.findPublishTxs().first() + actionsAlice2.hasOutgoingMessage() + + // Bob retries sending closing_complete. + val (bob4, actionsBob4) = bob3.process(ChannelCommand.Close.MutualClose(CompletableDeferred(), closingScript, TestConstants.feeratePerKw * 1.5)) + val closingCompleteBob3 = actionsBob4.findOutgoingMessage() + assertEquals(closingScript, closingCompleteBob3.closerScriptPubKey) + + // Alice handles Bob's updated closing_complete. + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(closingCompleteBob3)) + assertIs(alice3.state) + assertEquals(4, actionsAlice3.size) + actionsAlice3.has() + val closingTx2 = actionsAlice3.findPublishTxs().first() assertTrue(closingTx2.txOut.any { it.publicKeyScript == closingScript }) - actionsAlice2.hasWatchConfirmed(closingTx2.txid) - val closingSigAlice2 = actionsAlice2.findOutgoingMessage() + actionsAlice3.hasWatchConfirmed(closingTx2.txid) + val closingSigAlice2 = actionsAlice3.findOutgoingMessage() // Bob receives Alice's closing_sig for his updated closing_complete. - val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(closingSigAlice2)) - assertIs(bob4.state) - assertEquals(4, actionsBob4.size) - actionsBob4.has() - actionsBob4.has() - assertEquals(closingTx2, actionsBob4.findPublishTxs().first()) - actionsBob4.hasWatchConfirmed(closingTx2.txid) + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(closingSigAlice2)) + assertIs(bob5.state) + assertEquals(4, actionsBob5.size) + actionsBob5.has() + actionsBob5.has() + assertEquals(closingTx2, actionsBob5.findPublishTxs().first()) + actionsBob5.hasWatchConfirmed(closingTx2.txid) } @Test @@ -183,7 +192,7 @@ class NegotiatingTestsCommon : LightningTestSuite() { // Bob expects to receive a signature for a closing transaction containing his output, so he ignores Alice's // closing_complete instead of sending back his closing_sig. - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete.copy(tlvStream = TlvStream(ClosingCompleteTlv.CloserOutputOnly(closingComplete.closerOutputOnlySig!!))))) + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(closingComplete.copy(tlvStream = TlvStream(ClosingCompleteTlv.CloserOutputOnlyPartialSignature(closingComplete.closerOutputOnlyPartialSig!!))))) assertIs(bob1.state) assertEquals(1, actionsBob1.size) actionsBob1.hasOutgoingMessage() @@ -191,7 +200,7 @@ class NegotiatingTestsCommon : LightningTestSuite() { val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(closingComplete)) assertIs(bob2.state) val closingTxAlice = actionsBob2.findPublishTxs().first() - assertTrue(closingTxAlice.txOut.size == 2) + assertEquals(closingTxAlice.txOut.size, 2) actionsBob2.findOutgoingMessage() } @@ -453,7 +462,7 @@ class NegotiatingTestsCommon : LightningTestSuite() { data class Fixture(val alice: LNChannel, val bob: LNChannel, val closingCompleteAlice: ClosingComplete, val closingCompleteBob: ClosingComplete) fun init( - channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, aliceFundingAmount: Satoshi = TestConstants.aliceFundingAmount, bobFundingAmount: Satoshi = TestConstants.bobFundingAmount, aliceClosingFeerate: FeeratePerKw = TestConstants.feeratePerKw, 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 1ebfd656a..4d73995f4 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 @@ -72,7 +72,7 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Htlc_Add -- zero-reserve`() { - val (_, bob0) = reachNormal(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, bobFundingAmount = 10_000.sat) + val (_, bob0) = reachNormal(bobFundingAmount = 10_000.sat) assertEquals(bob0.commitments.availableBalanceForSend(), 10_000_000.msat) val add = defaultAdd.copy(amount = 10_000_000.msat, paymentHash = randomBytes32()) @@ -86,7 +86,7 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Htlc_Add -- zero-conf -- zero-reserve`() { - val (_, bob0) = reachNormal(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, bobFundingAmount = 10_000.sat, zeroConf = true) + val (_, bob0) = reachNormal(bobFundingAmount = 10_000.sat, zeroConf = true) assertEquals(bob0.commitments.availableBalanceForSend(), 10_000_000.msat) val add = defaultAdd.copy(amount = 10_000_000.msat, paymentHash = randomBytes32()) @@ -164,7 +164,7 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Htlc_Add -- increasing balance but still below reserve`() { - val (alice0, bob0) = reachNormal(bobFundingAmount = 0.sat) + val (alice0, bob0) = reachNormal(channelType = ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) assertFalse(alice0.commitments.channelParams.channelFeatures.hasFeature(Feature.ZeroReserveChannels)) assertFalse(bob0.commitments.channelParams.channelFeatures.hasFeature(Feature.ZeroReserveChannels)) assertEquals(0.msat, bob0.commitments.availableBalanceForSend()) @@ -186,7 +186,7 @@ class NormalTestsCommon : LightningTestSuite() { val add = defaultAdd.copy(amount = Int.MAX_VALUE.msat) val (alice1, actions) = alice0.process(add) val actualError = actions.findCommandError() - val expectError = InsufficientFunds(alice0.channelId, amount = Int.MAX_VALUE.msat, missing = 1_322_823.sat, reserve = 10_000.sat, fees = 7_140.sat) + val expectError = InsufficientFunds(alice0.channelId, amount = Int.MAX_VALUE.msat, missing = 1_311_263.sat, reserve = 0.sat, fees = 6_360.sat) assertEquals(expectError, actualError) assertEquals(alice0, alice1) } @@ -197,7 +197,7 @@ class NormalTestsCommon : LightningTestSuite() { val add = defaultAdd.copy(amount = bob0.commitments.availableBalanceForSend() + 1.sat.toMilliSatoshi()) val (bob1, actions) = bob0.process(add) val actualError = actions.findCommandError() - val expectedError = InsufficientFunds(bob0.channelId, amount = add.amount, missing = 1.sat, reserve = 10000.sat, fees = 0.sat) + val expectedError = InsufficientFunds(bob0.channelId, amount = add.amount, missing = 1.sat, reserve = 0.sat, fees = 0.sat) assertEquals(expectedError, actualError) assertEquals(bob0, bob1) } @@ -205,7 +205,7 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Htlc_Add -- commit tx fee greater than remote initiator balance`() { val (alice0, bob0) = reachNormal(bobFundingAmount = 200_000.sat) - val (alice1, bob1) = addHtlc(824_160_000.msat, alice0, bob0).first + val (alice1, bob1) = addHtlc(836_220_000.msat, alice0, bob0).first val (alice2, bob2) = crossSign(alice1, bob1) assertEquals(0.msat, alice2.state.commitments.availableBalanceForSend()) @@ -221,7 +221,7 @@ class NormalTestsCommon : LightningTestSuite() { } // Add a bunch of HTLCs, which increases the commit tx fee that Alice has to pay and consume almost all of her balance. - val (alice3, bob3) = addHtlcs(alice2, bob2, 21) + val (alice3, bob3) = addHtlcs(alice2, bob2, 8) run { // We can sign those HTLCs and make Alice drop below her reserve. val (_, alice4) = crossSign(bob3, alice3) @@ -230,7 +230,6 @@ class NormalTestsCommon : LightningTestSuite() { assertTrue(commitTx.txOut.all { txOut -> txOut.amount > 0.sat }) val aliceBalance = aliceCommit.spec.toLocal - commitTxFeeMsat(alice4.commitments.latest.localCommitParams.dustLimit, aliceCommit.spec, alice4.commitments.latest.commitmentFormat) assertTrue(aliceBalance >= 0.msat) - assertTrue(aliceBalance < alice4.commitments.latest.localChannelReserve) } run { // If we try adding one more HTLC, Alice won't be able to pay the commit tx fee. @@ -249,7 +248,7 @@ class NormalTestsCommon : LightningTestSuite() { actionsAlice2.hasOutgoingMessage() val (_, actionsAlice3) = alice2.process(defaultAdd.copy(amount = 500_000_000.msat)) val actualError = actionsAlice3.findCommandError() - val expectError = InsufficientFunds(alice0.channelId, amount = 500_000_000.msat, missing = 278_780.sat, reserve = 10_000.sat, fees = 8_860.sat) + val expectError = InsufficientFunds(alice0.channelId, amount = 500_000_000.msat, missing = 267_220.sat, reserve = 0.sat, fees = 8_080.sat) assertEquals(expectError, actualError) } @@ -260,11 +259,12 @@ class NormalTestsCommon : LightningTestSuite() { actionsAlice1.hasOutgoingMessage() val (alice2, actionsAlice2) = alice1.process(defaultAdd.copy(amount = 200_000_000.msat)) actionsAlice2.hasOutgoingMessage() - val (alice3, actionsAlice3) = alice2.process(defaultAdd.copy(amount = 121_120_000.msat)) + val (alice3, actionsAlice3) = alice2.process(defaultAdd.copy(amount = 132_780_000.msat)) actionsAlice3.hasOutgoingMessage() + assertEquals(0.msat, alice3.commitments.availableBalanceForSend()) val (_, actionsAlice4) = alice3.process(defaultAdd.copy(amount = 1_000_000.msat)) val actualError = actionsAlice4.findCommandError() - val expectedError = InsufficientFunds(alice0.channelId, amount = 1_000_000.msat, missing = 900.sat, reserve = 10_000.sat, fees = 8_860.sat) + val expectedError = InsufficientFunds(alice0.channelId, amount = 1_000_000.msat, missing = 1_000.sat, reserve = 0.sat, fees = 8_080.sat) assertEquals(expectedError, actualError) } @@ -366,7 +366,7 @@ class NormalTestsCommon : LightningTestSuite() { val failAdd = defaultAdd.copy(amount = alice0.commitments.latest.fundingAmount.toMilliSatoshi() * 2 / 3) val (_, actionsAlice3) = alice2.process(failAdd) val actualError = actionsAlice3.findCommandError() - val expectedError = InsufficientFunds(alice0.channelId, failAdd.amount, 510_393.sat, 10_000.sat, 8_000.sat) + val expectedError = InsufficientFunds(alice0.channelId, failAdd.amount, 498_833.sat, 0.sat, 7_220.sat) assertEquals(expectedError, actualError) } @@ -416,7 +416,7 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv UpdateAddHtlc -- zero-reserve`() { - val (alice0, _) = reachNormal(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, bobFundingAmount = 10_000.sat) + val (alice0, _) = reachNormal(bobFundingAmount = 10_000.sat) assertEquals(alice0.commitments.availableBalanceForReceive(), 10_000_000.msat) val add = UpdateAddHtlc(alice0.channelId, 0, 10_000_000.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(alice0.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket) val (alice1, actions1) = alice0.process(ChannelCommand.MessageReceived(add)) @@ -427,7 +427,7 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv UpdateAddHtlc -- zero-conf -- zero-reserve`() { - val (alice0, _) = reachNormal(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, bobFundingAmount = 10_000.sat, zeroConf = true) + val (alice0, _) = reachNormal(bobFundingAmount = 10_000.sat, zeroConf = true) assertEquals(alice0.commitments.availableBalanceForReceive(), 10_000_000.msat) val add = UpdateAddHtlc(alice0.channelId, 0, 10_000_000.msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(alice0.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket) val (alice1, actions1) = alice0.process(ChannelCommand.MessageReceived(add)) @@ -469,7 +469,7 @@ class NormalTestsCommon : LightningTestSuite() { val (bob1, actions1) = bob0.process(ChannelCommand.MessageReceived(add)) assertIs>(bob1) val error = actions1.hasOutgoingMessage() - assertEquals(error.toAscii(), InsufficientFunds(bob0.channelId, 800_000_000.msat, 17_140.sat, 10_000.sat, 7_140.sat).message) + assertEquals(error.toAscii(), InsufficientFunds(bob0.channelId, 800_000_000.msat, 6_360.sat, 0.sat, 6_360.sat).message) } @Test @@ -485,7 +485,7 @@ class NormalTestsCommon : LightningTestSuite() { val (bob4, actions4) = bob3.process(ChannelCommand.MessageReceived(add.copy(id = 3, amountMsat = 800_000_000.msat))) assertIs>(bob4) val error = actions4.hasOutgoingMessage() - assertEquals(error.toAscii(), InsufficientFunds(bob0.channelId, 800_000_000.msat, 14_720.sat, 10_000.sat, 9_720.sat).message) + assertEquals(error.toAscii(), InsufficientFunds(bob0.channelId, 800_000_000.msat, 3_940.sat, 0.sat, 8_940.sat).message) } @Test @@ -1407,7 +1407,7 @@ class NormalTestsCommon : LightningTestSuite() { actions.hasPublishTx(commitTx) actions.hasWatchConfirmed(commitTx.txid) val error = actions.findOutgoingMessage() - assertEquals(error.toAscii(), CannotAffordFees(bob.channelId, missing = 11_240.sat, reserve = 10_000.sat, fees = 26_580.sat).message) + assertEquals(error.toAscii(), CannotAffordFees(bob.channelId, missing = 9_680.sat, reserve = 0.sat, fees = 23_460.sat).message) } @Test @@ -1772,7 +1772,7 @@ class NormalTestsCommon : LightningTestSuite() { claimTx.txOut[0].amount }.sum() // at best we have a little less than 500 000 + 250 000 + 100 000 + 50 000 = 900 000 (because fees) - assertEquals(879_720.sat, amountClaimed) + assertEquals(880_880.sat, amountClaimed) val rcp = aliceClosing.state.remoteCommitPublished assertNotNull(rcp) @@ -1843,7 +1843,7 @@ class NormalTestsCommon : LightningTestSuite() { claimTx.txOut[0].amount }.sum() // at best we have a little less than 500 000 + 250 000 + 100 000 = 850 000 (because fees) - assertEquals(883_450.sat, amountClaimed) + assertEquals(884_535.sat, amountClaimed) val rcp = aliceClosing.state.nextRemoteCommitPublished assertNotNull(rcp) @@ -1927,12 +1927,12 @@ class NormalTestsCommon : LightningTestSuite() { actions2.hasWatchOutputsSpent(htlcInputs) // two main outputs are 760 000 and 200 000 (minus fees) - assertEquals(798_070.sat, mainTx.txOut[0].amount) - assertEquals(147_585.sat, penaltyTx.txOut[0].amount) - assertEquals(7_100.sat, htlcPenaltyTxs[0].txOut[0].amount) - assertEquals(7_100.sat, htlcPenaltyTxs[1].txOut[0].amount) - assertEquals(7_100.sat, htlcPenaltyTxs[2].txOut[0].amount) - assertEquals(7_100.sat, htlcPenaltyTxs[3].txOut[0].amount) + assertEquals(798_725.sat, mainTx.txOut[0].amount) + assertEquals(147_345.sat, penaltyTx.txOut[0].amount) + assertEquals(8_020.sat, htlcPenaltyTxs[0].txOut[0].amount) + assertEquals(8_020.sat, htlcPenaltyTxs[1].txOut[0].amount) + assertEquals(8_020.sat, htlcPenaltyTxs[2].txOut[0].amount) + assertEquals(8_020.sat, htlcPenaltyTxs[3].txOut[0].amount) } @Test 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 a88baccde..8447374de 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 @@ -23,7 +23,6 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `handle disconnect - connect events in WaitForChannelReady -- zeroconf`() { val (alice, aliceCommitSig, bob, _) = WaitForFundingSignedTestsCommon.init( - ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true, bobUsePeerStorage = false, ) @@ -78,7 +77,7 @@ class OfflineTestsCommon : LightningTestSuite() { ) assertEquals( ChannelReestablish(bob.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint), - channelReestablishB + channelReestablishB.copy(tlvStream = TlvStream.empty()) ) val (alice3, actions2) = alice2.process(ChannelCommand.MessageReceived(channelReestablishB)) @@ -125,9 +124,15 @@ class OfflineTestsCommon : LightningTestSuite() { val aliceCurrentPerCommitmentPoint = alice0.channelKeys.commitmentPoint(aliceCommitments.localCommitIndex) // alice didn't receive any update or sig - assertEquals(channelReestablishA, ChannelReestablish(alice0.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), aliceCurrentPerCommitmentPoint)) + assertEquals(1, channelReestablishA.nextLocalCommitmentNumber) + assertEquals(0, channelReestablishA.nextRemoteRevocationNumber) + assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishA.yourLastCommitmentSecret) + assertEquals(aliceCurrentPerCommitmentPoint, channelReestablishA.myCurrentPerCommitmentPoint) // bob did not receive alice's sig - assertEquals(channelReestablishB, ChannelReestablish(bob0.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint)) + assertEquals(1, channelReestablishB.nextLocalCommitmentNumber) + assertEquals(0, channelReestablishB.nextRemoteRevocationNumber) + assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishB.yourLastCommitmentSecret) + assertEquals(bobCurrentPerCommitmentPoint, channelReestablishB.myCurrentPerCommitmentPoint) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishB)) // alice sends ChannelReady again @@ -198,9 +203,15 @@ class OfflineTestsCommon : LightningTestSuite() { val aliceCurrentPerCommitmentPoint = alice0.channelKeys.commitmentPoint(aliceCommitments.localCommitIndex) // alice didn't receive any update or sig - assertEquals(channelReestablishA, ChannelReestablish(alice0.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), aliceCurrentPerCommitmentPoint)) + assertEquals(1, channelReestablishA.nextLocalCommitmentNumber) + assertEquals(0, channelReestablishA.nextRemoteRevocationNumber) + assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishA.yourLastCommitmentSecret) + assertEquals(aliceCurrentPerCommitmentPoint, channelReestablishA.myCurrentPerCommitmentPoint) // bob did receive alice's sig - assertEquals(channelReestablishB, ChannelReestablish(bob0.channelId, 2, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint)) + assertEquals(2, channelReestablishB.nextLocalCommitmentNumber) + assertEquals(0, channelReestablishB.nextRemoteRevocationNumber) + assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishB.yourLastCommitmentSecret) + assertEquals(bobCurrentPerCommitmentPoint, channelReestablishB.myCurrentPerCommitmentPoint) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishB)) // alice does not re-send messages bob already received @@ -564,7 +575,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `republish unconfirmed funding tx after restart`() { - val (alice, bob, fundingTx) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, fundingTx) = WaitForFundingConfirmedTestsCommon.init() // Alice restarts: val (alice1, actionsAlice1) = LNChannel(alice.ctx, WaitForInit).process(ChannelCommand.Init.Restore(alice.state)) assertEquals(alice1.state, Offline(alice.state)) @@ -581,7 +592,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `republish unconfirmed funding tx with previous funding txs after restart`() { - val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init() val (alice1, bob1, fundingTx) = WaitForFundingConfirmedTestsCommon.rbf(alice, bob, walletAlice) assertEquals(alice1.commitments.active.size, 2) assertNotEquals(previousFundingTx.txid, fundingTx.txid) @@ -603,7 +614,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `recv BITCOIN_FUNDING_DEPTHOK`() { - val (alice, bob, _) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, _) = WaitForFundingConfirmedTestsCommon.init() val fundingTx = alice.state.latestFundingTx.sharedTx.tx.buildUnsignedTx() val (alice1, bob1) = disconnect(alice, bob) // outer state is Offline, we check the inner states @@ -629,7 +640,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `recv BITCOIN_FUNDING_DEPTHOK -- previous funding tx`() { - val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init() val (alice1, bob1) = WaitForFundingConfirmedTestsCommon.rbf(alice, bob, walletAlice) val (alice2, bob2) = disconnect(alice1, bob1) assertIs(alice2.state.state) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt index 5c9dcead7..38a96bfbd 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt @@ -520,7 +520,7 @@ class QuiescenceTestsCommon : LightningTestSuite() { feerate = FeeratePerKw(253.sat), requestRemoteFunding = null, currentFeeCredit = 0.msat, - origins = listOf(), + origins = listOf() ) } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt index 74c65c300..011ba6d19 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt @@ -28,19 +28,9 @@ import kotlin.test.* class ShutdownTestsCommon : LightningTestSuite() { - @Test - fun `recv ChannelCommand_Htlc_Add`() { - val (_, bob) = init() - val add = ChannelCommand.Htlc.Add(500000000.msat, r1, cltvExpiry = CltvExpiry(300000), TestConstants.emptyOnionPacket, UUID.randomUUID()) - val (bob1, actions1) = bob.process(add) - assertIs>(bob1) - assertTrue(actions1.any { it is ChannelAction.ProcessCmdRes.AddFailed && it.error == ChannelUnavailable(bob.channelId) }) - assertEquals(bob1.commitments.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) - } - @Test fun `recv ChannelCommand_Htlc_Add -- zero-reserve`() { - val (_, bob) = init(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) + val (_, bob) = init() val add = ChannelCommand.Htlc.Add(500000000.msat, r1, cltvExpiry = CltvExpiry(300000), TestConstants.emptyOnionPacket, UUID.randomUUID()) val (bob1, actions1) = bob.process(add) assertIs>(bob1) @@ -277,7 +267,7 @@ class ShutdownTestsCommon : LightningTestSuite() { val sig = actionsBob2.hasOutgoingMessage() val (alice1, _) = alice0.process(ChannelCommand.MessageReceived(fulfill)) assertIs>(alice1) - val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(sig.copy(signature = ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes)))) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(sig.copy(tlvStream = TlvStream.empty()))) assertIs>(alice2) actionsAlice2.hasOutgoingMessage() assertNotNull(alice2.state.localCommitPublished) @@ -357,12 +347,9 @@ class ShutdownTestsCommon : LightningTestSuite() { @Test fun `recv CheckHtlcTimeout -- no htlc timed out`() { val (alice, _) = init() - - run { - val (alice1, actions1) = alice.process(ChannelCommand.Commitment.CheckHtlcTimeout) - assertEquals(alice, alice1) - assertTrue(actions1.isEmpty()) - } + val (alice1, actions1) = alice.process(ChannelCommand.Commitment.CheckHtlcTimeout) + assertEquals(alice, alice1) + assertTrue(actions1.isEmpty()) } @Test @@ -553,7 +540,7 @@ class ShutdownTestsCommon : LightningTestSuite() { val r2 = randomBytes32() fun init( - channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, currentBlockHeight: Int = TestConstants.defaultBlockHeight, aliceFeatures: Features = TestConstants.Alice.nodeParams.features, bobFeatures: Features = TestConstants.Bob.nodeParams.features, 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 869af93a7..1e2e2f44f 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 @@ -39,12 +39,34 @@ class SpliceTestsCommon : LightningTestSuite() { spliceOut(alice, bob, 50_000.sat) } + @Test + fun `splice funds out and upgrade to taproot`() { + val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, alice.commitments.latest.commitmentFormat) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, bob.commitments.latest.commitmentFormat) + + val (alice1, bob1) = spliceOut(alice, bob, 50_000.sat) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, alice1.commitments.latest.commitmentFormat) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, bob1.commitments.latest.commitmentFormat) + } + @Test fun `splice funds in`() { val (alice, bob) = reachNormal() spliceIn(alice, bob, listOf(50_000.sat)) } + @Test + fun `splice funds in and upgrade to taproot`() { + val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, alice.commitments.latest.commitmentFormat) + assertEquals(Transactions.CommitmentFormat.AnchorOutputs, bob.commitments.latest.commitmentFormat) + + val (alice1, bob1) = spliceIn(alice, bob, listOf(50_000.sat)) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, alice1.commitments.latest.commitmentFormat) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, bob1.commitments.latest.commitmentFormat) + } + @Test fun `splice funds in and out with pending htlcs`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() @@ -80,6 +102,41 @@ class SpliceTestsCommon : LightningTestSuite() { resolveHtlcs(alice6, bob6, htlcs, commitmentsCount = 2) } + @Test + fun `splice funds in and out with pending htlcs -- upgrade to taproot`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) + val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) + val (alice2, bob2) = spliceInAndOut(alice1, bob1, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) + + // Bob sends an HTLC that is applied to both commitments. + val (nodes3, preimage, add) = addHtlc(10_000_000.msat, bob2, alice2) + val (bob4, alice4) = crossSign(nodes3.first, nodes3.second, commitmentsCount = 2) + + alice4.commitments.active.forEach { c -> + val commitTx = c.fullySignedCommitTx(alice4.commitments.channelParams, alice4.channelKeys) + Transaction.correctlySpends(commitTx, mapOf(c.fundingInput to c.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + bob4.state.commitments.active.forEach { c -> + val commitTx = c.fullySignedCommitTx(bob4.commitments.channelParams, bob4.channelKeys) + Transaction.correctlySpends(commitTx, mapOf(c.fundingInput to c.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + // Alice fulfills that HTLC in both commitments. + val (bob5, alice5) = fulfillHtlc(add.id, preimage, bob4, alice4) + val (alice6, bob6) = crossSign(alice5, bob5, commitmentsCount = 2) + + alice6.state.commitments.active.forEach { c -> + val commitTx = c.fullySignedCommitTx(alice6.commitments.channelParams, alice6.channelKeys) + Transaction.correctlySpends(commitTx, mapOf(c.fundingInput to c.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + bob6.state.commitments.active.forEach { c -> + val commitTx = c.fullySignedCommitTx(bob6.commitments.channelParams, bob6.channelKeys) + Transaction.correctlySpends(commitTx, mapOf(c.fundingInput to c.localFundingStatus.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + resolveHtlcs(alice6, bob6, htlcs, commitmentsCount = 2) + } + @Test fun `splice funds in and out with pending htlcs resolved after splice locked`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() @@ -156,7 +213,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice funds out -- would go below reserve`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.AnchorOutputs) val (alice1, bob1, _) = setupHtlcs(alice, bob) val cmd = createSpliceOutRequest(810_000.sat) val (alice2, actionsAlice2) = alice1.process(cmd) @@ -191,7 +248,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice cpfp -- not enough funds`() { - val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, aliceFundingAmount = 75_000.sat, bobFundingAmount = 25_000.sat) + val (alice, bob) = reachNormal(aliceFundingAmount = 75_000.sat, bobFundingAmount = 25_000.sat) val (alice1, bob1) = spliceOut(alice, bob, 65_000.sat) // After the splice-out, Alice doesn't have enough funds to pay the mining fees to CPFP. val spliceCpfp = ChannelCommand.Commitment.Splice.Request( @@ -234,11 +291,11 @@ class SpliceTestsCommon : LightningTestSuite() { val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) val defaultSpliceAck = actionsBob2.findOutgoingMessage() assertNull(defaultSpliceAck.willFund) - val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey, Transactions.CommitmentFormat.AnchorOutputs).pubkeyScript + val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey, Transactions.CommitmentFormat.SimpleTaprootChannels).pubkeyScript run { val willFund = fundingRates.validateRequest(bob.staticParams.nodeParams.nodePrivateKey, fundingScript, cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)?.willFund assertNotNull(willFund) - val spliceAck = SpliceAck(alice.channelId, liquidityRequest.requestedAmount, defaultSpliceAck.fundingPubkey, willFund) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.requestedAmount, defaultSpliceAck.fundingPubkey, willFund, channelType = null) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) assertIs(alice2.state) assertIs(alice2.state.spliceStatus) @@ -257,7 +314,7 @@ class SpliceTestsCommon : LightningTestSuite() { // Bob uses a different funding script than what Alice expects. val willFund = fundingRates.validateRequest(bob.staticParams.nodeParams.nodePrivateKey, ByteVector("deadbeef"), cmd.feerate, spliceInit.requestFunding!!, isChannelCreation = false, 0.msat)?.willFund assertNotNull(willFund) - val spliceAck = SpliceAck(alice.channelId, liquidityRequest.requestedAmount, defaultSpliceAck.fundingPubkey, willFund) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.requestedAmount, defaultSpliceAck.fundingPubkey, willFund, channelType = null) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) assertIs(alice2.state) assertIs(alice2.state.spliceStatus) @@ -265,7 +322,7 @@ class SpliceTestsCommon : LightningTestSuite() { } run { // Bob doesn't fund the splice. - val spliceAck = SpliceAck(alice.channelId, liquidityRequest.requestedAmount, defaultSpliceAck.fundingPubkey, willFund = null) + val spliceAck = SpliceAck(alice.channelId, liquidityRequest.requestedAmount, defaultSpliceAck.fundingPubkey, willFund = null, channelType = null) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) assertIs(alice2.state) assertIs(alice2.state.spliceStatus) @@ -275,7 +332,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice to purchase inbound liquidity -- not enough funds`() { - val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, aliceFundingAmount = 100_000.sat, bobFundingAmount = 10_000.sat) + val (alice, bob) = reachNormal(aliceFundingAmount = 100_000.sat, bobFundingAmount = 10_000.sat) val fundingRate = LiquidityAds.FundingRate(100_000.sat, 10_000_000.sat, 0, 100 /* 1% */, 0.sat, 1000.sat) val fundingRates = LiquidityAds.WillFundRates(listOf(fundingRate), setOf(LiquidityAds.PaymentType.FromChannelBalance, LiquidityAds.PaymentType.FromFutureHtlc)) run { @@ -314,7 +371,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 = Transactions.makeFundingScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, Transactions.CommitmentFormat.AnchorOutputs).pubkeyScript + val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, Transactions.CommitmentFormat.SimpleTaprootChannels).pubkeyScript 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 +392,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 = Transactions.makeFundingScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, Transactions.CommitmentFormat.AnchorOutputs).pubkeyScript + val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, Transactions.CommitmentFormat.SimpleTaprootChannels).pubkeyScript 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) @@ -345,7 +402,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `splice to purchase inbound liquidity -- not enough funds but on-the-fly funding`() { - val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, bobFundingAmount = 0.sat) + val (alice, bob) = reachNormal(bobFundingAmount = 0.sat) val fundingRate = LiquidityAds.FundingRate(0.sat, 500_000.sat, 0, 50, 0.sat, 1000.sat) val fundingRates = LiquidityAds.WillFundRates(listOf(fundingRate), setOf(LiquidityAds.PaymentType.FromChannelBalanceForFutureHtlc, LiquidityAds.PaymentType.FromFutureHtlc)) val origin = Origin.OffChainPayment(randomBytes32(), 25_000_000.msat, ChannelManagementFees(0.sat, 500.sat)) @@ -401,7 +458,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 = Transactions.makeFundingScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, Transactions.CommitmentFormat.AnchorOutputs).pubkeyScript + val fundingScript = Transactions.makeFundingScript(spliceInit.fundingPubkey, spliceAck.fundingPubkey, Transactions.CommitmentFormat.SimpleTaprootChannels).pubkeyScript 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() @@ -1294,12 +1351,12 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- latest active commitment -- alternative feerate`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) 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(), Commitments.alternativeFeerates.last()) val commitment = alice1.commitments.active.first() val (alice3, actionsAlice3) = alice2.process(ChannelCommand.WatchReceived(WatchSpentTriggered(alice.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobCommitTx))) assertIs>(alice3) @@ -1321,7 +1378,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- previous active commitment -- alternative feerate`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) 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) @@ -1329,7 +1386,7 @@ class SpliceTestsCommon : LightningTestSuite() { // Bob force-closes using an older active commitment with an alternative feerate. assertEquals(bob4.commitments.active.map { it.localCommit.txId }.toSet().size, 3) - val bobCommitTx = useAlternativeCommitSig(bob4, bob4.commitments.active[1], commitSigAlice1.alternativeFeerateSigs.first()) + val bobCommitTx = useAlternativeCommitSig(bob4, bob4.commitments.active[1], Commitments.alternativeFeerates.first()) handlePreviousRemoteClose(alice4, bobCommitTx) } @@ -1371,10 +1428,10 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- revoked latest active commitment -- alternative feerate`() { - val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) 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(), Commitments.alternativeFeerates.first()) // Alice sends an HTLC to Bob, which revokes the previous commitment. val (nodes3, _, _) = addHtlc(25_000_000.msat, alice2, bob2) @@ -1466,6 +1523,91 @@ class SpliceTestsCommon : LightningTestSuite() { assertIs(alice18.state) } + @Test + fun `force-close -- revoked previous active commitment -- after taproot upgrade`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + assertEquals(alice0.commitments.latest.commitmentFormat, Transactions.CommitmentFormat.AnchorOutputs) + assertEquals(bob0.commitments.latest.commitmentFormat, Transactions.CommitmentFormat.AnchorOutputs) + val bobRevokedCommitTx = bob0.commitments.active.last().fullySignedCommitTx(bob.commitments.channelParams, bob.channelKeys) + + // We make a first splice transaction, but don't exchange splice_locked. + val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) + assertEquals(alice1.commitments.latest.commitmentFormat, Transactions.CommitmentFormat.SimpleTaprootChannels) + assertEquals(bob1.commitments.latest.commitmentFormat, Transactions.CommitmentFormat.SimpleTaprootChannels) + val spliceTx1 = bob1.commitments.latest.localFundingStatus.signedTx!! + // We make a second splice transaction, but don't exchange splice_locked. + val (alice2, bob2) = spliceOut(alice1, bob1, 60_000.sat) + // From Alice's point of view, we now have two unconfirmed splices, both active. + // They both send additional HTLCs, that apply to both commitments. + val (nodes3, _, _) = addHtlc(10_000_000.msat, bob2, alice2) + val (bob3, alice3) = nodes3 + val (nodes4, _, htlcOut1) = addHtlc(20_000_000.msat, alice3, bob3) + val (alice5, bob5) = crossSign(nodes4.first, nodes4.second, commitmentsCount = 3) + // Alice adds another HTLC that isn't signed by Bob. + val (nodes6, _, htlcOut2) = addHtlc(15_000_000.msat, alice5, bob5) + val (alice6, bob6) = nodes6 + val (alice7, actionsAlice7) = alice6.process(ChannelCommand.Commitment.Sign) + actionsAlice7.hasOutgoingMessage() // Bob ignores Alice's message + assertEquals(3, bob6.commitments.active.size) + assertEquals(3, alice7.commitments.active.size) + + // The first splice transaction confirms. + val (alice8, actionsAlice8) = alice7.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ChannelFundingDepthOk, 40, 2, spliceTx1))) + actionsAlice8.has() + actionsAlice8.hasWatchFundingSpent(spliceTx1.txid) + + // Bob publishes a revoked commitment for the first funding tx + val (alice9, actionsAlice9) = alice8.process(ChannelCommand.WatchReceived(WatchSpentTriggered(bob0.channelId, WatchSpent.ChannelSpent(TestConstants.fundingAmount), bobRevokedCommitTx))) + assertIs>(alice9) + assertEquals(WatchConfirmed.AlternativeCommitTxConfirmed, actionsAlice9.hasWatchConfirmed(bobRevokedCommitTx.txid).event) + + // Bob's revoked commit tx confirms. + val (alice10, actionsAlice10) = alice9.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(bob0.channelId, WatchConfirmed.AlternativeCommitTxConfirmed, 41, 7, bobRevokedCommitTx))) + assertIs>(alice10) + actionsAlice10.hasWatchConfirmed(bobRevokedCommitTx.txid).also { assertEquals(WatchConfirmed.ClosingTxConfirmed, it.event) } + val rvk = alice10.state.revokedCommitPublished.firstOrNull() + assertNotNull(rvk) + // Alice reacts by punishing Bob. + assertNotNull(rvk.localOutput) + val mainTx = actionsAlice10.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimRemoteDelayedOutputTx) + Transaction.correctlySpends(mainTx, bobRevokedCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actionsAlice10.hasWatchOutputSpent(mainTx.txIn.first().outPoint) + assertNotNull(rvk.remoteOutput) + val penaltyTx = actionsAlice10.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.MainPenaltyTx) + Transaction.correctlySpends(penaltyTx, bobRevokedCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + actionsAlice10.hasWatchOutputSpent(penaltyTx.txIn.first().outPoint) + // Alice marks every outgoing HTLC as failed, including the ones that don't appear in the revoked commitment. + val outgoingHtlcs = htlcs.aliceToBob.map { it.second }.toSet() + setOf(htlcOut1, htlcOut2) + val addSettled = actionsAlice10.filterIsInstance() + assertEquals(outgoingHtlcs, addSettled.map { it.htlc }.toSet()) + addSettled.forEach { assertEquals(it.result, ChannelAction.HtlcResult.Fail.OnChainFail(HtlcOverriddenByRemoteCommit(it.htlc.channelId, it.htlc))) } + val getHtlcInfos = actionsAlice10.find() + assertEquals(bobRevokedCommitTx.txid, getHtlcInfos.revokedCommitTxId) + // Alice claims every HTLC output from the revoked commitment. + val htlcInfos = (htlcs.aliceToBob + htlcs.bobToAlice).map { ChannelAction.Storage.HtlcInfo(bob0.channelId, getHtlcInfos.commitmentNumber, it.second.paymentHash, it.second.cltvExpiry) } + val (alice11, actionsAlice11) = alice10.process(ChannelCommand.Closing.GetHtlcInfosResponse(bobRevokedCommitTx.txid, htlcInfos)) + assertIs>(alice11) + val rvk1 = alice11.state.revokedCommitPublished.firstOrNull() + assertNotNull(rvk1) + val htlcPenaltyTxs = actionsAlice11.findPublishTxs(ChannelAction.Blockchain.PublishTx.Type.HtlcPenaltyTx) + assertEquals(htlcs.aliceToBob.size + htlcs.bobToAlice.size, rvk1.htlcOutputs.size) + assertEquals(rvk1.htlcOutputs, htlcPenaltyTxs.flatMap { tx -> tx.txIn.map { it.outPoint } }.toSet()) + htlcPenaltyTxs.forEach { Transaction.correctlySpends(it, bobRevokedCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } + actionsAlice11.hasWatchOutputsSpent(rvk1.htlcOutputs) + + // The remaining transactions confirm. + val (alice12, _) = alice11.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 0, bobRevokedCommitTx))) + val (alice13, _) = alice12.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 1, mainTx))) + val (alice14, _) = alice13.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 3, penaltyTx))) + val (alice15, _) = alice14.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 2, htlcPenaltyTxs[0]))) + val (alice16, _) = alice15.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 5, htlcPenaltyTxs[1]))) + val (alice17, _) = alice16.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 7, htlcPenaltyTxs[2]))) + assertIs(alice17.state) + val (alice18, _) = alice17.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice0.channelId, WatchConfirmed.ClosingTxConfirmed, 57, 6, htlcPenaltyTxs[3]))) + assertIs(alice18.state) + } + @Test fun `force-close -- revoked inactive commitment`() { val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) @@ -1594,8 +1736,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.SimpleTaprootChannels, zeroConf: Boolean = false): Pair, LNChannel> { + val (alice, bob) = reachNormal(channelType = channelType, zeroConf = zeroConf) return when (val fundingStatus = alice.commitments.latest.localFundingStatus) { is LocalFundingStatus.UnconfirmedFundingTx -> { val fundingTx = fundingStatus.signedTx!! @@ -1616,7 +1758,7 @@ class SpliceTestsCommon : LightningTestSuite() { requestRemoteFunding = null, currentFeeCredit = 0.msat, feerate = spliceFeerate, - origins = listOf(), + origins = listOf() ) private fun spliceOut(alice: LNChannel, bob: LNChannel, amount: Satoshi): Pair, LNChannel> { @@ -1663,7 +1805,7 @@ class SpliceTestsCommon : LightningTestSuite() { requestRemoteFunding = null, currentFeeCredit = 0.msat, feerate = spliceFeerate, - origins = listOf(), + origins = listOf() ) // Negotiate a splice transaction where Alice is the only contributor. val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) @@ -1737,7 +1879,7 @@ class SpliceTestsCommon : LightningTestSuite() { feerate = spliceFeerate, requestRemoteFunding = null, currentFeeCredit = 0.msat, - origins = listOf(), + origins = listOf() ) val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt index fddf45f9d..c61872fe9 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt @@ -55,7 +55,7 @@ class SyncingTestsCommon : LightningTestSuite() { @Test fun `reestablish channel with previous funding txs`() { - val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init() val (alice1, bob1, fundingTx) = WaitForFundingConfirmedTestsCommon.rbf(alice, bob, walletAlice) assertNotEquals(previousFundingTx.txid, fundingTx.txid) val (alice2, bob2, channelReestablishAlice, channelReestablishBob0) = disconnectWithBackup(alice1, bob1) @@ -63,13 +63,13 @@ class SyncingTestsCommon : LightningTestSuite() { assertNull(channelReestablishAlice.nextFundingTxId) val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(bob1, bob3) + assertEquals(bob1.commitments, bob3.commitments) assertEquals(1, actionsBob3.size) val channelReestablishBob = actionsBob3.hasOutgoingMessage() assertNull(channelReestablishBob.nextFundingTxId) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) - assertEquals(alice1, alice3) + assertEquals(alice1.commitments, alice3.commitments) assertTrue(actionsAlice3.isEmpty()) } @@ -350,7 +350,7 @@ class SyncingTestsCommon : LightningTestSuite() { @Test fun `recv ChannelFundingDepthOk`() { - val (alice, bob, _) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, _) = WaitForFundingConfirmedTestsCommon.init() val fundingTx = alice.state.latestFundingTx.sharedTx.tx.buildUnsignedTx() val (alice1, bob1, _) = disconnectWithBackup(alice, bob) assertIs(alice1.state.state) @@ -375,7 +375,7 @@ class SyncingTestsCommon : LightningTestSuite() { @Test fun `recv ChannelFundingDepthOk -- previous funding tx`() { - val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, previousFundingTx, walletAlice) = WaitForFundingConfirmedTestsCommon.init() val (alice1, bob1) = WaitForFundingConfirmedTestsCommon.rbf(alice, bob, walletAlice) val (alice2, bob2, _) = disconnectWithBackup(alice1, bob1) assertIs(alice2.state.state) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt index a098cfc8b..292209d52 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt @@ -30,8 +30,8 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { val txAddInput = actions1.findOutgoingMessage() assertNotEquals(txAddInput.channelId, accept.temporaryChannelId) assertEquals(alice1.channelId, txAddInput.channelId) - assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) - assertEquals(Transactions.CommitmentFormat.AnchorOutputs, alice1.state.interactiveTxSession.fundingParams.commitmentFormat) + assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, alice1.state.interactiveTxSession.fundingParams.commitmentFormat) } @Test @@ -53,14 +53,14 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { assertEquals(3, actions1.size) actions1.find() actions1.findOutgoingMessage() - assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) - assertEquals(Transactions.CommitmentFormat.AnchorOutputs, alice1.state.interactiveTxSession.fundingParams.commitmentFormat) + assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, alice1.state.interactiveTxSession.fundingParams.commitmentFormat) assertEquals(ChannelEvents.Creating(alice1.state), actions1.find().event) } @Test fun `recv AcceptChannel -- zero conf`() { - val (alice, _, accept) = init(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) + val (alice, _, accept) = init(zeroConf = true) assertEquals(0, accept.minimumDepth) val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(accept)) assertIs>(alice1) @@ -69,7 +69,7 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { assertEquals(ChannelEvents.Creating(alice1.state), actions1.find().event) actions1.findOutgoingMessage() assertEquals(alice1.state.channelFeatures, ChannelFeatures(setOf(Feature.ZeroReserveChannels, Feature.DualFunding))) - assertEquals(Transactions.CommitmentFormat.AnchorOutputs, alice1.state.interactiveTxSession.fundingParams.commitmentFormat) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, alice1.state.interactiveTxSession.fundingParams.commitmentFormat) } @Test @@ -88,7 +88,7 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { val (alice1, actions1) = alice.process(ChannelCommand.MessageReceived(accept.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.UnsupportedChannelType(Features.empty)))))) assertIs>(alice1) val error = actions1.hasOutgoingMessage() - assertEquals(error, Error(accept.temporaryChannelId, InvalidChannelType(accept.temporaryChannelId, ChannelType.SupportedChannelType.AnchorOutputs, ChannelType.UnsupportedChannelType(Features.empty)).message)) + assertEquals(error, Error(accept.temporaryChannelId, InvalidChannelType(accept.temporaryChannelId, ChannelType.SupportedChannelType.SimpleTaprootChannels, ChannelType.UnsupportedChannelType(Features.empty)).message)) } @Test @@ -189,7 +189,7 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { companion object { fun init( - channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, aliceFeatures: Features = TestConstants.Alice.nodeParams.features, bobFeatures: Features = TestConstants.Bob.nodeParams.features, currentHeight: Int = TestConstants.defaultBlockHeight, 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 787a7b711..a82cdc16b 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 @@ -21,7 +21,7 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { @Test fun `recv TxSignatures -- zero conf`() { - val (alice, _, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) + val (alice, _, bob, _) = init(zeroConf = true) val txSigsAlice = getFundingSigs(alice) val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(txSigsAlice)) assertIs(bob1.state) @@ -32,7 +32,7 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { @Test fun `recv TxSignatures and restart -- zero conf`() { - val (alice, _, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) + val (alice, _, bob, _) = init(zeroConf = true) val txSigsAlice = getFundingSigs(alice) val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(txSigsAlice)) val fundingTx = actionsBob1.find().tx @@ -53,7 +53,7 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { @Test fun `recv TxSignatures -- invalid`() { - val (alice, _, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) + val (alice, _, bob, _) = init(zeroConf = true) val invalidTxSigsAlice = getFundingSigs(alice).copy(tlvs = TlvStream.empty()) val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(invalidTxSigsAlice)) assertEquals(bob, bob1) @@ -170,7 +170,7 @@ class WaitForChannelReadyTestsCommon : LightningTestSuite() { data class Fixture(val alice: LNChannel, val channelReadyAlice: ChannelReady, val bob: LNChannel, val channelReadyBob: ChannelReady) fun init( - channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, aliceFeatures: Features = TestConstants.Alice.nodeParams.features, bobFeatures: Features = TestConstants.Bob.nodeParams.features, bobUsePeerStorage: Boolean = true, diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt index 142663938..fb0408e79 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt @@ -23,7 +23,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv TxSignatures -- duplicate`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, _) = init() val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(bob.state.latestFundingTx.sharedTx.localSigs)) assertIs(alice1.state) assertEquals(alice1.state.rbfStatus, RbfStatus.RbfAborted) @@ -33,7 +33,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelFundingDepthOk`() { - val (alice, bob, fundingTx) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, fundingTx) = init() run { val (alice1, actionsAlice1) = alice.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice.state.channelId, WatchConfirmed.ChannelFundingDepthOk, 42, 0, fundingTx))) assertIs(alice1.state) @@ -60,7 +60,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelFundingDepthOk -- rbf in progress`() { - val (alice, bob, fundingTx) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, fundingTx) = init() val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(TxInitRbf(alice.state.channelId, 0, FeeratePerKw(6000.sat), TestConstants.aliceFundingAmount))) assertIs(bob1.state) assertIs(bob1.state.rbfStatus) @@ -77,7 +77,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelFundingDepthOk -- previous funding tx`() { - val (alice, bob, previousFundingTx, walletAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, previousFundingTx, walletAlice) = init() val (alice1, bob1, fundingTx) = rbf(alice, bob, walletAlice) assertNotEquals(previousFundingTx.txid, fundingTx.txid) run { @@ -106,7 +106,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelFundingDepthOk -- after restart`() { - val (alice, bob, fundingTx) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, fundingTx) = init() run { val (alice1, _) = LNChannel(alice.ctx, WaitForInit).process(ChannelCommand.Init.Restore(alice.state)) .also { (state, actions) -> @@ -143,7 +143,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelFundingDepthOk -- after restart -- previous funding tx`() { - val (alice, bob, fundingTx1, walletAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, fundingTx1, walletAlice) = init() val (alice1, bob1, fundingTx2) = rbf(alice, bob, walletAlice) run { val (alice2, _) = LNChannel(alice.ctx, WaitForInit).process(ChannelCommand.Init.Restore(alice1.state)) @@ -189,7 +189,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv TxInitRbf`() { - val (alice, bob, _, walletAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, _, walletAlice) = init() val (alice1, bob1) = rbf(alice, bob, walletAlice) assertEquals(alice1.state.previousFundingTxs.size, 1) assertEquals(bob1.state.previousFundingTxs.size, 1) @@ -200,7 +200,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv TxInitRbf -- invalid feerate`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, _) = init() val (bob1, actions1) = bob.process(ChannelCommand.MessageReceived(TxInitRbf(alice.state.channelId, 0, TestConstants.feeratePerKw, alice.state.latestFundingTx.fundingParams.localContribution))) assertEquals(actions1.size, 1) assertEquals(actions1.hasOutgoingMessage().toAscii(), InvalidRbfFeerate(alice.state.channelId, TestConstants.feeratePerKw, TestConstants.feeratePerKw * 25 / 24).message) @@ -211,7 +211,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv TxInitRbf -- failed rbf attempt`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, _) = init() val (bob1, actions1) = bob.process(ChannelCommand.MessageReceived(TxInitRbf(alice.state.channelId, 0, TestConstants.feeratePerKw * 1.25, alice.state.latestFundingTx.fundingParams.localContribution))) assertIs(bob1.state) assertIs(bob1.state.rbfStatus) @@ -230,7 +230,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelReady`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, _) = init() val channelReadyAlice = ChannelReady(alice.state.channelId, randomKey().publicKey()) val channelReadyBob = ChannelReady(bob.state.channelId, randomKey().publicKey()) val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(channelReadyBob)) @@ -245,7 +245,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelReady -- no remote contribution`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (alice, bob, _) = init(bobFundingAmount = 0.sat) val channelReadyAlice = ChannelReady(alice.state.channelId, randomKey().publicKey()) val channelReadyBob = ChannelReady(bob.state.channelId, randomKey().publicKey()) val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(channelReadyBob)) @@ -260,7 +260,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv Error`() { - val (_, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (_, bob) = init() val (bob1, actions1) = bob.process(ChannelCommand.MessageReceived(Error(bob.state.channelId, "oops"))) assertIs(bob1.state) assertNotNull(bob1.state.localCommitPublished) @@ -274,7 +274,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv Error -- previous funding tx confirms`() { - val (alice, bob, previousFundingTx, walletAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, previousFundingTx, walletAlice) = init() val commitTxAlice1 = alice.signCommitTx() Transaction.correctlySpends(commitTxAlice1, listOf(previousFundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val commitTxBob1 = bob.signCommitTx() @@ -341,7 +341,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Close_MutualClose`() = runSuspendTest { - val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob) = init() listOf(alice, bob).forEach { state -> val cmd = ChannelCommand.Close.MutualClose(CompletableDeferred(), null, TestConstants.feeratePerKw) val (state1, actions1) = state.process(cmd) @@ -353,7 +353,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Close_ForceClose`() { - val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob) = init() listOf(alice, bob).forEach { state -> val (state1, actions1) = state.process(ChannelCommand.Close.ForceClose) assertIs(state1.state) @@ -369,7 +369,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv CheckHtlcTimeout`() { - val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob) = init() listOf(alice, bob).forEach { state -> run { val (state1, actions1) = state.process(ChannelCommand.Commitment.CheckHtlcTimeout) @@ -381,7 +381,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv Disconnected`() { - val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob) = init() val (alice1, actionsAlice1) = alice.process(ChannelCommand.Disconnected) assertIs(alice1.state) assertTrue(actionsAlice1.isEmpty()) @@ -394,7 +394,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { data class Fixture(val alice: LNChannel, val bob: LNChannel, val fundingTx: Transaction, val walletAlice: List) fun init( - channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, aliceFeatures: Features = TestConstants.Alice.nodeParams.features.initFeatures(), bobFeatures: Features = TestConstants.Bob.nodeParams.features.initFeatures(), bobUsePeerStorage: Boolean = true, 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 93869269c..d8e14eb60 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 @@ -40,7 +40,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `complete interactive-tx protocol`() = runSuspendTest { - val (alice, bob, inputAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (alice, bob, inputAlice) = init(bobFundingAmount = 0.sat) // Alice ---- tx_add_input ----> Bob val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(inputAlice)) // Alice <--- tx_complete ----- Bob @@ -60,8 +60,8 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { actionsBob3.has() assertIs(alice2.state) assertIs(bob3.state) - assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) - assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) + assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) assertIs(alice.state.replyTo.await()).also { assertEquals(0, it.fundingTxIndex) } assertIs(bob.state.replyTo.await()).also { assertEquals(0, it.fundingTxIndex) } verifyCommits(alice2.state.signingSession, bob3.state.signingSession, TestConstants.aliceFundingAmount.toMilliSatoshi(), 0.msat) @@ -69,7 +69,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `complete interactive-tx protocol -- with non-initiator contributions`() { - val (alice, bob, inputAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs) + val (alice, bob, inputAlice) = init() // Alice ---- tx_add_input ----> Bob val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(inputAlice)) // Alice <--- tx_add_input ----- Bob @@ -87,8 +87,8 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { actionsBob3.has() assertIs(alice2.state) assertIs(bob3.state) - assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) - assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) + assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) verifyCommits( alice2.state.signingSession, bob3.state.signingSession, @@ -100,7 +100,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `complete interactive-tx protocol -- with large non-initiator contributions`() { // Alice's funding amount is below the channel reserve: this is ok as long as she can pay the commit tx fees. - val (alice, bob, inputAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs, aliceFundingAmount = 10_000.sat, bobFundingAmount = 1_500_000.sat) + val (alice, bob, inputAlice) = init(aliceFundingAmount = 10_000.sat, bobFundingAmount = 1_500_000.sat) // Alice ---- tx_add_input ----> Bob val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(inputAlice)) // Alice <--- tx_add_input ----- Bob @@ -118,14 +118,14 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { actionsBob3.has() assertIs(alice2.state) assertIs(bob3.state) - assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) - assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) + assertEquals(alice2.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) + assertEquals(bob3.state.channelParams.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) verifyCommits(alice2.state.signingSession, bob3.state.signingSession, balanceAlice = 10_000_000.msat, balanceBob = 1_500_000_000.msat) } @Test fun `complete interactive-tx protocol -- zero conf -- zero reserve`() { - val (alice, bob, inputAlice) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) + val (alice, bob, inputAlice) = init(zeroConf = true) // Alice ---- tx_add_input ----> Bob val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(inputAlice)) // Alice <--- tx_add_input ----- Bob @@ -150,7 +150,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv invalid interactive-tx message`() { - val (_, bob, inputAlice) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (_, bob, inputAlice) = init(bobFundingAmount = 0.sat) run { // Invalid serial_id. val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(inputAlice.copy(serialId = 1))) @@ -168,7 +168,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv CommitSig`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (alice, bob, _) = init(bobFundingAmount = 0.sat) run { val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(CommitSig(alice.channelId, ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes), listOf()))) assertEquals(actionsAlice1.findOutgoingMessage().toAscii(), UnexpectedCommitSig(alice.channelId).message) @@ -183,7 +183,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv TxSignatures`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (alice, bob, _) = init(bobFundingAmount = 0.sat) run { val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(TxSignatures(alice.channelId, TxId(randomBytes32()), listOf()))) assertEquals(actionsAlice1.findOutgoingMessage().toAscii(), UnexpectedFundingSignatures(alice.channelId).message) @@ -198,7 +198,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv TxAbort`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (alice, bob, _) = init(bobFundingAmount = 0.sat) run { val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "changed my mind"))) assertEquals(actionsAlice1.size, 1) @@ -215,7 +215,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv TxInitRbf`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (alice, bob, _) = init(bobFundingAmount = 0.sat) run { val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(TxInitRbf(alice.channelId, 0, FeeratePerKw(7500.sat)))) assertEquals(actionsAlice1.size, 1) @@ -232,7 +232,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv TxAckRbf`() { - val (alice, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (alice, bob, _) = init(bobFundingAmount = 0.sat) run { val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(TxAckRbf(alice.channelId))) assertEquals(actionsAlice1.size, 1) @@ -249,7 +249,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv Error`() = runSuspendTest { - val (_, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (_, bob, _) = init(bobFundingAmount = 0.sat) val (bob1, actions1) = bob.process(ChannelCommand.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) assertIs(bob1.state) assertTrue(actions1.isEmpty()) @@ -258,7 +258,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv ChannelCommand_Close_ForceClose`() { - val (_, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (_, bob, _) = init(bobFundingAmount = 0.sat) val (bob1, actions1) = bob.process(ChannelCommand.Close.ForceClose) assertEquals(actions1.findOutgoingMessage().toAscii(), ForcedLocalCommit(bob.channelId).message) assertIs(bob1.state) @@ -266,7 +266,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv Disconnected`() = runSuspendTest { - val (_, bob, txAddInput) = init(ChannelType.SupportedChannelType.AnchorOutputs, bobFundingAmount = 0.sat) + val (_, bob, txAddInput) = init(bobFundingAmount = 0.sat) val (bob1, _) = bob.process(ChannelCommand.MessageReceived(txAddInput)) assertIs(bob1.state) val (bob2, actions2) = bob1.process(ChannelCommand.Disconnected) @@ -279,7 +279,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { data class Fixture(val alice: LNChannel, val bob: LNChannel, val aliceInput: TxAddInput, val aliceWallet: List) fun init( - channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, aliceFeatures: Features = TestConstants.Alice.nodeParams.features.initFeatures(), bobFeatures: Features = TestConstants.Bob.nodeParams.features.initFeatures(), bobUsePeerStorage: Boolean = true, 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 95a2217fc..d5f5127fa 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 @@ -1,6 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes import fr.acinq.lightning.Lightning.randomBytes32 @@ -44,7 +45,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { @Test fun `recv CommitSig -- zero conf`() { - val (alice, commitSigAlice, bob, commitSigBob) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) + val (alice, commitSigAlice, bob, commitSigBob) = init(zeroConf = true) run { alice.process(ChannelCommand.MessageReceived(commitSigBob)).also { (state, actions) -> assertIs(state.state) @@ -115,15 +116,16 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { @Test fun `recv CommitSig -- with invalid signature`() { val (alice, commitSigAlice, bob, commitSigBob) = init() + val dummySig = ChannelSpendSignature.PartialSignatureWithNonce(randomBytes32(), IndividualNonce(randomBytes(66))) run { - val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitSigBob.copy(signature = ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes)))) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitSigBob.copy(tlvStream = TlvStream(CommitSigTlv.PartialSignatureWithNonce(dummySig))))) assertEquals(actionsAlice1.size, 2) actionsAlice1.hasOutgoingMessage() actionsAlice1.find().also { assertEquals(alice.channelId, it.data.channelId) } assertIs(alice1.state) } run { - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice.copy(signature = ChannelSpendSignature.IndividualSignature(ByteVector64.Zeroes)))) + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice.copy(tlvStream = TlvStream(CommitSigTlv.PartialSignatureWithNonce(dummySig))))) assertEquals(actionsBob1.size, 2) actionsBob1.hasOutgoingMessage() actionsBob1.find().also { assertEquals(bob.channelId, it.data.channelId) } @@ -193,7 +195,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { @Test fun `recv TxSignatures -- zero-conf`() { - val (alice, commitSigAlice, bob, commitSigBob) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) + val (alice, commitSigAlice, bob, commitSigBob) = init(zeroConf = true) val txSigsBob = run { val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) assertIs(bob1.state) @@ -325,7 +327,7 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { data class Fixture(val alice: LNChannel, val commitSigAlice: CommitSig, val bob: LNChannel, val commitSigBob: CommitSig, val walletAlice: List) fun init( - channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.SimpleTaprootChannels, aliceFeatures: Features = TestConstants.Alice.nodeParams.features, bobFeatures: Features = TestConstants.Bob.nodeParams.features, bobUsePeerStorage: Boolean = true, diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt index 21400371a..93602f2b2 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt @@ -23,13 +23,13 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel -- without wumbo`() { val (_, bob, open) = TestsHelper.init(aliceFeatures = TestConstants.Alice.nodeParams.features.remove(Feature.Wumbo)) - assertEquals(open.tlvStream.get(), ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs)) + assertEquals(open.tlvStream.get(), ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.SimpleTaprootChannels)) val (bob1, actions) = bob.process(ChannelCommand.MessageReceived(open)) assertIs>(bob1) assertEquals(3, actions.size) assertTrue(bob1.state.channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) - assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) - assertEquals(Transactions.CommitmentFormat.AnchorOutputs, bob1.state.interactiveTxSession.fundingParams.commitmentFormat) + assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, bob1.state.interactiveTxSession.fundingParams.commitmentFormat) actions.hasOutgoingMessage() actions.has() assertEquals(ChannelEvents.Creating(bob1.state), actions.find().event) @@ -41,8 +41,8 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { val (bob1, actions) = bob.process(ChannelCommand.MessageReceived(open)) assertIs>(bob1) assertEquals(3, actions.size) - assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding))) - assertEquals(Transactions.CommitmentFormat.AnchorOutputs, bob1.state.interactiveTxSession.fundingParams.commitmentFormat) + assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.DualFunding, Feature.ZeroReserveChannels))) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, bob1.state.interactiveTxSession.fundingParams.commitmentFormat) actions.hasOutgoingMessage() actions.has() assertEquals(ChannelEvents.Creating(bob1.state), actions.find().event) @@ -50,13 +50,13 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel -- zero conf -- zero reserve`() { - val (_, bob, open) = TestsHelper.init(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, zeroConf = true) + val (_, bob, open) = TestsHelper.init(zeroConf = true) val (bob1, actions) = bob.process(ChannelCommand.MessageReceived(open)) assertIs>(bob1) assertEquals(3, actions.size) assertTrue(bob1.state.channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) assertEquals(bob1.state.channelFeatures, ChannelFeatures(setOf(Feature.ZeroReserveChannels, Feature.DualFunding))) - assertEquals(Transactions.CommitmentFormat.AnchorOutputs, bob1.state.interactiveTxSession.fundingParams.commitmentFormat) + assertEquals(Transactions.CommitmentFormat.SimpleTaprootChannels, bob1.state.interactiveTxSession.fundingParams.commitmentFormat) val accept = actions.hasOutgoingMessage() assertEquals(0, accept.minimumDepth) actions.has() @@ -81,7 +81,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.ChannelTypeTlv(unsupportedChannelType))) val (bob1, actions) = bob.process(ChannelCommand.MessageReceived(open1)) val error = actions.findOutgoingMessage() - assertEquals(error, Error(open.temporaryChannelId, InvalidChannelType(open.temporaryChannelId, ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, unsupportedChannelType).message)) + assertEquals(error, Error(open.temporaryChannelId, InvalidChannelType(open.temporaryChannelId, ChannelType.SupportedChannelType.SimpleTaprootChannels, unsupportedChannelType).message)) assertIs>(bob1) } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt index 4d021c487..92c38fbd8 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt @@ -28,7 +28,6 @@ import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.io.peer.* import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.tests.utils.runSuspendTest -import fr.acinq.lightning.tests.utils.testLoggerFactory import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat @@ -66,7 +65,7 @@ class PeerTest : LightningTestSuite() { randomKey().publicKey(), randomKey().publicKey(), ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false), - TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputsZeroReserve)) + TlvStream(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.SimpleTaprootChannels)) ) @Test @@ -129,7 +128,7 @@ class PeerTest : LightningTestSuite() { val (alice, bob, alice2bob, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false) val wallet = createWallet(nodeParams.first.keyManager, 300_000.sat).second - alice.send(OpenChannel(250_000.sat, wallet, FeeratePerKw(3000.sat), FeeratePerKw(2500.sat), ChannelType.SupportedChannelType.AnchorOutputsZeroReserve)) + alice.send(OpenChannel(250_000.sat, wallet, FeeratePerKw(3000.sat), FeeratePerKw(2500.sat))) val open = alice2bob.expect() bob.forward(open) @@ -140,12 +139,12 @@ class PeerTest : LightningTestSuite() { val txAddInput = alice2bob.expect() assertNotEquals(txAddInput.channelId, open.temporaryChannelId) // we now have the final channel_id bob.forward(txAddInput) - val txCompleteBob = bob2alice.expect() - alice.forward(txCompleteBob) + val txCompleteBob1 = bob2alice.expect() + alice.forward(txCompleteBob1) val txAddOutput = alice2bob.expect() bob.forward(txAddOutput) - bob2alice.expect() - alice.forward(txCompleteBob) + val txCompleteBob2 = bob2alice.expect() + alice.forward(txCompleteBob2) val txCompleteAlice = alice2bob.expect() bob.forward(txCompleteAlice) val commitSigBob = bob2alice.expect() @@ -189,7 +188,7 @@ class PeerTest : LightningTestSuite() { val (alice, bob, alice2bob, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false) val wallet = createWallet(nodeParams.first.keyManager, 300_000.sat).second - alice.send(OpenChannel(250_000.sat, wallet, FeeratePerKw(3000.sat), FeeratePerKw(2500.sat), ChannelType.SupportedChannelType.AnchorOutputsZeroReserve)) + alice.send(OpenChannel(250_000.sat, wallet, FeeratePerKw(3000.sat), FeeratePerKw(2500.sat))) val open = alice2bob.expect() bob.forward(open) @@ -200,12 +199,12 @@ class PeerTest : LightningTestSuite() { val txAddInput = alice2bob.expect() assertNotEquals(txAddInput.channelId, open.temporaryChannelId) // we now have the final channel_id bob.forward(txAddInput) - val txCompleteBob = bob2alice.expect() - alice.forward(txCompleteBob) + val txCompleteBob1 = bob2alice.expect() + alice.forward(txCompleteBob1) val txAddOutput = alice2bob.expect() bob.forward(txAddOutput) - bob2alice.expect() - alice.forward(txCompleteBob) + val txCompleteBob2 = bob2alice.expect() + alice.forward(txCompleteBob2) val txCompleteAlice = alice2bob.expect() bob.forward(txCompleteAlice) val commitSigBob = bob2alice.expect() @@ -242,7 +241,7 @@ class PeerTest : LightningTestSuite() { assertTrue(open.fundingAmount < 500_000.sat) // we pay the mining fees assertTrue(open.channelFlags.nonInitiatorPaysCommitFees) assertEquals(open.requestFunding?.requestedAmount, 100_000.sat) // we always request funds from the remote, because we ask them to pay the commit tx fees - assertEquals(open.channelType, ChannelType.SupportedChannelType.AnchorOutputsZeroReserve) + assertEquals(open.channelType, ChannelType.SupportedChannelType.SimpleTaprootChannels) // We cannot test the rest of the flow as lightning-kmp doesn't implement the LSP side that responds to the liquidity ads request. } @@ -367,8 +366,9 @@ class PeerTest : LightningTestSuite() { .first { it.size == 1 } .values .first() + assertIs(restoredChannel) assertEquals(bob1.state, restoredChannel) - assertEquals(peer.db.channels.listLocalChannels(), listOf(restoredChannel)) + assertEquals(peer.db.channels.listLocalChannels(), listOf(restoredChannel.copy(remoteNextCommitNonces = mapOf()))) } @Test @@ -398,8 +398,9 @@ class PeerTest : LightningTestSuite() { .first { it.size == 1 && it.values.first() is Normal } .values .first() + assertIs(restoredChannel) assertEquals(bob1.state, restoredChannel) - assertEquals(peer.db.channels.listLocalChannels(), listOf(restoredChannel)) + assertEquals(peer.db.channels.listLocalChannels(), listOf(restoredChannel.copy(remoteNextCommitNonces = mapOf()))) } @Test diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt index 5ef6633ce..2170bff67 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/OutgoingPaymentHandlerTestsCommon.kt @@ -510,8 +510,8 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { @Test fun `insufficient funds when retrying with higher fees`() = runSuspendTest { val (alice, _) = TestsHelper.reachNormal(aliceFundingAmount = 100_000.sat, bobFundingAmount = 0.sat) - assertTrue(83_500_000.msat < alice.commitments.availableBalanceForSend()) - assertTrue(alice.commitments.availableBalanceForSend() < 84_000_000.msat) + assertTrue(86_000_000.msat < alice.commitments.availableBalanceForSend()) + assertTrue(alice.commitments.availableBalanceForSend() < 86_500_000.msat) val walletParams = defaultWalletParams.copy( trampolineFees = listOf( TrampolineFees(100.sat, 0, CltvExpiryDelta(144)), @@ -520,12 +520,12 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { ) val outgoingPaymentHandler = OutgoingPaymentHandler(TestConstants.Alice.nodeParams, walletParams, InMemoryPaymentsDb()) val invoice = makeInvoice(amount = null, supportsTrampoline = true) - val payment = PayInvoice(UUID.randomUUID(), 83_000_000.msat, LightningOutgoingPayment.Details.Normal(invoice)) + val payment = PayInvoice(UUID.randomUUID(), 86_000_000.msat, LightningOutgoingPayment.Details.Normal(invoice)) val progress = outgoingPaymentHandler.sendPayment(payment, mapOf(alice.channelId to alice.state), TestConstants.defaultBlockHeight) assertIs(progress) val (_, add1) = findAddHtlcCommand(progress) - assertEquals(83_100_000.msat, add1.amount) + assertEquals(86_100_000.msat, add1.amount) val attempt = outgoingPaymentHandler.getPendingPayment(payment.paymentId)!! val fail = outgoingPaymentHandler.processAddSettledFailed(alice.channelId, createRemoteFailure(add1, attempt, TrampolineFeeInsufficient), mapOf(alice.channelId to alice.state), TestConstants.defaultBlockHeight) 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 a66bc9c0e..1ec65224b 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 @@ -29,11 +29,11 @@ class StateSerializationTestsCommon : LightningTestSuite() { val (alice, bob) = TestsHelper.reachNormal() val bytes = Serialization.serialize(alice.state) val check = Serialization.deserialize(bytes).value - assertEquals(alice.state, check) + assertEquals(alice.state.copy(remoteNextCommitNonces = mapOf(), localCloseeNonce = null, localCloserNonces = null), check) val bytes1 = Serialization.serialize(bob.state) val check1 = Serialization.deserialize(bytes1).value - assertEquals(bob.state, check1) + assertEquals(bob.state.copy(remoteNextCommitNonces = mapOf(), localCloseeNonce = null, localCloserNonces = null), check1) } @Test diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt index fbb1acbc8..57fc10fb9 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/tests/TestConstants.kt @@ -76,6 +76,7 @@ object TestConstants { Feature.Wumbo to FeatureSupport.Optional, Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, + Feature.SimpleTaprootChannels to FeatureSupport.Optional, Feature.RouteBlinding to FeatureSupport.Optional, Feature.DualFunding to FeatureSupport.Mandatory, Feature.ShutdownAnySegwit to FeatureSupport.Mandatory, 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 14ee6029b..832657a43 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 @@ -6,6 +6,7 @@ import fr.acinq.bitcoin.Script.pay2wpkh import fr.acinq.bitcoin.Script.write import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.bitcoin.utils.Either +import fr.acinq.bitcoin.utils.flatMap import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Lightning.randomBytes32 @@ -106,6 +107,7 @@ class TransactionsTestsCommon : LightningTestSuite() { assertTrue(actual <= expected + 2, "actual=$actual, expected=$expected") assertTrue(actual >= expected - 2, "actual=$actual, expected=$expected") } + Transactions.CommitmentFormat.SimpleTaprootChannels -> assertEquals(expected, actual) } } @@ -147,6 +149,9 @@ class TransactionsTestsCommon : LightningTestSuite() { toLocal = 400.mbtc.toMilliSatoshi(), toRemote = 300.mbtc.toMilliSatoshi() ) + val (secretLocalNonce, publicLocalNonce) = Musig2.generateNonce(randomBytes32(), Either.Left(localFundingPriv), listOf(localFundingPriv.publicKey()), null, null) + val (secretRemoteNonce, publicRemoteNonce) = Musig2.generateNonce(randomBytes32(), Either.Left(remoteFundingPriv), listOf(remoteFundingPriv.publicKey()), null, null) + val publicNonces = listOf(publicLocalNonce, publicRemoteNonce) val commitTxNumber = 0x404142434445L val commitTxOutputs = Transactions.makeCommitTxOutputs(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localKeys.publicKeys, payCommitTxFees = true, localDustLimit, toLocalDelay, commitmentFormat, spec) @@ -161,6 +166,11 @@ class TransactionsTestsCommon : LightningTestSuite() { assertFalse(txInfo.checkRemoteSig(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), invalidRemoteSig)) txInfo.aggregateSigs(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localSig, remoteSig) } + Transactions.CommitmentFormat.SimpleTaprootChannels -> { + val localPartialSig = txInfo.partialSign(localFundingPriv, remoteFundingPriv.publicKey(), mapOf(), Transactions.LocalNonce(secretLocalNonce, publicLocalNonce), publicNonces).right!! + val remotePartialSig = txInfo.partialSign(remoteFundingPriv, localFundingPriv.publicKey(), mapOf(), Transactions.LocalNonce(secretRemoteNonce, publicRemoteNonce), publicNonces).right!! + txInfo.aggregateSigs(localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localPartialSig, remotePartialSig, mapOf()).right!! + } } Transaction.correctlySpends(commitTx, listOf(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // We check the expected weight of the commit input: @@ -171,7 +181,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val htlcSuccessTxs = htlcTxs.filterIsInstance() val htlcTimeoutTxs = htlcTxs.filterIsInstance() when (commitmentFormat) { - Transactions.CommitmentFormat.AnchorOutputs -> { + Transactions.CommitmentFormat.AnchorOutputs, Transactions.CommitmentFormat.SimpleTaprootChannels -> { assertEquals(5, htlcTxs.size) assertEquals(mapOf(0L to 300L, 1L to 310L, 2L to 310L, 3L to 295L, 4L to 300L), expiries) assertEquals(2, htlcTimeoutTxs.size) @@ -213,7 +223,12 @@ class TransactionsTestsCommon : LightningTestSuite() { Transaction.correctlySpends(signedTx, listOf(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // local detects when remote doesn't use the right sighash flags val invalidSighash = when (commitmentFormat) { - Transactions.CommitmentFormat.AnchorOutputs -> listOf(SigHash.SIGHASH_ALL, SigHash.SIGHASH_ALL or SigHash.SIGHASH_ANYONECANPAY, SigHash.SIGHASH_SINGLE, SigHash.SIGHASH_NONE) + Transactions.CommitmentFormat.AnchorOutputs, Transactions.CommitmentFormat.SimpleTaprootChannels -> listOf( + SigHash.SIGHASH_ALL, + SigHash.SIGHASH_ALL or SigHash.SIGHASH_ANYONECANPAY, + SigHash.SIGHASH_SINGLE, + SigHash.SIGHASH_NONE + ) } invalidSighash.forEach { sighash -> val invalidRemoteSig = htlcTimeoutTx.sign(remoteKeys.ourHtlcKey, sighash, htlcTimeoutTx.redeemInfo(remoteKeys.publicKeys), mapOf()) @@ -241,7 +256,12 @@ class TransactionsTestsCommon : LightningTestSuite() { assertTrue(htlcSuccessTx.checkRemoteSig(localKeys, remoteSig)) // local detects when remote doesn't use the right sighash flags val invalidSighash = when (commitmentFormat) { - Transactions.CommitmentFormat.AnchorOutputs -> listOf(SigHash.SIGHASH_ALL, SigHash.SIGHASH_ALL or SigHash.SIGHASH_ANYONECANPAY, SigHash.SIGHASH_SINGLE, SigHash.SIGHASH_NONE) + Transactions.CommitmentFormat.AnchorOutputs, Transactions.CommitmentFormat.SimpleTaprootChannels -> listOf( + SigHash.SIGHASH_ALL, + SigHash.SIGHASH_ALL or SigHash.SIGHASH_ANYONECANPAY, + SigHash.SIGHASH_SINGLE, + SigHash.SIGHASH_NONE + ) } invalidSighash.forEach { sighash -> val invalidRemoteSig = htlcSuccessTx.sign(remoteKeys.ourHtlcKey, sighash, htlcSuccessTx.redeemInfo(remoteKeys.publicKeys), mapOf()) @@ -315,7 +335,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val skipped = penaltyTxs.mapNotNull { it.left } val claimed = penaltyTxs.mapNotNull { it.right?.sign()?.tx } when (commitmentFormat) { - Transactions.CommitmentFormat.AnchorOutputs -> { + Transactions.CommitmentFormat.AnchorOutputs, Transactions.CommitmentFormat.SimpleTaprootChannels -> { assertEquals(5, penaltyTxs.size) assertEquals(2, skipped.size) assertEquals(setOf(Transactions.TxGenerationSkipped.AmountBelowDustLimit), skipped.toSet()) @@ -350,6 +370,11 @@ class TransactionsTestsCommon : LightningTestSuite() { testCommitAndHtlcTxs(Transactions.CommitmentFormat.AnchorOutputs) } + @Test + fun `generate valid commitment and htlc transactions -- simple taproot channels`() { + testCommitAndHtlcTxs(Transactions.CommitmentFormat.SimpleTaprootChannels) + } + @Test fun `spend 2-of-2 legacy swap-in`() { val userWallet = TestConstants.Alice.keyManager.swapInOnChainWallet 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 31baaa6cb..e5ef7c88f 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 @@ -451,21 +451,21 @@ class LightningCodecsTestsCommon : LightningTestSuite() { fun `encode - decode commit_sig`() { val channelId = ByteVector32.fromValidHex("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25") val signature = ChannelSpendSignature.IndividualSignature(ByteVector64.fromValidHex("05e06d9a8fdfbb3625051ff2e3cdf82679cc2268beee6905941d6dd8a067cd62711e04b119a836aa0eebe07545172cefb228860fea6c797178453a319169bed7")) - val alternateSigs = listOf( - CommitSigTlv.AlternativeFeerateSig(FeeratePerKw(253.sat), ByteVector64.fromValidHex("c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3")), - CommitSigTlv.AlternativeFeerateSig(FeeratePerKw(500.sat), ByteVector64.fromValidHex("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5")), - CommitSigTlv.AlternativeFeerateSig(FeeratePerKw(750.sat), ByteVector64.fromValidHex("83a7a1a04141ac8ab2818f4a872ea86716ef9aac0852146bcdbc2cc49aecc985899a63513f41ed2502a321a4945689239d12bdab778c1a2e8bf7c3f19ec53b58")), + val partialSig = ChannelSpendSignature.PartialSignatureWithNonce( + ByteVector32("034ad8ca7bed68a934b633c4beeb7dc493cb0ff70e7aa9c86b895bbf3a3b5f82"), + IndividualNonce("a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d") ) val testCases = listOf( // @formatter:off - CommitSig(channelId, signature, listOf(), TlvStream(CommitSigTlv.AlternativeFeerateSigs(alternateSigs))) to "00842dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db2505e06d9a8fdfbb3625051ff2e3cdf82679cc2268beee6905941d6dd8a067cd62711e04b119a836aa0eebe07545172cefb228860fea6c797178453a319169bed70000fe47010001cd03000000fdc49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3000001f42dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5000002ee83a7a1a04141ac8ab2818f4a872ea86716ef9aac0852146bcdbc2cc49aecc985899a63513f41ed2502a321a4945689239d12bdab778c1a2e8bf7c3f19ec53b58", + CommitSig(channelId, signature, listOf()) to "0084 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 05e06d9a8fdfbb3625051ff2e3cdf82679cc2268beee6905941d6dd8a067cd62711e04b119a836aa0eebe07545172cefb228860fea6c797178453a319169bed7 0000", + CommitSig(channelId, partialSig, listOf(), batchSize = 1) to "0084 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000 0262034ad8ca7bed68a934b633c4beeb7dc493cb0ff70e7aa9c86b895bbf3a3b5f82a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d", // @formatter:on ) testCases.forEach { (commitSig, bin) -> val decoded = LightningMessage.decode(Hex.decode(bin)) assertEquals(decoded, commitSig) val encoded = LightningMessage.encode(commitSig) - assertEquals(Hex.encode(encoded), bin) + assertContentEquals(encoded, Hex.decode(bin)) } } @@ -491,7 +491,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TxSignaturesTlv.PartialSignature(ByteVector32("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), nonces[0], nonces[1]), TxSignaturesTlv.PartialSignature(ByteVector32("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), nonces[2], nonces[3]) ) - val signature = ByteVector64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + val signature = ChannelSpendSignature.IndividualSignature(ByteVector64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) // This is a random mainnet transaction. val tx1 = Transaction.read( "020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000" @@ -559,13 +559,15 @@ class LightningCodecsTestsCommon : LightningTestSuite() { SpliceInit(channelId, 100_000.sat, FeeratePerKw(2500.sat), 100, fundingPubkey) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), SpliceInit(channelId, 0.sat, FeeratePerKw(500.sat), 0, fundingPubkey) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500.sat), 0, fundingPubkey) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), - SpliceInit(channelId, 100_000.sat, FeeratePerKw(2500.sat), 100, fundingPubkey, LiquidityAds.RequestFunding(100_000.sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance)) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000"), + SpliceInit(channelId, 100_000.sat, FeeratePerKw(2500.sat), 100, fundingPubkey, LiquidityAds.RequestFunding(100_000.sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance), channelType = null) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000"), + SpliceInit(channelId, 100_000.sat, FeeratePerKw(2500.sat), 100, fundingPubkey, null, ChannelType.SupportedChannelType.SimpleTaprootChannels) to ByteVector("9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c400000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe 47000011 471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000"), SpliceAck(channelId, 25_000.sat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), SpliceAck(channelId, 0.sat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), SpliceAck(channelId, (-25_000).sat, fundingPubkey) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff9e58 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), - SpliceAck(channelId, 25_000.sat, fundingPubkey, LiquidityAds.WillFund(fundingRate, ByteVector("deadbeef"), ByteVector64.Zeroes)) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + SpliceAck(channelId, 25_000.sat, fundingPubkey, LiquidityAds.WillFund(fundingRate, ByteVector("deadbeef"), ByteVector64.Zeroes), channelType = null) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), SpliceAck(channelId, 25_000.sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(0.msat))) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda05200"), SpliceAck(channelId, 25_000.sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(1729.msat))) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda0520206c1"), + SpliceAck(channelId, 25_000.sat, fundingPubkey, null, ChannelType.SupportedChannelType.SimpleTaprootChannels) to ByteVector("908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe 47000011 471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000"), SpliceLocked(channelId, fundingTxId) to ByteVector("908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566"), // @formatter:on ) @@ -711,6 +713,14 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val sig1 = ByteVector64("01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") val sig2 = ByteVector64("02020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") val sig3 = ByteVector64("03030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") + + val partialSig1 = ByteVector32("0101010101010101010101010101010101010101010101010101010101010101") + val nonce1 = IndividualNonce("52682593fd0783ea60657ed2d118e8f958c4a7a198237749b6729eccf963be1bc559531ec4b83bcfc42009cd08f7e95747146cec2fd09571b3fa76656e3012a4c97a") + val partialSig2 = ByteVector32("0202020202020202020202020202020202020202020202020202020202020202") + val nonce2 = IndividualNonce("585b2fe8ca7a969bbda11ee9cbc95386abfddcc901967f84da4011c2a7cb5ada1dae51bdcd93a8b2933fcec7b2cda5a3f43ea2d0a29eb126bd329d4735d5389fe703") + val partialSig3 = ByteVector32("0303030303030303030303030303030303030303030303030303030303030303") + val nonce3 = IndividualNonce("19bed0825ceb5acf504cddea72e37a75505290a22850c183725963edfe2dfb9f26e27180b210c05635987b80b3de3b7d01732653565b9f25ec23f7aff26122e00bff") + val closerScript = Hex.decode("deadbeef").byteVector() val closeeScript = Hex.decode("d43db3ef1234").byteVector() val testCases = mapOf( @@ -720,11 +730,15 @@ class LightningCodecsTestsCommon : LightningTestSuite() { Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserAndCloseeOutputs(sig1))), Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserOutputOnly(sig1), ClosingCompleteTlv.CloserAndCloseeOutputs(sig2))), Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserOutputOnly(sig1), ClosingCompleteTlv.CloseeOutputOnly(sig2), ClosingCompleteTlv.CloserAndCloseeOutputs(sig3))), + Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 06620202020202020202020202020202020202020202020202020202020202020202585b2fe8ca7a969bbda11ee9cbc95386abfddcc901967f84da4011c2a7cb5ada1dae51bdcd93a8b2933fcec7b2cda5a3f43ea2d0a29eb126bd329d4735d5389fe703") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(ChannelSpendSignature.PartialSignatureWithNonce(partialSig2, nonce2)))), + Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 0562010101010101010101010101010101010101010101010101010101010101010152682593fd0783ea60657ed2d118e8f958c4a7a198237749b6729eccf963be1bc559531ec4b83bcfc42009cd08f7e95747146cec2fd09571b3fa76656e3012a4c97a 0762030303030303030303030303030303030303030303030303030303030303030319bed0825ceb5acf504cddea72e37a75505290a22850c183725963edfe2dfb9f26e27180b210c05635987b80b3de3b7d01732653565b9f25ec23f7aff26122e00bff") to ClosingComplete(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserOutputOnlyPartialSignature(ChannelSpendSignature.PartialSignatureWithNonce(partialSig1, nonce1)), ClosingCompleteTlv.CloserAndCloseeOutputsPartialSignature(ChannelSpendSignature.PartialSignatureWithNonce(partialSig3, nonce3)))), Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0), Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloseeOutputOnly(sig1))), Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloserAndCloseeOutputs(sig1))), Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloserOutputOnly(sig1), ClosingSigTlv.CloserAndCloseeOutputs(sig2))), Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloserOutputOnly(sig1), ClosingSigTlv.CloseeOutputOnly(sig2), ClosingSigTlv.CloserAndCloseeOutputs(sig3))), + Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 05200101010101010101010101010101010101010101010101010101010101010101") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloserOutputOnlyPartialSignature(partialSig1))), + Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0004deadbeef 0006d43db3ef1234 0000000000000451 00000000 06200202020202020202020202020202020202020202020202020202020202020202 07200303030303030303030303030303030303030303030303030303030303030303") to ClosingSig(channelId, closerScript, closeeScript, 1105.sat, 0, TlvStream(ClosingSigTlv.CloseeOutputOnlyPartialSignature(partialSig2), ClosingSigTlv.CloserAndCloseeOutputsPartialSignature(partialSig3))), // @formatter:on ) testCases.forEach { @@ -923,5 +937,5 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val msg = DNSAddressResponse(Chain.Testnet3.chainHash, "foo@bar.baz") assertEquals(msg, LightningMessage.decode(LightningMessage.encode(msg))) } +} -} \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Local_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Local_ebb9087c/data.json index 16a7c23ce..8892be1e1 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Local_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Local_ebb9087c/data.json @@ -139,7 +139,8 @@ }, "txId": "24f0d946a87c12c7fa345d04f2f2e9623074bd093d44229e8d00e1422257e015", "remoteSig": { - "sig": "58555dff0574ce320e281e1ff9d945674f710e5a33aeffcd2c0a105bbe1c2bad7389c84098615ab0f12dd7e9f951ede0000f205fda0f03fe958a42bfb59c9726" + "sig": "58555dff0574ce320e281e1ff9d945674f710e5a33aeffcd2c0a105bbe1c2bad7389c84098615ab0f12dd7e9f951ede0000f205fda0f03fe958a42bfb59c9726", + "nonce": null }, "htlcRemoteSigs": [ "29503e87f9b949d66cdbaef5a011d52d3f01fda29912c342f2e6ed69d74755bd029544a42cae34e45aaf92628b9eca98ae2441ec4433f8bf8714224f82cc852c" diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Mutual_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Mutual_ebb9087c/data.json index a151bbda5..37f0781aa 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Mutual_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Mutual_ebb9087c/data.json @@ -122,7 +122,8 @@ }, "txId": "0f9ad08d4eddee3c7d91464eb1dcca37548f48d95863ff67a0ef11b5ef8cd312", "remoteSig": { - "sig": "dc0d6153f56f4fb1238325ce8b38de8d2196f015e337502198ffaa43e15c3ff65ac254766dcab049e16bbd40bac3438fc3e0274c853bfc1eb88e7260b0f7fe11" + "sig": "dc0d6153f56f4fb1238325ce8b38de8d2196f015e337502198ffaa43e15c3ff65ac254766dcab049e16bbd40bac3438fc3e0274c853bfc1eb88e7260b0f7fe11", + "nonce": null }, "htlcRemoteSigs": [] }, diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Remote_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Remote_ebb9087c/data.json index 192b5e573..ab2371186 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Remote_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Remote_ebb9087c/data.json @@ -147,7 +147,8 @@ }, "txId": "fed55c7986d149ca8432c171cd163ab78658e4e0a7b70234218ee74644015f2a", "remoteSig": { - "sig": "125de9d83e8005bd73a01e24aadf5f387d15c4cc40990a3d5ccc42dc446461d11ce47aa07dc35658cca00f1c092c8f8273e6b11eca8f40e360b4f9ee0955f74a" + "sig": "125de9d83e8005bd73a01e24aadf5f387d15c4cc40990a3d5ccc42dc446461d11ce47aa07dc35658cca00f1c092c8f8273e6b11eca8f40e360b4f9ee0955f74a", + "nonce": null }, "htlcRemoteSigs": [ "e8f3c941cc5ec88b6f03ea64085742d99d8c1403f0fc78fc67d56271c07ef6e42776b1100b32062a087cf86acaeb7cd280676e792986dd6fb5b640f64d22694e", diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Revoked_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Revoked_ebb9087c/data.json index 7e5416191..d78b7bb4e 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Closing_Revoked_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Closing_Revoked_ebb9087c/data.json @@ -122,7 +122,8 @@ }, "txId": "1f369b753124adac5fedce5d6eae5539c08261b29fbc291f4e365c295f68dbb5", "remoteSig": { - "sig": "716bbeadfc5a323082f8c2e4c9b401f2cf07bd71d7d47fa31effa54ef1c6ff5f463be28b9beecba893f0c1df47610a0c201605391d312e8607a647db13938cf6" + "sig": "716bbeadfc5a323082f8c2e4c9b401f2cf07bd71d7d47fa31effa54ef1c6ff5f463be28b9beecba893f0c1df47610a0c201605391d312e8607a647db13938cf6", + "nonce": null }, "htlcRemoteSigs": [] }, diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Negotiating_fac54067/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Negotiating_fac54067/data.json index 99ebc51ae..7d7634ff3 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Negotiating_fac54067/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Negotiating_fac54067/data.json @@ -125,7 +125,8 @@ }, "txId": "a3cac82072d07b57f3b2b4246fa4d3e2f903e627b7dd1ee6b4b2a571cc4e34d8", "remoteSig": { - "sig": "1d69ef3d27a4ee4ea170b827e40255df0d335573bd4a856e702f6beefefd0b273dd8c5438bf26f5888efbd8917f4b9d94bc1fd769cd7d5dc844f9bad4c0174f7" + "sig": "1d69ef3d27a4ee4ea170b827e40255df0d335573bd4a856e702f6beefefd0b273dd8c5438bf26f5888efbd8917f4b9d94bc1fd769cd7d5dc844f9bad4c0174f7", + "nonce": null }, "htlcRemoteSigs": [] }, @@ -159,6 +160,7 @@ }, "remotePerCommitmentSecrets": "" }, + "remoteNextCommitNonces": {}, "localScript": "001405e0104aa726e34ff5cd3a6320d05c0862b5b01c", "remoteScript": "001434947cfb2e8f6054ddf12daed4308cbe342580d1", "proposedClosingTxs": [ @@ -216,5 +218,8 @@ "closeCommand": { "scriptPubKey": null, "feerate": 5000 - } + }, + "localCloseeNonce": null, + "remoteCloseeNonce": null, + "localCloserNonces": null } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Normal_77f198a3/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Normal_77f198a3/data.json index 5635afc14..479fc3964 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Normal_77f198a3/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Normal_77f198a3/data.json @@ -223,7 +223,8 @@ }, "txId": "bb7efa64ea149ca3f74c33e00ca72dbe096971d0ebb3a241c468771c187711ff", "remoteSig": { - "sig": "30b7c66f20b7ac100387047a159c1045364fc1fce5f1996f1230618a768819762f4d20baf053ab8058fcb9b8e81e931a7d4f9c31f5f1cd0b16791af1e54046c0" + "sig": "30b7c66f20b7ac100387047a159c1045364fc1fce5f1996f1230618a768819762f4d20baf053ab8058fcb9b8e81e931a7d4f9c31f5f1cd0b16791af1e54046c0", + "nonce": null }, "htlcRemoteSigs": [ "700471e0f8758cb5fde4c66a365feb2864ee80cb7946abfc2c1a15c94c9f6e9d00796b8f818a4e263112b4a04c6a904e8db076960549c22c8e9eb728367c24fb", @@ -490,7 +491,8 @@ }, "txId": "dcfb0ef853173058809a407afd88a3d99164bdbe6961194256fd944146872266", "remoteSig": { - "sig": "182390e02cd28be5673c229bc99c39033e9dbd1942616d30b69efa9cd4fc25694df27bd1aa9c2fe5e2901e6ecab2479417ddb277d201880968c0f46212e16c74" + "sig": "182390e02cd28be5673c229bc99c39033e9dbd1942616d30b69efa9cd4fc25694df27bd1aa9c2fe5e2901e6ecab2479417ddb277d201880968c0f46212e16c74", + "nonce": null }, "htlcRemoteSigs": [ "3229c56a2efa5d18d72805abfd61bbc0f6dfcd3cbbbaa1bf9e64061468518af86959a247ae8e3e60f964cc5e9d824bc7c35f24c8e0e40aabd36e0642806615e1", @@ -642,6 +644,7 @@ }, "remotePerCommitmentSecrets": "" }, + "remoteNextCommitNonces": {}, "shortChannelId": "42x0x0", "channelUpdate": { "signature": "6e9bd75886e3aa18389c5f5419bb2acf003c949be1b72c47c725e6705cf3b68e6387bab8a47da2bdf9e2f530debc375a0dc9f3d4700a7cde1ddc1cd3eb393fcd", @@ -662,5 +665,7 @@ }, "localShutdown": null, "remoteShutdown": null, - "closeCommand": null + "closeCommand": null, + "localCloseeNonce": null, + "localCloserNonces": null } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Normal_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Normal_ebb9087c/data.json index f85cc2c72..ae473e352 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Normal_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Normal_ebb9087c/data.json @@ -141,7 +141,8 @@ }, "txId": "fd49c1c1b8a84d2b00a0a1210c822cbbac8fc8fbf1afbbd69908c9b7e5df7cc4", "remoteSig": { - "sig": "4c028ec20ee39c107bc73033becdbb1cf6588e309e6e25f5a036c8b6504e04ea4a7d08b9739426c1bbbb4d15ff7984651506bbfb0a8af8b5acf6013d8eedcd67" + "sig": "4c028ec20ee39c107bc73033becdbb1cf6588e309e6e25f5a036c8b6504e04ea4a7d08b9739426c1bbbb4d15ff7984651506bbfb0a8af8b5acf6013d8eedcd67", + "nonce": null }, "htlcRemoteSigs": [ "6cdad11404a129da5b7b41869b8d3e319e27ceffb590214c730c8b2afa8f18ed0495867aa19597357ae3bd0c8c9e5addcc850008a8f66395b8bee5c78af6f031" @@ -230,7 +231,8 @@ }, "txId": "c9dfa8a557f54092d5f72d6ec6236e6ee71246a14f0bd79c6c4804443c483f4c", "remoteSig": { - "sig": "9dc287c5a78ad3480738aab05a14e8b36bdf1d3e97fa11af7e75edefcd6e5cbf404e33cc770464604896c65acbc31d7c403b90cf94c2c8a0e4ccd56af7ef68a5" + "sig": "9dc287c5a78ad3480738aab05a14e8b36bdf1d3e97fa11af7e75edefcd6e5cbf404e33cc770464604896c65acbc31d7c403b90cf94c2c8a0e4ccd56af7ef68a5", + "nonce": null }, "htlcRemoteSigs": [ "df602798b869e7f8176bea759bf5da249d4c00dfb47ebe14177e5c8634c9ca987c7a6895e46ed12aa0f3e6016dbccdff92c3d16c001b1a00958ab0987d1ae4ff" @@ -288,6 +290,7 @@ }, "remotePerCommitmentSecrets": "" }, + "remoteNextCommitNonces": {}, "shortChannelId": "0x8022898x0", "channelUpdate": { "signature": "df9689bc36fc0633fc8df8a8bedd183eb85b80f129ac56cb42c9f080ae61156339fe89171b01534633328ae9bba8d3ccd44629b7922b821ee1f96cef29672fea", @@ -308,5 +311,7 @@ }, "localShutdown": null, "remoteShutdown": null, - "closeCommand": null + "closeCommand": null, + "localCloseeNonce": null, + "localCloserNonces": null } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/Normal_ff34df87/data.json b/modules/core/src/commonTest/resources/nonreg/v4/Normal_ff34df87/data.json index e99c03a0c..3319166ad 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/Normal_ff34df87/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/Normal_ff34df87/data.json @@ -223,7 +223,8 @@ }, "txId": "0c74f8e6e0749a7815952ceedf0ee33f8f6e3298cf86eb83e45bf0f875a73044", "remoteSig": { - "sig": "bd71081f466344fb018eceabab814bdaf9d34df35327a43560677a13b96bbc3f1669df9f5075854a415a6ebd32bcf1e6fe482510930da7abfc15f319f29e7f00" + "sig": "bd71081f466344fb018eceabab814bdaf9d34df35327a43560677a13b96bbc3f1669df9f5075854a415a6ebd32bcf1e6fe482510930da7abfc15f319f29e7f00", + "nonce": null }, "htlcRemoteSigs": [ "47f187dab4904e505d7eecb6fa0ab9d1dd3ae4194a215d73c0ed3310bba02e3455dc93c903bee48ed0613431dd771030e0ba43007300570463934cdda090b7c4", @@ -375,6 +376,7 @@ }, "remotePerCommitmentSecrets": "" }, + "remoteNextCommitNonces": {}, "shortChannelId": "42x0x0", "channelUpdate": { "signature": "6f5237d843822495a739a69f8dc75755940490826264170a707523c5dbee8f423d14ea56f7b2d2a07dfbc37b18d27fe301550d047fc109a3638e183c05b1edc7", @@ -647,5 +649,7 @@ }, "localShutdown": null, "remoteShutdown": null, - "closeCommand": null + "closeCommand": null, + "localCloseeNonce": null, + "localCloserNonces": null } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/ShuttingDown_fac54067/data.json b/modules/core/src/commonTest/resources/nonreg/v4/ShuttingDown_fac54067/data.json index 5545e005b..a82e98d61 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/ShuttingDown_fac54067/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/ShuttingDown_fac54067/data.json @@ -143,7 +143,8 @@ }, "txId": "46e675be5b612cef94dbf2fe5d82dbfaa9f2f2f1542c941a711324f3f479ec66", "remoteSig": { - "sig": "4fbc9db98fa48f937fb60e364bbb8e32e8f7c6419745cf0f16b04b75d46e6cbe587cffe2a58b5ad40659ec171991b6a80fb63fbcf41b3757f511615dc3ba4703" + "sig": "4fbc9db98fa48f937fb60e364bbb8e32e8f7c6419745cf0f16b04b75d46e6cbe587cffe2a58b5ad40659ec171991b6a80fb63fbcf41b3757f511615dc3ba4703", + "nonce": null }, "htlcRemoteSigs": [ "b93c1ee4c31a629d64e8ec83f229db9cd9ea67dccf2fffc1f08c5443cdc548bf0ac41485cda6c3c40b019db559094ee0e7e2a7deb47f73d6d50ec0a7cd60685d", @@ -200,6 +201,7 @@ }, "remotePerCommitmentSecrets": "" }, + "remoteNextCommitNonces": {}, "localShutdown": { "channelId": "85ad5df602e4b1517db06754a5c1f3aa68d59973bd19e29798a825f3fb22babf", "scriptPubKey": "0014571c5ecb495ec4aeb6bd6f532af6817d70b8bc98" @@ -211,5 +213,6 @@ "closeCommand": { "scriptPubKey": "0014571c5ecb495ec4aeb6bd6f532af6817d70b8bc98", "feerate": 10000 - } + }, + "localCloseeNonce": null } \ No newline at end of file diff --git a/modules/core/src/commonTest/resources/nonreg/v4/WaitForChannelReady_fac54067/data.json b/modules/core/src/commonTest/resources/nonreg/v4/WaitForChannelReady_fac54067/data.json index b3ff939b5..b2c81be9f 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/WaitForChannelReady_fac54067/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/WaitForChannelReady_fac54067/data.json @@ -126,7 +126,8 @@ }, "txId": "e2ad5c195fd0ecb839650322bf4d14543a2f60d1c3f04cca670c3187a7ae2894", "remoteSig": { - "sig": "434a16a3d3092ce8dae91aca3b863501499c6b6a862d7e18fd580623294f65710334414fbbd1f54c87c6a52294feb09d9a76cdb10a74e890ec0b6cfb70f0e067" + "sig": "434a16a3d3092ce8dae91aca3b863501499c6b6a862d7e18fd580623294f65710334414fbbd1f54c87c6a52294feb09d9a76cdb10a74e890ec0b6cfb70f0e067", + "nonce": null }, "htlcRemoteSigs": [] }, @@ -160,6 +161,7 @@ }, "remotePerCommitmentSecrets": "" }, + "remoteNextCommitNonces": {}, "shortChannelId": "42x0x0", "lastSent": { "channelId": "1c9c6492fc038dd610071d689aff8a57f88e1163ad006643b9491bd0e6fcf8b1", diff --git a/modules/core/src/commonTest/resources/nonreg/v4/WaitForFundingConfirmed_fac54067/data.json b/modules/core/src/commonTest/resources/nonreg/v4/WaitForFundingConfirmed_fac54067/data.json index 662f442fe..03a1bc23c 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/WaitForFundingConfirmed_fac54067/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/WaitForFundingConfirmed_fac54067/data.json @@ -125,7 +125,8 @@ }, "txId": "75adc69132706111bfaa36188f158d5c967a831be2d154cda1e901666bf05f63", "remoteSig": { - "sig": "b717883c313d0e73ed638ac1e8bdd4b205138e260a321fe92e7c4ac80073b0955ecbb02b165d9c8201f4da4b92a298fd0dc8a35f878651319f8eacaef468640a" + "sig": "b717883c313d0e73ed638ac1e8bdd4b205138e260a321fe92e7c4ac80073b0955ecbb02b165d9c8201f4da4b92a298fd0dc8a35f878651319f8eacaef468640a", + "nonce": null }, "htlcRemoteSigs": [] }, @@ -159,6 +160,7 @@ }, "remotePerCommitmentSecrets": "" }, + "remoteNextCommitNonces": {}, "waitingSinceBlock": 400000, "deferred": { "channelId": "2a6bf35f6378987185051faa8cc4ca745d380181bad61ba770056e4911aab062", diff --git a/modules/core/src/commonTest/resources/nonreg/v4/WaitForRemotePublishFutureCommitment_ebb9087c/data.json b/modules/core/src/commonTest/resources/nonreg/v4/WaitForRemotePublishFutureCommitment_ebb9087c/data.json index ee1332f74..be7519e9d 100644 --- a/modules/core/src/commonTest/resources/nonreg/v4/WaitForRemotePublishFutureCommitment_ebb9087c/data.json +++ b/modules/core/src/commonTest/resources/nonreg/v4/WaitForRemotePublishFutureCommitment_ebb9087c/data.json @@ -121,7 +121,8 @@ }, "txId": "adf80ca3cdb8625fc144f8087766475abbf8331955825e597b2bca77169056c7", "remoteSig": { - "sig": "1d87fbb35b15229da1da1a4d10018e7b9b510b57ea41ee3e32d1b0fa72562bea4521bd19dac13089cacde07debb5950560b75a2a74ac9e45017babe136bdd719" + "sig": "1d87fbb35b15229da1da1a4d10018e7b9b510b57ea41ee3e32d1b0fa72562bea4521bd19dac13089cacde07debb5950560b75a2a74ac9e45017babe136bdd719", + "nonce": null }, "htlcRemoteSigs": [] }, diff --git a/modules/core/src/jvmTest/kotlin/fr/acinq/lightning/db/sqlite/SqliteChannelsDbTestsJvm.kt b/modules/core/src/jvmTest/kotlin/fr/acinq/lightning/db/sqlite/SqliteChannelsDbTestsJvm.kt index 6b36a7824..42c62f793 100644 --- a/modules/core/src/jvmTest/kotlin/fr/acinq/lightning/db/sqlite/SqliteChannelsDbTestsJvm.kt +++ b/modules/core/src/jvmTest/kotlin/fr/acinq/lightning/db/sqlite/SqliteChannelsDbTestsJvm.kt @@ -18,9 +18,11 @@ class SqliteChannelsDbTestsJvm : LightningTestSuite() { val db = SqliteChannelsDb(sqliteInMemory()) val (alice, _) = TestsHelper.reachNormal(currentHeight = 1, aliceFundingAmount = 1_000_000.sat) db.addOrUpdateChannel(alice.state) + val aliceWithoutNonces = alice.state.copy(remoteNextCommitNonces = mapOf(), localCloseeNonce = null, localCloserNonces = null) val (bob, _) = TestsHelper.reachNormal(currentHeight = 2, aliceFundingAmount = 2_000_000.sat) db.addOrUpdateChannel(bob.state) - assertEquals(db.listLocalChannels(), listOf(alice.state, bob.state)) + val bobWithoutNonces = bob.state.copy(remoteNextCommitNonces = mapOf(), localCloseeNonce = null, localCloserNonces = null) + assertEquals(db.listLocalChannels(), listOf(aliceWithoutNonces, bobWithoutNonces)) } } } \ No newline at end of file