From ab703aeed59d80626218a50d1ec756510f285385 Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 3 Mar 2025 15:48:51 +0100 Subject: [PATCH 1/3] Use official splice messages We replace our experimental version of `splice_init`, `splice_ack` and `splice_locked` by their official version. We also change the TLV fields added to `commit_sig`, `tx_add_input`, and `tx_signatures` to match the spec version. We only allow connecting to peers who support the official splicing feature. --- .../kotlin/fr/acinq/lightning/Features.kt | 17 +++-- .../kotlin/fr/acinq/lightning/NodeParams.kt | 3 +- .../fr/acinq/lightning/channel/Commitments.kt | 20 +++--- .../acinq/lightning/channel/InteractiveTx.kt | 4 +- .../acinq/lightning/channel/states/Syncing.kt | 6 +- .../kotlin/fr/acinq/lightning/io/Peer.kt | 17 +++-- .../acinq/lightning/json/JsonSerializers.kt | 8 +-- .../fr/acinq/lightning/wire/ChannelTlv.kt | 43 +++++++++--- .../acinq/lightning/wire/InteractiveTxTlv.kt | 4 +- .../acinq/lightning/wire/LightningMessages.kt | 46 ++++++++++--- .../channel/states/SpliceTestsCommon.kt | 8 ++- .../IncomingPaymentHandlerTestsCommon.kt | 2 +- .../OutgoingPaymentHandlerTestsCommon.kt | 1 + .../fr/acinq/lightning/tests/TestConstants.kt | 2 +- .../wire/LightningCodecsTestsCommon.kt | 69 ++++++++++++------- .../v4/Closing_Local_ebb9087c/data.json | 6 +- .../v4/Closing_Mutual_ebb9087c/data.json | 6 +- .../v4/Closing_Remote_ebb9087c/data.json | 6 +- .../v4/Closing_Revoked_ebb9087c/data.json | 6 +- .../nonreg/v4/Negotiating_fac54067/data.json | 10 +-- .../nonreg/v4/Normal_77f198a3/data.json | 10 +-- .../nonreg/v4/Normal_ebb9087c/data.json | 6 +- .../nonreg/v4/Normal_ff34df87/data.json | 10 +-- .../nonreg/v4/ShuttingDown_fac54067/data.json | 10 +-- .../v4/WaitForChannelReady_fac54067/data.json | 10 +-- .../data.json | 10 +-- .../data.json | 6 +- 27 files changed, 217 insertions(+), 129 deletions(-) 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 50b239b74..d576187e6 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/Features.kt @@ -161,6 +161,13 @@ sealed class Feature { override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) } + @Serializable + object Splicing : Feature() { + override val rfcName get() = "option_splice" + override val mandatory get() = 62 + override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) + } + // The following features have not been standardised, hence the high feature bits to avoid conflicts. /** This feature bit should be activated when a node accepts having their channel reserve set to 0. */ @@ -196,13 +203,6 @@ sealed class Feature { override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice) } - @Serializable - object ExperimentalSplice : Feature() { - override val rfcName get() = "splice_experimental" - override val mandatory get() = 154 - override val scopes: Set get() = setOf(FeatureScope.Init) - } - @Serializable object OnTheFlyFunding : Feature() { override val rfcName get() = "on_the_fly_funding" @@ -295,11 +295,11 @@ data class Features(val activated: Map, val unknown: Se Feature.ChannelType, Feature.PaymentMetadata, Feature.SimpleClose, + Feature.Splicing, Feature.ExperimentalTrampolinePayment, Feature.ZeroReserveChannels, Feature.WakeUpNotificationClient, Feature.WakeUpNotificationProvider, - Feature.ExperimentalSplice, Feature.OnTheFlyFunding, Feature.FundingFeeCredit, Feature.SimpleTaprootChannels @@ -335,7 +335,6 @@ data class Features(val activated: Map, val unknown: Se Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey), Feature.SimpleClose to listOf(Feature.ShutdownAnySegwit), Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret), - Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice), Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding) ) 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 4b4bc3a82..be9a912c0 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -202,6 +202,7 @@ data class NodeParams( require(features.hasFeature(Feature.PaymentSecret, FeatureSupport.Mandatory)) { "${Feature.PaymentSecret.rfcName} should be mandatory" } require(features.hasFeature(Feature.ChannelType, FeatureSupport.Mandatory)) { "${Feature.ChannelType.rfcName} should be mandatory" } require(features.hasFeature(Feature.DualFunding, FeatureSupport.Mandatory)) { "${Feature.DualFunding.rfcName} should be mandatory" } + require(features.hasFeature(Feature.Splicing, FeatureSupport.Mandatory)) { "${Feature.Splicing.rfcName} should be mandatory" } require(features.hasFeature(Feature.RouteBlinding)) { "${Feature.RouteBlinding.rfcName} should be supported" } require(features.hasFeature(Feature.ShutdownAnySegwit, FeatureSupport.Mandatory)) { "${Feature.ShutdownAnySegwit.rfcName} should be mandatory" } require(features.hasFeature(Feature.SimpleClose, FeatureSupport.Mandatory)) { "${Feature.SimpleClose.rfcName} should be mandatory" } @@ -233,10 +234,10 @@ data class NodeParams( Feature.ChannelType to FeatureSupport.Mandatory, Feature.PaymentMetadata to FeatureSupport.Optional, Feature.SimpleClose to FeatureSupport.Mandatory, + Feature.Splicing to FeatureSupport.Mandatory, Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional, Feature.ZeroReserveChannels to FeatureSupport.Optional, Feature.WakeUpNotificationClient to FeatureSupport.Optional, - Feature.ExperimentalSplice to FeatureSupport.Optional, Feature.OnTheFlyFunding to FeatureSupport.Optional, Feature.FundingFeeCredit to FeatureSupport.Optional, ), 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 15e0bb579..16dc5ceb4 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 @@ -167,7 +167,6 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, commitmentFormat: Transactions.CommitmentFormat, - batchSize: Int, remoteNonce: IndividualNonce?, logger: MDCLogger ): Either { @@ -194,7 +193,7 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI return when (commitmentFormat) { Transactions.CommitmentFormat.AnchorOutputs -> { val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey) - Either.Right(CommitSig(channelParams.channelId, sig, htlcSigs, batchSize)) + Either.Right(CommitSig(channelParams.channelId, commitInput.outPoint.txid, sig, htlcSigs)) } Transactions.CommitmentFormat.SimpleTaprootChannels -> when (remoteNonce) { null -> Either.Left(MissingCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index)) @@ -202,7 +201,7 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI 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)) + is Either.Right -> Either.Right(CommitSig(channelParams.channelId, commitInput.outPoint.txid, psig.value, htlcSigs)) } } } @@ -218,7 +217,6 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI signingSession.fundingParams.remoteFundingPubkey, signingSession.commitInput(channelKeys), signingSession.fundingParams.commitmentFormat, - batchSize = 1, remoteNonce, logger ) @@ -580,7 +578,6 @@ data class Commitment( commitKeys: RemoteCommitmentKeys, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, - batchSize: Int, nextRemoteNonce: IndividualNonce?, logger: MDCLogger ): Either> { @@ -618,7 +615,7 @@ data class Commitment( 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 commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), batchSize) + val commitSig = CommitSig(params.channelId, fundingTxId, sig, htlcSigs.toList()) val commitment1 = copy(nextRemoteCommit = RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint)) return Either.Right(Pair(commitment1, commitSig)) } @@ -885,7 +882,7 @@ data class Commitments( 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, logger)) { + when (val res = c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, remoteNonce, logger)) { is Either.Left -> return Either.Left(res.left) is Either.Right -> res.value } @@ -913,9 +910,12 @@ data class Commitments( return Either.Left(CommitSigCountMismatch(channelId, active.size, sigs.size)) } val commitKeys = channelKeys.localCommitmentKeys(channelParams, localCommitIndex + 1) - // Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments. - val active1 = active.zip(sigs).map { - when (val commitment1 = it.first.receiveCommit(channelParams, channelKeys, commitKeys, changes, it.second, logger)) { + val active1 = active.withIndex().map { (i, c) -> + // If the funding_txid isn't provided, we assume that signatures are sent in order (most recent first). + // This ensures that the case where we have a batch of a single element (no pending splice) works correctly. + // This also ensures that we get a signature failure when our set of funding txs doesn't match with our peer. + val commit = sigs.find { it.fundingTxId == c.fundingTxId } ?: sigs[i] + when (val commitment1 = c.receiveCommit(channelParams, channelKeys, commitKeys, changes, commit, logger)) { is Either.Left -> return Either.Left(commitment1.value) is Either.Right -> commitment1.value } 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 4209d59a7..fe12d578e 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 @@ -731,7 +731,7 @@ data class InteractiveTxSession( 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) }, + fundingContributions.inputs.map { i -> Either.Left(i) } + fundingContributions.outputs.map { o -> Either.Right(o) }, previousTxs, localHtlcs, localFundingNonce = fundingParams.sharedInput?.let { @@ -1273,7 +1273,7 @@ data class InteractiveTxSigningSession( val htlcsOut = spec.htlcs.incomings().map { it.id }.joinToString(",") "built remote commit number=$remoteCommitmentIndex toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txId=${firstCommitTx.remoteCommitTx.tx.txid} fundingTxId=${unsignedTx.txid}" } - val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, batchSize = 1) + val commitSig = CommitSig(channelParams.channelId, unsignedTx.txid, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs) // 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) 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 454bbfd72..5991f6dc9 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 @@ -69,7 +69,6 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput(channelKeys), state.commitments.latest.commitmentFormat, - batchSize = 1, remoteNonce = cmd.message.currentCommitNonce, logger )) { @@ -106,7 +105,6 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput(channelKeys), state.commitments.latest.commitmentFormat, - batchSize = 1, remoteNonce = cmd.message.currentCommitNonce, logger )) { @@ -170,7 +168,6 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput(channelKeys), state.commitments.latest.commitmentFormat, - batchSize = 1, remoteNonce = cmd.message.currentCommitNonce, logger )) { @@ -435,14 +432,13 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // We just sent a new commit_sig but they didn't receive it: we resend the same updates and sign them again, // and preserve the same ordering of messages. val signedUpdates = commitments.changes.localChanges.signed - 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, remoteNonce, logger)?.right + c.nextRemoteCommit?.sign(commitments.channelParams, c.remoteCommitParams, channelKeys, c.fundingTxIndex, c.remoteFundingPubkey, commitInput, c.commitmentFormat, remoteNonce, logger)?.right }) val retransmit = when (retransmitRevocation) { null -> buildList { 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 7493a54eb..f3636f612 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 @@ -115,7 +115,10 @@ data class PeerConnection(val id: Long, val output: Channel, v fun send(msg: LightningMessage) { when (msg) { - is CommitSigBatch -> msg.messages.map { sendInternal(it) } + is CommitSigBatch -> { + sendInternal(StartBatch(msg.channelId, msg.batchSize)) + msg.messages.map { sendInternal(it) } + } else -> sendInternal(msg) } } @@ -488,14 +491,14 @@ class Peer( try { while (isActive) { - val msg = when (val msg = receiveMessage()) { - is CommitSig -> { - val others = (1 until msg.batchSize).mapNotNull { receiveMessage() as CommitSig } - CommitSigs.fromSigs(listOf(msg) + others) + when (val msg = receiveMessage()) { + is StartBatch -> { + val sigs = (0 until msg.batchSize).mapNotNull { receiveMessage() as CommitSig } + input.send(MessageReceived(peerConnection.id, CommitSigs.fromSigs(sigs))) } - else -> msg + is LightningMessage -> input.send(MessageReceived(peerConnection.id, msg)) + else -> {} } - msg?.let { input.send(MessageReceived(peerConnection.id, it)) } } closeSocket(null) } catch (ex: Throwable) { 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 64c63fd7d..b35b459f6 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 @@ -97,7 +97,7 @@ JsonSerializers.ChannelReestablishTlvSerializer::class, JsonSerializers.ChannelReestablishTlvNextLocalNoncesSerializer::class, JsonSerializers.ChannelReadyTlvSerializer::class, - JsonSerializers.CommitSigTlvBatchSerializer::class, + JsonSerializers.CommitSigTlvFundingTxSerializer::class, JsonSerializers.CommitSigTlvPartialSignatureWithNonceSerializer::class, JsonSerializers.CommitSigTlvSerializer::class, JsonSerializers.UUIDSerializer::class, @@ -211,7 +211,7 @@ object JsonSerializers { polymorphic(Tlv::class) { subclass(ChannelReadyTlv.ShortChannelIdTlv::class, ChannelReadyTlvShortChannelIdTlvSerializer) subclass(ChannelReadyTlv.NextLocalNonce::class, ChannelReadyTlvNextLocalNonceSerializer) - subclass(CommitSigTlv.Batch::class, CommitSigTlvBatchSerializer) + subclass(CommitSigTlv.FundingTx::class, CommitSigTlvFundingTxSerializer) subclass(CommitSigTlv.PartialSignatureWithNonce::class, CommitSigTlvPartialSignatureWithNonceSerializer) subclass(UpdateAddHtlcTlv.PathKey::class, UpdateAddHtlcTlvPathKeySerializer) subclass(ShutdownTlv.ShutdownNonce::class, ShutdownTlvShutdownNonceSerializer) @@ -562,8 +562,8 @@ object JsonSerializers { @Serializer(forClass = ShutdownTlv.ShutdownNonce::class) object ShutdownTlvShutdownNonceSerializer - @Serializer(forClass = CommitSigTlv.Batch::class) - object CommitSigTlvBatchSerializer + @Serializer(forClass = CommitSigTlv.FundingTx::class) + object CommitSigTlvFundingTxSerializer @Serializer(forClass = CommitSigTlv.PartialSignatureWithNonce::class) object CommitSigTlvPartialSignatureWithNonceSerializer 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 64e31ec49..b78edabb0 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 @@ -153,7 +153,40 @@ sealed class ChannelReadyTlv : Tlv { } } +sealed class StartBatchTlv : Tlv { + /** Type of [LightningMessage] that is included in the batch, when batching a single message type. */ + data class MessageType(val msg: Int) : StartBatchTlv() { + override val tag: Long get() = MessageType.tag + override fun write(out: Output) { + LightningCodecs.writeU16(msg, out) + } + + companion object : TlvValueReader { + const val tag: Long = 1 + override fun read(input: Input): MessageType = MessageType(msg = LightningCodecs.u16(input)) + } + } +} + sealed class CommitSigTlv : Tlv { + /** + * While a splice is ongoing and not locked, we have multiple valid commitments. + * We send one [CommitSig] message for each valid commitment. + * + * @param txId the funding transaction spent by this commitment. + */ + data class FundingTx(val txId: TxId) : CommitSigTlv() { + override val tag: Long get() = FundingTx.tag + override fun write(out: Output) { + LightningCodecs.writeTxHash(TxHash(txId), out) + } + + companion object : TlvValueReader { + const val tag: Long = 1 + override fun read(input: Input): FundingTx = FundingTx(txId = TxId(LightningCodecs.txHash(input))) + } + } + /** 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 @@ -175,16 +208,6 @@ sealed class CommitSigTlv : Tlv { } } } - - data class Batch(val size: Int) : CommitSigTlv() { - override val tag: Long get() = Batch.tag - override fun write(out: Output) = LightningCodecs.writeTU16(size, out) - - companion object : TlvValueReader { - const val tag: Long = 0x47010005 - override fun read(input: Input): Batch = Batch(size = LightningCodecs.tu16(input)) - } - } } sealed class RevokeAndAckTlv : Tlv { 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 dd82ce9bd..60e51df17 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 @@ -18,7 +18,7 @@ sealed class TxAddInputTlv : Tlv { override fun write(out: Output) = LightningCodecs.writeTxHash(TxHash(txId), out) companion object : TlvValueReader { - const val tag: Long = 1105 + const val tag: Long = 0 override fun read(input: Input): SharedInputTxId = SharedInputTxId(TxId(LightningCodecs.txHash(input))) } } @@ -136,7 +136,7 @@ sealed class TxSignaturesTlv : Tlv { override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out) companion object : TlvValueReader { - const val tag: Long = 601 + const val tag: Long = 0 override fun read(input: Input): PreviousFundingTxSig = PreviousFundingTxSig(LightningCodecs.bytes(input, 64).toByteVector64()) } } 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 0f25f6cf5..d1f90d6e3 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 @@ -68,6 +68,7 @@ interface LightningMessage { TxAbort.type -> TxAbort.read(stream) CommitSig.type -> CommitSig.read(stream) RevokeAndAck.type -> RevokeAndAck.read(stream) + StartBatch.type -> StartBatch.read(stream) UpdateAddHtlc.type -> UpdateAddHtlc.read(stream) UpdateFailHtlc.type -> UpdateFailHtlc.read(stream) UpdateFailMalformedHtlc.type -> UpdateFailMalformedHtlc.read(stream) @@ -1003,7 +1004,7 @@ data class SpliceInit( } companion object : LightningMessageReader { - const val type: Long = 37000 + const val type: Long = 80 @Suppress("UNCHECKED_CAST") private val readers = mapOf( @@ -1055,7 +1056,7 @@ data class SpliceAck( } companion object : LightningMessageReader { - const val type: Long = 37002 + const val type: Long = 81 @Suppress("UNCHECKED_CAST") private val readers = mapOf( @@ -1088,7 +1089,7 @@ data class SpliceLocked( } companion object : LightningMessageReader { - const val type: Long = 37004 + const val type: Long = 77 private val readers = emptyMap>() @@ -1100,6 +1101,35 @@ data class SpliceLocked( } } +/** This message is used to indicate that the next [batchSize] messages form a single logical message. */ +data class StartBatch(override val channelId: ByteVector32, val batchSize: Int, val tlvStream: TlvStream) : ChannelMessage, HasChannelId { + // We only support batches of [CommitSig] messages. + constructor(channelId: ByteVector32, batchSize: Int) : this(channelId, batchSize, TlvStream(StartBatchTlv.MessageType(132))) + + override val type: Long get() = StartBatch.type + + override fun write(out: Output) { + LightningCodecs.writeBytes(channelId, out) + LightningCodecs.writeU16(batchSize, out) + TlvStreamSerializer(false, readers).write(tlvStream, out) + } + + companion object : LightningMessageReader { + const val type: Long = 127 + + @Suppress("UNCHECKED_CAST") + val readers: Map> = mapOf( + StartBatchTlv.MessageType.tag to StartBatchTlv.MessageType as TlvValueReader + ) + + override fun read(input: Input): StartBatch = StartBatch( + channelId = ByteVector32(LightningCodecs.bytes(input, 32)), + batchSize = LightningCodecs.u16(input), + tlvStream = TlvStreamSerializer(false, readers).read(input) + ) + } +} + data class UpdateAddHtlc( override val channelId: ByteVector32, val id: Long, @@ -1263,7 +1293,7 @@ data class CommitSig( val tlvStream: TlvStream = TlvStream.empty() ) : CommitSigs() { - constructor(channelId: ByteVector32, signature: ChannelSpendSignature, htlcSignatures: List, batchSize: Int) : this( + constructor(channelId: ByteVector32, fundingTxId: TxId, signature: ChannelSpendSignature, htlcSignatures: List) : this( channelId, when (signature) { is ChannelSpendSignature.IndividualSignature -> signature @@ -1272,7 +1302,7 @@ data class CommitSig( htlcSignatures, TlvStream( setOfNotNull( - if (batchSize > 1) CommitSigTlv.Batch(batchSize) else null, + CommitSigTlv.FundingTx(fundingTxId), when (signature) { is ChannelSpendSignature.PartialSignatureWithNonce -> CommitSigTlv.PartialSignatureWithNonce(signature) is ChannelSpendSignature.IndividualSignature -> null @@ -1285,7 +1315,7 @@ data class CommitSig( val partialSignature: ChannelSpendSignature.PartialSignatureWithNonce? = tlvStream.get()?.psig val sigOrPartialSig: ChannelSpendSignature = partialSignature ?: signature - val batchSize: Int = tlvStream.get()?.size ?: 1 + val fundingTxId: TxId? = tlvStream.get()?.txId override fun write(out: Output) { LightningCodecs.writeBytes(channelId, out) @@ -1300,8 +1330,8 @@ data class CommitSig( @Suppress("UNCHECKED_CAST") val readers = mapOf( + CommitSigTlv.FundingTx.tag to CommitSigTlv.FundingTx.Companion as TlvValueReader, CommitSigTlv.PartialSignatureWithNonce.tag to CommitSigTlv.PartialSignatureWithNonce.Companion as TlvValueReader, - CommitSigTlv.Batch.tag to CommitSigTlv.Batch.Companion as TlvValueReader, ) override fun read(input: Input): CommitSig { @@ -1961,7 +1991,7 @@ data class WillFailMalformedHtlc(val id: ByteVector32, val paymentHash: ByteVect } /** - * This message is sent in response to an [OpenDualFundedChannel] or [SpliceInit] message containing an invalid [LiquidityAds.RequestFunds]. + * This message is sent in response to an [OpenDualFundedChannel] or [SpliceInit] message containing an invalid [LiquidityAds.RequestFunding]. * The receiver must consider the funding attempt failed when receiving this message. */ data class CancelOnTheFlyFunding(override val channelId: ByteVector32, val paymentHashes: List, val reason: ByteVector) : OnTheFlyFundingMessage, HasChannelId { 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 90818800e..849be5245 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 @@ -830,6 +830,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice4, actionsAlice4) = alice3.process(ChannelCommand.Commitment.Sign) val commitSigsAlice = actionsAlice4.findOutgoingMessage() assertEquals(commitSigsAlice.batchSize, 3) + assertEquals(setOf(alice.commitments.latest.fundingTxId, alice1.commitments.latest.fundingTxId, spliceTx.txid), commitSigsAlice.messages.mapNotNull { it.fundingTxId }.toSet()) val (alice5, _) = alice4.process(ChannelCommand.MessageReceived(spliceLocked)) assertEquals(alice5.commitments.active.size, 1) assertEquals(alice5.commitments.inactive.size, 2) @@ -841,7 +842,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (bob6, actionsBob6) = bob5.process(ChannelCommand.Commitment.Sign) assertEquals(actionsBob6.size, 3) val commitSigBob = actionsBob6.findOutgoingMessage() - assertEquals(commitSigBob.batchSize, 1) + assertEquals(spliceTx.txid, commitSigBob.fundingTxId) actionsBob6.has() actionsBob6.has() val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(revokeAndAckBob)) @@ -1222,6 +1223,7 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- new changes before splice_locked -- partially locked`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() + val fundingTxId = alice.commitments.latest.fundingTxId val (alice1, bob1) = spliceOut(alice, bob, 70_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! @@ -1237,7 +1239,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice3, actionsAlice3) = nodes3.first.process(ChannelCommand.Commitment.Sign) actionsAlice3.hasOutgoingMessage().also { batch -> assertEquals(2, batch.batchSize) - batch.messages.forEach { sig -> assertEquals(2, sig.batchSize) } + assertEquals(setOf(fundingTxId, spliceTx.txid), batch.messages.mapNotNull { it.fundingTxId }.toSet()) } // At the same time, the splice confirms on Bob's side, who now expects a single commit_sig message. @@ -1256,7 +1258,7 @@ class SpliceTestsCommon : LightningTestSuite() { actionsAlice6.hasOutgoingMessage().also { assertEquals(htlc, it) } assertEquals(1, actionsAlice6.findOutgoingMessages().size) val commitSigAlice = actionsAlice6.hasOutgoingMessage() - assertEquals(1, commitSigAlice.batchSize) + assertEquals(spliceTx.txid, commitSigAlice.fundingTxId) val (bob6, _) = bob5.process(ChannelCommand.MessageReceived(htlc)) val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(commitSigAlice)) assertIs>(bob7) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt index 7df3e7ca0..c803823ef 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandlerTestsCommon.kt @@ -1898,7 +1898,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() { val defaultPreimage = randomBytes32() val defaultPaymentHash = Crypto.sha256(defaultPreimage).toByteVector32() val defaultAmount = 150_000_000.msat - val feeCreditFeatures = Features(Feature.ExperimentalSplice to FeatureSupport.Optional, Feature.OnTheFlyFunding to FeatureSupport.Optional, Feature.FundingFeeCredit to FeatureSupport.Optional) + val feeCreditFeatures = Features(Feature.OnTheFlyFunding to FeatureSupport.Optional, Feature.FundingFeeCredit to FeatureSupport.Optional) fun LightningIncomingPayment.Part.resetTimestamp() = when (this) { is LightningIncomingPayment.Part.Htlc -> copy(receivedAt = 0) 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 bef1d779f..ca082512d 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 @@ -58,6 +58,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() { Feature.RouteBlinding to FeatureSupport.Optional, Feature.ShutdownAnySegwit to FeatureSupport.Mandatory, Feature.SimpleClose to FeatureSupport.Mandatory, + Feature.Splicing to FeatureSupport.Mandatory, ) // The following invoice requires payment_metadata. val invoice1 = run { 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 8843e78a6..c394eef7a 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 @@ -82,10 +82,10 @@ object TestConstants { Feature.ChannelType to FeatureSupport.Mandatory, Feature.PaymentMetadata to FeatureSupport.Optional, Feature.SimpleClose to FeatureSupport.Mandatory, + Feature.Splicing to FeatureSupport.Mandatory, Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional, Feature.WakeUpNotificationProvider to FeatureSupport.Optional, Feature.ProvideStorage to FeatureSupport.Optional, - Feature.ExperimentalSplice to FeatureSupport.Optional, Feature.OnTheFlyFunding to FeatureSupport.Optional, ), dustLimit = 1_100.sat, 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 8c556524c..00e2f3076 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 @@ -450,6 +450,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { @Test fun `encode - decode commit_sig`() { val channelId = ByteVector32.fromValidHex("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25") + val fundingTxId = TxId("d7279c1cc0192b73a7e80d89ad045b57806a5ab1443c815207fa944ffa5718cb") val signature = ChannelSpendSignature.IndividualSignature(ByteVector64.fromValidHex("05e06d9a8fdfbb3625051ff2e3cdf82679cc2268beee6905941d6dd8a067cd62711e04b119a836aa0eebe07545172cefb228860fea6c797178453a319169bed7")) val partialSig = ChannelSpendSignature.PartialSignatureWithNonce( ByteVector32("034ad8ca7bed68a934b633c4beeb7dc493cb0ff70e7aa9c86b895bbf3a3b5f82"), @@ -458,7 +459,8 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val testCases = listOf( // @formatter:off CommitSig(channelId, signature, listOf()) to "0084 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 05e06d9a8fdfbb3625051ff2e3cdf82679cc2268beee6905941d6dd8a067cd62711e04b119a836aa0eebe07545172cefb228860fea6c797178453a319169bed7 0000", - CommitSig(channelId, partialSig, listOf(), batchSize = 1) to "0084 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000 0262034ad8ca7bed68a934b633c4beeb7dc493cb0ff70e7aa9c86b895bbf3a3b5f82a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d", + CommitSig(channelId, fundingTxId, signature, listOf()) to "0084 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 05e06d9a8fdfbb3625051ff2e3cdf82679cc2268beee6905941d6dd8a067cd62711e04b119a836aa0eebe07545172cefb228860fea6c797178453a319169bed7 0000 0120cb1857fa4f94fa0752813c44b15a6a80575b04ad890de8a7732b19c01c9c27d7", + CommitSig(channelId, fundingTxId, partialSig, listOf()) to "0084 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db25 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000 0120cb1857fa4f94fa0752813c44b15a6a80575b04ad890de8a7732b19c01c9c27d7 0262034ad8ca7bed68a934b633c4beeb7dc493cb0ff70e7aa9c86b895bbf3a3b5f82a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d", // @formatter:on ) testCases.forEach { (commitSig, bin) -> @@ -505,7 +507,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TxAddInput(channelId1, 561, tx1, 1, 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005"), TxAddInput(channelId2, 0, tx2, 2, 0u) to ByteVector("0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000"), TxAddInput(channelId1, 561, tx1, 0, 0xfffffffdu) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 fffffffd"), - TxAddInput(channelId1, 561, OutPoint(tx1, 1), 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106"), + TxAddInput(channelId1, 561, OutPoint(tx1, 1), 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 00201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106"), TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParamsLegacy(swapInUserKey, swapInServerKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534603462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f00000090"), TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInUserRefundKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04556703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f033a47288cdae4b25818d0d82802bc114c6f7184f2e071602fa4e3d69881ae2cce00000090"), TxAddOutput(channelId1, 1105, 2047.sat, ByteVector("00149357014afd0ccd265658c9ae81efa995e771f472")) to ByteVector("0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472"), @@ -519,14 +521,14 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), TxSignatures(channelId2, tx1, listOf(), null, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), - TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5 fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5 fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), TxInitRbf(channelId1, 8388607, FeeratePerKw(4000.sat)) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(1_500_000.sat), TxInitRbfTlv.RequireConfirmedInputsTlv)) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(0.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000"), @@ -558,19 +560,19 @@ class LightningCodecsTestsCommon : LightningTestSuite() { // @formatter:off Stfu(channelId, false) to ByteVector("0002 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00"), Stfu(channelId, true) to ByteVector("0002 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 01"), - 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), 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), 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"), + SpliceInit(channelId, 100_000.sat, FeeratePerKw(2500.sat), 100, fundingPubkey) to ByteVector("0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + SpliceInit(channelId, 0.sat, FeeratePerKw(500.sat), 0, fundingPubkey) to ByteVector("0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500.sat), 0, fundingPubkey) to ByteVector("0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + SpliceInit(channelId, 100_000.sat, FeeratePerKw(2500.sat), 100, fundingPubkey, LiquidityAds.RequestFunding(100_000.sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance), channelType = null) to ByteVector("0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000"), + SpliceInit(channelId, 100_000.sat, FeeratePerKw(2500.sat), 100, fundingPubkey, null, ChannelType.SupportedChannelType.SimpleTaprootChannels) to ByteVector("0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c400000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe 47000011 471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000"), + SpliceAck(channelId, 25_000.sat, fundingPubkey) to ByteVector("0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + SpliceAck(channelId, 0.sat, fundingPubkey) to ByteVector("0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + SpliceAck(channelId, (-25_000).sat, fundingPubkey) to ByteVector("0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff9e58 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + SpliceAck(channelId, 25_000.sat, fundingPubkey, LiquidityAds.WillFund(fundingRate, ByteVector("deadbeef"), ByteVector64.Zeroes), channelType = null) to ByteVector("0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + SpliceAck(channelId, 25_000.sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(0.msat))) to ByteVector("0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda05200"), + SpliceAck(channelId, 25_000.sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(1729.msat))) to ByteVector("0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda0520206c1"), + SpliceAck(channelId, 25_000.sat, fundingPubkey, null, ChannelType.SupportedChannelType.SimpleTaprootChannels) to ByteVector("0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe 47000011 471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000"), + SpliceLocked(channelId, fundingTxId) to ByteVector("004d aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566"), // @formatter:on ) testCases.forEach { (message, bin) -> @@ -582,6 +584,24 @@ class LightningCodecsTestsCommon : LightningTestSuite() { } } + @Test + fun `encode - decode start_batch`() { + val channelId = ByteVector32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + val testCases = listOf( + StartBatch(channelId, 1) to ByteVector("007f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0001 01020084"), + StartBatch(channelId, 7) to ByteVector("007f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0007 01020084"), + StartBatch(channelId, 32000) to ByteVector("007f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 7d00 01020084"), + StartBatch(channelId, 7, TlvStream(StartBatchTlv.MessageType(57331))) to ByteVector("007f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0007 0102dff3"), + ) + testCases.forEach { (message, bin) -> + val decoded = LightningMessage.decode(bin.toByteArray()) + assertNotNull(decoded) + assertEquals(decoded, message) + val encoded = LightningMessage.encode(message) + assertEquals(encoded.byteVector(), bin) + } + } + @Test fun `encode - decode channel_reestablish`() { val channelId = ByteVector32("c11b8fbd682b3c6ee11f9d7268e22bb5887cd4d3bf3338bfcc340583f685733c") @@ -782,7 +802,8 @@ class LightningCodecsTestsCommon : LightningTestSuite() { Hex.decode("0047") + channelId.toByteArray() + txHash.value.toByteArray() + Hex.decode("0000 2b012a") to TxSignatures(channelId, TxId(txHash), listOf(), TlvStream(setOf(), setOf(GenericTlv(43, ByteVector("2a"))))), // commit_sig Hex.decode("0084") + channelId.toByteArray() + signature.sig.toByteArray() + Hex.decode("0000") to CommitSig(channelId, signature, listOf()), - Hex.decode("0084") + channelId.toByteArray() + signature.sig.toByteArray() + Hex.decode("0000") + Hex.decode("01 02 0102") to CommitSig(channelId, signature, listOf(), TlvStream(setOf(), setOf(GenericTlv(1, ByteVector("0102"))))), + Hex.decode("0084") + channelId.toByteArray() + signature.sig.toByteArray() + Hex.decode("0000") + Hex.decode("01 20") + txHash.value.toByteArray() to CommitSig(channelId, signature, listOf(), TlvStream(CommitSigTlv.FundingTx(TxId(txHash)))), + Hex.decode("0084") + channelId.toByteArray() + signature.sig.toByteArray() + Hex.decode("0000") + Hex.decode("fd0231 02 0102") to CommitSig(channelId, signature, listOf(), TlvStream(setOf(), setOf(GenericTlv(561, ByteVector("0102"))))), // revoke_and_ack Hex.decode("0085") + channelId.toByteArray() + key.value.toByteArray() + point.value.toByteArray() to RevokeAndAck(channelId, key, point), Hex.decode("0085") + channelId.toByteArray() + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("01 02 0102") to RevokeAndAck(channelId, key, point, TlvStream(setOf(), setOf(GenericTlv(1, ByteVector("0102"))))), 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 8892be1e1..3c44db65b 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 @@ -60,12 +60,12 @@ "option_channel_type": "Mandatory", "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", - "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional" + "trampoline_payment_experimental": "Optional" }, "unknown": [ 137, - 145 + 145, + 155 ] } }, 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 37f0781aa..e753d779e 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 @@ -60,12 +60,12 @@ "option_channel_type": "Mandatory", "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", - "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional" + "trampoline_payment_experimental": "Optional" }, "unknown": [ 137, - 145 + 145, + 155 ] } }, 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 ab2371186..32692f999 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 @@ -60,12 +60,12 @@ "option_channel_type": "Mandatory", "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", - "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional" + "trampoline_payment_experimental": "Optional" }, "unknown": [ 137, - 145 + 145, + 155 ] } }, 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 d78b7bb4e..af9e3f35a 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 @@ -60,12 +60,12 @@ "option_channel_type": "Mandatory", "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", - "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional" + "trampoline_payment_experimental": "Optional" }, "unknown": [ 137, - 145 + 145, + 155 ] } }, 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 f303b36bf..7ea4ff448 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 @@ -35,10 +35,11 @@ "option_simple_close": "Mandatory", "wake_up_notification_provider": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "remoteParams": { @@ -65,11 +66,12 @@ "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional", "funding_fee_credit": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "channelFlags": { 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 29b22b63c..970b6f484 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 @@ -35,11 +35,12 @@ "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional", "funding_fee_credit": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "remoteParams": { @@ -66,10 +67,11 @@ "option_simple_close": "Mandatory", "wake_up_notification_provider": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "channelFlags": { 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 bc84ae02c..a23fb8361 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 @@ -33,12 +33,12 @@ "option_channel_type": "Mandatory", "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", - "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional" + "trampoline_payment_experimental": "Optional" }, "unknown": [ 137, - 145 + 145, + 155 ] } }, 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 3a5c81787..1d7419d7b 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 @@ -35,10 +35,11 @@ "option_simple_close": "Mandatory", "wake_up_notification_provider": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "remoteParams": { @@ -65,11 +66,12 @@ "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional", "funding_fee_credit": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "channelFlags": { 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 a82e98d61..51410709c 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 @@ -35,10 +35,11 @@ "option_simple_close": "Mandatory", "wake_up_notification_provider": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "remoteParams": { @@ -66,11 +67,12 @@ "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional", "funding_fee_credit": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "channelFlags": { 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 b2c81be9f..54c466270 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 @@ -35,10 +35,11 @@ "option_simple_close": "Mandatory", "wake_up_notification_provider": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "remoteParams": { @@ -66,11 +67,12 @@ "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional", "funding_fee_credit": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "channelFlags": { 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 03a1bc23c..e7ed94006 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 @@ -35,10 +35,11 @@ "option_simple_close": "Mandatory", "wake_up_notification_provider": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "remoteParams": { @@ -65,11 +66,12 @@ "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional", "on_the_fly_funding": "Optional", "funding_fee_credit": "Optional" }, - "unknown": [] + "unknown": [ + 155 + ] } }, "channelFlags": { 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 be7519e9d..2b5384bc2 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 @@ -60,11 +60,11 @@ "option_channel_type": "Mandatory", "zero_reserve_channels": "Optional", "wake_up_notification_client": "Optional", - "trampoline_payment_experimental": "Optional", - "splice_experimental": "Optional" + "trampoline_payment_experimental": "Optional" }, "unknown": [ - 137 + 137, + 155 ] } }, From d64734bdadb696c0cc56b017df2cfd6ba3e53687 Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 21 Jan 2026 14:29:02 +0100 Subject: [PATCH 2/3] Add `channel_reestablish` TLVs for retransmission With splicing, we introduce new TLVs to `channel_reestablish` to let our peer know: - the latest `splice_locked` (or `channel_ready`) we're ready to send or have sent before disconnecting - whether we need a retransmission of `commit_sig` for the next funding - whether we need a retransmission of `announcement_signatures` for the next funding (always false for mobile wallets) This lets us clean-up retransmission of those messages and follow the official splicing spec. --- .../fr/acinq/lightning/channel/Commitments.kt | 8 +- .../acinq/lightning/channel/InteractiveTx.kt | 7 +- .../acinq/lightning/channel/states/Channel.kt | 26 ++-- .../acinq/lightning/channel/states/Normal.kt | 13 +- .../acinq/lightning/channel/states/Syncing.kt | 48 +++--- .../channel/states/WaitForFundingConfirmed.kt | 13 +- .../acinq/lightning/json/JsonSerializers.kt | 5 + .../fr/acinq/lightning/wire/ChannelTlv.kt | 41 ++++- .../acinq/lightning/wire/LightningMessages.kt | 8 +- .../channel/states/OfflineTestsCommon.kt | 35 +++-- .../channel/states/SpliceTestsCommon.kt | 147 +++++++++--------- .../channel/states/SyncingTestsCommon.kt | 20 ++- .../fr/acinq/lightning/io/peer/PeerTest.kt | 8 +- .../wire/LightningCodecsTestsCommon.kt | 14 +- 14 files changed, 232 insertions(+), 161 deletions(-) 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 16dc5ceb4..894de5ef4 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 @@ -707,6 +707,8 @@ data class Commitments( // We always use the last commitment that was created, to make sure we never go back in time. val latest = FullCommitment(channelParams, changes, active.first()) + fun lastLocalLocked(zeroConf: Boolean): Commitment? = active.find { zeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx } + val all = buildList { addAll(active) addAll(inactive) @@ -1084,7 +1086,7 @@ data class Commitments( // This ensures that we only have to send splice_locked for the latest commitment instead of sending it for every commitment. // A side-effect is that previous commitments that are implicitly locked don't necessarily have their status correctly set. // That's why we look at locked commitments separately and then select the one with the oldest fundingTxIndex. - val lastLocalLocked = active.find { staticParams.useZeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx } + val lastLocal = lastLocalLocked(staticParams.useZeroConf) val lastRemoteLocked = active.find { it.remoteFundingStatus == RemoteFundingStatus.Locked } return when { // We select the locked commitment with the smaller value for fundingTxIndex, but both have to be defined. @@ -1093,9 +1095,9 @@ data class Commitments( // - transactions with the same fundingTxIndex double-spend each other, so only one of them can confirm // - we don't allow creating a splice on top of an unconfirmed transaction that has RBF attempts (because it // would become invalid if another of the RBF attempts end up being confirmed) - lastLocalLocked != null && lastRemoteLocked != null -> listOf(lastLocalLocked, lastRemoteLocked).minByOrNull { it.fundingTxIndex } + lastLocal != null && lastRemoteLocked != null -> listOf(lastLocal, lastRemoteLocked).minByOrNull { it.fundingTxIndex } // Special case for the initial funding tx, we only require a local lock because channel_ready doesn't explicitly reference a funding tx. - lastLocalLocked != null && lastLocalLocked.fundingTxIndex == 0L -> lastLocalLocked + lastLocal != null && lastLocal.fundingTxIndex == 0L -> lastLocal else -> null } } 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 fe12d578e..17b1dad34 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 @@ -1106,11 +1106,8 @@ data class InteractiveTxSigningSession( // +-------+ +-------+ 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 nextLocalCommitmentNumber = when (localCommit) { - is Either.Left -> localCommit.value.index - is Either.Right -> localCommit.value.index + 1 - } + // If we haven't received the remote commit_sig, we will request a retransmission on reconnection. + val retransmitRemoteCommitSig: Boolean = localCommit.isLeft fun localFundingKey(channelKeys: ChannelKeys): PrivateKey = fundingParams.fundingKey(channelKeys) 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 e82068220..08daa2c6d 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 @@ -317,12 +317,13 @@ sealed class PersistedChannelState : ChannelState() { } ChannelReestablish( channelId = channelId, - nextLocalCommitmentNumber = state.signingSession.nextLocalCommitmentNumber, + nextLocalCommitmentNumber = 1, nextRemoteRevocationNumber = 0, yourLastCommitmentSecret = PrivateKey(ByteVector32.Zeroes), myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint, nextCommitNonces = nextCommitNonce?.let { listOf(nextFundingTxId to it) } ?: listOf(), nextFundingTxId = nextFundingTxId, + retransmitCommitSig = state.signingSession.retransmitRemoteCommitSig, currentCommitNonce = currentCommitNonce ) } @@ -330,24 +331,13 @@ sealed class PersistedChannelState : ChannelState() { val channelKeys = channelKeys() val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes 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.nextLocalCommitmentNumber - else -> state.commitments.localCommitIndex + 1 - } - is Normal -> when (state.spliceStatus) { - is SpliceStatus.WaitingForSigs -> state.spliceStatus.session.nextLocalCommitmentNumber - else -> state.commitments.localCommitIndex + 1 - } - else -> state.commitments.localCommitIndex + 1 - } - // If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures. - val unsignedFundingTxId = when (state) { + // If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures and commit_sig. + val (unsignedFundingTxId, retransmitCommitSig) = when (state) { is WaitForFundingConfirmed -> state.getUnsignedFundingTxId() is Normal -> state.getUnsignedFundingTxId() - else -> null + else -> Pair(null, false) } + val lastFundingLocked = state.commitments.lastLocalLocked(staticParams.useZeroConf) // We send our verification nonces for all active commitments. val nextCommitNonces = state.commitments.active.mapNotNull { c -> when (c.commitmentFormat) { @@ -378,12 +368,14 @@ sealed class PersistedChannelState : ChannelState() { } ChannelReestablish( channelId = channelId, - nextLocalCommitmentNumber = nextLocalCommitmentNumber, + nextLocalCommitmentNumber = state.commitments.localCommitIndex + 1, nextRemoteRevocationNumber = state.commitments.remoteCommitIndex, yourLastCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret), myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint, nextCommitNonces = nextCommitNonces + listOfNotNull(interactiveTxNextCommitNonce), nextFundingTxId = unsignedFundingTxId, + retransmitCommitSig = retransmitCommitSig, + currentFundingLocked = lastFundingLocked?.fundingTxId, currentCommitNonce = interactiveTxCurrentCommitNonce, ) } 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 3aa63bc03..76c1dac94 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 @@ -935,11 +935,14 @@ data class Normal( return Pair(nextState, actions) } - /** If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. */ - fun getUnsignedFundingTxId(): TxId? = when { - spliceStatus is SpliceStatus.WaitingForSigs -> spliceStatus.session.fundingTx.txId - commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx && commitments.latest.localFundingStatus.sharedTx is PartiallySignedSharedTransaction -> commitments.latest.localFundingStatus.txId - else -> null + /** + * If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. + * The second parameter should be set to true when commit_sig also needs to be retransmitted. + */ + fun getUnsignedFundingTxId(): Pair = when { + spliceStatus is SpliceStatus.WaitingForSigs -> Pair(spliceStatus.session.fundingTx.txId, spliceStatus.session.retransmitRemoteCommitSig) + commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx && commitments.latest.localFundingStatus.sharedTx is PartiallySignedSharedTransaction -> Pair(commitments.latest.localFundingStatus.txId, false) + else -> Pair(null, false) } /** Returns true if the [shortChannelId] matches one of our commitments or our alias. */ 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 5991f6dc9..ff5107e72 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 @@ -25,7 +25,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: val (nextState, actions) = when (state) { is WaitForFundingSigned -> { val actions = buildList { - if (cmd.message.nextFundingTxId == state.signingSession.fundingTx.txId && cmd.message.nextLocalCommitmentNumber == 0L) { + if (cmd.message.nextFundingTxId == state.signingSession.fundingTx.txId && cmd.message.retransmitInteractiveTxCommitSig) { // 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}" } @@ -44,7 +44,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: else -> { if (state.rbfStatus is RbfStatus.WaitingForSigs && state.rbfStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) { val actions = buildList { - if (cmd.message.nextLocalCommitmentNumber == 0L) { + if (cmd.message.retransmitInteractiveTxCommitSig) { // 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}" } @@ -59,7 +59,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // 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) { + if (cmd.message.retransmitInteractiveTxCommitSig) { logger.info { "re-sending commit_sig for fundingTxId=${cmd.message.nextFundingTxId}" } when (val commitSig = state.commitments.latest.remoteCommit.sign( state.commitments.channelParams, @@ -91,11 +91,10 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: } is WaitForChannelReady -> { val actions = ArrayList() - // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures - // and our commit_sig if they haven't received it already. + // If they haven't received our signatures for the channel funding transaction, we retransmit them. if (state.commitments.latest.fundingTxId == cmd.message.nextFundingTxId) { if (state.commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx) { - if (cmd.message.nextLocalCommitmentNumber == 0L) { + if (cmd.message.retransmitInteractiveTxCommitSig) { logger.info { "re-sending commit_sig for fundingTxId=${state.commitments.latest.fundingTxId}" } when (val commitSig = state.commitments.latest.remoteCommit.sign( state.commitments.channelParams, @@ -143,7 +142,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // resume splice signing session if any val spliceStatus1 = if (state.spliceStatus is SpliceStatus.WaitingForSigs && state.spliceStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) { - if (cmd.message.nextLocalCommitmentNumber == state.commitments.remoteCommitIndex) { + if (cmd.message.retransmitInteractiveTxCommitSig) { // 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.fundingParams.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" } @@ -158,7 +157,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: is LocalFundingStatus.UnconfirmedFundingTx -> { // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures // and our commit_sig if they haven't received it already. - if (cmd.message.nextLocalCommitmentNumber == state.commitments.remoteCommitIndex) { + if (cmd.message.retransmitInteractiveTxCommitSig) { logger.info { "re-sending commit_sig for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" } when (val commitSig = state.commitments.latest.remoteCommit.sign( state.commitments.channelParams, @@ -177,6 +176,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: } logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" } actions.add(ChannelAction.Message.Send(localFundingStatus.sharedTx.localSigs)) + // If we're using 0-conf, we also retransmit our splice_locked. + if (staticParams.useZeroConf) actions.add(ChannelAction.Message.Send(SpliceLocked(channelId, cmd.message.nextFundingTxId))) } is LocalFundingStatus.ConfirmedFundingTx -> { // The funding tx is confirmed, and they have not received our tx_signatures, but they must have received our commit_sig, otherwise they @@ -195,26 +196,27 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: state.spliceStatus } - // Re-send splice_locked (must come *after* potentially retransmitting tx_signatures). - // NB: there is a key difference between channel_ready and splice_locked: - // - channel_ready: a non-zero commitment index implies that both sides have seen the channel_ready - // - splice_locked: the commitment index can be updated as long as it is compatible with all splices, so - // we must keep sending our most recent splice_locked at each reconnection - state.commitments.active - .filter { it.fundingTxIndex > 0L } // only consider splice txs - .firstOrNull { staticParams.useZeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx } - ?.let { - logger.debug { "re-sending splice_locked for fundingTxId=${it.fundingTxId}" } - val spliceLocked = SpliceLocked(channelId, it.fundingTxId) - actions.add(ChannelAction.Message.Send(spliceLocked)) + // Prune previous funding transactions and RBF attempts if we already sent splice_locked for the last funding + // transaction that is also locked by our counterparty; we either missed their splice_locked or it confirmed + // while disconnected. + val commitments1 = run { + val withRemoteLocked = when (val remoteFundingTxLocked = cmd.message.myCurrentFundingLocked) { + null -> state.commitments + else -> when (val commitments1 = state.commitments.run { updateRemoteFundingStatus(remoteFundingTxLocked) }) { + is Either.Left -> state.commitments + is Either.Right -> { + state.run { newlyLocked(state.commitments, commitments1.value.first) }.forEach { actions.add(ChannelAction.Storage.SetLocked(it.fundingTxId)) } + commitments1.value.first + } + } } + // Then we clean up unsigned updates. + discardUnsignedUpdates(withRemoteLocked) + } // we may need to retransmit updates and/or commit_sig and/or revocation actions.addAll(syncResult.retransmit.map { ChannelAction.Message.Send(it) }) - // then we clean up unsigned updates - val commitments1 = discardUnsignedUpdates(state.commitments) - if (commitments1.changes.localHasChanges()) { actions.add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign)) } 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 574a156ae..3afb01be4 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 @@ -352,13 +352,16 @@ data class WaitForFundingConfirmed( return Pair(nextState, actions) } - /** If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. */ - fun getUnsignedFundingTxId(): TxId? { + /** + * If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. + * The second parameter should be set to true when commit_sig also needs to be retransmitted. + */ + fun getUnsignedFundingTxId(): Pair { return when (rbfStatus) { - is RbfStatus.WaitingForSigs -> rbfStatus.session.fundingTx.txId + is RbfStatus.WaitingForSigs -> Pair(rbfStatus.session.fundingTx.txId, rbfStatus.session.retransmitRemoteCommitSig) else -> when (latestFundingTx.sharedTx) { - is PartiallySignedSharedTransaction -> latestFundingTx.txId - is FullySignedSharedTransaction -> null + is PartiallySignedSharedTransaction -> Pair(latestFundingTx.txId, false) + is FullySignedSharedTransaction -> Pair(null, false) } } } 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 b35b459f6..beab4ac89 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 @@ -94,6 +94,7 @@ JsonSerializers.ShutdownTlvShutdownNonceSerializer::class, JsonSerializers.ClosingCompleteTlvSerializer::class, JsonSerializers.ClosingSigTlvSerializer::class, + JsonSerializers.MyCurrentFundingLockedTlvSerializer::class, JsonSerializers.ChannelReestablishTlvSerializer::class, JsonSerializers.ChannelReestablishTlvNextLocalNoncesSerializer::class, JsonSerializers.ChannelReadyTlvSerializer::class, @@ -209,6 +210,7 @@ object JsonSerializers { subclass(UpdateFee::class, UpdateFeeSerializer) } polymorphic(Tlv::class) { + subclass(ChannelReestablishTlv.MyCurrentFundingLocked::class, MyCurrentFundingLockedTlvSerializer) subclass(ChannelReadyTlv.ShortChannelIdTlv::class, ChannelReadyTlvShortChannelIdTlvSerializer) subclass(ChannelReadyTlv.NextLocalNonce::class, ChannelReadyTlvNextLocalNonceSerializer) subclass(CommitSigTlv.FundingTx::class, CommitSigTlvFundingTxSerializer) @@ -589,6 +591,9 @@ object JsonSerializers { @Serializer(forClass = ChannelReadyTlv::class) object ChannelReadyTlvSerializer + @Serializer(forClass = ChannelReestablishTlv.MyCurrentFundingLocked::class) + object MyCurrentFundingLockedTlvSerializer + @Serializer(forClass = ChannelReestablishTlv::class) object ChannelReestablishTlvSerializer 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 b78edabb0..819f03079 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 @@ -235,13 +235,46 @@ sealed class RevokeAndAckTlv : Tlv { } sealed class ChannelReestablishTlv : Tlv { - data class NextFunding(val txId: TxId) : ChannelReestablishTlv() { + /** + * When disconnected in the middle of an interactive-tx session, this field is used to request a retransmission of + * [TxSignatures] for the given [txId]. + * + * @param txId the txId of the partially signed funding transaction. + * @param retransmitCommitSig true if [CommitSig] must be retransmitted before [TxSignatures]. + */ + data class NextFunding(val txId: TxId, val retransmitCommitSig: Boolean) : ChannelReestablishTlv() { override val tag: Long get() = NextFunding.tag - override fun write(out: Output) = LightningCodecs.writeTxHash(TxHash(txId), out) + override fun write(out: Output) { + LightningCodecs.writeTxHash(TxHash(txId), out) + LightningCodecs.writeByte(if (retransmitCommitSig) 1 else 0, out) + } companion object : TlvValueReader { - const val tag: Long = 0 - override fun read(input: Input): NextFunding = NextFunding(TxId(LightningCodecs.txHash(input))) + const val tag: Long = 1 + override fun read(input: Input): NextFunding = NextFunding( + txId = TxId(LightningCodecs.txHash(input)), + retransmitCommitSig = (LightningCodecs.byte(input) % 2) == 1, + ) + } + } + + /** + * @param txId the txId of our latest outgoing [ChannelReady] or [SpliceLocked] for this channel. + * @param retransmitAnnSigs true if [AnnouncementSignatures] must be retransmitted. + */ + data class MyCurrentFundingLocked(val txId: TxId, val retransmitAnnSigs: Boolean) : ChannelReestablishTlv() { + override val tag: Long get() = MyCurrentFundingLocked.tag + override fun write(out: Output) { + LightningCodecs.writeTxHash(TxHash(txId), out) + LightningCodecs.writeByte(if (retransmitAnnSigs) 1 else 0, out) + } + + companion object : TlvValueReader { + const val tag: Long = 5 + override fun read(input: Input): MyCurrentFundingLocked = MyCurrentFundingLocked( + txId = TxId(LightningCodecs.txHash(input)), + retransmitAnnSigs = (LightningCodecs.byte(input) % 2) == 1, + ) } } 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 d1f90d6e3..e8f581613 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 @@ -1450,6 +1450,8 @@ data class ChannelReestablish( myCurrentPerCommitmentPoint: PublicKey, nextCommitNonces: List>, nextFundingTxId: TxId? = null, + retransmitCommitSig: Boolean = false, + currentFundingLocked: TxId? = null, currentCommitNonce: IndividualNonce? = null ) : this( channelId = channelId, @@ -1459,8 +1461,9 @@ data class ChannelReestablish( myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint, tlvStream = TlvStream( setOfNotNull( + nextFundingTxId?.let { ChannelReestablishTlv.NextFunding(it, retransmitCommitSig) }, + currentFundingLocked?.let { ChannelReestablishTlv.MyCurrentFundingLocked(it, retransmitAnnSigs = false) }, if (nextCommitNonces.isNotEmpty()) ChannelReestablishTlv.NextLocalNonces(nextCommitNonces) else null, - nextFundingTxId?.let { ChannelReestablishTlv.NextFunding(it) }, currentCommitNonce?.let { ChannelReestablishTlv.CurrentCommitNonce(it) }, ) ) @@ -1469,6 +1472,8 @@ data class ChannelReestablish( override val type: Long get() = ChannelReestablish.type val nextFundingTxId: TxId? = tlvStream.get()?.txId + val retransmitInteractiveTxCommitSig: Boolean = tlvStream.get()?.retransmitCommitSig ?: false + val myCurrentFundingLocked: TxId? = tlvStream.get()?.txId val nextCommitNonces: Map = tlvStream.get()?.nonces?.toMap() ?: mapOf() val currentCommitNonce: IndividualNonce? = tlvStream.get()?.nonce @@ -1487,6 +1492,7 @@ data class ChannelReestablish( @Suppress("UNCHECKED_CAST") val readers = mapOf( ChannelReestablishTlv.NextFunding.tag to ChannelReestablishTlv.NextFunding.Companion as TlvValueReader, + ChannelReestablishTlv.MyCurrentFundingLocked.tag to ChannelReestablishTlv.MyCurrentFundingLocked.Companion as TlvValueReader, ChannelReestablishTlv.NextLocalNonces.tag to ChannelReestablishTlv.NextLocalNonces.Companion as TlvValueReader, ChannelReestablishTlv.CurrentCommitNonce.tag to ChannelReestablishTlv.CurrentCommitNonce.Companion as TlvValueReader, ) 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 8f63c9bc8..6aa9ecc78 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 @@ -65,28 +65,31 @@ class OfflineTestsCommon : LightningTestSuite() { assertIs(bob2.state) val channelReestablishB = actions1.findOutgoingMessage() + val fundingTxId = bob.commitments.latest.fundingTxId val bobCommitments = bob.commitments val aliceCommitments = alice.commitments val bobCurrentPerCommitmentPoint = bob.channelKeys.commitmentPoint(bobCommitments.localCommitIndex) val aliceCurrentPerCommitmentPoint = alice.channelKeys.commitmentPoint(aliceCommitments.localCommitIndex) // alice didn't receive any update or sig - assertEquals( - ChannelReestablish(alice.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), aliceCurrentPerCommitmentPoint), - channelReestablishA.copy(tlvStream = TlvStream.empty()) - ) - assertEquals( - ChannelReestablish(bob.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint), - channelReestablishB.copy(tlvStream = TlvStream.empty()) - ) + assertEquals(1, channelReestablishA.nextLocalCommitmentNumber) + assertEquals(0, channelReestablishA.nextRemoteRevocationNumber) + assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishA.yourLastCommitmentSecret) + assertEquals(aliceCurrentPerCommitmentPoint, channelReestablishA.myCurrentPerCommitmentPoint) + assertEquals(fundingTxId, channelReestablishA.myCurrentFundingLocked) + assertEquals(1, channelReestablishB.nextLocalCommitmentNumber) + assertEquals(0, channelReestablishB.nextRemoteRevocationNumber) + assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishB.yourLastCommitmentSecret) + assertEquals(bobCurrentPerCommitmentPoint, channelReestablishB.myCurrentPerCommitmentPoint) + assertEquals(fundingTxId, channelReestablishB.myCurrentFundingLocked) val (alice3, actions2) = alice2.process(ChannelCommand.MessageReceived(channelReestablishB)) - assertEquals(alice, alice3) + assertIs>(alice3) assertEquals(1, actions2.size) actions2.hasOutgoingMessage() val (bob3, actions3) = bob2.process(ChannelCommand.MessageReceived(channelReestablishA)) - assertEquals(bob, bob3) + assertIs>(bob3) assertEquals(1, actions3.size) actions3.hasOutgoingMessage() } @@ -118,6 +121,7 @@ class OfflineTestsCommon : LightningTestSuite() { assertIs(bob2.state) val channelReestablishB = actionsBob2.findOutgoingMessage() + val fundingTxId = bob0.commitments.latest.fundingTxId val bobCommitments = bob0.commitments val aliceCommitments = alice0.commitments val bobCurrentPerCommitmentPoint = bob0.channelKeys.commitmentPoint(bobCommitments.localCommitIndex) @@ -128,11 +132,17 @@ class OfflineTestsCommon : LightningTestSuite() { assertEquals(0, channelReestablishA.nextRemoteRevocationNumber) assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishA.yourLastCommitmentSecret) assertEquals(aliceCurrentPerCommitmentPoint, channelReestablishA.myCurrentPerCommitmentPoint) + assertEquals(fundingTxId, channelReestablishA.myCurrentFundingLocked) + assertNull(channelReestablishA.nextFundingTxId) + assertFalse(channelReestablishA.retransmitInteractiveTxCommitSig) // bob did not receive alice's sig assertEquals(1, channelReestablishB.nextLocalCommitmentNumber) assertEquals(0, channelReestablishB.nextRemoteRevocationNumber) assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishB.yourLastCommitmentSecret) assertEquals(bobCurrentPerCommitmentPoint, channelReestablishB.myCurrentPerCommitmentPoint) + assertEquals(fundingTxId, channelReestablishB.myCurrentFundingLocked) + assertNull(channelReestablishB.nextFundingTxId) + assertFalse(channelReestablishB.retransmitInteractiveTxCommitSig) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishB)) // alice sends ChannelReady again @@ -197,6 +207,7 @@ class OfflineTestsCommon : LightningTestSuite() { assertIs(bob2.state) val channelReestablishB = actionsBob2.findOutgoingMessage() + val fundingTxId = bob0.commitments.latest.fundingTxId val bobCommitments = bob0.commitments val aliceCommitments = alice0.commitments val bobCurrentPerCommitmentPoint = bob0.channelKeys.commitmentPoint(bobCommitments.localCommitIndex) @@ -207,11 +218,15 @@ class OfflineTestsCommon : LightningTestSuite() { assertEquals(0, channelReestablishA.nextRemoteRevocationNumber) assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishA.yourLastCommitmentSecret) assertEquals(aliceCurrentPerCommitmentPoint, channelReestablishA.myCurrentPerCommitmentPoint) + assertEquals(fundingTxId, channelReestablishA.myCurrentFundingLocked) + assertNull(channelReestablishA.nextFundingTxId) // bob did receive alice's sig assertEquals(2, channelReestablishB.nextLocalCommitmentNumber) assertEquals(0, channelReestablishB.nextRemoteRevocationNumber) assertEquals(PrivateKey(ByteVector32.Zeroes), channelReestablishB.yourLastCommitmentSecret) assertEquals(bobCurrentPerCommitmentPoint, channelReestablishB.myCurrentPerCommitmentPoint) + assertEquals(fundingTxId, channelReestablishB.myCurrentFundingLocked) + assertNull(channelReestablishB.nextFundingTxId) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishB)) // alice does not re-send messages bob already received 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 849be5245..b0009d767 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 @@ -883,7 +883,8 @@ class SpliceTestsCommon : LightningTestSuite() { assertNotNull(channelReestablishAlice.currentCommitNonce) assertContains(channelReestablishAlice.nextCommitNonces, alice.commitments.latest.fundingTxId) assertContains(channelReestablishAlice.nextCommitNonces, spliceStatus.session.fundingTx.txId) - assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, aliceCommitIndex) + assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, aliceCommitIndex + 1) + assertTrue(channelReestablishAlice.retransmitInteractiveTxCommitSig) val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(channelReestablishAlice)) assertIs>(bob3) assertEquals(actionsBob3.size, 4) @@ -894,7 +895,8 @@ class SpliceTestsCommon : LightningTestSuite() { val commitSigBob = actionsBob3.findOutgoingMessage() assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob3.filterIsInstance().map { it.add }.toSet()) assertEquals(channelReestablishBob.nextFundingTxId, spliceStatus.session.fundingTx.txId) - assertEquals(channelReestablishBob.nextLocalCommitmentNumber, bobCommitIndex) + assertEquals(channelReestablishBob.nextLocalCommitmentNumber, bobCommitIndex + 1) + assertTrue(channelReestablishBob.retransmitInteractiveTxCommitSig) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) assertIs>(alice3) assertEquals(actionsAlice3.size, 3) @@ -920,6 +922,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice4, bob3, channelReestablishAlice) = disconnect(alice3, bob2) assertEquals(channelReestablishAlice.nextFundingTxId, spliceTxId) + assertFalse(channelReestablishAlice.retransmitInteractiveTxCommitSig) assertNull(channelReestablishAlice.currentCommitNonce) assertContains(channelReestablishAlice.nextCommitNonces, alice.commitments.latest.fundingTxId) assertContains(channelReestablishAlice.nextCommitNonces, spliceTxId) @@ -933,7 +936,8 @@ class SpliceTestsCommon : LightningTestSuite() { assertNull(actionsBob4.findOutgoingMessageOpt()) assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) assertEquals(channelReestablishBob.nextFundingTxId, spliceTxId) - assertEquals(channelReestablishBob.nextLocalCommitmentNumber, bobCommitIndex) + assertEquals(channelReestablishBob.nextLocalCommitmentNumber, bobCommitIndex + 1) + assertTrue(channelReestablishBob.retransmitInteractiveTxCommitSig) val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(channelReestablishBob)) assertEquals(actionsAlice5.size, 3) val commitSigAlice = actionsAlice5.findOutgoingMessage() @@ -979,10 +983,11 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice2, bob3, channelReestablishAlice) = disconnect(alice1, bob2) assertEquals(channelReestablishAlice.nextFundingTxId, spliceTxId) + assertTrue(channelReestablishAlice.retransmitInteractiveTxCommitSig) assertNotNull(channelReestablishAlice.currentCommitNonce) assertContains(channelReestablishAlice.nextCommitNonces, alice.commitments.latest.fundingTxId) assertContains(channelReestablishAlice.nextCommitNonces, spliceTxId) - assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, aliceCommitIndex) + assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, aliceCommitIndex + 1) val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) assertEquals(actionsBob4.size, 5) val channelReestablishBob = actionsBob4.findOutgoingMessage() @@ -993,6 +998,7 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) val txSigsBob = actionsBob4.findOutgoingMessage() assertEquals(channelReestablishBob.nextFundingTxId, spliceTxId) + assertFalse(channelReestablishBob.retransmitInteractiveTxCommitSig) assertEquals(channelReestablishBob.nextLocalCommitmentNumber, bobCommitIndex + 1) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) assertEquals(actionsAlice3.size, 2) @@ -1036,24 +1042,26 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice2, bob3, channelReestablishAlice) = disconnect(alice1, bob2) assertEquals(channelReestablishAlice.nextFundingTxId, spliceTxId) + assertTrue(channelReestablishAlice.retransmitInteractiveTxCommitSig) assertNotNull(channelReestablishAlice.currentCommitNonce) assertContains(channelReestablishAlice.nextCommitNonces, alice.commitments.latest.fundingTxId) assertContains(channelReestablishAlice.nextCommitNonces, spliceTxId) val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) assertEquals(actionsBob4.size, 6) val channelReestablishBob = actionsBob4.findOutgoingMessage() + assertEquals(channelReestablishBob.nextFundingTxId, spliceTxId) + assertFalse(channelReestablishBob.retransmitInteractiveTxCommitSig) assertNull(channelReestablishBob.currentCommitNonce) assertContains(channelReestablishBob.nextCommitNonces, bob.commitments.latest.fundingTxId) assertContains(channelReestablishBob.nextCommitNonces, spliceTxId) val commitSigBob = actionsBob4.findOutgoingMessage() val txSigsBob = actionsBob4.findOutgoingMessage() - // splice_locked must always be sent *after* tx_signatures - assertIs(actionsBob4.filterIsInstance().last().message) val spliceLockedBob = actionsBob4.findOutgoingMessage() assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) - assertEquals(channelReestablishBob.nextFundingTxId, spliceTxId) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) + assertIs>(alice3) assertEquals(actionsAlice3.size, 2) + assertEquals(alice3.state.commitments.active.size, 1) assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice3.filterIsInstance().map { it.add }.toSet()) assertNull(actionsAlice3.findOutgoingMessageOpt()) @@ -1074,9 +1082,9 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(spliceLockedBob)) assertIs>(alice6) assertEquals(alice6.state.commitments.active.size, 1) - assertEquals(actionsAlice6.size, 2) - actionsAlice6.find().also { assertEquals(it.txId, spliceTxId) } + assertEquals(2, actionsAlice6.size) actionsAlice6.has() + actionsAlice6.find().also { assertEquals(it.txId, spliceTxId) } val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(txSigsAlice)) assertIs>(bob5) @@ -1138,9 +1146,9 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(channelReestablishBob)) assertIs>(alice6) assertEquals(alice6.state.spliceStatus, SpliceStatus.None) - assertEquals(4, actionsAlice6.size) + assertEquals(3, actionsAlice6.size) val txSigsAlice = actionsAlice6.hasOutgoingMessage() - actionsAlice6.hasOutgoingMessage() + assertNull(actionsAlice6.findOutgoingMessageOpt()) assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice6.filterIsInstance().map { it.add }.toSet()) // Bob receives tx_signatures, which completes the splice. @@ -1282,29 +1290,27 @@ class SpliceTestsCommon : LightningTestSuite() { // Alice disconnects before receiving Bob's splice_locked. val (alice3, bob4, channelReestablishAlice) = disconnect(alice2, bob3) + assertEquals(spliceTx.txid, channelReestablishAlice.myCurrentFundingLocked) + assertNull(channelReestablishAlice.nextFundingTxId) val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(actionsBob5.size, 4) + assertIs>(bob5) + assertEquals(actionsBob5.size, 3) + assertEquals(bob5.state.commitments.active.size, 1) val channelReestablishBob = actionsBob5.findOutgoingMessage() - val spliceLockedBob = actionsBob5.findOutgoingMessage() + assertEquals(spliceTx.txid, channelReestablishBob.myCurrentFundingLocked) + assertNull(channelReestablishBob.nextFundingTxId) + assertNull(actionsBob5.findOutgoingMessageOpt()) assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob5.filterIsInstance().map { it.add }.toSet()) + // Alice can lock the latest commitment as soon as she receives channel_reestablish. val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(channelReestablishBob)) + assertIs>(alice4) + assertEquals(alice4.state.commitments.active.size, 1) assertEquals(actionsAlice4.size, 3) - val spliceLockedAlice2 = actionsAlice4.hasOutgoingMessage() + assertNull(actionsAlice4.findOutgoingMessageOpt()) + actionsAlice4.has() assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice4.filterIsInstance().map { it.add }.toSet()) - val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(spliceLockedBob)) - assertIs>(alice5) - assertEquals(alice5.state.commitments.active.size, 1) - assertEquals(2, actionsAlice5.size) - actionsAlice5.has() - actionsAlice5.has() - - val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(spliceLockedAlice2)) - assertIs>(bob6) - assertEquals(bob6.state.commitments.active.size, 1) - assertEquals(actionsBob6.size, 1) - actionsBob6.has() - resolveHtlcs(alice5, bob6, htlcs, commitmentsCount = 1) + resolveHtlcs(alice4, bob5, htlcs, commitmentsCount = 1) } @Test @@ -1316,42 +1322,42 @@ class SpliceTestsCommon : LightningTestSuite() { // Alice and Bob have not received any remote splice_locked yet. assertEquals(alice2.commitments.active.size, 3) - alice2.commitments.active.forEach { assertEquals(it.remoteFundingStatus, RemoteFundingStatus.NotLocked) } + alice2.commitments.active.filter { it.fundingTxIndex > 0 }.forEach { assertEquals(it.remoteFundingStatus, RemoteFundingStatus.NotLocked) } assertEquals(bob2.commitments.active.size, 3) - bob2.commitments.active.forEach { assertEquals(it.remoteFundingStatus, RemoteFundingStatus.NotLocked) } + bob2.commitments.active.filter { it.fundingTxIndex > 0 }.forEach { assertEquals(it.remoteFundingStatus, RemoteFundingStatus.NotLocked) } - // On reconnection, Alice and Bob only send splice_locked for the latest commitment. + // On reconnection, Alice and Bob advertise that they want to lock the latest commitment. val (alice3, bob3, channelReestablishAlice) = disconnect(alice2, bob2) + assertEquals(alice2.commitments.active.first().fundingTxId, channelReestablishAlice.myCurrentFundingLocked) + assertNull(channelReestablishAlice.nextFundingTxId) + assertFalse(channelReestablishAlice.retransmitInteractiveTxCommitSig) val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(actionsBob4.size, 4) + assertIs>(bob4) + assertEquals(actionsBob4.size, 5) + // Bob immediately applies Alice's my_current_funding_locked, even though he hasn't received splice_locked yet. + assertEquals(bob4.commitments.active.size, 1) val channelReestablishBob = actionsBob4.findOutgoingMessage() - val spliceLockedBob = actionsBob4.findOutgoingMessage() + assertEquals(bob2.commitments.active.first().fundingTxId, channelReestablishBob.myCurrentFundingLocked) + assertNull(channelReestablishBob.nextFundingTxId) + assertFalse(channelReestablishBob.retransmitInteractiveTxCommitSig) + assertNull(actionsBob4.findOutgoingMessageOpt()) assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) - assertEquals(spliceLockedBob.fundingTxId, bob2.commitments.latest.fundingTxId) + assertContains(actionsBob4, ChannelAction.Storage.SetLocked(bob1.commitments.latest.fundingTxId)) + assertContains(actionsBob4, ChannelAction.Storage.SetLocked(bob2.commitments.latest.fundingTxId)) val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(channelReestablishBob)) - assertEquals(actionsAlice4.size, 3) - val spliceLockedAlice = actionsAlice4.hasOutgoingMessage() + assertIs>(alice4) + assertEquals(actionsAlice4.size, 4) + // Alice immediately applies Bob's my_current_funding_locked, even though she hasn't received splice_locked yet. + assertEquals(alice4.commitments.active.size, 1) + assertEquals(alice4.commitments.latest.fundingTxId, channelReestablishAlice.myCurrentFundingLocked) + assertNull(actionsAlice4.findOutgoingMessageOpt()) assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice4.filterIsInstance().map { it.add }.toSet()) - assertEquals(spliceLockedAlice.fundingTxId, spliceLockedBob.fundingTxId) - val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(spliceLockedBob)) - assertEquals(actionsAlice5.size, 3) - assertEquals(alice5.commitments.active.size, 1) - assertEquals(alice5.commitments.latest.fundingTxId, spliceLockedBob.fundingTxId) - actionsAlice5.has() - assertContains(actionsAlice5, ChannelAction.Storage.SetLocked(alice1.commitments.latest.fundingTxId)) - assertContains(actionsAlice5, ChannelAction.Storage.SetLocked(alice2.commitments.latest.fundingTxId)) + assertContains(actionsAlice4, ChannelAction.Storage.SetLocked(alice1.commitments.latest.fundingTxId)) + assertContains(actionsAlice4, ChannelAction.Storage.SetLocked(alice2.commitments.latest.fundingTxId)) - val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(spliceLockedAlice)) - assertEquals(actionsBob5.size, 3) - assertEquals(bob5.commitments.active.size, 1) - assertEquals(bob5.commitments.latest.fundingTxId, spliceLockedAlice.fundingTxId) - actionsBob5.has() - assertContains(actionsBob5, ChannelAction.Storage.SetLocked(bob1.commitments.latest.fundingTxId)) - assertContains(actionsBob5, ChannelAction.Storage.SetLocked(bob2.commitments.latest.fundingTxId)) - assertIs>(alice5) - assertIs>(bob5) - resolveHtlcs(alice5, bob5, htlcs, commitmentsCount = 1) + // Alice and Bob can now resolve HTLCs. + resolveHtlcs(alice4, bob4, htlcs, commitmentsCount = 1) } @Test @@ -1366,9 +1372,9 @@ class SpliceTestsCommon : LightningTestSuite() { // Alice and Bob have not received any remote splice_locked yet. assertEquals(alice2.commitments.active.size, 3) - alice2.commitments.active.forEach { assertEquals(it.remoteFundingStatus, RemoteFundingStatus.NotLocked) } + alice2.commitments.active.filter { it.fundingTxIndex > 0 }.forEach { assertEquals(it.remoteFundingStatus, RemoteFundingStatus.NotLocked) } assertEquals(bob2.commitments.active.size, 3) - bob2.commitments.active.forEach { assertEquals(it.remoteFundingStatus, RemoteFundingStatus.NotLocked) } + bob2.commitments.active.filter { it.fundingTxIndex > 0 }.forEach { assertEquals(it.remoteFundingStatus, RemoteFundingStatus.NotLocked) } // Alice locks the last commitment. val (alice3, actionsAlice3) = alice2.process(ChannelCommand.WatchReceived(WatchConfirmedTriggered(alice.channelId, WatchConfirmed.ChannelFundingDepthOk, 100, 0, spliceTx2))) @@ -1389,32 +1395,29 @@ class SpliceTestsCommon : LightningTestSuite() { // Alice and Bob disconnect before receiving each other's splice_locked. // On reconnection, the latest commitment is still unlocked by Bob so they have two active commitments. val (alice4, bob4, channelReestablishAlice) = disconnect(alice3, bob3) + assertEquals(spliceTx2.txid, channelReestablishAlice.myCurrentFundingLocked) + assertNull(channelReestablishAlice.nextFundingTxId) val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(channelReestablishAlice)) + assertIs>(bob5) assertEquals(actionsBob5.size, 4) + assertEquals(bob5.commitments.active.map { it.fundingTxId }, listOf(spliceTx2.txid, spliceTx1.txid)) val channelReestablishBob = actionsBob5.findOutgoingMessage() - val spliceLockedBob = actionsBob5.findOutgoingMessage() + assertEquals(spliceTx1.txid, channelReestablishBob.myCurrentFundingLocked) + assertNull(channelReestablishBob.nextFundingTxId) + assertNull(actionsBob5.findOutgoingMessageOpt()) assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob5.filterIsInstance().map { it.add }.toSet()) - assertEquals(spliceLockedBob.fundingTxId, spliceTx1.txid) + assertContains(actionsBob5, ChannelAction.Storage.SetLocked(spliceTx1.txid)) val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(channelReestablishBob)) + assertIs>(alice5) assertEquals(actionsAlice5.size, 3) - val spliceLockedAlice = actionsAlice5.hasOutgoingMessage() + assertEquals(alice5.commitments.active.map { it.fundingTxId }, listOf(spliceTx2.txid, spliceTx1.txid)) + assertNull(actionsAlice5.findOutgoingMessageOpt()) assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice5.filterIsInstance().map { it.add }.toSet()) - assertEquals(spliceLockedAlice.fundingTxId, spliceTx2.txid) - val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(spliceLockedBob)) - assertEquals(actionsAlice6.size, 2) - assertEquals(alice6.commitments.active.map { it.fundingTxId }, listOf(spliceTx2.txid, spliceTx1.txid)) - actionsAlice6.has() - actionsAlice6.contains(ChannelAction.Storage.SetLocked(spliceTx1.txid)) + actionsAlice5.contains(ChannelAction.Storage.SetLocked(spliceTx1.txid)) - val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(spliceLockedAlice)) - assertEquals(actionsBob6.size, 2) - assertEquals(bob6.commitments.active.map { it.fundingTxId }, listOf(spliceTx2.txid, spliceTx1.txid)) - actionsBob6.has() - actionsBob6.contains(ChannelAction.Storage.SetLocked(spliceTx1.txid)) - assertIs>(alice6) - assertIs>(bob6) - resolveHtlcs(alice6, bob6, htlcs, commitmentsCount = 2) + // Alice and Bob can now resolve HTLCs. + resolveHtlcs(alice5, bob5, htlcs, commitmentsCount = 2) } @Test 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 c61872fe9..05b188c94 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 @@ -95,9 +95,11 @@ class SyncingTestsCommon : LightningTestSuite() { val (alice1, bob1, channelReestablishAlice, channelReestablishBob) = disconnectWithBackup(alice, bob) assertNotNull(channelReestablishBob) assertEquals(channelReestablishAlice.nextFundingTxId, fundingTxId) - assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, 0) + assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, 1) + assertTrue(channelReestablishAlice.retransmitInteractiveTxCommitSig) assertEquals(channelReestablishBob.nextFundingTxId, fundingTxId) - assertEquals(channelReestablishBob.nextLocalCommitmentNumber, 0) + assertEquals(channelReestablishBob.nextLocalCommitmentNumber, 1) + assertTrue(channelReestablishBob.retransmitInteractiveTxCommitSig) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(channelReestablishAlice)) assertEquals(actionsBob2.size, 1) @@ -132,8 +134,10 @@ class SyncingTestsCommon : LightningTestSuite() { assertNotNull(channelReestablishBob) assertEquals(channelReestablishAlice.nextFundingTxId, fundingTxId) assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, 1) + assertFalse(channelReestablishAlice.retransmitInteractiveTxCommitSig) assertEquals(channelReestablishBob.nextFundingTxId, fundingTxId) - assertEquals(channelReestablishBob.nextLocalCommitmentNumber, 0) + assertEquals(channelReestablishBob.nextLocalCommitmentNumber, 1) + assertTrue(channelReestablishBob.retransmitInteractiveTxCommitSig) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(channelReestablishAlice)) // Bob is waiting for Alice's commit_sig before sending his tx_signatures. @@ -169,12 +173,14 @@ class SyncingTestsCommon : LightningTestSuite() { val (alice1, bob2, channelReestablishAlice, channelReestablishBob0) = disconnectWithBackup(alice, bob1) assertNull(channelReestablishBob0) assertEquals(channelReestablishAlice.nextFundingTxId, fundingTxId) - assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, 0) + assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, 1) + assertTrue(channelReestablishAlice.retransmitInteractiveTxCommitSig) val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(channelReestablishAlice)) val channelReestablishBob = actionsBob3.hasOutgoingMessage() assertEquals(channelReestablishBob.nextFundingTxId, fundingTxId) assertEquals(channelReestablishBob.nextLocalCommitmentNumber, 1) + assertFalse(channelReestablishBob.retransmitInteractiveTxCommitSig) val commitSigBob = actionsBob3.hasOutgoingMessage() val txSigsBob = actionsBob3.hasOutgoingMessage() @@ -276,13 +282,15 @@ class SyncingTestsCommon : LightningTestSuite() { val (alice1, bob1, channelReestablishAlice, channelReestablishBob0) = disconnectWithBackup(alice, bob) assertNull(channelReestablishBob0) assertEquals(channelReestablishAlice.nextFundingTxId, rbfFundingTxId) - assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, 0) + assertEquals(channelReestablishAlice.nextLocalCommitmentNumber, 1) + assertTrue(channelReestablishAlice.retransmitInteractiveTxCommitSig) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(channelReestablishAlice)) assertEquals(actionsBob2.size, 2) val channelReestablishBob = actionsBob2.hasOutgoingMessage() assertEquals(channelReestablishBob.nextFundingTxId, rbfFundingTxId) - assertEquals(channelReestablishBob.nextLocalCommitmentNumber, 0) + assertEquals(channelReestablishBob.nextLocalCommitmentNumber, 1) + assertTrue(channelReestablishBob.retransmitInteractiveTxCommitSig) val commitSigBob = actionsBob2.hasOutgoingMessage() val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(channelReestablishBob)) 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 c67cb978b..057c14a95 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 @@ -361,10 +361,11 @@ class PeerTest : LightningTestSuite() { // Simulate a reconnection with Alice. peer.send(MessageReceived(connectionId = 0, Init(features = alice0.staticParams.nodeParams.features))) peer.send(MessageReceived(connectionId = 0, PeerStorageRetrieval(backup))) + // We remove TLVs which may affect channel_ready / splice_locked behavior. val aliceReestablish = alice1.state.run { alice1.ctx.createChannelReestablish() } - peer.send(MessageReceived(connectionId = 0, aliceReestablish)) + peer.send(MessageReceived(connectionId = 0, aliceReestablish.copy(tlvStream = TlvStream(aliceReestablish.tlvStream.records.filterNot { it is ChannelReestablishTlv.MyCurrentFundingLocked }.toSet())))) - // Wait until the channels are Syncing + // Wait until the channels are Syncing. val restoredChannel = peer.channelsFlow .first { it.size == 1 } .values @@ -393,8 +394,9 @@ class PeerTest : LightningTestSuite() { // Simulate a reconnection with Alice. peer.send(MessageReceived(connectionId = 0, Init(features = alice0.staticParams.nodeParams.features))) peer.send(MessageReceived(connectionId = 0, PeerStorageRetrieval(backup))) + // We remove TLVs which may affect channel_ready / splice_locked behavior. val aliceReestablish = alice1.state.run { alice1.ctx.createChannelReestablish() } - peer.send(MessageReceived(connectionId = 0, aliceReestablish)) + peer.send(MessageReceived(connectionId = 0, aliceReestablish.copy(tlvStream = TlvStream(aliceReestablish.tlvStream.records.filterNot { it is ChannelReestablishTlv.MyCurrentFundingLocked }.toSet())))) // Wait until the channels are Syncing val restoredChannel = peer.channelsFlow 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 00e2f3076..88f926122 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 @@ -614,9 +614,9 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val testCases = listOf( // @formatter:off ChannelReestablish(channelId, 242842, 42, commitmentSecret, commitmentPoint) to ByteVector("0088 c11b8fbd682b3c6ee11f9d7268e22bb5887cd4d3bf3338bfcc340583f685733c 000000000003b49a 000000000000002a 34f159d37cf7b5de52ec0adc3968886232f90d272e8c82e8b6f7fcb7e57c4b55 02bf050efff417efc09eb211ca9e4e845920e2503740800e88505b25e6f0e1e867"), - ChannelReestablish(channelId, 242842, 42, commitmentSecret, commitmentPoint, nextCommitNonces = listOf(), nextFundingTxId = nextFundingTxId) to ByteVector("0088 c11b8fbd682b3c6ee11f9d7268e22bb5887cd4d3bf3338bfcc340583f685733c 000000000003b49a 000000000000002a 34f159d37cf7b5de52ec0adc3968886232f90d272e8c82e8b6f7fcb7e57c4b55 02bf050efff417efc09eb211ca9e4e845920e2503740800e88505b25e6f0e1e867 00 20 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566"), + ChannelReestablish(channelId, 242842, 42, commitmentSecret, commitmentPoint, nextCommitNonces = listOf(), nextFundingTxId = nextFundingTxId) to ByteVector("0088 c11b8fbd682b3c6ee11f9d7268e22bb5887cd4d3bf3338bfcc340583f685733c 000000000003b49a 000000000000002a 34f159d37cf7b5de52ec0adc3968886232f90d272e8c82e8b6f7fcb7e57c4b55 02bf050efff417efc09eb211ca9e4e845920e2503740800e88505b25e6f0e1e867 012124e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f1456600"), ChannelReestablish(channelId, 242842, 42, commitmentSecret, commitmentPoint, listOf(Pair(fundingTxId, commitNonce), Pair(nextFundingTxId, nextCommitNonce))) to ByteVector("0088 c11b8fbd682b3c6ee11f9d7268e22bb5887cd4d3bf3338bfcc340583f685733c 000000000003b49a 000000000000002a 34f159d37cf7b5de52ec0adc3968886232f90d272e8c82e8b6f7fcb7e57c4b55 02bf050efff417efc09eb211ca9e4e845920e2503740800e88505b25e6f0e1e867 16c4422c292416d1151e5bfd210990b2306e5c4d3e2e3f9d53247bbd5557c96fbfa0798cd38fe6ea51c1e8f021007c809f613034f1cb702cc64f4b9370c3bb73a67844ed280d26ae541a9c855a035fe0d87daf6b5de90a2f8785947aa4c10f294a5131f524e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566d8bc06ffaf24f29fb6310290262651a3154928bc8ad0d1546257e1bfe19ae6ac4ec02c15ee3f397f601d2c521ff65c604d04170b42ffee95db597d82df2bfde5b14a"), - ChannelReestablish(channelId, 242842, 42, commitmentSecret, commitmentPoint, listOf(Pair(nextFundingTxId, nextCommitNonce)), nextFundingTxId, commitNonce) to ByteVector("0088 c11b8fbd682b3c6ee11f9d7268e22bb5887cd4d3bf3338bfcc340583f685733c 000000000003b49a 000000000000002a 34f159d37cf7b5de52ec0adc3968886232f90d272e8c82e8b6f7fcb7e57c4b55 02bf050efff417efc09eb211ca9e4e845920e2503740800e88505b25e6f0e1e867 002024e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566 166224e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566d8bc06ffaf24f29fb6310290262651a3154928bc8ad0d1546257e1bfe19ae6ac4ec02c15ee3f397f601d2c521ff65c604d04170b42ffee95db597d82df2bfde5b14a 1842798cd38fe6ea51c1e8f021007c809f613034f1cb702cc64f4b9370c3bb73a67844ed280d26ae541a9c855a035fe0d87daf6b5de90a2f8785947aa4c10f294a5131f5"), + ChannelReestablish(channelId, 242842, 42, commitmentSecret, commitmentPoint, listOf(Pair(nextFundingTxId, nextCommitNonce)), nextFundingTxId, retransmitCommitSig = true, currentFundingLocked = fundingTxId, commitNonce) to ByteVector("0088 c11b8fbd682b3c6ee11f9d7268e22bb5887cd4d3bf3338bfcc340583f685733c 000000000003b49a 000000000000002a 34f159d37cf7b5de52ec0adc3968886232f90d272e8c82e8b6f7fcb7e57c4b55 02bf050efff417efc09eb211ca9e4e845920e2503740800e88505b25e6f0e1e867 012124e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f1456601 0521422c292416d1151e5bfd210990b2306e5c4d3e2e3f9d53247bbd5557c96fbfa000 166224e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566d8bc06ffaf24f29fb6310290262651a3154928bc8ad0d1546257e1bfe19ae6ac4ec02c15ee3f397f601d2c521ff65c604d04170b42ffee95db597d82df2bfde5b14a 1842798cd38fe6ea51c1e8f021007c809f613034f1cb702cc64f4b9370c3bb73a67844ed280d26ae541a9c855a035fe0d87daf6b5de90a2f8785947aa4c10f294a5131f5"), // @formatter:on ) testCases.forEach { (message, bin) -> @@ -791,12 +791,12 @@ class LightningCodecsTestsCommon : LightningTestSuite() { // @formatter:off val refs = mapOf( // channel_reestablish - Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point), - Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("01 02 0102") to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(setOf(), setOf(GenericTlv(1, ByteVector("0102"))))), Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream()), - Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("01 02 0102") to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(setOf(), setOf(GenericTlv(1, ByteVector("0102"))))), - Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream()), - Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("01 02 0102") to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(setOf(), setOf(GenericTlv(1, ByteVector("0102"))))), + Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("01 21") + nextTxHash.value.toByteArray() + Hex.decode("00") to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFunding(TxId(nextTxHash), retransmitCommitSig = false))), + Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("01 21") + nextTxHash.value.toByteArray() + Hex.decode("01") to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFunding(TxId(nextTxHash), retransmitCommitSig = true))), + Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("05 21") + txHash.value.toByteArray() + Hex.decode("00") to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.MyCurrentFundingLocked(TxId(txHash), retransmitAnnSigs = false))), + Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("05 21") + txHash.value.toByteArray() + Hex.decode("01") to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.MyCurrentFundingLocked(TxId(txHash), retransmitAnnSigs = true))), + Hex.decode("0088") + channelId.toByteArray() + Hex.decode("0001020304050607 0809aabbccddeeff") + key.value.toByteArray() + point.value.toByteArray() + Hex.decode("71 02 0102") to ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(setOf(), setOf(GenericTlv(113, ByteVector("0102"))))), // tx_signatures Hex.decode("0047") + channelId.toByteArray() + txHash.value.toByteArray() + Hex.decode("0000") to TxSignatures(channelId, TxId(txHash), listOf()), Hex.decode("0047") + channelId.toByteArray() + txHash.value.toByteArray() + Hex.decode("0000 2b012a") to TxSignatures(channelId, TxId(txHash), listOf(), TlvStream(setOf(), setOf(GenericTlv(43, ByteVector("2a"))))), From 8da060515874eb174f9d2b3fb81b24510048d755 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 12 May 2026 18:06:26 +0200 Subject: [PATCH 3/3] Update remote funding status on `channel_ready` When we receive the initial `channel_ready`, we update the remote funding status to be consistent with our reestablish behavior, where we would update it when receiving `my_current_funding_locked`. --- .../lightning/channel/states/WaitForChannelReady.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 d90ac9425..2163a24c6 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 @@ -62,6 +62,7 @@ data class WaitForChannelReady( Pair(this@WaitForChannelReady, listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidRbfTxConfirmed(channelId, commitments.latest.fundingTxId).message)))) } is ChannelReady -> { + val fundingTxId = commitments.latest.fundingTxId // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced val initialChannelUpdate = Announcements.makeChannelUpdate( staticParams.nodeParams.chainHash, @@ -77,10 +78,14 @@ data class WaitForChannelReady( ) val remoteNextCommitNonces1 = when (val nextCommitNonce = cmd.message.nextLocalNonce) { null -> remoteNextCommitNonces - else -> remoteNextCommitNonces + mapOf(commitments.latest.fundingTxId to nextCommitNonce) + else -> remoteNextCommitNonces + mapOf(fundingTxId to nextCommitNonce) + } + val commitments1 = when (val commitments1 = commitments.run { updateRemoteFundingStatus(fundingTxId) }) { + is Either.Left -> commitments1.value + is Either.Right -> commitments1.value.first } val nextState = Normal( - commitments, + commitments1, remoteNextCommitNonces1, shortChannelId, initialChannelUpdate, @@ -93,7 +98,7 @@ data class WaitForChannelReady( ) val actions = listOf( ChannelAction.Storage.StoreState(nextState), - ChannelAction.Storage.SetLocked(commitments.latest.fundingTxId), + ChannelAction.Storage.SetLocked(fundingTxId), ChannelAction.EmitEvent(ChannelEvents.Confirmed(nextState)), ) Pair(nextState, actions)