From 49b31e30ea06cfbb0f4bf5d6587c8a5c55b22c69 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Fri, 4 Feb 2022 18:36:43 +0100 Subject: [PATCH] Require explicit channel type negotiation (#277) * Rename ChannelTypes -> ChannelData And move commands to a new ChannelCommands file. This commit doesn't contain any logic changes. * Require explicit channel type negotiation This feature was introduced by lightningnetwork/lightning-rfc#880 and the feature bit was added in https://github.com/lightning/bolts/pull/906 We also clean up the channel state machine to leverage the feature bits added in #237 --- .../fr/acinq/lightning/tests/TestConstants.kt | 24 +- .../acinq/lightning/tests/io/peer/builders.kt | 2 +- .../kotlin/fr/acinq/lightning/Features.kt | 7 + .../kotlin/fr/acinq/lightning/NodeParams.kt | 1 + .../fr/acinq/lightning/channel/Channel.kt | 274 +++--- .../lightning/channel/ChannelCommands.kt | 34 + .../acinq/lightning/channel/ChannelConfig.kt | 70 ++ .../{ChannelTypes.kt => ChannelData.kt} | 88 +- .../lightning/channel/ChannelException.kt | 2 + .../lightning/channel/ChannelFeatures.kt | 100 ++ .../fr/acinq/lightning/channel/Commitments.kt | 9 +- .../fr/acinq/lightning/channel/Helpers.kt | 53 +- .../fr/acinq/lightning/crypto/KeyManager.kt | 21 +- .../kotlin/fr/acinq/lightning/io/Peer.kt | 3 +- .../lightning/serialization/Serialization.kt | 32 +- .../serialization/v1/ChannelState.kt | 50 +- .../serialization/v1/Serialization.kt | 2 +- .../serialization/v2/ChannelState.kt | 52 +- .../serialization/v2/Serialization.kt | 5 +- .../serialization/v3/ChannelState.kt | 923 ++++++++++++++++++ .../serialization/v3/Serialization.kt | 246 +++++ .../serialization/v3/bitcoinKSerializers.kt | 243 +++++ .../kotlin/fr/acinq/lightning/utils/Try.kt | 9 +- .../fr/acinq/lightning/wire/ChannelTlv.kt | 66 +- .../acinq/lightning/wire/LightningMessages.kt | 18 +- .../channel/ChannelConfigTestsCommon.kt | 23 + ...stsCommon.kt => ChannelDataTestsCommon.kt} | 8 +- .../channel/ChannelFeaturesTestsCommon.kt | 79 ++ .../channel/CommitmentsTestsCommon.kt | 12 +- .../fr/acinq/lightning/channel/TestsHelper.kt | 83 +- .../channel/states/ClosingTestsCommon.kt | 21 +- .../channel/states/NegotiatingTestsCommon.kt | 15 +- .../channel/states/NormalTestsCommon.kt | 26 +- .../channel/states/OfflineTestsCommon.kt | 80 +- .../channel/states/ShutdownTestsCommon.kt | 43 +- .../channel/states/SyncingTestsCommon.kt | 22 +- .../states/WaitForAcceptChannelTestsCommon.kt | 68 +- .../WaitForFundingConfirmedTestsCommon.kt | 59 +- .../WaitForFundingCreatedTestsCommon.kt | 24 +- .../states/WaitForFundingLockedTestsCommon.kt | 4 +- .../states/WaitForFundingSignedTestsCommon.kt | 22 +- .../states/WaitForOpenChannelTestsCommon.kt | 131 ++- .../crypto/LocalKeyManagerTestsCommon.kt | 10 +- .../fr/acinq/lightning/io/peer/PeerTest.kt | 7 +- .../serialization/CompatibilityTestsCommon.kt | 25 +- .../StateSerializationTestsCommon.kt | 16 +- .../transactions/AnchorOutputsTestsCommon.kt | 10 +- .../wire/LightningCodecsTestsCommon.kt | 66 +- .../lightning/wire/OpenTlvTestsCommon.kt | 80 +- src/jvmTest/kotlin/fr/acinq/lightning/Node.kt | 1 + .../db/sqlite/SqliteChannelsDbTestsJvm.kt | 5 +- 51 files changed, 2699 insertions(+), 575 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommands.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelConfig.kt rename src/commonMain/kotlin/fr/acinq/lightning/channel/{ChannelTypes.kt => ChannelData.kt} (83%) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/Serialization.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/bitcoinKSerializers.kt create mode 100644 src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelConfigTestsCommon.kt rename src/commonTest/kotlin/fr/acinq/lightning/channel/{ChannelTypesTestsCommon.kt => ChannelDataTestsCommon.kt} (98%) create mode 100644 src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelFeaturesTestsCommon.kt diff --git a/lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/TestConstants.kt b/lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/TestConstants.kt index 40eb3c467..62324f21a 100644 --- a/lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/TestConstants.kt +++ b/lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/TestConstants.kt @@ -34,8 +34,8 @@ object TestConstants { object Alice { private val entropy = Hex.decode("0101010101010101010101010101010101010101010101010101010101010101") - val mnemonics = MnemonicCode.toMnemonics(entropy) - val seed = MnemonicCode.toSeed(mnemonics, "").toByteVector32() + private val mnemonics = MnemonicCode.toMnemonics(entropy) + private val seed = MnemonicCode.toSeed(mnemonics, "").toByteVector32() val keyManager = LocalKeyManager(seed, Block.RegtestGenesisBlock.hash) val walletParams = WalletParams(NodeUri(randomKey().publicKey(), "alice.com", 9735), trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144))) @@ -53,13 +53,12 @@ object TestConstants { Feature.Wumbo to FeatureSupport.Optional, Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, + Feature.ChannelType to FeatureSupport.Mandatory, Feature.TrampolinePayment to FeatureSupport.Optional, - Feature.ZeroReserveChannels to FeatureSupport.Optional, - Feature.ZeroConfChannels to FeatureSupport.Optional, - Feature.WakeUpNotificationClient to FeatureSupport.Optional, - Feature.PayToOpenClient to FeatureSupport.Optional, - Feature.TrustedSwapInClient to FeatureSupport.Optional, - Feature.ChannelBackupClient to FeatureSupport.Optional, + Feature.WakeUpNotificationProvider to FeatureSupport.Optional, + Feature.PayToOpenProvider to FeatureSupport.Optional, + Feature.TrustedSwapInProvider to FeatureSupport.Optional, + Feature.ChannelBackupProvider to FeatureSupport.Optional, ), dustLimit = 1_100.sat, maxRemoteDustLimit = 1_500.sat, @@ -100,7 +99,7 @@ object TestConstants { enableTrampolinePayment = true ) - val closingPubKeyInfo = keyManager.closingPubkeyScript(PublicKey.Generator) + private val closingPubKeyInfo = keyManager.closingPubkeyScript(PublicKey.Generator) val channelParams: LocalParams = PeerChannels.makeChannelParams( nodeParams, defaultFinalScriptPubkey = ByteVector(closingPubKeyInfo.second), @@ -112,7 +111,7 @@ object TestConstants { object Bob { private val entropy = Hex.decode("0202020202020202020202020202020202020202020202020202020202020202") val mnemonics = MnemonicCode.toMnemonics(entropy) - val seed = MnemonicCode.toSeed(mnemonics, "").toByteVector32() + private val seed = MnemonicCode.toSeed(mnemonics, "").toByteVector32() val keyManager = LocalKeyManager(seed, Block.RegtestGenesisBlock.hash) val walletParams = WalletParams(NodeUri(randomKey().publicKey(), "bob.com", 9735), trampolineFees, InvoiceDefaultRoutingFees(1_000.msat, 100, CltvExpiryDelta(144))) val nodeParams = NodeParams( @@ -129,9 +128,8 @@ object TestConstants { Feature.Wumbo to FeatureSupport.Optional, Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, + Feature.ChannelType to FeatureSupport.Mandatory, Feature.TrampolinePayment to FeatureSupport.Optional, - Feature.ZeroReserveChannels to FeatureSupport.Optional, - Feature.ZeroConfChannels to FeatureSupport.Optional, Feature.WakeUpNotificationClient to FeatureSupport.Optional, Feature.PayToOpenClient to FeatureSupport.Optional, Feature.TrustedSwapInClient to FeatureSupport.Optional, @@ -176,7 +174,7 @@ object TestConstants { enableTrampolinePayment = true ) - val closingPubKeyInfo = keyManager.closingPubkeyScript(PublicKey.Generator) + private val closingPubKeyInfo = keyManager.closingPubkeyScript(PublicKey.Generator) val channelParams: LocalParams = PeerChannels.makeChannelParams( nodeParams, defaultFinalScriptPubkey = ByteVector(closingPubKeyInfo.second), diff --git a/lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt b/lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt index 79a257052..a548eb8e4 100644 --- a/lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt +++ b/lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt @@ -136,7 +136,7 @@ public suspend fun CoroutineScope.newPeer( } val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes - val channelKeyPath = peer.nodeParams.keyManager.channelKeyPath(state.commitments.localParams, state.commitments.channelVersion) + val channelKeyPath = peer.nodeParams.keyManager.channelKeyPath(state.commitments.localParams, state.commitments.channelConfig) val myCurrentPerCommitmentPoint = peer.nodeParams.keyManager.commitmentPoint(channelKeyPath, state.commitments.localCommit.index) val channelReestablish = ChannelReestablish( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt index 923af625e..6d6d6f64a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt @@ -91,6 +91,12 @@ sealed class Feature { override val mandatory get() = 20 } + @Serializable + object ChannelType : Feature() { + override val rfcName get() = "option_channel_type" + override val mandatory get() = 44 + } + // The following features have not been standardised, hence the high feature bits to avoid conflicts. @Serializable @@ -222,6 +228,7 @@ data class Features(val activated: Map, val unknown: Se Feature.BasicMultiPartPayment, Feature.Wumbo, Feature.AnchorOutputs, + Feature.ChannelType, Feature.TrampolinePayment, Feature.ZeroReserveChannels, Feature.ZeroConfChannels, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt index 4bc45bfa7..b9746fac6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -120,6 +120,7 @@ data class NodeParams( init { require(features.hasFeature(Feature.VariableLengthOnion, FeatureSupport.Mandatory)) { "${Feature.VariableLengthOnion.rfcName} should be mandatory" } 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" } Features.validateFeatureGraph(features) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt index f1708a67c..fcb9211fe 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt @@ -43,16 +43,12 @@ sealed class ChannelEvent { val localParams: LocalParams, val remoteInit: Init, val channelFlags: Byte, - val channelVersion: ChannelVersion, + val channelConfig: ChannelConfig, + val channelType: ChannelType.SupportedChannelType, val channelOrigin: ChannelOrigin? = null - ) : ChannelEvent() { - init { - require(channelVersion.hasStaticRemotekey) { "channel version $channelVersion is invalid (static_remote_key is not set)" } - require(channelVersion.hasAnchorOutputs) { "invalid channel version $channelVersion (anchor_outputs is not set)" } - } - } + ) : ChannelEvent() - data class InitFundee(val temporaryChannelId: ByteVector32, val localParams: LocalParams, val remoteInit: Init) : ChannelEvent() + data class InitFundee(val temporaryChannelId: ByteVector32, val localParams: LocalParams, val channelConfig: ChannelConfig, val remoteInit: Init) : ChannelEvent() data class Restore(val state: ChannelState) : ChannelEvent() object CheckHtlcTimeout : ChannelEvent() data class MessageReceived(val message: LightningMessage) : ChannelEvent() @@ -225,7 +221,7 @@ sealed class ChannelState { /** Update outgoing messages to include an encrypted backup when necessary. */ private fun updateActions(actions: List): List = when { - this is ChannelStateWithCommitments && this.isZeroReserve -> actions.map { + this is ChannelStateWithCommitments && staticParams.nodeParams.features.hasFeature(Feature.ChannelBackupClient) -> actions.map { when { it is ChannelAction.Message.Send && it.message is FundingSigned -> it.copy(message = it.message.withChannelData(Serialization.encrypt(privateKey.value, this))) it is ChannelAction.Message.Send && it.message is CommitSig -> it.copy(message = it.message.withChannelData(Serialization.encrypt(privateKey.value, this))) @@ -324,7 +320,6 @@ sealed class ChannelStateWithCommitments : ChannelState() { abstract val commitments: Commitments val channelId: ByteVector32 get() = commitments.channelId val isFunder: Boolean get() = commitments.localParams.isFunder - val isZeroReserve: Boolean get() = commitments.isZeroReserve abstract fun updateCommitments(input: Commitments): ChannelStateWithCommitments @@ -574,10 +569,10 @@ data class WaitForInit(override val staticParams: StaticParams, override val cur override fun processInternal(event: ChannelEvent): Pair> { return when { event is ChannelEvent.InitFundee -> { - val nextState = WaitForOpenChannel(staticParams, currentTip, currentOnChainFeerates, event.temporaryChannelId, event.localParams, event.remoteInit) + val nextState = WaitForOpenChannel(staticParams, currentTip, currentOnChainFeerates, event.temporaryChannelId, event.localParams, event.channelConfig, event.remoteInit) Pair(nextState, listOf()) } - event is ChannelEvent.InitFunder -> { + event is ChannelEvent.InitFunder && isValidChannelType(event.channelType) -> { val fundingPubKey = event.localParams.channelKeys.fundingPubKey val paymentBasepoint = event.localParams.channelKeys.paymentBasepoint val open = OpenChannel( @@ -599,12 +594,12 @@ data class WaitForInit(override val staticParams: StaticParams, override val cur htlcBasepoint = event.localParams.channelKeys.htlcBasepoint, firstPerCommitmentPoint = keyManager.commitmentPoint(event.localParams.channelKeys.shaSeed, 0), channelFlags = event.channelFlags, - // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script. - // See https://github.com/lightningnetwork/lightning-rfc/pull/714. tlvStream = TlvStream( buildList { - add(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)) - if (event.channelVersion.isSet(ChannelVersion.ZERO_RESERVE_BIT)) add(ChannelTlv.ChannelVersionTlv(event.channelVersion)) + // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script. + // See https://github.com/lightningnetwork/lightning-rfc/pull/714. + add(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)) + add(ChannelTlv.ChannelTypeTlv(event.channelType)) if (event.channelOrigin != null) add(ChannelTlv.ChannelOriginTlv(event.channelOrigin)) } ) @@ -612,6 +607,10 @@ data class WaitForInit(override val staticParams: StaticParams, override val cur val nextState = WaitForAcceptChannel(staticParams, currentTip, currentOnChainFeerates, event, open) Pair(nextState, listOf(ChannelAction.Message.Send(open))) } + event is ChannelEvent.InitFunder -> { + logger.warning { "c:${event.temporaryChannelId} cannot open channel with invalid channel_type=${event.channelType.name}" } + Pair(Aborted(staticParams, currentTip, currentOnChainFeerates), listOf()) + } event is ChannelEvent.Restore && event.state is Closing && event.state.commitments.nothingAtStake() -> { logger.info { "c:${event.state.channelId} we have nothing at stake, going straight to CLOSED" } Pair(Closed(event.state), listOf()) @@ -703,6 +702,14 @@ data class WaitForInit(override val staticParams: StaticParams, override val cur } } + private fun isValidChannelType(channelType: ChannelType.SupportedChannelType): Boolean { + return when (channelType) { + ChannelType.SupportedChannelType.AnchorOutputs -> true + ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve -> true + else -> false + } + } + override fun handleLocalError(event: ChannelEvent, t: Throwable): Pair> { logger.error(t) { "error on event ${event::class} in state ${this::class}" } return Pair(this, listOf(ChannelAction.ProcessLocalError(t, event))) @@ -727,7 +734,8 @@ data class Offline(val state: ChannelStateWithCommitments) : ChannelState() { val nextState = state.updateCommitments(state.commitments.updateFeatures(event.localInit, event.remoteInit)) Pair(nextState, listOf(ChannelAction.Message.Send(error))) } - state.isZeroReserve -> { + staticParams.nodeParams.features.hasFeature(Feature.ChannelBackupClient) -> { + // We wait for them to go first, which lets us restore from the latest backup if we've lost data. logger.info { "c:${state.channelId} syncing ${state::class}, waiting fo their channelReestablish message" } val nextState = state.updateCommitments(state.commitments.updateFeatures(event.localInit, event.remoteInit)) Pair(Syncing(nextState, true), listOf()) @@ -882,7 +890,7 @@ data class Syncing(val state: ChannelStateWithCommitments, val waitForTheirReest staticParams.nodeParams.minDepthBlocks } else { // when we're fundee we scale the min_depth confirmations depending on the funding amount - if (state.commitments.isZeroReserve) 0 else Helpers.minDepthForFunding(staticParams.nodeParams, state.commitments.commitInput.txOut.amount) + if (state.commitments.channelFeatures.hasFeature(Feature.ZeroConfChannels)) 0 else Helpers.minDepthForFunding(staticParams.nodeParams, state.commitments.commitInput.txOut.amount) } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE val watchConfirmed = WatchConfirmed( @@ -1151,6 +1159,7 @@ data class WaitForOpenChannel( override val currentOnChainFeerates: OnChainFeerates, val temporaryChannelId: ByteVector32, val localParams: LocalParams, + val channelConfig: ChannelConfig, val remoteInit: Init ) : ChannelState() { override fun processInternal(event: ChannelEvent): Pair> { @@ -1158,72 +1167,76 @@ data class WaitForOpenChannel( event is ChannelEvent.MessageReceived -> when (event.message) { is OpenChannel -> { - require(Features.canUseFeature(localParams.features, Features.invoke(remoteInit.features), Feature.StaticRemoteKey)) { "static_remote_key is not set" } - val channelVersion = event.message.channelVersion ?: ChannelVersion.STANDARD - require(channelVersion.hasStaticRemotekey) { "invalid channel version $channelVersion (static_remote_key is not set)" } - require(channelVersion.hasAnchorOutputs) { "invalid channel version $channelVersion (anchor_outputs is not set)" } - when (val err = Helpers.validateParamsFundee(staticParams.nodeParams, event.message, channelVersion)) { + when (val res = Helpers.validateParamsFundee(staticParams.nodeParams, event.message, localParams, Features(remoteInit.features))) { + is Either.Right -> { + val channelFeatures = res.value + val channelOrigin = event.message.tlvStream.records.filterIsInstance().firstOrNull()?.channelOrigin + val fundingPubkey = localParams.channelKeys.fundingPubKey + val minimumDepth = if (channelFeatures.hasFeature(Feature.ZeroConfChannels)) 0 else Helpers.minDepthForFunding(staticParams.nodeParams, event.message.fundingSatoshis) + val paymentBasepoint = localParams.channelKeys.paymentBasepoint + val accept = AcceptChannel( + temporaryChannelId = event.message.temporaryChannelId, + dustLimitSatoshis = localParams.dustLimit, + maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, + channelReserveSatoshis = localParams.channelReserve, + minimumDepth = minimumDepth.toLong(), + htlcMinimumMsat = localParams.htlcMinimum, + toSelfDelay = localParams.toSelfDelay, + maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, + fundingPubkey = fundingPubkey, + revocationBasepoint = localParams.channelKeys.revocationBasepoint, + paymentBasepoint = paymentBasepoint, + delayedPaymentBasepoint = localParams.channelKeys.delayedPaymentBasepoint, + htlcBasepoint = localParams.channelKeys.htlcBasepoint, + firstPerCommitmentPoint = keyManager.commitmentPoint(keyManager.channelKeyPath(localParams, channelConfig), 0), + tlvStream = TlvStream( + listOf( + // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script. + // See https://github.com/lightningnetwork/lightning-rfc/pull/714. + ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), + ChannelTlv.ChannelTypeTlv(channelFeatures.channelType), + ) + ), + ) + val remoteParams = RemoteParams( + nodeId = staticParams.remoteNodeId, + dustLimit = event.message.dustLimitSatoshis, + maxHtlcValueInFlightMsat = event.message.maxHtlcValueInFlightMsat, + channelReserve = event.message.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + htlcMinimum = event.message.htlcMinimumMsat, + toSelfDelay = event.message.toSelfDelay, + maxAcceptedHtlcs = event.message.maxAcceptedHtlcs, + fundingPubKey = event.message.fundingPubkey, + revocationBasepoint = event.message.revocationBasepoint, + paymentBasepoint = event.message.paymentBasepoint, + delayedPaymentBasepoint = event.message.delayedPaymentBasepoint, + htlcBasepoint = event.message.htlcBasepoint, + features = Features(remoteInit.features) + ) + val nextState = WaitForFundingCreated( + staticParams, + currentTip, + currentOnChainFeerates, + event.message.temporaryChannelId, + localParams, + remoteParams, + event.message.fundingSatoshis, + event.message.pushMsat, + event.message.feeratePerKw, + event.message.firstPerCommitmentPoint, + event.message.channelFlags, + channelConfig, + channelFeatures, + channelOrigin, + accept + ) + Pair(nextState, listOf(ChannelAction.Message.Send(accept))) + } is Either.Left -> { - logger.error(err.value) { "c:$temporaryChannelId invalid ${event.message::class} in state ${this::class}" } - return Pair(Aborted(staticParams, currentTip, currentOnChainFeerates), listOf(ChannelAction.Message.Send(Error(temporaryChannelId, err.value.message)))) + logger.error(res.value) { "c:$temporaryChannelId invalid ${event.message::class} in state ${this::class}" } + Pair(Aborted(staticParams, currentTip, currentOnChainFeerates), listOf(ChannelAction.Message.Send(Error(temporaryChannelId, res.value.message)))) } } - val channelOrigin = event.message.tlvStream.records.filterIsInstance().firstOrNull()?.channelOrigin - - val fundingPubkey = localParams.channelKeys.fundingPubKey - val minimumDepth = Helpers.minDepthForFunding(staticParams.nodeParams, event.message.fundingSatoshis) - val paymentBasepoint = localParams.channelKeys.paymentBasepoint - val accept = AcceptChannel( - temporaryChannelId = event.message.temporaryChannelId, - dustLimitSatoshis = localParams.dustLimit, - maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserve, - minimumDepth = minimumDepth.toLong(), - htlcMinimumMsat = localParams.htlcMinimum, - toSelfDelay = localParams.toSelfDelay, - maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubkey, - revocationBasepoint = localParams.channelKeys.revocationBasepoint, - paymentBasepoint = paymentBasepoint, - delayedPaymentBasepoint = localParams.channelKeys.delayedPaymentBasepoint, - htlcBasepoint = localParams.channelKeys.htlcBasepoint, - firstPerCommitmentPoint = keyManager.commitmentPoint(keyManager.channelKeyPath(localParams, channelVersion), 0), - // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script. - // See https://github.com/lightningnetwork/lightning-rfc/pull/714. - tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))) - ) - val remoteParams = RemoteParams( - nodeId = staticParams.remoteNodeId, - dustLimit = event.message.dustLimitSatoshis, - maxHtlcValueInFlightMsat = event.message.maxHtlcValueInFlightMsat, - channelReserve = event.message.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimum = event.message.htlcMinimumMsat, - toSelfDelay = event.message.toSelfDelay, - maxAcceptedHtlcs = event.message.maxAcceptedHtlcs, - fundingPubKey = event.message.fundingPubkey, - revocationBasepoint = event.message.revocationBasepoint, - paymentBasepoint = event.message.paymentBasepoint, - delayedPaymentBasepoint = event.message.delayedPaymentBasepoint, - htlcBasepoint = event.message.htlcBasepoint, - features = Features.invoke(remoteInit.features) - ) - val nextState = WaitForFundingCreated( - staticParams, - currentTip, - currentOnChainFeerates, - event.message.temporaryChannelId, - localParams, - remoteParams, - event.message.fundingSatoshis, - event.message.pushMsat, - event.message.feeratePerKw, - event.message.firstPerCommitmentPoint, - event.message.channelFlags, - channelVersion, - channelOrigin, - accept - ) - Pair(nextState, listOf(ChannelAction.Message.Send(accept))) } is Error -> { logger.error { "c:$temporaryChannelId peer sent error: ascii=${event.message.toAscii()} bin=${event.message.data.toHex()}" } @@ -1258,7 +1271,8 @@ data class WaitForFundingCreated( val initialFeerate: FeeratePerKw, val remoteFirstPerCommitmentPoint: PublicKey, val channelFlags: Byte, - val channelVersion: ChannelVersion, + val channelConfig: ChannelConfig, + val channelFeatures: ChannelFeatures, val channelOrigin: ChannelOrigin?, val lastSent: AcceptChannel ) : ChannelState() { @@ -1309,7 +1323,8 @@ data class WaitForFundingCreated( val commitInput = firstCommitTx.localCommitTx.input val fundingSigned = FundingSigned(channelId, localSigOfRemoteTx) val commitments = Commitments( - channelVersion, + channelConfig, + channelFeatures, localParams, remoteParams, channelFlags, @@ -1327,8 +1342,7 @@ data class WaitForFundingCreated( ) // NB: we don't send a ChannelSignatureSent for the first commit logger.info { "c:$channelId waiting for them to publish the funding tx with fundingTxid=${commitInput.outPoint.txid}" } - // phoenix channels have a zero mindepth for funding tx - val fundingMinDepth = if (commitments.isZeroReserve) 0 else Helpers.minDepthForFunding(staticParams.nodeParams, fundingAmount) + val fundingMinDepth = if (commitments.channelFeatures.hasFeature(Feature.ZeroConfChannels)) 0 else Helpers.minDepthForFunding(staticParams.nodeParams, fundingAmount) logger.info { "c:$channelId will wait for $fundingMinDepth confirmations" } val watchSpent = WatchSpent(channelId, commitInput.outPoint.txid, commitInput.outPoint.index.toInt(), commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) val watchConfirmed = WatchConfirmed(channelId, commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, fundingMinDepth.toLong(), BITCOIN_FUNDING_DEPTHOK) @@ -1388,45 +1402,49 @@ data class WaitForAcceptChannel( override fun processInternal(event: ChannelEvent): Pair> { return when { event is ChannelEvent.MessageReceived && event.message is AcceptChannel -> { - when (val err = Helpers.validateParamsFunder(staticParams.nodeParams, lastSent, event.message)) { + when (val res = Helpers.validateParamsFunder(staticParams.nodeParams, initFunder, lastSent, event.message)) { + is Either.Right -> { + val channelFeatures = res.value + val remoteParams = RemoteParams( + nodeId = staticParams.remoteNodeId, + dustLimit = event.message.dustLimitSatoshis, + maxHtlcValueInFlightMsat = event.message.maxHtlcValueInFlightMsat, + channelReserve = event.message.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + htlcMinimum = event.message.htlcMinimumMsat, + toSelfDelay = event.message.toSelfDelay, + maxAcceptedHtlcs = event.message.maxAcceptedHtlcs, + fundingPubKey = event.message.fundingPubkey, + revocationBasepoint = event.message.revocationBasepoint, + paymentBasepoint = event.message.paymentBasepoint, + delayedPaymentBasepoint = event.message.delayedPaymentBasepoint, + htlcBasepoint = event.message.htlcBasepoint, + features = Features(initFunder.remoteInit.features) + ) + val localFundingPubkey = initFunder.localParams.channelKeys.fundingPubKey + val fundingPubkeyScript = ByteVector(Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey)))) + val makeFundingTx = ChannelAction.Blockchain.MakeFundingTx(fundingPubkeyScript, initFunder.fundingAmount, initFunder.fundingTxFeerate) + val nextState = WaitForFundingInternal( + staticParams, + currentTip, + currentOnChainFeerates, + initFunder.temporaryChannelId, + initFunder.localParams, + remoteParams, + initFunder.fundingAmount, + initFunder.pushAmount, + initFunder.initialFeerate, + event.message.firstPerCommitmentPoint, + initFunder.channelConfig, + channelFeatures, + lastSent + ) + Pair(nextState, listOf(makeFundingTx)) + } is Either.Left -> { - logger.error(err.value) { "c:$temporaryChannelId invalid ${event.message::class} in state ${this::class}" } - return Pair(Aborted(staticParams, currentTip, currentOnChainFeerates), listOf(ChannelAction.Message.Send(Error(initFunder.temporaryChannelId, err.value.message)))) + logger.error(res.value) { "c:$temporaryChannelId invalid ${event.message::class} in state ${this::class}" } + return Pair(Aborted(staticParams, currentTip, currentOnChainFeerates), listOf(ChannelAction.Message.Send(Error(initFunder.temporaryChannelId, res.value.message)))) } } - val remoteParams = RemoteParams( - nodeId = staticParams.remoteNodeId, - dustLimit = event.message.dustLimitSatoshis, - maxHtlcValueInFlightMsat = event.message.maxHtlcValueInFlightMsat, - channelReserve = event.message.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimum = event.message.htlcMinimumMsat, - toSelfDelay = event.message.toSelfDelay, - maxAcceptedHtlcs = event.message.maxAcceptedHtlcs, - fundingPubKey = event.message.fundingPubkey, - revocationBasepoint = event.message.revocationBasepoint, - paymentBasepoint = event.message.paymentBasepoint, - delayedPaymentBasepoint = event.message.delayedPaymentBasepoint, - htlcBasepoint = event.message.htlcBasepoint, - features = Features(initFunder.remoteInit.features) - ) - val localFundingPubkey = initFunder.localParams.channelKeys.fundingPubKey - val fundingPubkeyScript = ByteVector(Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey)))) - val makeFundingTx = ChannelAction.Blockchain.MakeFundingTx(fundingPubkeyScript, initFunder.fundingAmount, initFunder.fundingTxFeerate) - val nextState = WaitForFundingInternal( - staticParams, - currentTip, - currentOnChainFeerates, - initFunder.temporaryChannelId, - initFunder.localParams, - remoteParams, - initFunder.fundingAmount, - initFunder.pushAmount, - initFunder.initialFeerate, - event.message.firstPerCommitmentPoint, - initFunder.channelVersion, - lastSent - ) - Pair(nextState, listOf(makeFundingTx)) } event is ChannelEvent.MessageReceived && event.message is Error -> { logger.error { "c:$temporaryChannelId peer sent error: ascii=${event.message.toAscii()} bin=${event.message.data.toHex()}" } @@ -1458,7 +1476,8 @@ data class WaitForFundingInternal( val pushAmount: MilliSatoshi, val initialFeerate: FeeratePerKw, val remoteFirstPerCommitmentPoint: PublicKey, - val channelVersion: ChannelVersion, + val channelConfig: ChannelConfig, + val channelFeatures: ChannelFeatures, val lastSent: OpenChannel ) : ChannelState() { override fun processInternal(event: ChannelEvent): Pair> { @@ -1510,7 +1529,8 @@ data class WaitForFundingInternal( firstCommitTx.localCommitTx, RemoteCommit(0, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), lastSent.channelFlags, - channelVersion, + channelConfig, + channelFeatures, fundingCreated ) Pair(nextState, listOf(channelIdAssigned, ChannelAction.Message.Send(fundingCreated))) @@ -1549,7 +1569,8 @@ data class WaitForFundingSigned( val localCommitTx: Transactions.TransactionWithInputInfo.CommitTx, val remoteCommit: RemoteCommit, val channelFlags: Byte, - val channelVersion: ChannelVersion, + val channelConfig: ChannelConfig, + val channelFeatures: ChannelFeatures, val lastSent: FundingCreated ) : ChannelState() { override fun processInternal(event: ChannelEvent): Pair> { @@ -1564,7 +1585,7 @@ data class WaitForFundingSigned( is Try.Success -> { val commitInput = localCommitTx.input val commitments = Commitments( - channelVersion, localParams, remoteParams, channelFlags, + channelConfig, channelFeatures, localParams, remoteParams, channelFlags, LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, listOf())), remoteCommit, LocalChanges(listOf(), listOf(), listOf()), RemoteChanges(listOf(), listOf(), listOf()), localNextHtlcId = 0L, remoteNextHtlcId = 0L, @@ -1580,8 +1601,7 @@ data class WaitForFundingSigned( commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT ) - // phoenix channels have a zero mindepth for funding tx - val minDepthBlocks = if (commitments.channelVersion.isSet(ChannelVersion.ZERO_RESERVE_BIT)) 0 else staticParams.nodeParams.minDepthBlocks + val minDepthBlocks = if (commitments.channelFeatures.hasFeature(Feature.ZeroConfChannels)) 0 else staticParams.nodeParams.minDepthBlocks val watchConfirmed = WatchConfirmed( this.channelId, commitments.commitInput.outPoint.txid, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommands.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommands.kt new file mode 100644 index 000000000..7e6cce837 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommands.kt @@ -0,0 +1,34 @@ +package fr.acinq.lightning.channel + +import fr.acinq.bitcoin.ByteVector +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.lightning.CltvExpiry +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.utils.UUID +import fr.acinq.lightning.wire.FailureMessage +import fr.acinq.lightning.wire.OnionRoutingPacket + +sealed class Command + +data class CMD_ADD_HTLC(val amount: MilliSatoshi, val paymentHash: ByteVector32, val cltvExpiry: CltvExpiry, val onion: OnionRoutingPacket, val paymentId: UUID, val commit: Boolean = false) : Command() + +sealed class HtlcSettlementCommand : Command() { + abstract val id: Long +} + +data class CMD_FULFILL_HTLC(override val id: Long, val r: ByteVector32, val commit: Boolean = false) : HtlcSettlementCommand() +data class CMD_FAIL_MALFORMED_HTLC(override val id: Long, val onionHash: ByteVector32, val failureCode: Int, val commit: Boolean = false) : HtlcSettlementCommand() +data class CMD_FAIL_HTLC(override val id: Long, val reason: Reason, val commit: Boolean = false) : HtlcSettlementCommand() { + sealed class Reason { + data class Bytes(val bytes: ByteVector) : Reason() + data class Failure(val message: FailureMessage) : Reason() + } +} + +object CMD_SIGN : Command() +data class CMD_UPDATE_FEE(val feerate: FeeratePerKw, val commit: Boolean = false) : Command() + +sealed class CloseCommand : Command() +data class CMD_CLOSE(val scriptPubKey: ByteVector?) : CloseCommand() +object CMD_FORCECLOSE : CloseCommand() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelConfig.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelConfig.kt new file mode 100644 index 000000000..a7ac9bd8b --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelConfig.kt @@ -0,0 +1,70 @@ +package fr.acinq.lightning.channel + +import fr.acinq.bitcoin.ByteVector +import fr.acinq.lightning.utils.BitField +import kotlinx.serialization.Serializable + +/** + * Internal configuration option impacting the channel's structure or behavior. + * This must be set when creating the channel and cannot be changed afterwards. + */ +@Serializable +sealed class ChannelConfigOption { + + abstract val name: String + abstract val supportBit: Int + + /** + * If set, the channel's BIP32 key path will be deterministically derived from the funding public key. + * It makes it very easy to retrieve funds when channel data has been lost: + * - connect to your peer and use option_data_loss_protect to get them to publish their remote commit tx + * - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data + * - recompute your channel keys and spend your output + */ + @Serializable + object FundingPubKeyBasedChannelKeyPath : ChannelConfigOption() { + override val name: String get() = "funding_pubkey_based_channel_keypath" + override val supportBit: Int get() = 0 + } + +} + +@Serializable +data class ChannelConfig(val options: Set) { + + fun hasOption(option: ChannelConfigOption): Boolean = options.contains(option) + + fun toByteArray(): ByteArray { + val bits = options.map { it.supportBit }.toHashSet() + if (bits.isEmpty()) return ByteArray(0) + + val buf = BitField.forAtMost(bits.maxOrNull()!! + 1) + bits.forEach { buf.setRight(it) } + return buf.bytes + } + + companion object { + val standard = ChannelConfig(setOf(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) + + private val allOptions = setOf( + ChannelConfigOption.FundingPubKeyBasedChannelKeyPath + ) + + operator fun invoke(vararg options: ChannelConfigOption): ChannelConfig = ChannelConfig(setOf(*options)) + + operator fun invoke(bytes: ByteVector): ChannelConfig = invoke(bytes.toByteArray()) + + operator fun invoke(bytes: ByteArray): ChannelConfig = invoke(BitField.from(bytes)) + + operator fun invoke(bits: BitField): ChannelConfig { + val options = bits.asRightSequence() + .withIndex() + .filter { it.value } + .map { (idx, _) -> allOptions.find { it.supportBit == idx } } + .filterNotNull() + .toSet() + return ChannelConfig(options) + } + + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelTypes.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt similarity index 83% rename from src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelTypes.kt rename to src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt index d68343bed..be017f6c1 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelTypes.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelData.kt @@ -1,69 +1,17 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* -import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Features import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.Helpers.publishIfNeeded import fr.acinq.lightning.channel.Helpers.watchConfirmedIfNeeded import fr.acinq.lightning.channel.Helpers.watchSpentIfNeeded import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* -import fr.acinq.lightning.utils.BitField -import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.wire.ClosingSigned -import fr.acinq.lightning.wire.FailureMessage -import fr.acinq.lightning.wire.OnionRoutingPacket import kotlinx.serialization.Serializable -/* - .d8888b. .d88888b. 888b d888 888b d888 d8888 888b 888 8888888b. .d8888b. - d88P Y88b d88P" "Y88b 8888b d8888 8888b d8888 d88888 8888b 888 888 "Y88b d88P Y88b - 888 888 888 888 88888b.d88888 88888b.d88888 d88P888 88888b 888 888 888 Y88b. - 888 888 888 888Y88888P888 888Y88888P888 d88P 888 888Y88b 888 888 888 "Y888b. - 888 888 888 888 Y888P 888 888 Y888P 888 d88P 888 888 Y88b888 888 888 "Y88b. - 888 888 888 888 888 Y8P 888 888 Y8P 888 d88P 888 888 Y88888 888 888 "888 - Y88b d88P Y88b. .d88P 888 " 888 888 " 888 d8888888888 888 Y8888 888 .d88P Y88b d88P - "Y8888P" "Y88888P" 888 888 888 888 d88P 888 888 Y888 8888888P" "Y8888P" - */ - -sealed class Command - -data class CMD_ADD_HTLC(val amount: MilliSatoshi, val paymentHash: ByteVector32, val cltvExpiry: CltvExpiry, val onion: OnionRoutingPacket, val paymentId: UUID, val commit: Boolean = false) : Command() - -sealed class HtlcSettlementCommand : Command() { - abstract val id: Long -} - -data class CMD_FULFILL_HTLC(override val id: Long, val r: ByteVector32, val commit: Boolean = false) : HtlcSettlementCommand() -data class CMD_FAIL_MALFORMED_HTLC(override val id: Long, val onionHash: ByteVector32, val failureCode: Int, val commit: Boolean = false) : HtlcSettlementCommand() -data class CMD_FAIL_HTLC(override val id: Long, val reason: Reason, val commit: Boolean = false) : HtlcSettlementCommand() { - sealed class Reason { - data class Bytes(val bytes: ByteVector) : Reason() - data class Failure(val message: FailureMessage) : Reason() - } -} - -object CMD_SIGN : Command() -data class CMD_UPDATE_FEE(val feerate: FeeratePerKw, val commit: Boolean = false) : Command() - -sealed class CloseCommand : Command() -data class CMD_CLOSE(val scriptPubKey: ByteVector?) : CloseCommand() -object CMD_FORCECLOSE : CloseCommand() - -/* - 8888888b. d8888 88888888888 d8888 - 888 "Y88b d88888 888 d88888 - 888 888 d88P888 888 d88P888 - 888 888 d88P 888 888 d88P 888 - 888 888 d88P 888 888 d88P 888 - 888 888 d88P 888 888 d88P 888 - 888 .d88P d8888888888 888 d8888888888 - 8888888P" d88P 888 888 d88P 888 - */ - /** * Details about a force-close where we published our commitment. * @@ -431,7 +379,7 @@ data class ChannelKeys( } @OptIn(ExperimentalUnsignedTypes::class) -data class LocalParams constructor( +data class LocalParams( val nodeId: PublicKey, val channelKeys: ChannelKeys, val dustLimit: Satoshi, @@ -462,40 +410,6 @@ data class RemoteParams( val features: Features ) -@Serializable -data class ChannelVersion(val bits: BitField) { - init { - require(bits.byteSize == SIZE_BYTE) { "channel version takes 4 bytes" } - } - - infix fun or(other: ChannelVersion) = ChannelVersion(bits or other.bits) - infix fun and(other: ChannelVersion) = ChannelVersion(bits and other.bits) - infix fun xor(other: ChannelVersion) = ChannelVersion(bits xor other.bits) - - fun isSet(bit: Int) = bits.getRight(bit) - - val hasStaticRemotekey: Boolean by lazy { isSet(USE_STATIC_REMOTEKEY_BIT) } - val hasAnchorOutputs: Boolean by lazy { isSet(USE_ANCHOR_OUTPUTS_BIT) } - - companion object { - const val SIZE_BYTE = 4 - val ZEROES = ChannelVersion(BitField(SIZE_BYTE)) - const val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0 - const val USE_STATIC_REMOTEKEY_BIT = 1 - const val USE_ANCHOR_OUTPUTS_BIT = 2 - const val ZERO_RESERVE_BIT = 3 - - private fun fromBit(bit: Int) = ChannelVersion(BitField(SIZE_BYTE).apply { setRight(bit) }) - - private val USE_PUBKEY_KEYPATH = fromBit(USE_PUBKEY_KEYPATH_BIT) - private val USE_STATIC_REMOTEKEY = fromBit(USE_STATIC_REMOTEKEY_BIT) - private val USE_ANCHOR_OUTPUTS = fromBit(USE_ANCHOR_OUTPUTS_BIT) - val ZERO_RESERVE = fromBit(ZERO_RESERVE_BIT) - - val STANDARD = ZEROES or USE_PUBKEY_KEYPATH or USE_STATIC_REMOTEKEY or USE_ANCHOR_OUTPUTS - } -} - object ChannelFlags { const val AnnounceChannel = 0x01.toByte() const val Empty = 0x00.toByte() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt index 82c5de461..bc25af69a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt @@ -21,6 +21,8 @@ data class InvalidChainHash (override val channelId: Byte data class InvalidFundingAmount (override val channelId: ByteVector32, val fundingAmount: Satoshi, val min: Satoshi, val max: Satoshi) : ChannelException(channelId, "invalid funding_satoshis=$fundingAmount (min=$min max=$max)") data class InvalidPushAmount (override val channelId: ByteVector32, val pushAmount: MilliSatoshi, val max: MilliSatoshi) : ChannelException(channelId, "invalid pushAmount=$pushAmount (max=$max)") data class InvalidMaxAcceptedHtlcs (override val channelId: ByteVector32, val maxAcceptedHtlcs: Int, val max: Int) : ChannelException(channelId, "invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)") +data class InvalidChannelType (override val channelId: ByteVector32, val ourChannelType: ChannelType, val theirChannelType: ChannelType) : ChannelException(channelId, "invalid channel_type=${theirChannelType.name}, expected channel_type=${ourChannelType.name}") +data class MissingChannelType (override val channelId: ByteVector32) : ChannelException(channelId, "option_channel_type was negotiated but channel_type is missing") data class DustLimitTooSmall (override val channelId: ByteVector32, val dustLimit: Satoshi, val min: Satoshi) : ChannelException(channelId, "dustLimit=$dustLimit is too small (min=$min)") data class DustLimitTooLarge (override val channelId: ByteVector32, val dustLimit: Satoshi, val max: Satoshi) : ChannelException(channelId, "dustLimit=$dustLimit is too large (max=$max)") data class DustLimitAboveOurChannelReserve (override val channelId: ByteVector32, val dustLimit: Satoshi, val channelReserve: Satoshi) : ChannelException(channelId, "dustLimit=$dustLimit is above our channelReserve=$channelReserve") diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt new file mode 100644 index 000000000..b5df746f2 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelFeatures.kt @@ -0,0 +1,100 @@ +package fr.acinq.lightning.channel + +import fr.acinq.lightning.Feature +import fr.acinq.lightning.FeatureSupport +import fr.acinq.lightning.Features +import fr.acinq.secp256k1.Hex +import kotlinx.serialization.Serializable + +/** + * Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel. + * Even if one of these features is later disabled at the connection level, it will still apply to the channel until the + * channel is upgraded or closed. + */ +@Serializable +data class ChannelFeatures(val features: Set) { + + val channelType: ChannelType.SupportedChannelType = when { + features.contains(Feature.AnchorOutputs) -> { + if (features.contains(Feature.ZeroConfChannels) && features.contains(Feature.ZeroReserveChannels)) { + ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve + } else { + ChannelType.SupportedChannelType.AnchorOutputs + } + } + features.contains(Feature.StaticRemoteKey) -> ChannelType.SupportedChannelType.StaticRemoteKey + else -> ChannelType.SupportedChannelType.Standard + } + + fun hasFeature(feature: Feature): Boolean = features.contains(feature) + + override fun toString(): String = features.joinToString(",") + +} + +/** A channel type is a specific set of feature bits that represent persistent channel features as defined in Bolt 2. */ +@Serializable +sealed class ChannelType { + + abstract val name: String + abstract val features: Set + + override fun toString(): String = name + + @Serializable + sealed class SupportedChannelType : ChannelType() { + + fun toFeatures(): Features = Features(features.associateWith { FeatureSupport.Mandatory }) + + @Serializable + object Standard : SupportedChannelType() { + override val name: String get() = "standard" + override val features: Set get() = setOf() + } + + @Serializable + object StaticRemoteKey : SupportedChannelType() { + override val name: String get() = "static_remotekey" + override val features: Set get() = setOf(Feature.StaticRemoteKey) + } + + @Serializable + object AnchorOutputs : SupportedChannelType() { + override val name: String get() = "anchor_outputs" + override val features: Set get() = setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs) + } + + @Serializable + object AnchorOutputsZeroConfZeroReserve : SupportedChannelType() { + override val name: String get() = "anchor_outputs_zero_conf_zero_reserve" + override val features: Set get() = setOf(Feature.Wumbo, Feature.ZeroReserveChannels, Feature.ZeroConfChannels, Feature.StaticRemoteKey, Feature.AnchorOutputs) + } + + } + + @Serializable + data class UnsupportedChannelType(val featureBits: Features) : ChannelType() { + override val name: String get() = "0x${Hex.encode(featureBits.toByteArray())}" + override val features: Set get() = featureBits.activated.keys + } + + companion object { + + // NB: Bolt 2: features must exactly match in order to identify a channel type. + fun fromFeatures(features: Features): ChannelType = when (features) { + Features( + Feature.StaticRemoteKey to FeatureSupport.Mandatory, + Feature.AnchorOutputs to FeatureSupport.Mandatory, + Feature.Wumbo to FeatureSupport.Mandatory, + Feature.ZeroConfChannels to FeatureSupport.Mandatory, + Feature.ZeroReserveChannels to FeatureSupport.Mandatory + ) -> SupportedChannelType.AnchorOutputsZeroConfZeroReserve + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory) -> SupportedChannelType.AnchorOutputs + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory) -> SupportedChannelType.StaticRemoteKey + Features.empty -> SupportedChannelType.Standard + else -> UnsupportedChannelType(features) + } + + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt index 78070c692..703d25b81 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Crypto.sha256 import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Feature import fr.acinq.lightning.Features import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -50,7 +51,8 @@ data class WaitingForRevocation(val nextRemoteCommit: RemoteCommit, val sent: Co * theirNextCommitInfo is their next commit tx. The rest of the time, it is their next per-commitment point */ data class Commitments( - val channelVersion: ChannelVersion, + val channelConfig: ChannelConfig, + val channelFeatures: ChannelFeatures, val localParams: LocalParams, val remoteParams: RemoteParams, val channelFlags: Byte, @@ -68,8 +70,7 @@ data class Commitments( val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty ) { init { - require(channelVersion.hasStaticRemotekey) { "invalid channel version $channelVersion (static_remote_key is not set)" } - require(channelVersion.hasAnchorOutputs) { "invalid channel version $channelVersion (anchor_outputs is not set)" } + require(channelFeatures.hasFeature(Feature.AnchorOutputs)) { "invalid channel type: ${channelFeatures.channelType.name}" } } fun updateFeatures(localInit: Init, remoteInit: Init) = this.copy( @@ -114,8 +115,6 @@ data class Commitments( return localCommit.spec.htlcs.incomings().filter { relayedFulfills.contains(it.id) && blockHeight >= (it.cltvExpiry - fulfillSafety).toLong() }.toSet() } - val isZeroReserve: Boolean get() = channelVersion.isSet(ChannelVersion.ZERO_RESERVE_BIT) - private fun addLocalProposal(proposal: UpdateMessage): Commitments = copy(localChanges = localChanges.copy(proposed = localChanges.proposed + proposal)) private fun addRemoteProposal(proposal: UpdateMessage): Commitments = copy(remoteChanges = remoteChanges.copy(proposed = remoteChanges.proposed + proposal)) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt index a5b2e5eb3..987159fbe 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt @@ -6,6 +6,7 @@ import fr.acinq.bitcoin.Crypto.sha256 import fr.acinq.bitcoin.Script.pay2wsh import fr.acinq.bitcoin.Script.write import fr.acinq.lightning.Feature +import fr.acinq.lightning.Features import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.BITCOIN_OUTPUT_SPENT @@ -56,7 +57,13 @@ object Helpers { } /** Called by the fundee. */ - fun validateParamsFundee(nodeParams: NodeParams, open: OpenChannel, channelVersion: ChannelVersion): Either { + fun validateParamsFundee(nodeParams: NodeParams, open: OpenChannel, localParams: LocalParams, remoteFeatures: Features): Either { + // NB: we only accept channels from peers who support explicit channel type negotiation. + val channelType = open.channelType ?: return Either.Left(MissingChannelType(open.temporaryChannelId)) + if (!setOf(ChannelType.SupportedChannelType.AnchorOutputs, ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve).contains(channelType)) { + return Either.Left(InvalidChannelType(open.temporaryChannelId, ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, channelType)) + } + // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) { @@ -96,8 +103,8 @@ object Helpers { return Either.Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, nodeParams.maxRemoteDustLimit)) } - if (channelVersion.isSet(ChannelVersion.ZERO_RESERVE_BIT)) { - // in zero-reserve channels, we don't make any requirements on the fundee's reserve (set by the funder in the open_message). + if (open.channelReserveSatoshis == 0.sat && localParams.features.hasFeature(Feature.ZeroReserveChannels)) { + // NB: if the funder doesn't require us to have a reserve, we can't ensure that dust_limit_satoshis is greater than our reserve. } else { // BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis. if (open.dustLimitSatoshis > open.channelReserveSatoshis) { @@ -128,11 +135,28 @@ object Helpers { return Either.Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)) } - return Either.Right(Unit) + val channelFeatures = ChannelFeatures( + buildSet { + addAll(channelType.features) + if (Features.canUseFeature(localParams.features, remoteFeatures, Feature.Wumbo)) add(Feature.Wumbo) + if (open.channelReserveSatoshis == 0.sat) add(Feature.ZeroReserveChannels) + } + ) + + return Either.Right(channelFeatures) } /** Called by the funder. */ - fun validateParamsFunder(nodeParams: NodeParams, open: OpenChannel, accept: AcceptChannel): Either { + fun validateParamsFunder(nodeParams: NodeParams, init: ChannelEvent.InitFunder, open: OpenChannel, accept: AcceptChannel): Either { + require(open.channelType != null) { "we should have sent a channel type in open_channel" } + if (accept.channelType == null) { + // We only open channels to peers who support explicit channel type negotiation. + return Either.Left(MissingChannelType(accept.temporaryChannelId)) + } + if (open.channelType != accept.channelType) { + return Either.Left(InvalidChannelType(accept.temporaryChannelId, open.channelType!!, accept.channelType!!)) + } + if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) { return Either.Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) } @@ -155,8 +179,8 @@ object Helpers { return Either.Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.maxToLocalDelayBlocks)) } - if ((open.channelVersion ?: ChannelVersion.STANDARD).isSet(ChannelVersion.ZERO_RESERVE_BIT)) { - // in zero-reserve channels, we don't make any requirements on the fundee's reserve (set by the funder in the open_message). + if (open.channelReserveSatoshis == 0.sat) { + // if we proposed a zero-reserve channel to our peer, we don't make any additional requirement on dust limit. } else { // if channel_reserve_satoshis from the open_channel message is less than dust_limit_satoshis: // MUST reject the channel. Other fields have the same requirements as their counterparts in open_channel. @@ -175,7 +199,15 @@ object Helpers { return Either.Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)) } - return Either.Right(Unit) + val channelFeatures = ChannelFeatures( + buildSet { + addAll(init.channelType.features) + if (Features.canUseFeature(init.localParams.features, Features(init.remoteInit.features), Feature.Wumbo)) add(Feature.Wumbo) + if (open.channelReserveSatoshis == 0.sat || accept.channelReserveSatoshis == 0.sat) add(Feature.ZeroReserveChannels) + } + ) + + return Either.Right(channelFeatures) } /** @@ -565,7 +597,6 @@ object Helpers { * @return a list of transactions (one per output that we can claim). */ fun claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, feerates: OnChainFeerates): RemoteCommitPublished { - val channelVersion = commitments.channelVersion val localParams = commitments.localParams val remoteParams = commitments.remoteParams val commitInput = commitments.commitInput @@ -579,7 +610,7 @@ object Helpers { ) require(remoteCommitTx.tx.txid == tx.txid) { "txid mismatch, provided tx is not the current remote commit tx" } - val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) + val channelKeyPath = keyManager.channelKeyPath(localParams, commitments.channelConfig) val localPaymentPubkey = keyManager.paymentPoint(channelKeyPath).publicKey val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) @@ -693,7 +724,7 @@ object Helpers { */ private fun extractTxNumber(keyManager: KeyManager, commitments: Commitments, tx: Transaction): Long { require(tx.txIn.size == 1) { "commitment tx should have 1 input" } - val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) + val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelConfig) val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn.first().sequence, tx.lockTime) val localPaymentPoint = keyManager.paymentPoint(channelKeyPath) // this tx has been published by remote, so we need to invert local/remote params diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 3a48c0245..ef7e9e25c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -4,10 +4,7 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Crypto.sha256 import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey import fr.acinq.bitcoin.crypto.Pack -import fr.acinq.lightning.channel.ChannelKeys -import fr.acinq.lightning.channel.ChannelVersion -import fr.acinq.lightning.channel.LocalParams -import fr.acinq.lightning.channel.RecoveredChannelKeys +import fr.acinq.lightning.channel.* import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo interface KeyManager { @@ -35,16 +32,14 @@ interface KeyManager { fun commitmentPoint(shaSeed: ByteVector32, index: Long): PublicKey - fun channelKeyPath(fundingKeyPath: KeyPath, channelVersion: ChannelVersion): KeyPath = - if (channelVersion.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) { - // deterministic mode: use the funding pubkey to compute the channel key path - channelKeyPath(fundingPublicKey(fundingKeyPath)) - } else { - // legacy mode: we reuse the funding key path as our channel key path - fundingKeyPath - } + fun channelKeyPath(fundingKeyPath: KeyPath, channelConfig: ChannelConfig): KeyPath = when { + // deterministic mode: use the funding pubkey to compute the channel key path + channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath) -> channelKeyPath(fundingPublicKey(fundingKeyPath)) + // legacy mode: we reuse the funding key path as our channel key path + else -> fundingKeyPath + } - fun channelKeyPath(localParams: LocalParams, channelVersion: ChannelVersion): KeyPath = channelKeyPath(localParams.channelKeys.fundingKeyPath, channelVersion) + fun channelKeyPath(localParams: LocalParams, channelConfig: ChannelConfig): KeyPath = channelKeyPath(localParams.channelKeys.fundingKeyPath, channelConfig) /** * diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index b5bde0585..76f76817d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -558,7 +558,8 @@ class Peer( currentTipFlow.filterNotNull().first(), onChainFeeratesFlow.filterNotNull().first() ) - val (state1, actions1) = state.process(ChannelEvent.InitFundee(msg.temporaryChannelId, localParams, theirInit!!)) + val channelConfig = ChannelConfig.standard + val (state1, actions1) = state.process(ChannelEvent.InitFundee(msg.temporaryChannelId, localParams, channelConfig, theirInit!!)) val (state2, actions2) = state1.process(ChannelEvent.MessageReceived(msg)) processActions(msg.temporaryChannelId, actions1 + actions2) _channels = _channels + (msg.temporaryChannelId to state2) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/Serialization.kt index e4ee98f7b..854c7b954 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/Serialization.kt @@ -3,37 +3,39 @@ package fr.acinq.lightning.serialization import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PrivateKey import fr.acinq.lightning.NodeParams +import fr.acinq.lightning.utils.runTrying import fr.acinq.lightning.wire.EncryptedChannelData object Serialization { fun serialize(state: fr.acinq.lightning.channel.ChannelStateWithCommitments): ByteArray { - return fr.acinq.lightning.serialization.v2.Serialization.serialize(state) + return fr.acinq.lightning.serialization.v3.Serialization.serialize(state) } fun deserialize(bin: ByteArray, nodeParams: NodeParams): fr.acinq.lightning.channel.ChannelStateWithCommitments { - return try { - fr.acinq.lightning.serialization.v2.Serialization.deserialize(bin, nodeParams) - } catch (e: Throwable) { - // try legacy format - fr.acinq.lightning.serialization.v1.Serialization.deserialize(bin, nodeParams) - } + return runTrying { + fr.acinq.lightning.serialization.v3.Serialization.deserialize(bin, nodeParams) + }.recoverWith { + runTrying { fr.acinq.lightning.serialization.v2.Serialization.deserialize(bin, nodeParams) } + }.recoverWith { + runTrying { fr.acinq.lightning.serialization.v1.Serialization.deserialize(bin, nodeParams) } + }.get() } fun encrypt(key: ByteVector32, state: fr.acinq.lightning.channel.ChannelStateWithCommitments): EncryptedChannelData { - // always use v2 serialization - return fr.acinq.lightning.serialization.v2.Serialization.encrypt(key, state) + return fr.acinq.lightning.serialization.v3.Serialization.encrypt(key, state) } fun encrypt(key: PrivateKey, state: fr.acinq.lightning.channel.ChannelStateWithCommitments): EncryptedChannelData = encrypt(key.value, state) fun decrypt(key: ByteVector32, data: ByteArray, nodeParams: NodeParams): fr.acinq.lightning.channel.ChannelStateWithCommitments { - return try { - fr.acinq.lightning.serialization.v2.Serialization.decrypt(key, data, nodeParams) - } catch (e: Throwable) { - // try legacy format - fr.acinq.lightning.serialization.v1.Serialization.decrypt(key, data, nodeParams) - } + return runTrying { + fr.acinq.lightning.serialization.v3.Serialization.decrypt(key, data, nodeParams) + }.recoverWith { + runTrying { fr.acinq.lightning.serialization.v2.Serialization.decrypt(key, data, nodeParams) } + }.recoverWith { + runTrying { fr.acinq.lightning.serialization.v1.Serialization.decrypt(key, data, nodeParams) } + }.get() } fun decrypt(key: PrivateKey, data: ByteArray, nodeParams: NodeParams): fr.acinq.lightning.channel.ChannelStateWithCommitments = decrypt(key.value, data, nodeParams) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v1/ChannelState.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v1/ChannelState.kt index 60f8c7fbd..3766588ad 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v1/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v1/ChannelState.kt @@ -5,10 +5,8 @@ import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.utils.BitField import fr.acinq.lightning.utils.Either import fr.acinq.lightning.utils.UUID -import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer @@ -196,7 +194,19 @@ data class LocalParams constructor( from.features ) - fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.LocalParams(nodeId, nodeParams.keyManager.channelKeys(fundingKeyPath), dustLimit, maxHtlcValueInFlightMsat, channelReserve, htlcMinimum, toSelfDelay, maxAcceptedHtlcs, isFunder, defaultFinalScriptPubKey, features) + fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.LocalParams( + nodeId, + nodeParams.keyManager.channelKeys(fundingKeyPath), + dustLimit, + maxHtlcValueInFlightMsat, + channelReserve, + htlcMinimum, + toSelfDelay, + maxAcceptedHtlcs, + isFunder, + defaultFinalScriptPubKey, + features + ) } @OptIn(ExperimentalUnsignedTypes::class) @@ -255,9 +265,24 @@ data class ChannelVersion(@Serializable(with = ByteVectorKSerializer::class) val require(bits.size() == 4) { "channel version takes 4 bytes" } } - constructor(from: fr.acinq.lightning.channel.ChannelVersion) : this(from.bits.bytes.toByteVector()) - - fun export() = fr.acinq.lightning.channel.ChannelVersion(BitField.from(bits.toByteArray())) + companion object { + // NB: this is the only value that was supported in v1 + val standard = ChannelVersion(ByteVector("0000000f")) + + // This is the corresponding channel config + val channelConfig = fr.acinq.lightning.channel.ChannelConfig.standard + + // These are the corresponding channel features + val channelFeatures = fr.acinq.lightning.channel.ChannelFeatures( + setOf( + Feature.Wumbo, + Feature.StaticRemoteKey, + Feature.AnchorOutputs, + Feature.ZeroReserveChannels, + Feature.ZeroConfChannels, + ) + ) + } } @Serializable @@ -287,7 +312,7 @@ data class Commitments( val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty ) { constructor(from: fr.acinq.lightning.channel.Commitments) : this( - ChannelVersion(from.channelVersion), + ChannelVersion.standard, LocalParams(from.localParams), RemoteParams(from.remoteParams), from.channelFlags, @@ -306,7 +331,8 @@ data class Commitments( ) fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.Commitments( - channelVersion.export(), + ChannelVersion.channelConfig, + ChannelVersion.channelFeatures, localParams.export(nodeParams), remoteParams.export(), channelFlags, @@ -491,7 +517,7 @@ data class WaitForFundingCreated( from.initialFeerate, from.remoteFirstPerCommitmentPoint, from.channelFlags, - ChannelVersion(from.channelVersion), + ChannelVersion.standard, from.lastSent ) } @@ -517,7 +543,7 @@ data class InitFunder( LocalParams(from.localParams), from.remoteInit, from.channelFlags, - ChannelVersion(from.channelVersion), + ChannelVersion.standard, ) } @@ -564,7 +590,7 @@ data class WaitForFundingInternal( from.pushAmount, from.initialFeerate, from.remoteFirstPerCommitmentPoint, - ChannelVersion(from.channelVersion), + ChannelVersion.standard, from.lastSent ) } @@ -599,7 +625,7 @@ data class WaitForFundingSigned( from.localCommitTx, RemoteCommit(from.remoteCommit), from.channelFlags, - ChannelVersion(from.channelVersion), + ChannelVersion.standard, from.lastSent ) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v1/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v1/Serialization.kt index 7893c0376..410d95204 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v1/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v1/Serialization.kt @@ -41,7 +41,7 @@ object Serialization { private val tlvSerializersModule = SerializersModule { polymorphic(Tlv::class) { - subclass(ChannelTlv.UpfrontShutdownScript.serializer()) + subclass(ChannelTlv.UpfrontShutdownScriptTlv.serializer()) subclass(ChannelTlv.ChannelVersionTlv.serializer()) subclass(ChannelTlv.ChannelOriginTlv.serializer()) subclass(InitTlv.Networks.serializer()) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt index 0673dd1ff..f8bf4b9fb 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt @@ -5,10 +5,8 @@ import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.utils.BitField import fr.acinq.lightning.utils.Either import fr.acinq.lightning.utils.UUID -import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -189,7 +187,19 @@ data class LocalParams constructor( from.features ) - fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.LocalParams(nodeId, nodeParams.keyManager.channelKeys(fundingKeyPath), dustLimit, maxHtlcValueInFlightMsat, channelReserve, htlcMinimum, toSelfDelay, maxAcceptedHtlcs, isFunder, defaultFinalScriptPubKey, features) + fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.LocalParams( + nodeId, + nodeParams.keyManager.channelKeys(fundingKeyPath), + dustLimit, + maxHtlcValueInFlightMsat, + channelReserve, + htlcMinimum, + toSelfDelay, + maxAcceptedHtlcs, + isFunder, + defaultFinalScriptPubKey, + features + ) } @OptIn(ExperimentalUnsignedTypes::class) @@ -248,9 +258,24 @@ data class ChannelVersion(@Serializable(with = ByteVectorKSerializer::class) val require(bits.size() == 4) { "channel version takes 4 bytes" } } - constructor(from: fr.acinq.lightning.channel.ChannelVersion) : this(from.bits.bytes.toByteVector()) - - fun export() = fr.acinq.lightning.channel.ChannelVersion(BitField.from(bits.toByteArray())) + companion object { + // NB: this is the only value that was supported in v1 + val standard = ChannelVersion(ByteVector("0000000f")) + + // This is the corresponding channel config + val channelConfig = fr.acinq.lightning.channel.ChannelConfig.standard + + // These are the corresponding channel features + val channelFeatures = fr.acinq.lightning.channel.ChannelFeatures( + setOf( + Feature.Wumbo, + Feature.StaticRemoteKey, + Feature.AnchorOutputs, + Feature.ZeroReserveChannels, + Feature.ZeroConfChannels, + ) + ) + } } @Serializable @@ -280,7 +305,7 @@ data class Commitments( val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty ) { constructor(from: fr.acinq.lightning.channel.Commitments) : this( - ChannelVersion(from.channelVersion), + ChannelVersion.standard, LocalParams(from.localParams), RemoteParams(from.remoteParams), from.channelFlags, @@ -299,7 +324,8 @@ data class Commitments( ) fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.Commitments( - channelVersion.export(), + ChannelVersion.channelConfig, + ChannelVersion.channelFeatures, localParams.export(nodeParams), remoteParams.export(), channelFlags, @@ -484,7 +510,7 @@ data class WaitForFundingCreated( from.initialFeerate, from.remoteFirstPerCommitmentPoint, from.channelFlags, - ChannelVersion(from.channelVersion), + ChannelVersion.standard, from.lastSent ) } @@ -510,7 +536,7 @@ data class InitFunder( LocalParams(from.localParams), from.remoteInit, from.channelFlags, - ChannelVersion(from.channelVersion), + ChannelVersion.standard, ) } @@ -557,7 +583,7 @@ data class WaitForFundingInternal( from.pushAmount, from.initialFeerate, from.remoteFirstPerCommitmentPoint, - ChannelVersion(from.channelVersion), + ChannelVersion.standard, from.lastSent ) } @@ -592,7 +618,7 @@ data class WaitForFundingSigned( from.localCommitTx, RemoteCommit(from.remoteCommit), from.channelFlags, - ChannelVersion(from.channelVersion), + ChannelVersion.standard, from.lastSent ) } @@ -868,7 +894,7 @@ object ShaChainSerializer : KSerializer { override fun deserialize(decoder: Decoder): ShaChain { val surrogate = decoder.decodeSerializableValue(Surrogate.serializer()) - return ShaChain(surrogate.knownHashes.map { it.first.toBooleanList() to ByteVector32(it.second) }.toMap(), surrogate.lastIndex) + return ShaChain(surrogate.knownHashes.associate { it.first.toBooleanList() to ByteVector32(it.second) }, surrogate.lastIndex) } private fun List.toBinaryString(): String = this.map { if (it) '1' else '0' }.joinToString(separator = "") diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/Serialization.kt index 5c74487b2..71451e357 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/Serialization.kt @@ -51,7 +51,7 @@ object Serialization { private val tlvSerializersModule = SerializersModule { polymorphic(Tlv::class) { - subclass(ChannelTlv.UpfrontShutdownScript.serializer()) + subclass(ChannelTlv.UpfrontShutdownScriptTlv.serializer()) subclass(ChannelTlv.ChannelVersionTlv.serializer()) subclass(ChannelTlv.ChannelOriginTlv.serializer()) subclass(InitTlv.Networks.serializer()) @@ -225,8 +225,7 @@ object Serialization { override fun decodeString(): String { val len = decodeInt() require(len <= input.availableBytes) - val decoded = input.readNBytes(len)!!.decodeToString() - return decoded + return input.readNBytes(len)!!.decodeToString() } override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = input.read() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt new file mode 100644 index 000000000..f6308fb21 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt @@ -0,0 +1,923 @@ +package fr.acinq.lightning.serialization.v3 + +import fr.acinq.bitcoin.* +import fr.acinq.lightning.* +import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.crypto.ShaChain +import fr.acinq.lightning.transactions.Transactions +import fr.acinq.lightning.utils.Either +import fr.acinq.lightning.utils.UUID +import fr.acinq.lightning.utils.toByteVector +import fr.acinq.lightning.wire.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable +sealed class DirectedHtlc { + abstract val add: UpdateAddHtlc + + fun to(): fr.acinq.lightning.transactions.DirectedHtlc = when (this) { + is IncomingHtlc -> fr.acinq.lightning.transactions.IncomingHtlc(this.add) + is OutgoingHtlc -> fr.acinq.lightning.transactions.OutgoingHtlc(this.add) + } + + companion object { + fun from(input: fr.acinq.lightning.transactions.DirectedHtlc): DirectedHtlc = when (input) { + is fr.acinq.lightning.transactions.IncomingHtlc -> IncomingHtlc(input.add) + is fr.acinq.lightning.transactions.OutgoingHtlc -> OutgoingHtlc(input.add) + } + } +} + +@Serializable +data class IncomingHtlc(override val add: UpdateAddHtlc) : DirectedHtlc() + +@Serializable +data class OutgoingHtlc(override val add: UpdateAddHtlc) : DirectedHtlc() + +@Serializable +data class CommitmentSpec( + val htlcs: Set, + val feerate: FeeratePerKw, + val toLocal: MilliSatoshi, + val toRemote: MilliSatoshi +) { + constructor(from: fr.acinq.lightning.transactions.CommitmentSpec) : this(from.htlcs.map { DirectedHtlc.from(it) }.toSet(), from.feerate, from.toLocal, from.toRemote) + + fun export() = fr.acinq.lightning.transactions.CommitmentSpec(htlcs.map { it.to() }.toSet(), feerate, toLocal, toRemote) + +} + +@Serializable +data class LocalChanges(val proposed: List, val signed: List, val acked: List) { + constructor(from: fr.acinq.lightning.channel.LocalChanges) : this(from.proposed, from.signed, from.acked) + + fun export() = fr.acinq.lightning.channel.LocalChanges(proposed, signed, acked) +} + +@Serializable +data class RemoteChanges(val proposed: List, val acked: List, val signed: List) { + constructor(from: fr.acinq.lightning.channel.RemoteChanges) : this(from.proposed, from.acked, from.signed) + + fun export() = fr.acinq.lightning.channel.RemoteChanges(proposed, acked, signed) +} + +@Serializable +data class HtlcTxAndSigs( + val txinfo: Transactions.TransactionWithInputInfo.HtlcTx, + @Serializable(with = ByteVector64KSerializer::class) val localSig: ByteVector64, + @Serializable(with = ByteVector64KSerializer::class) val remoteSig: ByteVector64 +) { + constructor(from: fr.acinq.lightning.channel.HtlcTxAndSigs) : this(from.txinfo, from.localSig, from.remoteSig) + + fun export() = fr.acinq.lightning.channel.HtlcTxAndSigs(txinfo, localSig, remoteSig) +} + +@Serializable +data class PublishableTxs(val commitTx: Transactions.TransactionWithInputInfo.CommitTx, val htlcTxsAndSigs: List) { + constructor(from: fr.acinq.lightning.channel.PublishableTxs) : this(from.commitTx, from.htlcTxsAndSigs.map { HtlcTxAndSigs(it) }) + + fun export() = fr.acinq.lightning.channel.PublishableTxs(commitTx, htlcTxsAndSigs.map { it.export() }) +} + +@Serializable +data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishableTxs: PublishableTxs) { + constructor(from: fr.acinq.lightning.channel.LocalCommit) : this(from.index, CommitmentSpec(from.spec), PublishableTxs(from.publishableTxs)) + + fun export() = fr.acinq.lightning.channel.LocalCommit(index, spec.export(), publishableTxs.export()) +} + +@Serializable +data class RemoteCommit(val index: Long, val spec: CommitmentSpec, @Serializable(with = ByteVector32KSerializer::class) val txid: ByteVector32, @Serializable(with = PublicKeyKSerializer::class) val remotePerCommitmentPoint: PublicKey) { + constructor(from: fr.acinq.lightning.channel.RemoteCommit) : this(from.index, CommitmentSpec(from.spec), from.txid, from.remotePerCommitmentPoint) + + fun export() = fr.acinq.lightning.channel.RemoteCommit(index, spec.export(), txid, remotePerCommitmentPoint) +} + +@Serializable +data class WaitingForRevocation(val nextRemoteCommit: RemoteCommit, val sent: CommitSig, val sentAfterLocalCommitIndex: Long, val reSignAsap: Boolean = false) { + constructor(from: fr.acinq.lightning.channel.WaitingForRevocation) : this(RemoteCommit(from.nextRemoteCommit), from.sent, from.sentAfterLocalCommitIndex, from.reSignAsap) + + fun export() = fr.acinq.lightning.channel.WaitingForRevocation(nextRemoteCommit.export(), sent, sentAfterLocalCommitIndex, reSignAsap) +} + +@Serializable +data class LocalCommitPublished( + @Serializable(with = TransactionKSerializer::class) val commitTx: Transaction, + val claimMainDelayedOutputTx: Transactions.TransactionWithInputInfo.ClaimLocalDelayedOutputTx? = null, + val htlcTxs: Map<@Serializable(with = OutPointKSerializer::class) OutPoint, Transactions.TransactionWithInputInfo.HtlcTx?> = emptyMap(), + val claimHtlcDelayedTxs: List = emptyList(), + val claimAnchorTxs: List = emptyList(), + val irrevocablySpent: Map<@Serializable(with = OutPointKSerializer::class) OutPoint, @Serializable(with = TransactionKSerializer::class) Transaction> = emptyMap() +) { + constructor(from: fr.acinq.lightning.channel.LocalCommitPublished) : this(from.commitTx, from.claimMainDelayedOutputTx, from.htlcTxs, from.claimHtlcDelayedTxs, from.claimAnchorTxs, from.irrevocablySpent) + + fun export() = fr.acinq.lightning.channel.LocalCommitPublished(commitTx, claimMainDelayedOutputTx, htlcTxs, claimHtlcDelayedTxs, claimAnchorTxs, irrevocablySpent) +} + +@Serializable +data class RemoteCommitPublished( + @Serializable(with = TransactionKSerializer::class) val commitTx: Transaction, + val claimMainOutputTx: Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx? = null, + val claimHtlcTxs: Map<@Serializable(with = OutPointKSerializer::class) OutPoint, Transactions.TransactionWithInputInfo.ClaimHtlcTx?> = emptyMap(), + val claimAnchorTxs: List = emptyList(), + val irrevocablySpent: Map<@Serializable(with = OutPointKSerializer::class) OutPoint, @Serializable(with = TransactionKSerializer::class) Transaction> = emptyMap() +) { + constructor(from: fr.acinq.lightning.channel.RemoteCommitPublished) : this(from.commitTx, from.claimMainOutputTx, from.claimHtlcTxs, from.claimAnchorTxs, from.irrevocablySpent) + + fun export() = fr.acinq.lightning.channel.RemoteCommitPublished(commitTx, claimMainOutputTx, claimHtlcTxs, claimAnchorTxs, irrevocablySpent) +} + +@Serializable +data class RevokedCommitPublished( + @Serializable(with = TransactionKSerializer::class) val commitTx: Transaction, + @Serializable(with = PrivateKeyKSerializer::class) val remotePerCommitmentSecret: PrivateKey, + val claimMainOutputTx: Transactions.TransactionWithInputInfo.ClaimRemoteCommitMainOutputTx? = null, + val mainPenaltyTx: Transactions.TransactionWithInputInfo.MainPenaltyTx? = null, + val htlcPenaltyTxs: List = emptyList(), + val claimHtlcDelayedPenaltyTxs: List = emptyList(), + val irrevocablySpent: Map<@Serializable(with = OutPointKSerializer::class) OutPoint, @Serializable(with = TransactionKSerializer::class) Transaction> = emptyMap() +) { + constructor(from: fr.acinq.lightning.channel.RevokedCommitPublished) : this( + from.commitTx, + from.remotePerCommitmentSecret, + from.claimMainOutputTx, + from.mainPenaltyTx, + from.htlcPenaltyTxs, + from.claimHtlcDelayedPenaltyTxs, + from.irrevocablySpent + ) + + fun export() = fr.acinq.lightning.channel.RevokedCommitPublished(commitTx, remotePerCommitmentSecret, claimMainOutputTx, mainPenaltyTx, htlcPenaltyTxs, claimHtlcDelayedPenaltyTxs, irrevocablySpent) +} + +/** + * README: by design, we do not include channel private keys and secret here, so they won't be included in our backups (local files, encrypted peer backup, ...), so even + * if these backups were compromised channel private keys would not be leaked unless the main seed was also compromised. + * This means that they will be recomputed once when we convert serialized data to their "live" counterparts. + */ +@OptIn(ExperimentalUnsignedTypes::class) +@Serializable +data class LocalParams constructor( + @Serializable(with = PublicKeyKSerializer::class) val nodeId: PublicKey, + @Serializable(with = KeyPathKSerializer::class) val fundingKeyPath: KeyPath, + @Serializable(with = SatoshiKSerializer::class) val dustLimit: Satoshi, + val maxHtlcValueInFlightMsat: Long, + @Serializable(with = SatoshiKSerializer::class) val channelReserve: Satoshi, + val htlcMinimum: MilliSatoshi, + val toSelfDelay: CltvExpiryDelta, + val maxAcceptedHtlcs: Int, + val isFunder: Boolean, + @Serializable(with = ByteVectorKSerializer::class) val defaultFinalScriptPubKey: ByteVector, + val features: Features +) { + constructor(from: fr.acinq.lightning.channel.LocalParams) : this( + from.nodeId, + from.channelKeys.fundingKeyPath, + from.dustLimit, + from.maxHtlcValueInFlightMsat, + from.channelReserve, + from.htlcMinimum, + from.toSelfDelay, + from.maxAcceptedHtlcs, + from.isFunder, + from.defaultFinalScriptPubKey, + from.features + ) + + fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.LocalParams( + nodeId, + nodeParams.keyManager.channelKeys(fundingKeyPath), + dustLimit, + maxHtlcValueInFlightMsat, + channelReserve, + htlcMinimum, + toSelfDelay, + maxAcceptedHtlcs, + isFunder, + defaultFinalScriptPubKey, + features + ) +} + +@OptIn(ExperimentalUnsignedTypes::class) +@Serializable +data class RemoteParams( + @Serializable(with = PublicKeyKSerializer::class) val nodeId: PublicKey, + @Serializable(with = SatoshiKSerializer::class) val dustLimit: Satoshi, + val maxHtlcValueInFlightMsat: Long, + @Serializable(with = SatoshiKSerializer::class) val channelReserve: Satoshi, + val htlcMinimum: MilliSatoshi, + val toSelfDelay: CltvExpiryDelta, + val maxAcceptedHtlcs: Int, + @Serializable(with = PublicKeyKSerializer::class) val fundingPubKey: PublicKey, + @Serializable(with = PublicKeyKSerializer::class) val revocationBasepoint: PublicKey, + @Serializable(with = PublicKeyKSerializer::class) val paymentBasepoint: PublicKey, + @Serializable(with = PublicKeyKSerializer::class) val delayedPaymentBasepoint: PublicKey, + @Serializable(with = PublicKeyKSerializer::class) val htlcBasepoint: PublicKey, + val features: Features +) { + constructor(from: fr.acinq.lightning.channel.RemoteParams) : this( + from.nodeId, + from.dustLimit, + from.maxHtlcValueInFlightMsat, + from.channelReserve, + from.htlcMinimum, + from.toSelfDelay, + from.maxAcceptedHtlcs, + from.fundingPubKey, + from.revocationBasepoint, + from.paymentBasepoint, + from.delayedPaymentBasepoint, + from.htlcBasepoint, + from.features + ) + + fun export() = fr.acinq.lightning.channel.RemoteParams( + nodeId, + dustLimit, + maxHtlcValueInFlightMsat, + channelReserve, + htlcMinimum, + toSelfDelay, + maxAcceptedHtlcs, + fundingPubKey, + revocationBasepoint, + paymentBasepoint, + delayedPaymentBasepoint, + htlcBasepoint, + features + ) +} + +@Serializable +data class ChannelConfig(@Serializable(with = ByteVectorKSerializer::class) val bin: ByteVector) { + constructor(from: fr.acinq.lightning.channel.ChannelConfig) : this(from.toByteArray().toByteVector()) + + fun export() = fr.acinq.lightning.channel.ChannelConfig(bin.toByteArray()) +} + +@Serializable +data class ChannelType(@Serializable(with = ByteVectorKSerializer::class) val bin: ByteVector) { + constructor(from: fr.acinq.lightning.channel.ChannelType.SupportedChannelType) : this(from.toFeatures().toByteArray().toByteVector()) +} + +@Serializable +data class ChannelFeatures(@Serializable(with = ByteVectorKSerializer::class) val bin: ByteVector) { + constructor(from: fr.acinq.lightning.channel.ChannelFeatures) : this(Features(from.features.associateWith { FeatureSupport.Mandatory }).toByteArray().toByteVector()) + + fun export() = fr.acinq.lightning.channel.ChannelFeatures(Features(bin.toByteArray()).activated.keys) +} + +@Serializable +data class ClosingTxProposed(val unsignedTx: Transactions.TransactionWithInputInfo.ClosingTx, val localClosingSigned: ClosingSigned) { + constructor(from: fr.acinq.lightning.channel.ClosingTxProposed) : this(from.unsignedTx, from.localClosingSigned) + + fun export() = fr.acinq.lightning.channel.ClosingTxProposed(unsignedTx, localClosingSigned) +} + +@Serializable +data class Commitments( + val channelConfig: ChannelConfig, + val channelFeatures: ChannelFeatures, + val localParams: LocalParams, + val remoteParams: RemoteParams, + val channelFlags: Byte, + val localCommit: LocalCommit, + val remoteCommit: RemoteCommit, + val localChanges: LocalChanges, + val remoteChanges: RemoteChanges, + val localNextHtlcId: Long, + val remoteNextHtlcId: Long, + val payments: Map, + @Serializable(with = EitherSerializer::class) val remoteNextCommitInfo: Either, + val commitInput: Transactions.InputInfo, + @Serializable(with = ShaChainSerializer::class) val remotePerCommitmentSecrets: ShaChain, + @Serializable(with = ByteVector32KSerializer::class) val channelId: ByteVector32, + val remoteChannelData: EncryptedChannelData = EncryptedChannelData.empty +) { + constructor(from: fr.acinq.lightning.channel.Commitments) : this( + ChannelConfig(from.channelConfig), + ChannelFeatures(from.channelFeatures), + LocalParams(from.localParams), + RemoteParams(from.remoteParams), + from.channelFlags, + LocalCommit(from.localCommit), + RemoteCommit(from.remoteCommit), + LocalChanges(from.localChanges), + RemoteChanges(from.remoteChanges), + from.localNextHtlcId, + from.remoteNextHtlcId, + from.payments, + from.remoteNextCommitInfo.transform({ x -> WaitingForRevocation(x) }, { y -> y }), + from.commitInput, + from.remotePerCommitmentSecrets, + from.channelId, + from.remoteChannelData + ) + + fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.Commitments( + channelConfig.export(), + channelFeatures.export(), + localParams.export(nodeParams), + remoteParams.export(), + channelFlags, + localCommit.export(), + remoteCommit.export(), + localChanges.export(), + remoteChanges.export(), + localNextHtlcId, + remoteNextHtlcId, + payments, + remoteNextCommitInfo.transform({ x -> x.export() }, { y -> y }), + commitInput, + remotePerCommitmentSecrets, + channelId, + remoteChannelData + ) +} + +@Serializable +data class OnChainFeerates(val mutualCloseFeerate: FeeratePerKw, val claimMainFeerate: FeeratePerKw, val fastFeerate: FeeratePerKw) { + constructor(from: fr.acinq.lightning.blockchain.fee.OnChainFeerates) : this(from.mutualCloseFeerate, from.claimMainFeerate, from.fastFeerate) + + fun export() = fr.acinq.lightning.blockchain.fee.OnChainFeerates(mutualCloseFeerate, claimMainFeerate, fastFeerate) +} + +@Serializable +data class StaticParams(@Serializable(with = ByteVector32KSerializer::class) val chainHash: ByteVector32, @Serializable(with = PublicKeyKSerializer::class) val remoteNodeId: PublicKey) { + constructor(from: fr.acinq.lightning.channel.StaticParams) : this(from.nodeParams.chainHash, from.remoteNodeId) + + fun export(nodeParams: NodeParams): fr.acinq.lightning.channel.StaticParams { + require(chainHash == nodeParams.chainHash) { "restoring data from a different chain" } + return fr.acinq.lightning.channel.StaticParams(nodeParams, this.remoteNodeId) + } +} + +@Serializable +sealed class ChannelState { + abstract val staticParams: StaticParams + abstract val currentTip: Pair + abstract val currentOnChainFeerates: OnChainFeerates + + companion object { + fun import(from: fr.acinq.lightning.channel.ChannelState): ChannelState = when (from) { + is fr.acinq.lightning.channel.WaitForInit -> WaitForInit(from) + is fr.acinq.lightning.channel.Aborted -> Aborted(from) + is fr.acinq.lightning.channel.WaitForOpenChannel -> WaitForOpenChannel(from) + is fr.acinq.lightning.channel.WaitForAcceptChannel -> WaitForAcceptChannel(from) + is fr.acinq.lightning.channel.WaitForFundingInternal -> WaitForFundingInternal(from) + is fr.acinq.lightning.channel.WaitForFundingLocked -> WaitForFundingLocked(from) + is fr.acinq.lightning.channel.WaitForFundingConfirmed -> WaitForFundingConfirmed(from) + is fr.acinq.lightning.channel.WaitForRemotePublishFutureCommitment -> WaitForRemotePublishFutureCommitment(from) + is fr.acinq.lightning.channel.WaitForFundingCreated -> WaitForFundingCreated(from) + is fr.acinq.lightning.channel.WaitForFundingSigned -> WaitForFundingSigned(from) + is fr.acinq.lightning.channel.Offline -> Offline(from) + is fr.acinq.lightning.channel.Syncing -> Syncing(from) + is fr.acinq.lightning.channel.ChannelStateWithCommitments -> ChannelStateWithCommitments.import(from) + } + } +} + +@Serializable +sealed class ChannelStateWithCommitments : ChannelState() { + abstract val commitments: Commitments + val channelId: ByteVector32 get() = commitments.channelId + abstract fun export(nodeParams: NodeParams): fr.acinq.lightning.channel.ChannelStateWithCommitments + + companion object { + fun import(from: fr.acinq.lightning.channel.ChannelStateWithCommitments): ChannelStateWithCommitments = when (from) { + is fr.acinq.lightning.channel.WaitForRemotePublishFutureCommitment -> WaitForRemotePublishFutureCommitment(from) + is fr.acinq.lightning.channel.WaitForFundingConfirmed -> WaitForFundingConfirmed(from) + is fr.acinq.lightning.channel.WaitForFundingLocked -> WaitForFundingLocked(from) + is fr.acinq.lightning.channel.Normal -> Normal(from) + is fr.acinq.lightning.channel.ShuttingDown -> ShuttingDown(from) + is fr.acinq.lightning.channel.Negotiating -> Negotiating(from) + is fr.acinq.lightning.channel.Closing -> Closing(from) + is fr.acinq.lightning.channel.Closed -> Closed(from) + is fr.acinq.lightning.channel.ErrorInformationLeak -> ErrorInformationLeak(from) + } + } +} + +@Serializable +data class Aborted( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates +) : ChannelState() { + constructor(from: fr.acinq.lightning.channel.Aborted) : this(StaticParams(from.staticParams), from.currentTip, OnChainFeerates(from.currentOnChainFeerates)) +} + +@Serializable +data class WaitForInit( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates +) : ChannelState() { + constructor(from: fr.acinq.lightning.channel.WaitForInit) : this(StaticParams(from.staticParams), from.currentTip, OnChainFeerates(from.currentOnChainFeerates)) +} + +@Serializable +data class Offline(val state: ChannelStateWithCommitments) : ChannelState() { + override val staticParams: StaticParams get() = state.staticParams + override val currentTip: Pair get() = state.currentTip + override val currentOnChainFeerates: OnChainFeerates get() = state.currentOnChainFeerates + + constructor(from: fr.acinq.lightning.channel.Offline) : this(ChannelStateWithCommitments.import(from.state)) +} + +@Serializable +data class Syncing(val state: ChannelStateWithCommitments, val waitForTheirReestablishMessage: Boolean) : ChannelState() { + override val staticParams: StaticParams get() = state.staticParams + override val currentTip: Pair get() = state.currentTip + override val currentOnChainFeerates: OnChainFeerates get() = state.currentOnChainFeerates + + constructor(from: fr.acinq.lightning.channel.Syncing) : this(ChannelStateWithCommitments.import(from.state), from.waitForTheirReestablishMessage) +} + +@Serializable +data class WaitForOpenChannel( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + @Serializable(with = ByteVector32KSerializer::class) val temporaryChannelId: ByteVector32, + val localParams: LocalParams, + val remoteInit: Init +) : ChannelState() { + constructor(from: fr.acinq.lightning.channel.WaitForOpenChannel) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + from.temporaryChannelId, + LocalParams(from.localParams), + from.remoteInit + ) +} + +@Serializable +data class WaitForRemotePublishFutureCommitment( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + override val commitments: Commitments, + val remoteChannelReestablish: ChannelReestablish +) : ChannelStateWithCommitments() { + constructor(from: fr.acinq.lightning.channel.WaitForRemotePublishFutureCommitment) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + Commitments(from.commitments), + from.remoteChannelReestablish + ) + + override fun export(nodeParams: NodeParams) = + fr.acinq.lightning.channel.WaitForRemotePublishFutureCommitment(staticParams.export(nodeParams), currentTip, currentOnChainFeerates.export(), commitments.export(nodeParams), remoteChannelReestablish) +} + +@Serializable +data class WaitForFundingCreated( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + @Serializable(with = ByteVector32KSerializer::class) val temporaryChannelId: ByteVector32, + val localParams: LocalParams, + val remoteParams: RemoteParams, + @Serializable(with = SatoshiKSerializer::class) val fundingAmount: Satoshi, + val pushAmount: MilliSatoshi, + val initialFeerate: FeeratePerKw, + @Serializable(with = PublicKeyKSerializer::class) val remoteFirstPerCommitmentPoint: PublicKey, + val channelFlags: Byte, + val channelConfig: ChannelConfig, + val channelFeatures: ChannelFeatures, + val lastSent: AcceptChannel +) : ChannelState() { + constructor(from: fr.acinq.lightning.channel.WaitForFundingCreated) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + from.temporaryChannelId, + LocalParams(from.localParams), + RemoteParams(from.remoteParams), + from.fundingAmount, + from.pushAmount, + from.initialFeerate, + from.remoteFirstPerCommitmentPoint, + from.channelFlags, + ChannelConfig(from.channelConfig), + ChannelFeatures(from.channelFeatures), + from.lastSent + ) +} + +@Serializable +data class InitFunder( + @Serializable(with = ByteVector32KSerializer::class) val temporaryChannelId: ByteVector32, + @Serializable(with = SatoshiKSerializer::class) val fundingAmount: Satoshi, + val pushAmount: MilliSatoshi, + val initialFeerate: FeeratePerKw, + val fundingTxFeerate: FeeratePerKw, + val localParams: LocalParams, + val remoteInit: Init, + val channelFlags: Byte, + val channelConfig: ChannelConfig, + val channelType: ChannelType +) { + constructor(from: fr.acinq.lightning.channel.ChannelEvent.InitFunder) : this( + from.temporaryChannelId, + from.fundingAmount, + from.pushAmount, + from.initialFeerate, + from.fundingTxFeerate, + LocalParams(from.localParams), + from.remoteInit, + from.channelFlags, + ChannelConfig(from.channelConfig), + ChannelType(from.channelType) + ) +} + +@Serializable +data class WaitForAcceptChannel( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + val initFunder: InitFunder, + val lastSent: OpenChannel +) : ChannelState() { + constructor(from: fr.acinq.lightning.channel.WaitForAcceptChannel) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + InitFunder(from.initFunder), + from.lastSent + ) +} + +@Serializable +data class WaitForFundingInternal( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + @Serializable(with = ByteVector32KSerializer::class) val temporaryChannelId: ByteVector32, + val localParams: LocalParams, + val remoteParams: RemoteParams, + @Serializable(with = SatoshiKSerializer::class) val fundingAmount: Satoshi, + val pushAmount: MilliSatoshi, + val initialFeerate: FeeratePerKw, + @Serializable(with = PublicKeyKSerializer::class) val remoteFirstPerCommitmentPoint: PublicKey, + val channelConfig: ChannelConfig, + val channelFeatures: ChannelFeatures, + val lastSent: OpenChannel +) : ChannelState() { + constructor(from: fr.acinq.lightning.channel.WaitForFundingInternal) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + from.temporaryChannelId, + LocalParams(from.localParams), + RemoteParams(from.remoteParams), + from.fundingAmount, + from.pushAmount, + from.initialFeerate, + from.remoteFirstPerCommitmentPoint, + ChannelConfig(from.channelConfig), + ChannelFeatures(from.channelFeatures), + from.lastSent + ) +} + +@Serializable +data class WaitForFundingSigned( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + @Serializable(with = ByteVector32KSerializer::class) val channelId: ByteVector32, + val localParams: LocalParams, + val remoteParams: RemoteParams, + @Serializable(with = TransactionKSerializer::class) val fundingTx: Transaction, + @Serializable(with = SatoshiKSerializer::class) val fundingTxFee: Satoshi, + val localSpec: CommitmentSpec, + val localCommitTx: Transactions.TransactionWithInputInfo.CommitTx, + val remoteCommit: RemoteCommit, + val channelFlags: Byte, + val channelConfig: ChannelConfig, + val channelFeatures: ChannelFeatures, + val lastSent: FundingCreated +) : ChannelState() { + constructor(from: fr.acinq.lightning.channel.WaitForFundingSigned) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + from.channelId, + LocalParams(from.localParams), + RemoteParams(from.remoteParams), + from.fundingTx, + from.fundingTxFee, + CommitmentSpec(from.localSpec), + from.localCommitTx, + RemoteCommit(from.remoteCommit), + from.channelFlags, + ChannelConfig(from.channelConfig), + ChannelFeatures(from.channelFeatures), + from.lastSent + ) +} + +@Serializable +data class WaitForFundingConfirmed( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + override val commitments: Commitments, + @Serializable(with = TransactionKSerializer::class) val fundingTx: Transaction?, + val waitingSinceBlock: Long, // how long have we been waiting for the funding tx to confirm + val deferred: FundingLocked?, + @Serializable(with = EitherSerializer::class) val lastSent: Either +) : ChannelStateWithCommitments() { + constructor(from: fr.acinq.lightning.channel.WaitForFundingConfirmed) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + Commitments(from.commitments), + from.fundingTx, + from.waitingSinceBlock, + from.deferred, + from.lastSent + ) + + override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.WaitForFundingConfirmed( + staticParams.export(nodeParams), + currentTip, + currentOnChainFeerates.export(), + commitments.export(nodeParams), + fundingTx, + waitingSinceBlock, + deferred, + lastSent + ) +} + +@Serializable +data class WaitForFundingLocked( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + override val commitments: Commitments, + val shortChannelId: ShortChannelId, + val lastSent: FundingLocked +) : ChannelStateWithCommitments() { + constructor(from: fr.acinq.lightning.channel.WaitForFundingLocked) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + Commitments(from.commitments), + from.shortChannelId, + from.lastSent + ) + + override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.WaitForFundingLocked( + staticParams.export(nodeParams), + currentTip, + currentOnChainFeerates.export(), + commitments.export(nodeParams), + shortChannelId, + lastSent + ) +} + +@Serializable +data class Normal( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + override val commitments: Commitments, + val shortChannelId: ShortChannelId, + val buried: Boolean, + val channelAnnouncement: ChannelAnnouncement?, + val channelUpdate: ChannelUpdate, + val remoteChannelUpdate: ChannelUpdate?, + val localShutdown: Shutdown?, + val remoteShutdown: Shutdown? +) : ChannelStateWithCommitments() { + constructor(from: fr.acinq.lightning.channel.Normal) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + Commitments(from.commitments), + from.shortChannelId, + from.buried, + from.channelAnnouncement, + from.channelUpdate, + from.remoteChannelUpdate, + from.localShutdown, + from.remoteShutdown + ) + + override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.Normal( + staticParams.export(nodeParams), + currentTip, + currentOnChainFeerates.export(), + commitments.export(nodeParams), + shortChannelId, + buried, + channelAnnouncement, + channelUpdate, + remoteChannelUpdate, + localShutdown, + remoteShutdown + ) +} + +@Serializable +data class ShuttingDown( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + override val commitments: Commitments, + val localShutdown: Shutdown, + val remoteShutdown: Shutdown +) : ChannelStateWithCommitments() { + constructor(from: fr.acinq.lightning.channel.ShuttingDown) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + Commitments(from.commitments), + from.localShutdown, + from.remoteShutdown + ) + + override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.ShuttingDown( + staticParams.export(nodeParams), + currentTip, + currentOnChainFeerates.export(), + commitments.export(nodeParams), + localShutdown, + remoteShutdown + ) +} + +@Serializable +data class Negotiating( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + override val commitments: Commitments, + val localShutdown: Shutdown, + val remoteShutdown: Shutdown, + val closingTxProposed: List>, + val bestUnpublishedClosingTx: Transactions.TransactionWithInputInfo.ClosingTx? +) : ChannelStateWithCommitments() { + init { + require(closingTxProposed.isNotEmpty()) { "there must always be a list for the current negotiation" } + require(!commitments.localParams.isFunder || !closingTxProposed.any { it.isEmpty() }) { "funder must have at least one closing signature for every negotiation attempt because it initiates the closing" } + } + + constructor(from: fr.acinq.lightning.channel.Negotiating) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + Commitments(from.commitments), + from.localShutdown, + from.remoteShutdown, + from.closingTxProposed.map { x -> x.map { ClosingTxProposed(it) } }, + from.bestUnpublishedClosingTx + ) + + override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.Negotiating( + staticParams.export(nodeParams), + currentTip, + currentOnChainFeerates.export(), + commitments.export(nodeParams), + localShutdown, + remoteShutdown, + closingTxProposed.map { x -> x.map { it.export() } }, + bestUnpublishedClosingTx + ) +} + +@Serializable +data class Closing( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + override val commitments: Commitments, + @Serializable(with = TransactionKSerializer::class) val fundingTx: Transaction?, + val waitingSinceBlock: Long, + val mutualCloseProposed: List = emptyList(), + val mutualClosePublished: List = emptyList(), + val localCommitPublished: LocalCommitPublished? = null, + val remoteCommitPublished: RemoteCommitPublished? = null, + val nextRemoteCommitPublished: RemoteCommitPublished? = null, + val futureRemoteCommitPublished: RemoteCommitPublished? = null, + val revokedCommitPublished: List = emptyList() +) : ChannelStateWithCommitments() { + constructor(from: fr.acinq.lightning.channel.Closing) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + Commitments(from.commitments), + from.fundingTx, + from.waitingSinceBlock, + from.mutualCloseProposed, + from.mutualClosePublished, + from.localCommitPublished?.let { LocalCommitPublished(it) }, + from.remoteCommitPublished?.let { RemoteCommitPublished(it) }, + from.nextRemoteCommitPublished?.let { RemoteCommitPublished(it) }, + from.futureRemoteCommitPublished?.let { RemoteCommitPublished(it) }, + from.revokedCommitPublished.map { RevokedCommitPublished(it) } + ) + + override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.Closing( + staticParams.export(nodeParams), + currentTip, + currentOnChainFeerates.export(), + commitments.export(nodeParams), + fundingTx, + waitingSinceBlock, + mutualCloseProposed, + mutualClosePublished, + localCommitPublished?.export(), + remoteCommitPublished?.export(), + nextRemoteCommitPublished?.export(), + futureRemoteCommitPublished?.export(), + revokedCommitPublished.map { it.export() } + ) +} + +@Serializable +data class Closed(val state: Closing) : ChannelStateWithCommitments() { + override val commitments: Commitments get() = state.commitments + override val staticParams: StaticParams get() = state.staticParams + override val currentTip: Pair get() = state.currentTip + override val currentOnChainFeerates: OnChainFeerates get() = state.currentOnChainFeerates + + constructor(from: fr.acinq.lightning.channel.Closed) : this(Closing(from.state)) + + override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.Closed(state.export(nodeParams)) +} + +@Serializable +data class ErrorInformationLeak( + override val staticParams: StaticParams, + override val currentTip: Pair, + override val currentOnChainFeerates: OnChainFeerates, + override val commitments: Commitments +) : ChannelStateWithCommitments() { + constructor(from: fr.acinq.lightning.channel.ErrorInformationLeak) : this( + StaticParams(from.staticParams), + from.currentTip, + OnChainFeerates(from.currentOnChainFeerates), + Commitments(from.commitments) + ) + + override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.ErrorInformationLeak( + staticParams.export(nodeParams), + currentTip, + currentOnChainFeerates.export(), + commitments.export(nodeParams) + ) +} + +object ShaChainSerializer : KSerializer { + @Serializable + private data class Surrogate(val knownHashes: List>, val lastIndex: Long? = null) + + override val descriptor: SerialDescriptor = Surrogate.serializer().descriptor + + override fun serialize(encoder: Encoder, value: ShaChain) { + val surrogate = Surrogate( + value.knownHashes.map { Pair(it.key.toBinaryString(), it.value.toByteArray()) }, + value.lastIndex + ) + return encoder.encodeSerializableValue(Surrogate.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): ShaChain { + val surrogate = decoder.decodeSerializableValue(Surrogate.serializer()) + return ShaChain(surrogate.knownHashes.associate { it.first.toBooleanList() to ByteVector32(it.second) }, surrogate.lastIndex) + } + + private fun List.toBinaryString(): String = this.map { if (it) '1' else '0' }.joinToString(separator = "") + private fun String.toBooleanList(): List = this.map { it == '1' } +} + +class EitherSerializer(val aSer: KSerializer, val bSer: KSerializer) : KSerializer> { + @Serializable + data class Surrogate(val isRight: Boolean, val left: A?, val right: B?) + + override val descriptor = Surrogate.serializer(aSer, bSer).descriptor + + override fun serialize(encoder: Encoder, value: Either) { + val surrogate = Surrogate(value.isRight, value.left, value.right) + return encoder.encodeSerializableValue(Surrogate.serializer(aSer, bSer), surrogate) + } + + override fun deserialize(decoder: Decoder): Either { + val surrogate = decoder.decodeSerializableValue(Surrogate.serializer(aSer, bSer)) + return if (surrogate.isRight) Either.Right(surrogate.right!!) else Either.Left(surrogate.left!!) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/Serialization.kt new file mode 100644 index 000000000..119eb008d --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/Serialization.kt @@ -0,0 +1,246 @@ +package fr.acinq.lightning.serialization.v3 + +import fr.acinq.bitcoin.ByteVector +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.Crypto +import fr.acinq.bitcoin.PrivateKey +import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.bitcoin.io.ByteArrayInput +import fr.acinq.bitcoin.io.ByteArrayOutput +import fr.acinq.bitcoin.io.readNBytes +import fr.acinq.lightning.NodeParams +import fr.acinq.lightning.crypto.ChaCha20Poly1305 +import fr.acinq.lightning.utils.toByteVector +import fr.acinq.lightning.wire.* +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass + +object Serialization { + private const val versionMagic = 3 + + /** + * Versioned serialized data. + * + * @README DO NOT change the structure of this class !! + * + * If a new serialization format is added, just change the `version` field and update serialize()/deserialize() methods + * @param version version of the serialization algorithm + * @param data serialized data + */ + @Serializable + private data class SerializedData(val version: Int, @Serializable(with = ByteVectorKSerializer::class) val data: ByteVector) + + private val updateSerializersModule = SerializersModule { + polymorphic(UpdateMessage::class) { + subclass(UpdateAddHtlc.serializer()) + subclass(UpdateFailHtlc.serializer()) + subclass(UpdateFailMalformedHtlc.serializer()) + subclass(UpdateFee.serializer()) + subclass(UpdateFulfillHtlc.serializer()) + } + } + + private val tlvSerializersModule = SerializersModule { + polymorphic(Tlv::class) { + subclass(ChannelTlv.UpfrontShutdownScriptTlv.serializer()) + subclass(ChannelTlv.ChannelVersionTlv.serializer()) + subclass(ChannelTlv.ChannelOriginTlv.serializer()) + subclass(InitTlv.Networks.serializer()) + subclass(OnionTlv.AmountToForward.serializer()) + subclass(OnionTlv.OutgoingCltv.serializer()) + subclass(OnionTlv.OutgoingChannelId.serializer()) + subclass(OnionTlv.PaymentData.serializer()) + subclass(OnionTlv.InvoiceFeatures.serializer()) + subclass(OnionTlv.OutgoingNodeId.serializer()) + subclass(OnionTlv.InvoiceRoutingInfo.serializer()) + subclass(OnionTlv.TrampolineOnion.serializer()) + subclass(GenericTlv.serializer()) + } + } + + private val serializersModule = SerializersModule { + polymorphic(ChannelStateWithCommitments::class) { + subclass(Normal::class) + subclass(WaitForFundingConfirmed::class) + subclass(WaitForFundingLocked::class) + subclass(WaitForRemotePublishFutureCommitment::class) + subclass(ShuttingDown::class) + subclass(Negotiating::class) + subclass(Closing::class) + subclass(Closed::class) + subclass(ErrorInformationLeak::class) + } + } + + private val serializationModules = SerializersModule { + include(tlvSerializersModule) + include(updateSerializersModule) + include(SerializersModule { + contextual(ByteVector64KSerializer) + contextual(ByteVector32KSerializer) + contextual(ByteVectorKSerializer) + contextual(SatoshiKSerializer) + contextual(PrivateKeyKSerializer) + contextual(PublicKeyKSerializer) + contextual(OutPointKSerializer) + contextual(TxInKSerializer) + contextual(TxOutKSerializer) + contextual(TransactionKSerializer) + contextual(BlockHeaderKSerializer) + }) + } + + // used by the "test node" JSON API + val lightningSerializersModule = SerializersModule { + include(serializersModule) + include(serializationModules) + } + + @OptIn(ExperimentalSerializationApi::class) + fun serialize(state: ChannelStateWithCommitments): ByteArray { + val output = ByteArrayOutput() + val encoder = DataOutputEncoder(output) + encoder.encodeSerializableValue(ChannelStateWithCommitments.serializer(), state) + val bytes = output.toByteArray() + val versioned = SerializedData(version = versionMagic, data = bytes.toByteVector()) + val output1 = ByteArrayOutput() + val encoder1 = DataOutputEncoder(output1) + encoder1.encodeSerializableValue(SerializedData.serializer(), versioned) + return output1.toByteArray() + } + + @OptIn(ExperimentalSerializationApi::class) + fun serialize(state: fr.acinq.lightning.channel.ChannelStateWithCommitments): ByteArray { + return serialize(ChannelStateWithCommitments.import(state)) + } + + @OptIn(ExperimentalSerializationApi::class) + fun deserialize(bin: ByteArray, nodeParams: NodeParams): fr.acinq.lightning.channel.ChannelStateWithCommitments { + val input = ByteArrayInput(bin) + val decoder = DataInputDecoder(input) + val versioned = decoder.decodeSerializableValue(SerializedData.serializer()) + return when (versioned.version) { + versionMagic -> { + val input1 = ByteArrayInput(versioned.data.toByteArray()) + val decoder1 = DataInputDecoder(input1) + decoder1.decodeSerializableValue(ChannelStateWithCommitments.serializer()).export(nodeParams) + } + else -> error("unknown serialization version ${versioned.version}") + } + } + + @OptIn(ExperimentalSerializationApi::class) + private fun deserialize(bin: ByteVector, nodeParams: NodeParams): fr.acinq.lightning.channel.ChannelStateWithCommitments = deserialize(bin.toByteArray(), nodeParams) + + @OptIn(ExperimentalSerializationApi::class) + fun encrypt(key: ByteVector32, state: ChannelStateWithCommitments): EncryptedChannelData { + val bin = serialize(state) + // NB: there is a chance of collision here, due to how the nonce is calculated. Probability of collision is once every 2.2E19 times. + // See https://en.wikipedia.org/wiki/Birthday_attack + val nonce = Crypto.sha256(bin).take(12).toByteArray() + val (ciphertext, tag) = ChaCha20Poly1305.encrypt(key.toByteArray(), nonce, bin, ByteArray(0)) + return EncryptedChannelData((ciphertext + nonce + tag).toByteVector()) + } + + @OptIn(ExperimentalSerializationApi::class) + fun encrypt(key: ByteVector32, state: fr.acinq.lightning.channel.ChannelStateWithCommitments): EncryptedChannelData { + val bin = serialize(state) + // NB: there is a chance of collision here, due to how the nonce is calculated. Probability of collision is once every 2.2E19 times. + // See https://en.wikipedia.org/wiki/Birthday_attack + val nonce = Crypto.sha256(bin).take(12).toByteArray() + val (ciphertext, tag) = ChaCha20Poly1305.encrypt(key.toByteArray(), nonce, bin, ByteArray(0)) + return EncryptedChannelData((ciphertext + nonce + tag).toByteVector()) + } + + fun encrypt(key: PrivateKey, state: fr.acinq.lightning.channel.ChannelStateWithCommitments): EncryptedChannelData = encrypt(key.value, state) + + @OptIn(ExperimentalSerializationApi::class) + fun decrypt(key: ByteVector32, data: ByteArray, nodeParams: NodeParams): fr.acinq.lightning.channel.ChannelStateWithCommitments { + // nonce is 12B, tag is 16B + val ciphertext = data.dropLast(12 + 16) + val nonce = data.takeLast(12 + 16).take(12) + val tag = data.takeLast(16) + val plaintext = ChaCha20Poly1305.decrypt(key.toByteArray(), nonce.toByteArray(), ciphertext.toByteArray(), ByteArray(0), tag.toByteArray()) + return deserialize(plaintext, nodeParams) + } + + fun decrypt(key: PrivateKey, data: ByteArray, nodeParams: NodeParams): fr.acinq.lightning.channel.ChannelStateWithCommitments = decrypt(key.value, data, nodeParams) + fun decrypt(key: PrivateKey, backup: EncryptedChannelData, nodeParams: NodeParams): fr.acinq.lightning.channel.ChannelStateWithCommitments = decrypt(key, backup.data.toByteArray(), nodeParams) + + @OptIn(ExperimentalSerializationApi::class) + class DataOutputEncoder(val output: ByteArrayOutput) : AbstractEncoder() { + override val serializersModule: SerializersModule = serializationModules + override fun encodeBoolean(value: Boolean) = output.write(if (value) 1 else 0) + override fun encodeByte(value: Byte) = output.write(value.toInt()) + override fun encodeShort(value: Short) = output.write(Pack.writeInt16BE(value)) + override fun encodeInt(value: Int) = output.write(Pack.writeInt32BE(value)) + override fun encodeLong(value: Long) = output.write(Pack.writeInt64BE(value)) + override fun encodeFloat(value: Float) { + TODO() + } + + override fun encodeDouble(value: Double) { + TODO() + } + + override fun encodeChar(value: Char) = output.write(value.code) + override fun encodeString(value: String) { + val bytes = value.encodeToByteArray() + encodeInt(bytes.size) + output.write(bytes) + } + + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = output.write(index) + override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder { + encodeInt(collectionSize) + return this + } + + override fun encodeNull() = encodeBoolean(false) + override fun encodeNotNullMark() = encodeBoolean(true) + } + + @OptIn(ExperimentalSerializationApi::class) + @ExperimentalSerializationApi + class DataInputDecoder(val input: ByteArrayInput, var elementsCount: Int = 0) : AbstractDecoder() { + private var elementIndex = 0 + override val serializersModule: SerializersModule = serializationModules + override fun decodeBoolean(): Boolean = input.read() != 0 + override fun decodeByte(): Byte = input.read().toByte() + override fun decodeShort(): Short = Pack.int16BE(input.readNBytes(2)!!) + override fun decodeInt(): Int = Pack.int32BE(input.readNBytes(4)!!) + override fun decodeLong(): Long = Pack.int64BE(input.readNBytes(8)!!) + override fun decodeFloat(): Float = TODO() + override fun decodeDouble(): Double = TODO() + override fun decodeChar(): Char = input.read().toChar() + override fun decodeString(): String { + val len = decodeInt() + require(len <= input.availableBytes) + return input.readNBytes(len)!!.decodeToString() + } + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = input.read() + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + if (elementIndex == elementsCount) return CompositeDecoder.DECODE_DONE + return elementIndex++ + } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = DataInputDecoder(input, descriptor.elementsCount) + override fun decodeSequentially(): Boolean = true + override fun decodeCollectionSize(descriptor: SerialDescriptor): Int = decodeInt().also { + require(it <= input.availableBytes) + elementsCount = it + } + + override fun decodeNotNullMark(): Boolean = decodeBoolean() + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/bitcoinKSerializers.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/bitcoinKSerializers.kt new file mode 100644 index 000000000..8e9984356 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/bitcoinKSerializers.kt @@ -0,0 +1,243 @@ +package fr.acinq.lightning.serialization.v3 + +import fr.acinq.bitcoin.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object ByteVectorKSerializer : KSerializer { + @Serializable + private data class ByteVectorSurrogate(val value: ByteArray) + + override val descriptor: SerialDescriptor = ByteVectorSurrogate.serializer().descriptor + + override fun serialize(encoder: Encoder, value: ByteVector) { + val surrogate = ByteVectorSurrogate(value.toByteArray()) + return encoder.encodeSerializableValue(ByteVectorSurrogate.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): ByteVector { + val surrogate = decoder.decodeSerializableValue(ByteVectorSurrogate.serializer()) + return ByteVector(surrogate.value) + } +} + +object ByteVector32KSerializer : KSerializer { + @Serializable + private data class ByteVector32Surrogate(val value: ByteArray) { + init { + require(value.size == 32) + } + } + + override val descriptor: SerialDescriptor = ByteVector32Surrogate.serializer().descriptor + + override fun serialize(encoder: Encoder, value: ByteVector32) { + val surrogate = ByteVector32Surrogate(value.toByteArray()) + return encoder.encodeSerializableValue(ByteVector32Surrogate.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): ByteVector32 { + val surrogate = decoder.decodeSerializableValue(ByteVector32Surrogate.serializer()) + return ByteVector32(surrogate.value) + } +} + +object ByteVector64KSerializer : KSerializer { + @Serializable + private data class ByteVector64Surrogate(val value: ByteArray) + + override val descriptor: SerialDescriptor = ByteVector64Surrogate.serializer().descriptor + + override fun serialize(encoder: Encoder, value: ByteVector64) { + val surrogate = ByteVector64Surrogate(value.toByteArray()) + return encoder.encodeSerializableValue(ByteVector64Surrogate.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): ByteVector64 { + val surrogate = decoder.decodeSerializableValue(ByteVector64Surrogate.serializer()) + return ByteVector64(surrogate.value) + } +} + +object PrivateKeyKSerializer : KSerializer { + + override fun deserialize(decoder: Decoder): PrivateKey { + return PrivateKey(ByteVector32KSerializer.deserialize(decoder)) + } + + override val descriptor: SerialDescriptor get() = ByteVector32KSerializer.descriptor + + override fun serialize(encoder: Encoder, value: PrivateKey) { + ByteVector32KSerializer.serialize(encoder, value.value) + } +} + +object PublicKeyKSerializer : KSerializer { + + override fun deserialize(decoder: Decoder): PublicKey { + return PublicKey(ByteVectorKSerializer.deserialize(decoder)) + } + + override val descriptor: SerialDescriptor get() = ByteVectorKSerializer.descriptor + + override fun serialize(encoder: Encoder, value: PublicKey) { + ByteVectorKSerializer.serialize(encoder, value.value) + } +} + +object SatoshiKSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Satoshi", PrimitiveKind.LONG) + + override fun serialize(encoder: Encoder, value: Satoshi) { + encoder.encodeLong(value.toLong()) + } + + override fun deserialize(decoder: Decoder): Satoshi { + return Satoshi(decoder.decodeLong()) + } +} + +abstract class AbstractBtcSerializableKSerializer>(val name: String, val btcSerializer: BtcSerializer) : KSerializer { + @Serializable + data class Surrogate(val name: String, val bytes: ByteArray) + + override val descriptor: SerialDescriptor = Surrogate.serializer().descriptor + + override fun serialize(encoder: Encoder, value: T) { + val surrogate = Surrogate(name, btcSerializer.write(value)) + return encoder.encodeSerializableValue(Surrogate.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): T { + val surrogate = decoder.decodeSerializableValue(Surrogate.serializer()) + return btcSerializer.read(surrogate.bytes) + } +} + +object BlockHeaderKSerializer : AbstractBtcSerializableKSerializer("BlockHeader", BlockHeader) + +object OutPointKSerializer : AbstractBtcSerializableKSerializer("OutPoint", OutPoint) + +object ScriptWitnessKSerializer : AbstractBtcSerializableKSerializer("ScriptWitness", ScriptWitness) + +object TxInKSerializer : AbstractBtcSerializableKSerializer("TxIn", TxIn) + +object TxOutKSerializer : AbstractBtcSerializableKSerializer("TxOut", TxOut) + +object TransactionKSerializer : AbstractBtcSerializableKSerializer("Transaction", Transaction) + +object ExtendedPrivateKeyKSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ExtendedPublicKey") { + element("secretkeybytes", ByteVector32KSerializer.descriptor) + element("chaincode", ByteVector32KSerializer.descriptor) + element("depth") + element("path", KeyPathKSerializer.descriptor) + element("parent") + } + + override fun serialize(encoder: Encoder, value: DeterministicWallet.ExtendedPrivateKey) { + val compositeEncoder = encoder.beginStructure(ExtendedPublicKeyKSerializer.descriptor) + compositeEncoder.encodeSerializableElement(ExtendedPublicKeyKSerializer.descriptor, 0, ByteVector32KSerializer, value.secretkeybytes) + compositeEncoder.encodeSerializableElement(ExtendedPublicKeyKSerializer.descriptor, 1, ByteVector32KSerializer, value.chaincode) + compositeEncoder.encodeIntElement(ExtendedPublicKeyKSerializer.descriptor, 2, value.depth) + compositeEncoder.encodeSerializableElement(ExtendedPublicKeyKSerializer.descriptor, 3, KeyPathKSerializer, value.path) + compositeEncoder.encodeLongElement(ExtendedPublicKeyKSerializer.descriptor, 4, value.parent) + compositeEncoder.endStructure(ExtendedPublicKeyKSerializer.descriptor) + } + + override fun deserialize(decoder: Decoder): DeterministicWallet.ExtendedPrivateKey { + var secretkeybytes: ByteVector32? = null + var chaincode: ByteVector32? = null + var depth: Int? = null + var path: KeyPath? = null + var parent: Long? = null + + val compositeDecoder = decoder.beginStructure(ExtendedPublicKeyKSerializer.descriptor) + loop@ while (true) { + when (compositeDecoder.decodeElementIndex(ExtendedPublicKeyKSerializer.descriptor)) { + CompositeDecoder.DECODE_DONE -> break@loop + 0 -> secretkeybytes = compositeDecoder.decodeSerializableElement(ExtendedPublicKeyKSerializer.descriptor, 0, ByteVector32KSerializer) + 1 -> chaincode = compositeDecoder.decodeSerializableElement(ExtendedPublicKeyKSerializer.descriptor, 1, ByteVector32KSerializer) + 2 -> depth = compositeDecoder.decodeIntElement(ExtendedPublicKeyKSerializer.descriptor, 2) + 3 -> path = compositeDecoder.decodeSerializableElement(ExtendedPublicKeyKSerializer.descriptor, 3, KeyPathKSerializer) + 4 -> parent = compositeDecoder.decodeLongElement(ExtendedPublicKeyKSerializer.descriptor, 4) + } + } + compositeDecoder.endStructure(ExtendedPublicKeyKSerializer.descriptor) + + return DeterministicWallet.ExtendedPrivateKey(secretkeybytes!!, chaincode!!, depth!!, path!!, parent!!) + } + +} + +object ExtendedPublicKeyKSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ExtendedPublicKey") { + element("publickeybytes", ByteVectorKSerializer.descriptor) + element("chaincode", ByteVector32KSerializer.descriptor) + element("depth") + element("path", KeyPathKSerializer.descriptor) + element("parent") + } + + override fun serialize(encoder: Encoder, value: DeterministicWallet.ExtendedPublicKey) { + val compositeEncoder = encoder.beginStructure(descriptor) + compositeEncoder.encodeSerializableElement(descriptor, 0, ByteVectorKSerializer, value.publickeybytes) + compositeEncoder.encodeSerializableElement(descriptor, 1, ByteVector32KSerializer, value.chaincode) + compositeEncoder.encodeIntElement(descriptor, 2, value.depth) + compositeEncoder.encodeSerializableElement(descriptor, 3, KeyPathKSerializer, value.path) + compositeEncoder.encodeLongElement(descriptor, 4, value.parent) + compositeEncoder.endStructure(descriptor) + } + + override fun deserialize(decoder: Decoder): DeterministicWallet.ExtendedPublicKey { + var publickeybytes: ByteVector? = null + var chaincode: ByteVector32? = null + var depth: Int? = null + var path: KeyPath? = null + var parent: Long? = null + + val compositeDecoder = decoder.beginStructure(descriptor) + loop@ while (true) { + when (compositeDecoder.decodeElementIndex(descriptor)) { + CompositeDecoder.DECODE_DONE -> break@loop + 0 -> publickeybytes = compositeDecoder.decodeSerializableElement(descriptor, 0, ByteVectorKSerializer) + 1 -> chaincode = compositeDecoder.decodeSerializableElement(descriptor, 1, ByteVector32KSerializer) + 2 -> depth = compositeDecoder.decodeIntElement(descriptor, 2) + 3 -> path = compositeDecoder.decodeSerializableElement(descriptor, 3, KeyPathKSerializer) + 4 -> parent = compositeDecoder.decodeLongElement(descriptor, 4) + } + } + compositeDecoder.endStructure(descriptor) + + return DeterministicWallet.ExtendedPublicKey(publickeybytes!!, chaincode!!, depth!!, path!!, parent!!) + } + +} + +object KeyPathKSerializer : KSerializer { + private val listSerializer = ListSerializer(Long.serializer()) + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("KeyPath") { + element("path", listSerializer.descriptor) + } + + override fun serialize(encoder: Encoder, value: KeyPath) { + val compositeEncoder = encoder.beginStructure(ExtendedPublicKeyKSerializer.descriptor) + compositeEncoder.encodeSerializableElement(descriptor, 0, listSerializer, value.path) + compositeEncoder.endStructure(descriptor) + } + + override fun deserialize(decoder: Decoder): KeyPath { + val compositeDecoder = decoder.beginStructure(ExtendedPublicKeyKSerializer.descriptor) + require(compositeDecoder.decodeElementIndex(descriptor) == 0) + val path = compositeDecoder.decodeSerializableElement(descriptor, 0, listSerializer) + compositeDecoder.endStructure(descriptor) + return KeyPath(path) + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/utils/Try.kt b/src/commonMain/kotlin/fr/acinq/lightning/utils/Try.kt index 46ca93505..45aae4712 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/utils/Try.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/utils/Try.kt @@ -1,27 +1,26 @@ package fr.acinq.lightning.utils - sealed class Try { abstract val isSuccess: Boolean val isFailure: Boolean get() = !isSuccess abstract fun get(): T abstract fun getOrElse(f: () -> T): T + abstract fun recoverWith(f: () -> Try): Try abstract fun map(f: (T) -> R): Try data class Success(val result: T) : Try() { override val isSuccess: Boolean = true override fun get(): T = result override fun getOrElse(f: () -> T): T = result + override fun recoverWith(f: () -> Try): Try = this override fun map(f: (T) -> R): Try = runTrying { f(result) } } data class Failure(val error: Throwable) : Try() { override val isSuccess: Boolean = false - override fun get(): T { - throw error - } - + override fun get(): T = throw error override fun getOrElse(f: () -> T): T = f() + override fun recoverWith(f: () -> Try): Try = f() @Suppress("UNCHECKED_CAST") override fun map(f: (T) -> R): Try = this as Try diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt index 5bd7188fb..099eace13 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt @@ -5,8 +5,9 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Satoshi import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.lightning.Features import fr.acinq.lightning.channel.ChannelOrigin -import fr.acinq.lightning.channel.ChannelVersion +import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.utils.BitField import fr.acinq.lightning.utils.toByteVector import kotlinx.serialization.Contextual @@ -17,32 +18,72 @@ import kotlinx.serialization.Serializable sealed class ChannelTlv : Tlv { /** Commitment to where the funds will go in case of a mutual close, which remote node will enforce in case we're compromised. */ @Serializable - data class UpfrontShutdownScript(@Contextual val scriptPubkey: ByteVector) : ChannelTlv() { + data class UpfrontShutdownScriptTlv(@Contextual val scriptPubkey: ByteVector) : ChannelTlv() { val isEmpty: Boolean get() = scriptPubkey.isEmpty() - override val tag: Long get() = UpfrontShutdownScript.tag + override val tag: Long get() = UpfrontShutdownScriptTlv.tag override fun write(out: Output) { LightningCodecs.writeBytes(scriptPubkey, out) } - companion object : TlvValueReader { + companion object : TlvValueReader { const val tag: Long = 0 - override fun read(input: Input): UpfrontShutdownScript { + override fun read(input: Input): UpfrontShutdownScriptTlv { val len = input.availableBytes val script = LightningCodecs.bytes(input, len) - return UpfrontShutdownScript(ByteVector(script)) + return UpfrontShutdownScriptTlv(ByteVector(script)) } } } @Serializable - data class ChannelVersionTlv(val channelVersion: ChannelVersion) : ChannelTlv() { + data class ChannelTypeTlv(val channelType: ChannelType) : ChannelTlv() { + override val tag: Long get() = ChannelTypeTlv.tag + + override fun write(out: Output) { + val features = when (channelType) { + is ChannelType.SupportedChannelType -> channelType.toFeatures() + is ChannelType.UnsupportedChannelType -> channelType.featureBits + } + LightningCodecs.writeBytes(features.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 1 + + override fun read(input: Input): ChannelTypeTlv { + val len = input.availableBytes + val features = LightningCodecs.bytes(input, len) + return ChannelTypeTlv(ChannelType.fromFeatures(Features(features))) + } + } + } + + /** This legacy TLV was used before ChannelType was introduced: it should be removed whenever possible. */ + @Serializable + data class ChannelVersionTlv(val channelType: ChannelType) : ChannelTlv() { override val tag: Long get() = ChannelVersionTlv.tag override fun write(out: Output) { - LightningCodecs.writeBytes(channelVersion.bits.bytes, out) + val bits = BitField(4) + when (channelType) { + ChannelType.SupportedChannelType.AnchorOutputs, ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve -> { + bits.setRight(1) + bits.setRight(2) + bits.setRight(3) + } + ChannelType.SupportedChannelType.StaticRemoteKey -> { + bits.setRight(1) + bits.setRight(3) + } + ChannelType.SupportedChannelType.Standard -> { + bits.setRight(3) + } + is ChannelType.UnsupportedChannelType -> throw IllegalArgumentException("unsupported channel type: ${channelType.name}") + } + LightningCodecs.writeBytes(bits.bytes, out) } companion object : TlvValueReader { @@ -50,8 +91,13 @@ sealed class ChannelTlv : Tlv { override fun read(input: Input): ChannelVersionTlv { val len = input.availableBytes - val buffer = LightningCodecs.bytes(input, len) - return ChannelVersionTlv(ChannelVersion(BitField.from(buffer))) + val bits = BitField.from(LightningCodecs.bytes(input, len)) + val channelType = when { + bits.getRight(2) -> ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve + bits.getRight(1) -> ChannelType.SupportedChannelType.StaticRemoteKey + else -> ChannelType.SupportedChannelType.Standard + } + return ChannelVersionTlv(channelType) } } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 050c39d1b..afc52fd40 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -7,7 +7,7 @@ import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.ChannelVersion +import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.router.Announcements import fr.acinq.lightning.utils.* import fr.acinq.secp256k1.Hex @@ -306,14 +306,15 @@ data class OpenChannel( val channelFlags: Byte, val tlvStream: TlvStream = TlvStream.empty() ) : ChannelMessage, HasTemporaryChannelId, HasChainHash { - val channelVersion: ChannelVersion? get() = tlvStream.get()?.channelVersion + val channelType: ChannelType? get() = tlvStream.get()?.channelType ?: tlvStream.get()?.channelType override val type: Long get() = OpenChannel.type override fun write(out: Output) { @Suppress("UNCHECKED_CAST") val readers = mapOf( - ChannelTlv.UpfrontShutdownScript.tag to ChannelTlv.UpfrontShutdownScript.Companion as TlvValueReader, + ChannelTlv.UpfrontShutdownScriptTlv.tag to ChannelTlv.UpfrontShutdownScriptTlv.Companion as TlvValueReader, + ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader, ChannelTlv.ChannelVersionTlv.tag to ChannelTlv.ChannelVersionTlv.Companion as TlvValueReader, ChannelTlv.ChannelOriginTlv.tag to ChannelTlv.ChannelOriginTlv.Companion as TlvValueReader ) @@ -344,7 +345,8 @@ data class OpenChannel( override fun read(input: Input): OpenChannel { @Suppress("UNCHECKED_CAST") val readers = mapOf( - ChannelTlv.UpfrontShutdownScript.tag to ChannelTlv.UpfrontShutdownScript.Companion as TlvValueReader, + ChannelTlv.UpfrontShutdownScriptTlv.tag to ChannelTlv.UpfrontShutdownScriptTlv.Companion as TlvValueReader, + ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader, ChannelTlv.ChannelVersionTlv.tag to ChannelTlv.ChannelVersionTlv.Companion as TlvValueReader, ChannelTlv.ChannelOriginTlv.tag to ChannelTlv.ChannelOriginTlv.Companion as TlvValueReader ) @@ -392,12 +394,15 @@ data class AcceptChannel( @Contextual val firstPerCommitmentPoint: PublicKey, val tlvStream: TlvStream = TlvStream.empty() ) : ChannelMessage, HasTemporaryChannelId { + val channelType: ChannelType? get() = tlvStream.get()?.channelType + override val type: Long get() = AcceptChannel.type override fun write(out: Output) { @Suppress("UNCHECKED_CAST") val readers = mapOf( - ChannelTlv.UpfrontShutdownScript.tag to ChannelTlv.UpfrontShutdownScript.Companion as TlvValueReader, + ChannelTlv.UpfrontShutdownScriptTlv.tag to ChannelTlv.UpfrontShutdownScriptTlv.Companion as TlvValueReader, + ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader, ChannelTlv.ChannelVersionTlv.tag to ChannelTlv.ChannelVersionTlv.Companion as TlvValueReader ) LightningCodecs.writeBytes(temporaryChannelId, out) @@ -423,7 +428,8 @@ data class AcceptChannel( override fun read(input: Input): AcceptChannel { @Suppress("UNCHECKED_CAST") val readers = mapOf( - ChannelTlv.UpfrontShutdownScript.tag to ChannelTlv.UpfrontShutdownScript.Companion as TlvValueReader, + ChannelTlv.UpfrontShutdownScriptTlv.tag to ChannelTlv.UpfrontShutdownScriptTlv.Companion as TlvValueReader, + ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader, ChannelTlv.ChannelVersionTlv.tag to ChannelTlv.ChannelVersionTlv.Companion as TlvValueReader ) return AcceptChannel( diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelConfigTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelConfigTestsCommon.kt new file mode 100644 index 000000000..39d8e332b --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelConfigTestsCommon.kt @@ -0,0 +1,23 @@ +package fr.acinq.lightning.channel + +import fr.acinq.lightning.crypto.assertArrayEquals +import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.secp256k1.Hex +import kotlin.test.Test +import kotlin.test.assertEquals + +class ChannelConfigTestsCommon : LightningTestSuite() { + + @Test + fun `convert from bytes`() { + assertArrayEquals(ChannelConfig(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath).toByteArray(), Hex.decode("01")) + assertEquals(ChannelConfig(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath), ChannelConfig(Hex.decode("ff"))) + assertEquals(ChannelConfig(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath), ChannelConfig(Hex.decode("01"))) + assertEquals(ChannelConfig(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath), ChannelConfig(Hex.decode("0001"))) + assertEquals(ChannelConfig(), ChannelConfig(Hex.decode("00"))) + assertEquals(ChannelConfig(), ChannelConfig(Hex.decode("0000"))) + assertEquals(ChannelConfig(), ChannelConfig(Hex.decode("02"))) + assertEquals(ChannelConfig(), ChannelConfig(Hex.decode("fe"))) + } + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelTypesTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt similarity index 98% rename from src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelTypesTestsCommon.kt rename to src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt index eb60045a5..5f32f4a08 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelTypesTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelDataTestsCommon.kt @@ -25,13 +25,7 @@ import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import kotlin.test.* -class ChannelTypesTestsCommon : LightningTestSuite() { - - @Test - fun `standard channel features include deterministic channel key path`() { - assertTrue(ChannelVersion.STANDARD.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) - assertTrue(!ChannelVersion.ZEROES.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) - } +class ChannelDataTestsCommon : LightningTestSuite() { @Test fun `local commit published`() { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelFeaturesTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelFeaturesTestsCommon.kt new file mode 100644 index 000000000..7bc8837e5 --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/ChannelFeaturesTestsCommon.kt @@ -0,0 +1,79 @@ +package fr.acinq.lightning.channel + +import fr.acinq.lightning.Feature +import fr.acinq.lightning.FeatureSupport +import fr.acinq.lightning.Features +import fr.acinq.lightning.tests.utils.LightningTestSuite +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ChannelFeaturesTestsCommon : LightningTestSuite() { + + @Test + fun `channel type uses mandatory features`() { + assertTrue(ChannelType.SupportedChannelType.Standard.features.isEmpty()) + assertEquals(ChannelType.SupportedChannelType.StaticRemoteKey.toFeatures(), Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory)) + assertEquals(ChannelType.SupportedChannelType.AnchorOutputs.toFeatures(), Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory)) + assertEquals( + ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve.toFeatures(), + Features( + Feature.Wumbo to FeatureSupport.Mandatory, + Feature.ZeroConfChannels to FeatureSupport.Mandatory, + Feature.ZeroReserveChannels to FeatureSupport.Mandatory, + Feature.StaticRemoteKey to FeatureSupport.Mandatory, + Feature.AnchorOutputs to FeatureSupport.Mandatory + ) + ) + } + + @Test + fun `extract channel type from channel features`() { + assertEquals(ChannelType.SupportedChannelType.Standard, ChannelFeatures(setOf()).channelType) + assertEquals(ChannelType.SupportedChannelType.Standard, ChannelFeatures(setOf(Feature.ZeroReserveChannels, Feature.ZeroConfChannels)).channelType) + assertEquals(ChannelType.SupportedChannelType.StaticRemoteKey, ChannelFeatures(setOf(Feature.StaticRemoteKey)).channelType) + assertEquals(ChannelType.SupportedChannelType.StaticRemoteKey, ChannelFeatures(setOf(Feature.ZeroReserveChannels, Feature.StaticRemoteKey)).channelType) + assertEquals(ChannelType.SupportedChannelType.AnchorOutputs, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs)).channelType) + assertEquals(ChannelType.SupportedChannelType.AnchorOutputs, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo, Feature.ZeroConfChannels)).channelType) + assertEquals(ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroConfChannels, Feature.ZeroReserveChannels)).channelType) + assertEquals( + ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, + ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo, Feature.ZeroConfChannels, Feature.ZeroReserveChannels)).channelType + ) + } + + @Test + fun `extract channel type from features`() { + assertEquals(ChannelType.SupportedChannelType.Standard, ChannelType.fromFeatures(Features.empty)) + assertEquals(ChannelType.SupportedChannelType.StaticRemoteKey, ChannelType.fromFeatures(Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory))) + assertEquals(ChannelType.SupportedChannelType.AnchorOutputs, ChannelType.fromFeatures(Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory))) + assertEquals( + ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, ChannelType.fromFeatures( + Features( + Feature.Wumbo to FeatureSupport.Mandatory, + Feature.ZeroConfChannels to FeatureSupport.Mandatory, + Feature.ZeroReserveChannels to FeatureSupport.Mandatory, + Feature.StaticRemoteKey to FeatureSupport.Mandatory, + Feature.AnchorOutputs to FeatureSupport.Mandatory + ) + ) + ) + // Bolt 2 mandates that features match exactly. + listOf( + Features(Feature.ZeroReserveChannels to FeatureSupport.Optional), + Features(Feature.ZeroReserveChannels to FeatureSupport.Mandatory), + Features(Feature.StaticRemoteKey to FeatureSupport.Optional), + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.PaymentSecret to FeatureSupport.Mandatory), + Features(Feature.StaticRemoteKey to FeatureSupport.Optional, Feature.AnchorOutputs to FeatureSupport.Optional), + Features(Feature.StaticRemoteKey to FeatureSupport.Optional, Feature.AnchorOutputs to FeatureSupport.Mandatory), + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Optional), + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.Wumbo to FeatureSupport.Mandatory), + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroConfChannels to FeatureSupport.Mandatory), + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory), + Features(Feature.StaticRemoteKey to FeatureSupport.Mandatory, Feature.AnchorOutputs to FeatureSupport.Mandatory, Feature.ZeroConfChannels to FeatureSupport.Mandatory, Feature.ZeroReserveChannels to FeatureSupport.Mandatory), + ).forEach { features -> + assertEquals(ChannelType.UnsupportedChannelType(features), ChannelType.fromFeatures(features)) + } + } + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt index 854be813d..748c54f86 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/CommitmentsTestsCommon.kt @@ -3,9 +3,9 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Features import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey -import fr.acinq.lightning.Features import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_SPENT import fr.acinq.lightning.blockchain.WatchEventSpent @@ -472,7 +472,7 @@ class CommitmentsTestsCommon : LightningTestSuite() { @OptIn(ExperimentalUnsignedTypes::class) companion object { fun makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0.sat), dustLimit: Satoshi = 0.sat, isFunder: Boolean = true, announceChannel: Boolean = true): Commitments { - val channelKeys = ChannelKeys(KeyPath("42"), randomKey(), randomKey(), randomKey(), randomKey() , randomKey(), randomBytes32()) + val channelKeys = ChannelKeys(KeyPath("42"), randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomBytes32()) val localParams = LocalParams( randomKey().publicKey(), channelKeys, dustLimit, Long.MAX_VALUE, 0.sat, 1.msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, Features.empty ) @@ -487,7 +487,8 @@ class CommitmentsTestsCommon : LightningTestSuite() { ) val localCommitTx = Transactions.TransactionWithInputInfo.CommitTx(commitmentInput, Transaction(2, listOf(), listOf(), 0)) return Commitments( - ChannelVersion.STANDARD, + ChannelConfig.standard, + ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features), localParams, remoteParams, channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty, @@ -506,7 +507,7 @@ class CommitmentsTestsCommon : LightningTestSuite() { } fun makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announceChannel: Boolean): Commitments { - val channelKeys = ChannelKeys(KeyPath("42"), randomKey(), randomKey(), randomKey(), randomKey() , randomKey(), randomBytes32()) + val channelKeys = ChannelKeys(KeyPath("42"), randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomBytes32()) val localParams = LocalParams( localNodeId, channelKeys, 0.sat, Long.MAX_VALUE, 0.sat, 1.msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, Features.empty ) @@ -518,7 +519,8 @@ class CommitmentsTestsCommon : LightningTestSuite() { ) val localCommitTx = Transactions.TransactionWithInputInfo.CommitTx(commitmentInput, Transaction(2, listOf(), listOf(), 0)) return Commitments( - ChannelVersion.STANDARD, + ChannelConfig.standard, + ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features), localParams, remoteParams, channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index b66ab70e6..1441267e1 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -1,10 +1,8 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* -import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 -import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.blockchain.fee.OnChainFeerates @@ -13,10 +11,7 @@ import fr.acinq.lightning.router.ChannelHop import fr.acinq.lightning.serialization.Serialization import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.utils.UUID -import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.utils.toByteVector32 +import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* import fr.acinq.secp256k1.Hex import org.kodein.memory.file.FileSystem @@ -68,32 +63,41 @@ internal inline fun List.doesNotHave( fun Normal.updateFeerate(feerate: FeeratePerKw): Normal = this.copy(currentOnChainFeerates = OnChainFeerates(feerate, feerate, feerate)) fun Negotiating.updateFeerate(feerate: FeeratePerKw): Negotiating = this.copy(currentOnChainFeerates = OnChainFeerates(feerate, feerate, feerate)) +fun Features.add(vararg pairs: Pair): Features = this.copy(activated = this.activated + mapOf(*pairs)) +fun Features.remove(vararg features: Feature): Features = this.copy(activated = activated.filterKeys { f -> !features.contains(f) }) + object TestsHelper { + fun init( - channelVersion: ChannelVersion = ChannelVersion.STANDARD, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + aliceFeatures: Features = TestConstants.Alice.nodeParams.features, + bobFeatures: Features = TestConstants.Bob.nodeParams.features, currentHeight: Int = TestConstants.defaultBlockHeight, fundingAmount: Satoshi = TestConstants.fundingAmount, pushMsat: MilliSatoshi = TestConstants.pushMsat, channelOrigin: ChannelOrigin? = null ): Triple { + val aliceNodeParams = TestConstants.Alice.nodeParams.copy(features = aliceFeatures) + val bobNodeParams = TestConstants.Bob.nodeParams.copy(features = bobFeatures) var alice: ChannelState = WaitForInit( - StaticParams(TestConstants.Alice.nodeParams, TestConstants.Bob.keyManager.nodeId), + StaticParams(aliceNodeParams, TestConstants.Bob.keyManager.nodeId), currentTip = Pair(currentHeight, Block.RegtestGenesisBlock.header), currentOnChainFeerates = OnChainFeerates(TestConstants.feeratePerKw, TestConstants.feeratePerKw, TestConstants.feeratePerKw) ) var bob: ChannelState = WaitForInit( - StaticParams(TestConstants.Bob.nodeParams, TestConstants.Alice.keyManager.nodeId), + StaticParams(bobNodeParams, TestConstants.Alice.keyManager.nodeId), currentTip = Pair(currentHeight, Block.RegtestGenesisBlock.header), currentOnChainFeerates = OnChainFeerates(TestConstants.feeratePerKw, TestConstants.feeratePerKw, TestConstants.feeratePerKw) ) val channelFlags = 0.toByte() - var aliceChannelParams = TestConstants.Alice.channelParams - val bobChannelParams = TestConstants.Bob.channelParams - if (channelVersion.isSet(ChannelVersion.ZERO_RESERVE_BIT)) { + var aliceChannelParams = TestConstants.Alice.channelParams.copy(features = aliceFeatures) + val bobChannelParams = TestConstants.Bob.channelParams.copy(features = bobFeatures) + // If Bob accepts zero-reserve channels, Alice is nice and doesn't require a reserve from Bob. + if (bobFeatures.hasFeature(Feature.ZeroReserveChannels)) { aliceChannelParams = aliceChannelParams.copy(channelReserve = 0.sat) } - val aliceInit = Init(ByteVector(aliceChannelParams.features.toByteArray())) - val bobInit = Init(ByteVector(bobChannelParams.features.toByteArray())) + val aliceInit = Init(aliceFeatures.toByteArray().toByteVector()) + val bobInit = Init(bobFeatures.toByteArray().toByteVector()) val ra = alice.process( ChannelEvent.InitFunder( ByteVector32.Zeroes, @@ -104,13 +108,14 @@ object TestsHelper { aliceChannelParams, bobInit, channelFlags, - channelVersion, + ChannelConfig.standard, + channelType, channelOrigin ) ) alice = ra.first assertTrue(alice is WaitForAcceptChannel) - val rb = bob.process(ChannelEvent.InitFundee(ByteVector32.Zeroes, bobChannelParams, aliceInit)) + val rb = bob.process(ChannelEvent.InitFundee(ByteVector32.Zeroes, bobChannelParams, ChannelConfig.standard, aliceInit)) bob = rb.first assertTrue(bob is WaitForOpenChannel) val open = ra.second.findOutgoingMessage() @@ -118,12 +123,14 @@ object TestsHelper { } fun reachNormal( - channelVersion: ChannelVersion = ChannelVersion.STANDARD, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + aliceFeatures: Features = TestConstants.Alice.nodeParams.features, + bobFeatures: Features = TestConstants.Bob.nodeParams.features, currentHeight: Int = TestConstants.defaultBlockHeight, fundingAmount: Satoshi = TestConstants.fundingAmount, pushMsat: MilliSatoshi = TestConstants.pushMsat ): Pair { - val (a, b, open) = init(channelVersion, currentHeight, fundingAmount, pushMsat) + val (a, b, open) = init(channelType, aliceFeatures, bobFeatures, currentHeight, fundingAmount, pushMsat) var alice = a as ChannelState var bob = b as ChannelState var rb = bob.process(ChannelEvent.MessageReceived(open)) @@ -133,7 +140,7 @@ object TestsHelper { alice = ra.first val makeFundingTx = run { val candidates = ra.second.filterIsInstance() - if (candidates.isEmpty()) throw IllegalArgumentException("cannot find MakeFundingTx") + assertTrue(candidates.isNotEmpty(), "cannot find funding tx") candidates.first() } val fundingTx = Transaction( @@ -152,7 +159,7 @@ object TestsHelper { alice = ra.first val watchConfirmed = run { val candidates = ra.second.findWatches() - if (candidates.isEmpty()) throw IllegalArgumentException("cannot find WatchConfirmed") + assertTrue(candidates.isNotEmpty(), "cannot find watch confirmed on funding tx") candidates.first() } @@ -203,8 +210,8 @@ object TestsHelper { fun localClose(s: ChannelState): Pair { assertTrue(s is ChannelStateWithCommitments) - assertEquals(ChannelVersion.STANDARD, s.commitments.channelVersion) - // an error occurs and alice publishes her commit tx + assertEquals(ChannelType.SupportedChannelType.AnchorOutputs, s.commitments.channelFeatures.channelType) + // an error occurs and s publishes their commit tx val commitTx = s.commitments.localCommit.publishableTxs.commitTx.tx val (s1, actions1) = s.process(ChannelEvent.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) assertTrue(s1 is Closing) @@ -247,7 +254,7 @@ object TestsHelper { fun remoteClose(rCommitTx: Transaction, s: ChannelState): Pair { assertTrue(s is ChannelStateWithCommitments) - assertEquals(ChannelVersion.STANDARD, s.commitments.channelVersion) + assertEquals(ChannelType.SupportedChannelType.AnchorOutputs, s.commitments.channelFeatures.channelType) // we make s believe r unilaterally closed the channel val (s1, actions1) = s.process(ChannelEvent.WatchReceived(WatchEventSpent(s.channelId, BITCOIN_FUNDING_SPENT, rCommitTx))) assertTrue(s1 is Closing) @@ -421,26 +428,45 @@ object TestsHelper { } // we check that serialization works by checking that deserialize(serialize(state)) == state - fun checkSerialization(state: ChannelStateWithCommitments, saveFiles: Boolean = false) { + private fun checkSerialization(state: ChannelStateWithCommitments, saveFiles: Boolean = false) { val serializedv1 = fr.acinq.lightning.serialization.v1.Serialization.serialize(state) val serializedv2 = fr.acinq.lightning.serialization.v2.Serialization.serialize(state) + val serializedv3 = fr.acinq.lightning.serialization.v3.Serialization.serialize(state) fun save(blob: ByteArray, suffix: String) { val name = (state::class.simpleName ?: "serialized") + "_${Hex.encode(Crypto.sha256(blob).take(8).toByteArray())}.$suffix" val file: Path = FileSystem.workingDir().resolve(name) file.openWriteableFile(false).putBytes(blob) } + if (saveFiles) { save(serializedv1, "v1") save(serializedv2, "v2") + save(serializedv3, "v3") } + + // Before v3, we had a single set of hard-coded channel features, so they will not match if the test added new channel features that weren't supported then. + fun maskChannelFeatures(state: ChannelStateWithCommitments): ChannelStateWithCommitments = when (state) { + is WaitForRemotePublishFutureCommitment -> state.copy(commitments = state.commitments.copy(channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features))) + is WaitForFundingConfirmed -> state.copy(commitments = state.commitments.copy(channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features))) + is WaitForFundingLocked -> state.copy(commitments = state.commitments.copy(channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features))) + is Normal -> state.copy(commitments = state.commitments.copy(channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features))) + is ShuttingDown -> state.copy(commitments = state.commitments.copy(channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features))) + is Negotiating -> state.copy(commitments = state.commitments.copy(channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features))) + is Closing -> state.copy(commitments = state.commitments.copy(channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features))) + is Closed -> state.copy(state = state.state.copy(commitments = state.commitments.copy(channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features)))) + is ErrorInformationLeak -> state.copy(commitments = state.commitments.copy(channelFeatures = ChannelFeatures(ChannelType.SupportedChannelType.AnchorOutputs.features))) + } + val deserializedv1 = Serialization.deserialize(serializedv1, state.staticParams.nodeParams) - assertEquals(deserializedv1, state, "serialization error (v1)") + assertEquals(maskChannelFeatures(deserializedv1), maskChannelFeatures(state), "serialization error (v1)") val deserializedv2 = Serialization.deserialize(serializedv2, state.staticParams.nodeParams) - assertEquals(deserializedv2, state, "serialization error (v2)") + assertEquals(maskChannelFeatures(deserializedv2), maskChannelFeatures(state), "serialization error (v2)") + val deserializedv3 = Serialization.deserialize(serializedv3, state.staticParams.nodeParams) + assertEquals(deserializedv3, state, "serialization error (v3)") } - fun checkSerialization(actions: List) { + private fun checkSerialization(actions: List) { // we check that serialization works everytime we're suppose to persist channel data actions.filterIsInstance().forEach { checkSerialization(it.data) } } @@ -451,4 +477,5 @@ object TestsHelper { checkSerialization(result.second) return result } + } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt index 6fe403a01..48d6a8528 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ClosingTestsCommon.kt @@ -4,6 +4,7 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Bitcoin.computeBIP84Address import fr.acinq.bitcoin.Bitcoin.computeP2PkhAddress import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning import fr.acinq.lightning.blockchain.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -32,6 +33,7 @@ import fr.acinq.lightning.wire.* import kotlin.test.* class ClosingTestsCommon : LightningTestSuite() { + @Test fun `start fee negotiation from configured block target`() { val (alice, bob) = reachNormal() @@ -1061,7 +1063,7 @@ class ClosingTestsCommon : LightningTestSuite() { @Test fun `recv BITCOIN_TX_CONFIRMED (future remote commit)`() { - val (alice0, bob0) = reachNormal() + val (alice0, bob0) = reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val (_, bobDisconnected) = run { // This HTLC will be fulfilled. val (nodes1, preimage, htlc) = addHtlc(25_000_000.msat, alice0, bob0) @@ -1082,8 +1084,8 @@ class ClosingTestsCommon : LightningTestSuite() { Pair(alice7, bob7) } - val localInit = Init(ByteVector(TestConstants.Alice.nodeParams.features.toByteArray())) - val remoteInit = Init(ByteVector(TestConstants.Bob.nodeParams.features.toByteArray())) + val localInit = Init(ByteVector(alice0.commitments.localParams.features.toByteArray())) + val remoteInit = Init(ByteVector(bob0.commitments.localParams.features.toByteArray())) // then we manually replace alice's state with an older one and reconnect them. val (alice1, aliceActions1) = Offline(alice0).processEx(ChannelEvent.Connected(localInit, remoteInit)) @@ -1572,7 +1574,7 @@ class ClosingTestsCommon : LightningTestSuite() { fun `recv ChannelReestablish`() { val (alice0, bob0, _) = initMutualClose() val bobCurrentPerCommitmentPoint = bob0.keyManager.commitmentPoint( - bob0.keyManager.channelKeyPath(bob0.commitments.localParams, bob0.commitments.channelVersion), + bob0.keyManager.channelKeyPath(bob0.commitments.localParams, bob0.commitments.channelConfig), bob0.commitments.localCommit.index ) val channelReestablish = ChannelReestablish(bob0.channelId, 42, 42, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint) @@ -1880,7 +1882,7 @@ class ClosingTestsCommon : LightningTestSuite() { } fun initForceClose(): Pair { - val (alice, bob) = WaitForFundingConfirmedTestsCommon.init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) // funder val alice1 = run { val (alice1, actions1) = alice.process(ChannelEvent.ExecuteCommand(CMD_FORCECLOSE)) @@ -1891,11 +1893,11 @@ class ClosingTestsCommon : LightningTestSuite() { if (channelBalance > 0.msat) { val onChainPayment = actions1.filterIsInstance().firstOrNull() assertNotNull(onChainPayment) - assertTrue(onChainPayment.amount == channelBalance) + assertEquals(onChainPayment.amount, channelBalance) assertTrue(onChainPayment.isSentToDefaultAddress) val (closingPubKey, _) = alice1.keyManager.closingPubkeyScript(PublicKey.Generator) // param ignored val closingAddress2 = computeBIP84Address(closingPubKey, alice1.staticParams.nodeParams.chainHash) - assertTrue(onChainPayment.closingAddress == closingAddress2) + assertEquals(onChainPayment.closingAddress, closingAddress2) } val error = actions1.hasOutgoingMessage() @@ -1928,11 +1930,11 @@ class ClosingTestsCommon : LightningTestSuite() { if (channelBalance > 0.msat) { val onChainPayment = actions1.filterIsInstance().firstOrNull() assertNotNull(onChainPayment) - assertTrue(onChainPayment.amount == channelBalance) + assertEquals(onChainPayment.amount, channelBalance) assertTrue(onChainPayment.isSentToDefaultAddress) val (closingPubKey, _) = bob1.keyManager.closingPubkeyScript(PublicKey.Generator) // param ignored val closingAddress = computeBIP84Address(closingPubKey, bob1.staticParams.nodeParams.chainHash) - assertTrue(onChainPayment.closingAddress == closingAddress) + assertEquals(onChainPayment.closingAddress, closingAddress) } val error = actions1.hasOutgoingMessage() @@ -1958,4 +1960,5 @@ class ClosingTestsCommon : LightningTestSuite() { return alice1 to bob1 } } + } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt index 976159555..f0c46a4bc 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NegotiatingTestsCommon.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* +import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.* @@ -140,8 +141,13 @@ class NegotiatingTestsCommon : LightningTestSuite() { @Test fun `recv ClosingSigned with encrypted channel data`() { - val (_, _, aliceCloseSig) = init(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE) - assertFalse(aliceCloseSig.channelData.isEmpty()) + val (alice, bob, aliceCloseSig) = init() + assertTrue(alice.commitments.localParams.features.hasFeature(Feature.ChannelBackupProvider)) + assertTrue(bob.commitments.localParams.features.hasFeature(Feature.ChannelBackupClient)) + assertTrue(aliceCloseSig.channelData.isEmpty()) + val (_, actions1) = bob.processEx(ChannelEvent.MessageReceived(aliceCloseSig)) + val bobCloseSig = actions1.hasOutgoingMessage() + assertFalse(bobCloseSig.channelData.isEmpty()) } @Test @@ -187,8 +193,8 @@ class NegotiatingTestsCommon : LightningTestSuite() { } companion object { - fun init(channelVersion: ChannelVersion = ChannelVersion.STANDARD, tweakFees: Boolean = false, pushMsat: MilliSatoshi = TestConstants.pushMsat): Triple { - val (alice, bob) = reachNormal(channelVersion = channelVersion, pushMsat = pushMsat) + fun init(channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, tweakFees: Boolean = false, pushMsat: MilliSatoshi = TestConstants.pushMsat): Triple { + val (alice, bob) = reachNormal(channelType = channelType, pushMsat = pushMsat) return mutualClose(alice, bob, tweakFees) } @@ -215,4 +221,5 @@ class NegotiatingTestsCommon : LightningTestSuite() { } } } + } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt index ed9726662..2f06fdabc 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.* @@ -132,8 +133,8 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv CMD_ADD_HTLC (increasing balance but still below reserve)`() { val (alice0, bob0) = reachNormal(pushMsat = 0.msat) - assertFalse(alice0.commitments.isZeroReserve) - assertFalse(bob0.commitments.isZeroReserve) + assertFalse(alice0.commitments.channelFeatures.hasFeature(Feature.ZeroReserveChannels)) + assertFalse(bob0.commitments.channelFeatures.hasFeature(Feature.ZeroReserveChannels)) assertEquals(0.msat, bob0.commitments.availableBalanceForSend()) val cmdAdd = defaultAdd.copy(amount = 1_500.msat) @@ -647,8 +648,10 @@ class NormalTestsCommon : LightningTestSuite() { } @Test - fun `recv CMD_SIGN (channel backup, zero-reserve channel, fundee)`() { - val (alice, bob) = reachNormal(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE) + fun `recv CMD_SIGN (channel backup, fundee)`() { + val (alice, bob) = reachNormal() + assertTrue(alice.commitments.localParams.features.hasFeature(Feature.ChannelBackupProvider)) + assertTrue(bob.commitments.localParams.features.hasFeature(Feature.ChannelBackupClient)) val (_, cmdAdd) = makeCmdAdd(50_000_000.msat, alice.staticParams.nodeParams.nodeId, alice.currentBlockHeight.toLong()) val (bob1, actions) = bob.processEx(ChannelEvent.ExecuteCommand(cmdAdd)) val add = actions.findOutgoingMessage() @@ -662,13 +665,15 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv CommitSig (one htlc received)`() { - val (alice0, bob0) = reachNormal() + val (alice0, bob0) = reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val (nodes0, _, htlc) = addHtlc(50_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes0 assertTrue(bob1 is Normal) val (_, bob2) = signAndRevack(alice1, bob1) - val (bob3, _) = bob2.processEx(ChannelEvent.ExecuteCommand(CMD_SIGN)) + val (bob3, actions3) = bob2.processEx(ChannelEvent.ExecuteCommand(CMD_SIGN)) + val commitSig = actions3.findOutgoingMessage() + assertTrue(commitSig.channelData.isEmpty()) assertTrue(bob3 is Normal) assertTrue(bob3.commitments.localCommit.spec.htlcs.incomings().any { it.id == htlc.id }) assertEquals(1, bob3.commitments.localCommit.publishableTxs.htlcTxsAndSigs.size) @@ -879,8 +884,10 @@ class NormalTestsCommon : LightningTestSuite() { } @Test - fun `recv RevokeAndAck (channel backup, zero-reserve channel, fundee)`() { - val (alice, bob) = reachNormal(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE) + fun `recv RevokeAndAck (channel backup, fundee)`() { + val (alice, bob) = reachNormal() + assertTrue(alice.commitments.localParams.features.hasFeature(Feature.ChannelBackupProvider)) + assertTrue(bob.commitments.localParams.features.hasFeature(Feature.ChannelBackupClient)) val (_, cmdAdd) = makeCmdAdd(50_000_000.msat, alice.staticParams.nodeParams.nodeId, alice.currentBlockHeight.toLong()) val (bob1, actions) = bob.processEx(ChannelEvent.ExecuteCommand(cmdAdd)) val add = actions.findOutgoingMessage() @@ -902,7 +909,7 @@ class NormalTestsCommon : LightningTestSuite() { @Test fun `recv RevokeAndAck (one htlc sent)`() { - val (alice0, bob0) = reachNormal() + val (alice0, bob0) = reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val (alice1, bob1) = addHtlc(50_000_000.msat, alice0, bob0).first val (alice2, actionsAlice2) = alice1.processEx(ChannelEvent.ExecuteCommand(CMD_SIGN)) @@ -911,6 +918,7 @@ class NormalTestsCommon : LightningTestSuite() { val commitSig = actionsAlice2.findOutgoingMessage() val (_, actionsBob2) = bob1.processEx(ChannelEvent.MessageReceived(commitSig)) val revokeAndAck = actionsBob2.findOutgoingMessage() + assertTrue(revokeAndAck.channelData.isEmpty()) val (alice3, _) = alice2.processEx(ChannelEvent.MessageReceived(revokeAndAck)) assertTrue(alice3 is Normal) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt index 7d8c417cd..07749b076 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/OfflineTestsCommon.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_SPENT import fr.acinq.lightning.blockchain.WatchConfirmed @@ -22,14 +23,14 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `handle disconnect - connect events (no messages sent yet)`() { - val (alice, bob) = TestsHelper.reachNormal() + val (alice, bob) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val (alice1, _) = alice.processEx(ChannelEvent.Disconnected) val (bob1, _) = bob.processEx(ChannelEvent.Disconnected) assertTrue(alice1 is Offline) assertTrue(bob1 is Offline) - val localInit = Init(ByteVector(TestConstants.Alice.channelParams.features.toByteArray())) - val remoteInit = Init(ByteVector(TestConstants.Bob.channelParams.features.toByteArray())) + val localInit = Init(ByteVector(alice.commitments.localParams.features.toByteArray())) + val remoteInit = Init(ByteVector(bob.commitments.localParams.features.toByteArray())) val (alice2, actions) = alice1.processEx(ChannelEvent.Connected(localInit, remoteInit)) assertTrue(alice2 is Syncing) @@ -41,11 +42,11 @@ class OfflineTestsCommon : LightningTestSuite() { val bobCommitments = bob.commitments val aliceCommitments = alice.commitments val bobCurrentPerCommitmentPoint = bob.keyManager.commitmentPoint( - bob.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelVersion), + bob.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelConfig), bobCommitments.localCommit.index ) val aliceCurrentPerCommitmentPoint = alice.keyManager.commitmentPoint( - alice.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelVersion), + alice.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelConfig), aliceCommitments.localCommit.index ) @@ -75,7 +76,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `re-send update and sig after first commitment`() { val (alice0, bob0) = run { - val (alice0, bob0) = TestsHelper.reachNormal() + val (alice0, bob0) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val cmdAdd = CMD_ADD_HTLC(1_000_000.msat, ByteVector32.Zeroes, CltvExpiryDelta(144).toCltvExpiry(alice0.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket, UUID.randomUUID()) val (alice1, actions1) = alice0.processEx(ChannelEvent.ExecuteCommand(cmdAdd)) val add = actions1.hasOutgoingMessage() @@ -93,8 +94,8 @@ class OfflineTestsCommon : LightningTestSuite() { assertTrue(alice1 is Offline) assertTrue(bob1 is Offline) - val localInit = Init(ByteVector(TestConstants.Alice.channelParams.features.toByteArray())) - val remoteInit = Init(ByteVector(TestConstants.Bob.channelParams.features.toByteArray())) + val localInit = Init(ByteVector(alice0.commitments.localParams.features.toByteArray())) + val remoteInit = Init(ByteVector(bob0.commitments.localParams.features.toByteArray())) val (alice2, actionsAlice2) = alice1.processEx(ChannelEvent.Connected(localInit, remoteInit)) assertTrue(alice2 is Syncing) @@ -106,11 +107,11 @@ class OfflineTestsCommon : LightningTestSuite() { val bobCommitments = bob0.commitments val aliceCommitments = alice0.commitments val bobCurrentPerCommitmentPoint = bob0.keyManager.commitmentPoint( - bob0.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelVersion), + bob0.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelConfig), bobCommitments.localCommit.index ) val aliceCurrentPerCommitmentPoint = alice0.keyManager.commitmentPoint( - alice0.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelVersion), + alice0.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelConfig), aliceCommitments.localCommit.index ) @@ -153,7 +154,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `re-send lost revocation`() { val (alice0, bob0) = run { - val (alice0, bob0) = TestsHelper.reachNormal() + val (alice0, bob0) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val cmdAdd = CMD_ADD_HTLC(1_000_000.msat, ByteVector32.Zeroes, CltvExpiryDelta(144).toCltvExpiry(alice0.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket, UUID.randomUUID()) val (alice1, actionsAlice1) = alice0.processEx(ChannelEvent.ExecuteCommand(cmdAdd)) val add = actionsAlice1.hasOutgoingMessage() @@ -176,8 +177,8 @@ class OfflineTestsCommon : LightningTestSuite() { assertTrue(alice1 is Offline) assertTrue(bob1 is Offline) - val localInit = Init(ByteVector(TestConstants.Alice.channelParams.features.toByteArray())) - val remoteInit = Init(ByteVector(TestConstants.Bob.channelParams.features.toByteArray())) + val localInit = Init(ByteVector(alice0.commitments.localParams.features.toByteArray())) + val remoteInit = Init(ByteVector(bob0.commitments.localParams.features.toByteArray())) val (alice2, actionsAlice2) = alice1.processEx(ChannelEvent.Connected(localInit, remoteInit)) assertTrue(alice2 is Syncing) @@ -189,11 +190,11 @@ class OfflineTestsCommon : LightningTestSuite() { val bobCommitments = bob0.commitments val aliceCommitments = alice0.commitments val bobCurrentPerCommitmentPoint = bob0.keyManager.commitmentPoint( - bob0.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelVersion), + bob0.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelConfig), bobCommitments.localCommit.index ) val aliceCurrentPerCommitmentPoint = alice0.keyManager.commitmentPoint( - alice0.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelVersion), + alice0.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelConfig), aliceCommitments.localCommit.index ) @@ -227,7 +228,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `resume htlc settlement`() { val (alice0, bob0, revB) = run { - val (alice0, bob0) = TestsHelper.reachNormal() + val (alice0, bob0) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val (nodes1, r1, htlc1) = TestsHelper.addHtlc(15_000_000.msat, bob0, alice0) val (bob1, alice1) = TestsHelper.crossSign(nodes1.first, nodes1.second) val (bob2, alice2) = TestsHelper.fulfillHtlc(htlc1.id, r1, bob1, alice1) @@ -290,7 +291,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `discover that we have a revoked commitment`() { val (alice, aliceOld, bob) = run { - val (alice0, bob0) = TestsHelper.reachNormal() + val (alice0, bob0) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val (nodes1, r1, htlc1) = TestsHelper.addHtlc(250_000_000.msat, alice0, bob0) val (alice1, bob1) = TestsHelper.crossSign(nodes1.first, nodes1.second) val (nodes2, r2, htlc2) = TestsHelper.addHtlc(100_000_000.msat, alice1, bob1) @@ -305,6 +306,8 @@ class OfflineTestsCommon : LightningTestSuite() { val (bob7, alice7) = TestsHelper.crossSign(bob6, alice6) val (alice8, bob8) = TestsHelper.fulfillHtlc(htlc3.id, r3, alice7, bob7) val (bob9, alice9) = TestsHelper.crossSign(bob8, alice8) + assertTrue(alice9 is Normal) + assertTrue(bob9 is Normal) Triple(alice9, alice3, bob9) } @@ -315,8 +318,8 @@ class OfflineTestsCommon : LightningTestSuite() { // we manually replace alice's state with an older one val alice1 = aliceTmp1.copy(state = aliceOld) - val localInit = Init(ByteVector(TestConstants.Alice.channelParams.features.toByteArray())) - val remoteInit = Init(ByteVector(TestConstants.Bob.channelParams.features.toByteArray())) + val localInit = Init(ByteVector(alice.commitments.localParams.features.toByteArray())) + val remoteInit = Init(ByteVector(bob.commitments.localParams.features.toByteArray())) val (alice2, actionsAlice2) = alice1.processEx(ChannelEvent.Connected(localInit, remoteInit)) assertTrue(alice2 is Syncing) @@ -352,14 +355,14 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `counterparty lies about having a more recent commitment`() { - val (alice0, bob0) = TestsHelper.reachNormal() + val (alice0, bob0) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val (alice1, _) = alice0.processEx(ChannelEvent.Disconnected) val (bob1, _) = bob0.processEx(ChannelEvent.Disconnected) assertTrue(alice1 is Offline) assertTrue(bob1 is Offline) - val localInit = Init(ByteVector(TestConstants.Alice.channelParams.features.toByteArray())) - val remoteInit = Init(ByteVector(TestConstants.Bob.channelParams.features.toByteArray())) + val localInit = Init(ByteVector(alice0.commitments.localParams.features.toByteArray())) + val remoteInit = Init(ByteVector(bob0.commitments.localParams.features.toByteArray())) val (alice2, actionsAlice2) = alice1.processEx(ChannelEvent.Connected(localInit, remoteInit)) assertTrue(alice2 is Syncing) @@ -382,7 +385,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `reprocess pending incoming htlcs after disconnection or wallet restart`() { val (alice, bob, htlcs) = run { - val (alice0, bob0) = TestsHelper.reachNormal() + val (alice0, bob0) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val (aliceId, bobId) = Pair(alice0.staticParams.nodeParams.nodeId, bob0.staticParams.nodeParams.nodeId) val currentBlockHeight = alice0.currentBlockHeight.toLong() // We add some htlcs Alice ---> Bob @@ -415,8 +418,8 @@ class OfflineTestsCommon : LightningTestSuite() { assertEquals(alice.commitments.commitInput.outPoint.txid, getFundingTx.txid) assertTrue(alice1 is Offline) - val localInit = Init(ByteVector(TestConstants.Alice.channelParams.features.toByteArray())) - val remoteInit = Init(ByteVector(TestConstants.Bob.channelParams.features.toByteArray())) + val localInit = Init(ByteVector(alice.commitments.localParams.features.toByteArray())) + val remoteInit = Init(ByteVector(bob.commitments.localParams.features.toByteArray())) val (alice2, actionsAlice2) = alice1.processEx(ChannelEvent.Connected(localInit, remoteInit)) assertTrue(alice2 is Syncing) @@ -444,7 +447,7 @@ class OfflineTestsCommon : LightningTestSuite() { @Test fun `reprocess pending incoming htlcs after disconnection or wallet restart (htlc settlement signed by us)`() { val (alice, bob, htlcs) = run { - val (alice0, bob0) = TestsHelper.reachNormal() + val (alice0, bob0) = TestsHelper.reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) val (aliceId, bobId) = Pair(alice0.staticParams.nodeParams.nodeId, bob0.staticParams.nodeParams.nodeId) val currentBlockHeight = alice0.currentBlockHeight.toLong() val preimage = randomBytes32() @@ -469,8 +472,8 @@ class OfflineTestsCommon : LightningTestSuite() { assertTrue(alice1 is Offline) assertTrue(bob1 is Offline) - val aliceInit = Init(ByteVector(TestConstants.Alice.channelParams.features.toByteArray())) - val bobInit = Init(ByteVector(TestConstants.Bob.channelParams.features.toByteArray())) + val aliceInit = Init(ByteVector(alice.commitments.localParams.features.toByteArray())) + val bobInit = Init(ByteVector(bob.commitments.localParams.features.toByteArray())) val (alice2, actionsAlice) = alice1.processEx(ChannelEvent.Connected(aliceInit, bobInit)) val (bob2, _) = bob1.processEx(ChannelEvent.Connected(bobInit, aliceInit)) @@ -490,6 +493,27 @@ class OfflineTestsCommon : LightningTestSuite() { actionsBob.hasWatch() } + @Test + fun `wait for their channel reestablish when using channel backup`() { + val (alice, bob) = TestsHelper.reachNormal() + assertTrue(bob.commitments.localParams.features.hasFeature(Feature.ChannelBackupClient)) + val (alice1, _) = alice.processEx(ChannelEvent.Disconnected) + val (bob1, _) = bob.processEx(ChannelEvent.Disconnected) + assertTrue(alice1 is Offline) + assertTrue(bob1 is Offline) + + val localInit = Init(ByteVector(alice.commitments.localParams.features.toByteArray())) + val remoteInit = Init(ByteVector(bob.commitments.localParams.features.toByteArray())) + + val (alice2, actions) = alice1.processEx(ChannelEvent.Connected(localInit, remoteInit)) + assertTrue(alice2 is Syncing) + actions.findOutgoingMessage() + val (bob2, actions1) = bob1.processEx(ChannelEvent.Connected(remoteInit, localInit)) + assertTrue(bob2 is Syncing) + // Bob waits to receive Alice's channel reestablish before sending his own. + assertTrue(actions1.isEmpty()) + } + @Test fun `recv NewBlock (no htlc timed out)`() { val (alice0, bob0) = TestsHelper.reachNormal() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt index 38992ff60..db3850ebe 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/ShutdownTestsCommon.kt @@ -2,6 +2,9 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* import fr.acinq.lightning.CltvExpiry +import fr.acinq.lightning.Feature +import fr.acinq.lightning.FeatureSupport +import fr.acinq.lightning.Features import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_SPENT @@ -34,17 +37,19 @@ class ShutdownTestsCommon : LightningTestSuite() { val (_, bob) = init() val add = CMD_ADD_HTLC(500000000.msat, r1, cltvExpiry = CltvExpiry(300000), TestConstants.emptyOnionPacket, UUID.randomUUID()) val (bob1, actions1) = bob.processEx(ChannelEvent.ExecuteCommand(add)) - assertTrue { bob1 is ShuttingDown } - assertTrue { actions1.any { it is ChannelAction.ProcessCmdRes.AddFailed && it.error == ChannelUnavailable(bob.channelId) } } + assertTrue(bob1 is ShuttingDown) + assertTrue(actions1.any { it is ChannelAction.ProcessCmdRes.AddFailed && it.error == ChannelUnavailable(bob.channelId) }) + assertEquals(bob1.commitments.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo))) } @Test fun `recv CMD_ADD_HTLC (zero-reserve)`() { - val (_, bob) = init(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE) + val (_, bob) = init(bobFeatures = TestConstants.Bob.nodeParams.features.add(Feature.ZeroReserveChannels to FeatureSupport.Optional)) val add = CMD_ADD_HTLC(500000000.msat, r1, cltvExpiry = CltvExpiry(300000), TestConstants.emptyOnionPacket, UUID.randomUUID()) val (bob1, actions1) = bob.processEx(ChannelEvent.ExecuteCommand(add)) - assertTrue { bob1 is ShuttingDown } - assertTrue { actions1.any { it is ChannelAction.ProcessCmdRes.AddFailed && it.error == ChannelUnavailable(bob.channelId) } } + assertTrue(bob1 is ShuttingDown) + assertTrue(actions1.any { it is ChannelAction.ProcessCmdRes.AddFailed && it.error == ChannelUnavailable(bob.channelId) }) + assertEquals(bob1.commitments.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo, Feature.ZeroReserveChannels))) } @Test @@ -353,10 +358,12 @@ class ShutdownTestsCommon : LightningTestSuite() { @Test fun `recv Shutdown with encrypted channel data`() { - val (alice0, _) = reachNormal(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE) - val (alice1, actions1) = alice0.processEx(ChannelEvent.ExecuteCommand(CMD_CLOSE(null))) - assertTrue(alice1 is Normal) - val blob = Serialization.encrypt(alice1.staticParams.nodeParams.nodePrivateKey.value, alice1) + val (_, bob0) = reachNormal() + assertTrue(bob0.commitments.localParams.features.hasFeature(Feature.ChannelBackupClient)) + assertFalse(bob0.commitments.channelFeatures.hasFeature(Feature.ChannelBackupClient)) // this isn't a permanent channel feature + val (bob1, actions1) = bob0.processEx(ChannelEvent.ExecuteCommand(CMD_CLOSE(null))) + assertTrue(bob1 is Normal) + val blob = Serialization.encrypt(bob1.staticParams.nodeParams.nodePrivateKey.value, bob1) val shutdown = actions1.findOutgoingMessage() assertEquals(blob, shutdown.channelData) } @@ -409,7 +416,7 @@ class ShutdownTestsCommon : LightningTestSuite() { @Test fun `recv BITCOIN_FUNDING_SPENT (their next commit)`() { val (alice, bob) = run { - val (alice0, bob0) = reachNormal(ChannelVersion.STANDARD) + val (alice0, bob0) = reachNormal() val (nodes1, _, _) = addHtlc(25_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 val (alice2, bob2) = crossSign(alice1, bob1) @@ -439,7 +446,7 @@ class ShutdownTestsCommon : LightningTestSuite() { @Test fun `recv BITCOIN_FUNDING_SPENT (revoked tx)`() { val (alice, _, revokedTx) = run { - val (alice0, bob0) = reachNormal(ChannelVersion.STANDARD) + val (alice0, bob0) = reachNormal() val (nodes1, _, _) = addHtlc(25_000_000.msat, alice0, bob0) val (alice1, bob1) = nodes1 val (alice2, bob2) = crossSign(alice1, bob1) @@ -532,8 +539,13 @@ class ShutdownTestsCommon : LightningTestSuite() { val r1 = randomBytes32() val r2 = randomBytes32() - fun init(channelVersion: ChannelVersion = ChannelVersion.STANDARD, currentBlockHeight: Int = TestConstants.defaultBlockHeight): Pair { - val (alice, bob) = reachNormal(channelVersion) + fun init( + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, + currentBlockHeight: Int = TestConstants.defaultBlockHeight, + aliceFeatures: Features = TestConstants.Alice.nodeParams.features, + bobFeatures: Features = TestConstants.Bob.nodeParams.features, + ): Pair { + val (alice, bob) = reachNormal(channelType, aliceFeatures, bobFeatures, currentBlockHeight) val (_, cmdAdd1) = makeCmdAdd(300_000_000.msat, bob.staticParams.nodeParams.nodeId, currentBlockHeight.toLong(), r1) val (alice1, actions) = alice.processEx(ChannelEvent.ExecuteCommand(cmdAdd1)) val htlc1 = actions.findOutgoingMessage() @@ -561,9 +573,10 @@ class ShutdownTestsCommon : LightningTestSuite() { val (alice2, _) = alice1.processEx(ChannelEvent.MessageReceived(shutdown1)) assertTrue(alice2 is ShuttingDown) assertTrue(bob1 is ShuttingDown) - if (alice2.commitments.isZeroReserve) assertFalse(shutdown.channelData.isEmpty()) - if (bob1.commitments.isZeroReserve) assertFalse(shutdown1.channelData.isEmpty()) + if (alice2.commitments.channelFeatures.hasFeature(Feature.ChannelBackupClient)) assertFalse(shutdown.channelData.isEmpty()) + if (bob1.commitments.channelFeatures.hasFeature(Feature.ChannelBackupClient)) assertFalse(shutdown1.channelData.isEmpty()) return Pair(alice2, bob1) } } + } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt index 3f3a77809..d5cc99bdc 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.Transaction +import fr.acinq.lightning.Feature import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_SPENT import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchEventSpent @@ -20,10 +21,11 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class SyncingTestsCommon : LightningTestSuite() { + @Test fun `detect that a remote commit tx was published`() { val (alice, bob, _) = run { - val (alice, bob) = reachNormal() + val (alice, bob) = init() disconnect(alice, bob) } val aliceCommitTx = alice.state.commitments.localCommit.publishableTxs.commitTx.tx @@ -39,7 +41,7 @@ class SyncingTestsCommon : LightningTestSuite() { @Test fun `detect that a revoked commit tx was published`() { val (_, bob, revokedTx) = run { - val (alice, bob) = reachNormal() + val (alice, bob) = init() val (nodes, _, _) = TestsHelper.addHtlc(10_000_000.msat, payer = alice, payee = bob) val (alice1, bob1) = nodes val (alice2, bob2) = TestsHelper.crossSign(alice1, bob1) @@ -61,7 +63,7 @@ class SyncingTestsCommon : LightningTestSuite() { @Test fun `recv CMD_FORCECLOSE`() { val (alice, _, _) = run { - val (alice, bob) = reachNormal() + val (alice, bob) = init() disconnect(alice, bob) } val (alice1, actions1) = alice.process(ChannelEvent.ExecuteCommand(CMD_FORCECLOSE)) @@ -70,22 +72,28 @@ class SyncingTestsCommon : LightningTestSuite() { } companion object { + fun init(): Pair { + // NB: we disable channel backups to ensure Bob sends his channel_reestablish on reconnection. + return reachNormal(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) + } + fun disconnect(alice: Normal, bob: Normal): Triple> { val (alice1, _) = alice.processEx(ChannelEvent.Disconnected) val (bob1, _) = bob.processEx(ChannelEvent.Disconnected) assertTrue(alice1 is Offline) assertTrue(bob1 is Offline) - val localInit = Init(ByteVector(TestConstants.Alice.channelParams.features.toByteArray())) - val remoteInit = Init(ByteVector(TestConstants.Bob.channelParams.features.toByteArray())) + val aliceInit = Init(ByteVector(alice1.state.commitments.localParams.features.toByteArray())) + val bobInit = Init(ByteVector(bob1.state.commitments.localParams.features.toByteArray())) - val (alice2, actions) = alice1.processEx(ChannelEvent.Connected(localInit, remoteInit)) + val (alice2, actions) = alice1.processEx(ChannelEvent.Connected(aliceInit, bobInit)) assertTrue(alice2 is Syncing) val channelReestablishA = actions.findOutgoingMessage() - val (bob2, actions1) = bob1.processEx(ChannelEvent.Connected(remoteInit, localInit)) + val (bob2, actions1) = bob1.processEx(ChannelEvent.Connected(bobInit, aliceInit)) assertTrue(bob2 is Syncing) val channelReestablishB = actions1.findOutgoingMessage() return Triple(alice2, bob2, Pair(channelReestablishA, channelReestablishB)) } } + } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt index 3dac3db2d..9a9fad18d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannelTestsCommon.kt @@ -4,12 +4,17 @@ import fr.acinq.bitcoin.Block import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Feature +import fr.acinq.lightning.FeatureSupport +import fr.acinq.lightning.Features import fr.acinq.lightning.channel.* import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.AcceptChannel +import fr.acinq.lightning.wire.ChannelTlv import fr.acinq.lightning.wire.Error +import fr.acinq.lightning.wire.TlvStream import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -25,6 +30,53 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { val funding = actions1.find() assertEquals(TestConstants.fundingAmount, funding.amount) assertEquals(TestConstants.feeratePerKw, funding.feerate) + assertEquals(alice1.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo))) + } + + @Test + fun `recv AcceptChannel (zero conf)`() { + val (alice, _, accept) = init( + channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, + aliceFeatures = TestConstants.Alice.nodeParams.features.add(Feature.ZeroConfChannels to FeatureSupport.Optional), + bobFeatures = TestConstants.Bob.nodeParams.features.add(Feature.ZeroConfChannels to FeatureSupport.Optional), + ) + assertEquals(0, accept.minimumDepth) + val (alice1, actions1) = alice.process(ChannelEvent.MessageReceived(accept)) + assertTrue(alice1 is WaitForFundingInternal) + assertEquals(1, actions1.size) + actions1.find() + assertEquals(alice1.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo, Feature.ZeroConfChannels, Feature.ZeroReserveChannels))) + } + + @Test + fun `recv AcceptChannel (zero reserve)`() { + val (alice, _, accept) = init( + bobFeatures = TestConstants.Bob.nodeParams.features.add(Feature.ZeroReserveChannels to FeatureSupport.Optional), + ) + assertTrue(accept.minimumDepth > 0) + val (alice1, actions1) = alice.process(ChannelEvent.MessageReceived(accept)) + assertTrue(alice1 is WaitForFundingInternal) + assertEquals(1, actions1.size) + actions1.find() + assertEquals(alice1.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo, Feature.ZeroReserveChannels))) + } + + @Test + fun `recv AcceptChannel (missing channel type)`() { + val (alice, _, accept) = init() + val (alice1, actions1) = alice.process(ChannelEvent.MessageReceived(accept.copy(tlvStream = TlvStream(listOf())))) + assertTrue(alice1 is Aborted) + val error = actions1.hasOutgoingMessage() + assertEquals(error, Error(accept.temporaryChannelId, MissingChannelType(accept.temporaryChannelId).message)) + } + + @Test + fun `recv AcceptChannel (invalid channel type)`() { + val (alice, _, accept) = init() + val (alice1, actions1) = alice.process(ChannelEvent.MessageReceived(accept.copy(tlvStream = TlvStream(listOf(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.Standard)))))) + assertTrue(alice1 is Aborted) + val error = actions1.hasOutgoingMessage() + assertEquals(error, Error(accept.temporaryChannelId, InvalidChannelType(accept.temporaryChannelId, ChannelType.SupportedChannelType.AnchorOutputs, ChannelType.SupportedChannelType.Standard).message)) } @Test @@ -134,15 +186,23 @@ class WaitForAcceptChannelTestsCommon : LightningTestSuite() { companion object { fun init( - channelVersion: ChannelVersion = ChannelVersion.STANDARD, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, currentHeight: Int = TestConstants.defaultBlockHeight, - fundingAmount: Satoshi = TestConstants.fundingAmount + fundingAmount: Satoshi = TestConstants.fundingAmount, + aliceFeatures: Features = TestConstants.Alice.nodeParams.features, + bobFeatures: Features = TestConstants.Bob.nodeParams.features, ): Triple { - val (alice, bob, open) = TestsHelper.init(channelVersion, currentHeight, fundingAmount) + val (alice, bob, open) = TestsHelper.init(channelType, aliceFeatures, bobFeatures, currentHeight, fundingAmount) + assertEquals(open.tlvStream.get(), ChannelTlv.ChannelTypeTlv(channelType)) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open)) assertTrue(bob1 is WaitForFundingCreated) val accept = actions.hasOutgoingMessage() - assertEquals(3, accept.minimumDepth) + assertEquals(accept.tlvStream.get(), ChannelTlv.ChannelTypeTlv(channelType)) + if (bob1.channelFeatures.hasFeature(Feature.ZeroConfChannels)) { + assertEquals(0, accept.minimumDepth) + } else { + assertEquals(3, accept.minimumDepth) + } return Triple(alice, bob1, accept) } } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt index 5f1d8d954..09e26f1a7 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt @@ -1,8 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* -import fr.acinq.lightning.Lightning -import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.* import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.TestsHelper.processEx @@ -16,9 +15,10 @@ import fr.acinq.lightning.wire.* import kotlin.test.* class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { + @Test fun `receive FundingLocked`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val fundingTx = alice.fundingTx!! val (bob1, actionsBob) = bob.processEx(ChannelEvent.WatchReceived(WatchEventConfirmed(bob.channelId, BITCOIN_FUNDING_DEPTHOK, 42, 0, fundingTx))) val fundingLocked = actionsBob.findOutgoingMessage() @@ -30,7 +30,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `receive BITCOIN_FUNDING_DEPTHOK`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val fundingTx = alice.fundingTx!! val (bob1, actions) = bob.processEx(ChannelEvent.WatchReceived(WatchEventConfirmed(bob.channelId, BITCOIN_FUNDING_DEPTHOK, 42, 0, fundingTx))) actions.findOutgoingMessage() @@ -41,7 +41,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `receive BITCOIN_FUNDING_DEPTHOK (bad funding pubkey script)`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val fundingTx = alice.fundingTx!! val badOutputScript = Scripts.multiSig2of2(Lightning.randomKey().publicKey(), Lightning.randomKey().publicKey()) val badFundingTx = fundingTx.copy(txOut = fundingTx.txOut.updated(0, fundingTx.txOut[0].updatePublicKeyScript(badOutputScript))) @@ -70,7 +70,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `receive BITCOIN_FUNDING_DEPTHOK (bad funding amount)`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val fundingTx = alice.fundingTx assertNotNull(fundingTx) val badAmount = 1_234_567.sat @@ -100,7 +100,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv BITCOIN_FUNDING_SPENT (remote commit)`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) // case 1: alice publishes her commitment tx run { @@ -123,7 +123,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv BITCOIN_FUNDING_SPENT (other commit)`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val spendingTx = Transaction(version = 2, txIn = alice.commitments.localCommit.publishableTxs.commitTx.tx.txIn, txOut = listOf(), lockTime = 0) listOf(alice, bob).forEach { state -> val (state1, actions1) = state.processEx(ChannelEvent.WatchReceived(WatchEventSpent(state.channelId, BITCOIN_FUNDING_SPENT, spendingTx))) @@ -134,7 +134,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv Error`() { - val (_, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (_, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (bob1, actions1) = bob.processEx(ChannelEvent.MessageReceived(Error(bob.channelId, "oops"))) assertTrue(bob1 is Closing) assertNotNull(bob1.localCommitPublished) @@ -144,7 +144,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv CMD_CLOSE`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) listOf(alice, bob).forEach { state -> val (state1, actions1) = state.processEx(ChannelEvent.ExecuteCommand(CMD_CLOSE(null))) assertEquals(state, state1) @@ -154,7 +154,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv CMD_FORCECLOSE`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) listOf(alice, bob).forEach { state -> val (state1, actions1) = state.processEx(ChannelEvent.ExecuteCommand(CMD_FORCECLOSE)) assertTrue(state1 is Closing) @@ -167,7 +167,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv CMD_FORCECLOSE (nothing at stake)`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, 0.msat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, 0.msat) val (bob1, actions1) = bob.processEx(ChannelEvent.ExecuteCommand(CMD_FORCECLOSE)) assertTrue(bob1 is Aborted) assertEquals(1, actions1.size) @@ -177,7 +177,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv NewBlock`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) listOf(alice, bob).forEach { state -> run { val (state1, actions1) = state.processEx(ChannelEvent.NewBlock(state.currentBlockHeight + 1, Block.RegtestGenesisBlock.header)) @@ -194,7 +194,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv Disconnected`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (alice1, _) = alice.processEx(ChannelEvent.Disconnected) assertTrue { alice1 is Offline } val (bob1, _) = bob.processEx(ChannelEvent.Disconnected) @@ -203,7 +203,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv Disconnected (get funding tx successful)`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (alice1, _) = alice.processEx(ChannelEvent.Disconnected) assertTrue { alice1 is Offline } val (bob1, _) = bob.processEx(ChannelEvent.Disconnected) @@ -218,7 +218,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `recv Disconnected (get funding tx error)`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (alice1, _) = alice.processEx(ChannelEvent.Disconnected) assertTrue { alice1 is Offline } val (bob1, _) = bob.processEx(ChannelEvent.Disconnected) @@ -233,21 +233,21 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { } @Test - fun `on reconnection a zero-reserve channel needs to register a zero-confirmation watch on funding tx`() { - val (alice, bob) = init(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE, TestConstants.fundingAmount, TestConstants.pushMsat) + fun `on reconnection a zero-conf channel needs to register a zero-confirmation watch on funding tx`() { + val bobFeatures = TestConstants.Bob.nodeParams.features.add(Feature.ZeroConfChannels to FeatureSupport.Mandatory) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, TestConstants.fundingAmount, TestConstants.pushMsat, bobFeatures = bobFeatures) val (bob1, _) = bob.processEx(ChannelEvent.Disconnected) - assertTrue { bob1 is Offline } + assertTrue(bob1 is Offline) val localInit = Init(ByteVector(TestConstants.Alice.channelParams.features.toByteArray())) - val remoteInit = Init(ByteVector(TestConstants.Bob.channelParams.features.toByteArray())) - + val remoteInit = Init(ByteVector(bobFeatures.toByteArray())) val (bob2, actions2) = bob1.processEx(ChannelEvent.Connected(remoteInit, localInit)) assertTrue(bob2 is Syncing) assertTrue(actions2.isEmpty()) val channelReestablishAlice = run { val yourLastPerCommitmentSecret = alice.commitments.remotePerCommitmentSecrets.lastIndex?.let { alice.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes - val channelKeyPath = alice.keyManager.channelKeyPath(alice.commitments.localParams, alice.commitments.channelVersion) + val channelKeyPath = alice.keyManager.channelKeyPath(alice.commitments.localParams, alice.commitments.channelConfig) val myCurrentPerCommitmentPoint = alice.keyManager.commitmentPoint(channelKeyPath, alice.commitments.localCommit.index) ChannelReestablish( @@ -269,7 +269,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `get funding tx in Syncing state`() { - val (alice, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (bob1, _) = bob.processEx(ChannelEvent.Disconnected) assertTrue { bob1 is Offline && bob1.state is WaitForFundingConfirmed } val localInit = Init(ByteVector(bob.commitments.localParams.features.toByteArray())) @@ -293,7 +293,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { @Test fun `get funding tx in Offline state`() { - val (_, bob) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (_, bob) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (bob1, _) = bob.process(ChannelEvent.Disconnected) assertTrue { bob1 is Offline && bob1.state is WaitForFundingConfirmed } @@ -312,12 +312,19 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { } companion object { - fun init(channelVersion: ChannelVersion, fundingAmount: Satoshi, pushAmount: MilliSatoshi): Pair { - val (alice, bob, fundingCreated) = WaitForFundingCreatedTestsCommon.init(channelVersion, fundingAmount, pushAmount) + fun init( + channelType: ChannelType.SupportedChannelType, + fundingAmount: Satoshi, + pushAmount: MilliSatoshi, + aliceFeatures: Features = TestConstants.Alice.nodeParams.features, + bobFeatures: Features = TestConstants.Bob.nodeParams.features, + ): Pair { + val (alice, bob, fundingCreated) = WaitForFundingCreatedTestsCommon.init(channelType, fundingAmount, pushAmount, aliceFeatures, bobFeatures) val (bob1, actions1) = bob.processEx(ChannelEvent.MessageReceived(fundingCreated)) val fundingSigned = actions1.findOutgoingMessage() val (alice1, _) = alice.processEx(ChannelEvent.MessageReceived(fundingSigned)) return Pair(alice1 as WaitForFundingConfirmed, bob1 as WaitForFundingConfirmed) } } + } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt index 0868023a6..113c9ddb2 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreatedTestsCommon.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* +import fr.acinq.lightning.Features import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchSpent @@ -19,7 +20,7 @@ import kotlin.test.assertTrue class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv FundingCreated`() { - val (_, bob, fundingCreated) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (_, bob, fundingCreated) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (bob1, actions1) = bob.process(ChannelEvent.MessageReceived(fundingCreated)) assertTrue { bob1 is WaitForFundingConfirmed } actions1.findOutgoingMessage() @@ -31,7 +32,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv FundingCreated (with channel origin)`() { - val (_, bob, fundingCreated) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat, ChannelOrigin.PayToOpenOrigin(ByteVector32.One, 42.sat)) + val (_, bob, fundingCreated) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat, channelOrigin = ChannelOrigin.PayToOpenOrigin(ByteVector32.One, 42.sat)) val (bob1, actions1) = bob.process(ChannelEvent.MessageReceived(fundingCreated)) assertTrue { bob1 is WaitForFundingConfirmed } actions1.findOutgoingMessage() @@ -44,7 +45,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv FundingCreated (funder can't pay fees)`() { - val (_, bob, fundingCreated) = init(ChannelVersion.STANDARD, 1_000_100.sat, 1_000_000.sat.toMilliSatoshi()) + val (_, bob, fundingCreated) = init(ChannelType.SupportedChannelType.AnchorOutputs, 1_000_100.sat, 1_000_000.sat.toMilliSatoshi()) val (bob1, actions1) = bob.process(ChannelEvent.MessageReceived(fundingCreated)) actions1.hasOutgoingMessage() assertTrue { bob1 is Aborted } @@ -52,7 +53,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv Error`() { - val (_, bob, _) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (_, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (bob1, actions1) = bob.process(ChannelEvent.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) assertTrue { bob1 is Aborted } assertTrue { actions1.isEmpty() } @@ -60,7 +61,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv CMD_CLOSE`() { - val (_, bob, _) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (_, bob, _) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (bob1, actions1) = bob.process(ChannelEvent.ExecuteCommand(CMD_CLOSE(null))) assertTrue { bob1 is Aborted } assertTrue { actions1.isEmpty() } @@ -68,7 +69,7 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { @Test fun `recv Disconnected`() { - val (_, bob, fundingCreated) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (_, bob, fundingCreated) = init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val (bob1, _) = bob.process(ChannelEvent.MessageReceived(fundingCreated)) assertTrue { bob1 is WaitForFundingConfirmed } val (bob2, actions2) = bob1.process(ChannelEvent.Disconnected) @@ -77,8 +78,15 @@ class WaitForFundingCreatedTestsCommon : LightningTestSuite() { } companion object { - fun init(channelVersion: ChannelVersion, fundingAmount: Satoshi, pushAmount: MilliSatoshi, channelOrigin: ChannelOrigin? = null): Triple { - val (a, b, open) = TestsHelper.init(channelVersion, 0, fundingAmount, pushAmount, channelOrigin) + fun init( + channelType: ChannelType.SupportedChannelType, + fundingAmount: Satoshi, + pushAmount: MilliSatoshi, + aliceFeatures: Features = TestConstants.Alice.nodeParams.features, + bobFeatures: Features = TestConstants.Bob.nodeParams.features, + channelOrigin: ChannelOrigin? = null + ): Triple { + val (a, b, open) = TestsHelper.init(channelType, aliceFeatures, bobFeatures, 0, fundingAmount, pushAmount, channelOrigin) val (b1, actions) = b.process(ChannelEvent.MessageReceived(open)) val accept = actions.findOutgoingMessage() val (a1, actions2) = a.process(ChannelEvent.MessageReceived(accept)) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingLockedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingLockedTestsCommon.kt index 1ee3b1667..4796993a9 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingLockedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingLockedTestsCommon.kt @@ -114,12 +114,12 @@ class WaitForFundingLockedTestsCommon : LightningTestSuite() { companion object { fun init( - channelVersion: ChannelVersion = ChannelVersion.STANDARD, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, currentHeight: Int = TestConstants.defaultBlockHeight, fundingAmount: Satoshi = TestConstants.fundingAmount, pushMsat: MilliSatoshi = TestConstants.pushMsat ): Triple> { - val (alice, bob, open) = TestsHelper.init(channelVersion, currentHeight, fundingAmount, pushMsat) + val (alice, bob, open) = TestsHelper.init(channelType, currentHeight = currentHeight, fundingAmount = fundingAmount, pushMsat = pushMsat) val (bob1, actionsBob1) = bob.processEx(ChannelEvent.MessageReceived(open)) val accept = actionsBob1.findOutgoingMessage() val (alice1, actionsAlice1) = alice.processEx(ChannelEvent.MessageReceived(accept)) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt index b2726ed6e..308d4867c 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingSignedTestsCommon.kt @@ -1,6 +1,8 @@ package fr.acinq.lightning.channel.states import fr.acinq.bitcoin.* +import fr.acinq.lightning.Feature +import fr.acinq.lightning.Features import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_SPENT import fr.acinq.lightning.blockchain.WatchConfirmed @@ -34,11 +36,21 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { @Test fun `recv FundingSigned with encrypted channel data`() { - val (_, bob, fundingSigned) = init(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE) + val (alice, bob, fundingSigned) = init() + assertTrue(alice.localParams.features.hasFeature(Feature.ChannelBackupProvider)) + assertTrue(bob.commitments.localParams.features.hasFeature(Feature.ChannelBackupClient)) val blob = Serialization.encrypt(bob.staticParams.nodeParams.nodePrivateKey.value, bob) assertEquals(blob, fundingSigned.channelData) } + @Test + fun `recv FundingSigned without encrypted channel data`() { + val (alice, bob, fundingSigned) = init(bobFeatures = TestConstants.Bob.nodeParams.features.remove(Feature.ChannelBackupClient)) + assertTrue(alice.localParams.features.hasFeature(Feature.ChannelBackupProvider)) + assertFalse(bob.commitments.localParams.features.hasFeature(Feature.ChannelBackupClient)) + assertTrue(fundingSigned.channelData.isEmpty()) + } + @Test fun `recv FundingSigned with invalid signature`() { val (alice, _, fundingSigned) = init() @@ -73,11 +85,13 @@ class WaitForFundingSignedTestsCommon : LightningTestSuite() { companion object { fun init( - channelVersion: ChannelVersion = ChannelVersion.STANDARD, + channelType: ChannelType.SupportedChannelType = ChannelType.SupportedChannelType.AnchorOutputs, currentHeight: Int = TestConstants.defaultBlockHeight, - fundingAmount: Satoshi = TestConstants.fundingAmount + fundingAmount: Satoshi = TestConstants.fundingAmount, + aliceFeatures: Features = TestConstants.Alice.nodeParams.features, + bobFeatures: Features = TestConstants.Bob.nodeParams.features, ): Triple { - val (alice, bob, open) = TestsHelper.init(channelVersion, currentHeight, fundingAmount) + val (alice, bob, open) = TestsHelper.init(channelType, aliceFeatures, bobFeatures, currentHeight = currentHeight, fundingAmount = fundingAmount) val (bob1, actionsBob1) = bob.process(ChannelEvent.MessageReceived(open)) val accept = actionsBob1.findOutgoingMessage() val (alice1, actionsAlice1) = alice.process(ChannelEvent.MessageReceived(accept)) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt index 9a868c232..7704b992b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannelTestsCommon.kt @@ -4,43 +4,123 @@ import fr.acinq.bitcoin.Block import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.ByteVector32 import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Feature +import fr.acinq.lightning.FeatureSupport +import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.channel.* import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.utils.toMilliSatoshi -import fr.acinq.lightning.wire.AcceptChannel -import fr.acinq.lightning.wire.ChannelTlv -import fr.acinq.lightning.wire.Error -import fr.acinq.lightning.wire.TlvStream +import fr.acinq.lightning.wire.* import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class WaitForOpenChannelTestsCommon : LightningTestSuite() { + + @Test + fun `reject other channel types than anchor outputs`() { + val alice = WaitForInit( + StaticParams(TestConstants.Alice.nodeParams, TestConstants.Bob.keyManager.nodeId), + Pair(TestConstants.defaultBlockHeight, Block.RegtestGenesisBlock.header), + OnChainFeerates(TestConstants.feeratePerKw, TestConstants.feeratePerKw, TestConstants.feeratePerKw) + ) + val bobInit = Init(TestConstants.Bob.nodeParams.features.toByteArray().toByteVector()) + val unsupportedChannelTypes = listOf(ChannelType.SupportedChannelType.Standard, ChannelType.SupportedChannelType.StaticRemoteKey) + unsupportedChannelTypes.forEach { channelType -> + val (alice1, actions1) = alice.process( + ChannelEvent.InitFunder( + ByteVector32.Zeroes, + TestConstants.fundingAmount, + 0.msat, + FeeratePerKw.CommitmentFeerate, + TestConstants.feeratePerKw, + TestConstants.Alice.channelParams, + bobInit, + 0, + ChannelConfig.standard, + channelType, + null + ) + ) + assertTrue(alice1 is Aborted) + assertTrue(actions1.isEmpty()) + } + } + @Test - fun `recv OpenChannel`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + fun `recv OpenChannel (without wumbo)`() { + val (_, bob, open) = TestsHelper.init(aliceFeatures = TestConstants.Alice.nodeParams.features.remove(Feature.Wumbo)) // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. - assertEquals(TlvStream(listOf(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))), open.tlvStream) + assertEquals(open.tlvStream.get(), ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)) + assertEquals(open.tlvStream.get(), ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs)) + assertTrue(open.channelReserveSatoshis > 0.sat) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open)) - assertTrue { bob1 is WaitForFundingCreated } + assertTrue(bob1 is WaitForFundingCreated) + assertTrue(bob1.channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) + assertEquals(bob1.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs))) actions.hasOutgoingMessage() } @Test fun `recv OpenChannel (wumbo)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, 20_000_000.sat) - assertEquals(TlvStream(listOf(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))), open.tlvStream) + val (_, bob, open) = TestsHelper.init(fundingAmount = 20_000_000.sat) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open)) - assertTrue { bob1 is WaitForFundingCreated } + assertTrue(bob1 is WaitForFundingCreated) + assertEquals(bob1.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo))) + actions.hasOutgoingMessage() + } + + @Test + fun `recv OpenChannel (zero-reserve)`() { + val (_, bob, open) = TestsHelper.init(bobFeatures = TestConstants.Bob.nodeParams.features.add(Feature.ZeroReserveChannels to FeatureSupport.Optional)) + assertEquals(0.sat, open.channelReserveSatoshis) + val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open)) + assertTrue(bob1 is WaitForFundingCreated) + assertTrue(bob1.channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) + assertEquals(bob1.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo, Feature.ZeroReserveChannels))) actions.hasOutgoingMessage() } + @Test + fun `recv OpenChannel (zero-conf)`() { + val (_, bob, open) = TestsHelper.init(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, bobFeatures = TestConstants.Bob.nodeParams.features.add(Feature.ZeroConfChannels to FeatureSupport.Optional)) + assertTrue(open.channelReserveSatoshis > 0.sat) + val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open)) + assertTrue(bob1 is WaitForFundingCreated) + assertTrue(bob1.channelConfig.hasOption(ChannelConfigOption.FundingPubKeyBasedChannelKeyPath)) + assertEquals(bob1.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo, Feature.ZeroConfChannels, Feature.ZeroReserveChannels))) + val accept = actions.hasOutgoingMessage() + assertEquals(0, accept.minimumDepth) + } + + @Test + fun `recv OpenChannel (missing channel type)`() { + val (_, bob, open) = TestsHelper.init() + val open1 = open.copy(tlvStream = TlvStream(listOf())) + val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open1)) + val error = actions.findOutgoingMessage() + assertEquals(error, Error(open.temporaryChannelId, MissingChannelType(open.temporaryChannelId).message)) + assertTrue(bob1 is Aborted) + } + + @Test + fun `recv OpenChannel (invalid channel type)`() { + val (_, bob, open) = TestsHelper.init() + val open1 = open.copy(tlvStream = TlvStream(listOf(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.StaticRemoteKey)))) + val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open1)) + val error = actions.findOutgoingMessage() + assertEquals(error, Error(open.temporaryChannelId, InvalidChannelType(open.temporaryChannelId, ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, ChannelType.SupportedChannelType.StaticRemoteKey).message)) + assertTrue(bob1 is Aborted) + } + @Test fun `recv OpenChannel (invalid chain)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, open) = TestsHelper.init() val open1 = open.copy(chainHash = Block.LivenetGenesisBlock.hash) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open1)) val error = actions.findOutgoingMessage() @@ -50,7 +130,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (funding too low)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, 100.sat) + val (_, bob, open) = TestsHelper.init(fundingAmount = 100.sat) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open)) val error = actions.findOutgoingMessage() assertEquals(error, Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, 100.sat, bob.staticParams.nodeParams.minFundingSatoshis, bob.staticParams.nodeParams.maxFundingSatoshis).message)) @@ -59,7 +139,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (funding too high)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, 30_000_000.sat) + val (_, bob, open) = TestsHelper.init(fundingAmount = 30_000_000.sat) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open)) val error = actions.findOutgoingMessage() assertEquals(error, Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, 30_000_000.sat, bob.staticParams.nodeParams.minFundingSatoshis, bob.staticParams.nodeParams.maxFundingSatoshis).message)) @@ -68,7 +148,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (invalid max accepted htlcs)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, open) = TestsHelper.init() val open1 = open.copy(maxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open1)) val error = actions.findOutgoingMessage() @@ -78,7 +158,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (invalid push_msat)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, open) = TestsHelper.init() val open1 = open.copy(pushMsat = open.fundingSatoshis.toMilliSatoshi() + 10.msat) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open1)) val error = actions.findOutgoingMessage() @@ -88,7 +168,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (to_self_delay too high)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, open) = TestsHelper.init() val invalidToSelfDelay = bob.staticParams.nodeParams.maxToLocalDelayBlocks + CltvExpiryDelta(10) val open1 = open.copy(toSelfDelay = invalidToSelfDelay) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open1)) @@ -99,7 +179,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (reserve too high)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, open) = TestsHelper.init() val reserveTooHigh = open.fundingSatoshis * 0.3 val open1 = open.copy(channelReserveSatoshis = reserveTooHigh) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open1)) @@ -110,7 +190,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (reserve below dust)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, open) = TestsHelper.init() val reserveTooSmall = open.dustLimitSatoshis - 1.sat val open1 = open.copy(channelReserveSatoshis = reserveTooSmall) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open1)) @@ -121,7 +201,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (reserve below dust, zero-reserve channel)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, open) = TestsHelper.init(bobFeatures = TestConstants.Bob.nodeParams.features.add(Feature.ZeroReserveChannels to FeatureSupport.Optional)) assertEquals(0.sat, open.channelReserveSatoshis) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open)) assertTrue { bob1 is WaitForFundingCreated } @@ -130,7 +210,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (dust limit too high)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, open) = TestsHelper.init() val dustLimitTooHigh = 2000.sat val open1 = open.copy(dustLimitSatoshis = dustLimitTooHigh) val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(open1)) @@ -141,7 +221,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv OpenChannel (toLocal + toRemote below reserve)`() { - val (_, bob, open) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, open) = TestsHelper.init() val fundingAmount = open.channelReserveSatoshis + 499.sat val pushMsat = 500.sat.toMilliSatoshi() val open1 = open.copy(fundingSatoshis = fundingAmount, pushMsat = pushMsat) @@ -153,7 +233,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv Error`() { - val (_, bob, _) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, _) = TestsHelper.init() val (bob1, actions) = bob.process(ChannelEvent.MessageReceived(Error(ByteVector32.Zeroes, "oops"))) assertTrue { bob1 is Aborted } assertTrue { actions.isEmpty() } @@ -161,7 +241,7 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv CMD_CLOSE`() { - val (_, bob, _) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, TestConstants.fundingAmount) + val (_, bob, _) = TestsHelper.init() val (bob1, actions) = bob.process(ChannelEvent.ExecuteCommand(CMD_CLOSE(null))) assertTrue { bob1 is Aborted } assertTrue { actions.isEmpty() } @@ -169,9 +249,10 @@ class WaitForOpenChannelTestsCommon : LightningTestSuite() { @Test fun `recv Disconnected`() { - val (_, bob, _) = TestsHelper.init(ChannelVersion.STANDARD, TestConstants.defaultBlockHeight, 100.sat) + val (_, bob, _) = TestsHelper.init() val (bob1, actions) = bob.process(ChannelEvent.Disconnected) assertTrue { bob1 is WaitForOpenChannel } assertTrue { actions.isEmpty() } } + } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt index 4e229263a..9ec52f473 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -2,8 +2,8 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.lightning.channel.ChannelConfig import fr.acinq.lightning.channel.ChannelKeys -import fr.acinq.lightning.channel.ChannelVersion import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import kotlin.test.Test @@ -94,7 +94,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(channelKeys = keyManager.channelKeys(fundingKeyPath)) - val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelConfig.standard) assertEquals(fundingPub.publicKey, PrivateKey.fromHex("730c0f99408dbfbff00146acf84183ce539fabeeb22c143212f459d71374f715").publicKey()) assertEquals(keyManager.revocationPoint(channelKeyPath).publicKey, PrivateKey.fromHex("ef2aa0a9b4d0bdbc5ee5025f0d16285dc9d17228af1b2cc1e1456252c2d9d207").publicKey()) @@ -112,7 +112,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(channelKeys = keyManager.channelKeys(fundingKeyPath)) - val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelConfig.standard) assertEquals(fundingPub.publicKey, PrivateKey.fromHex("cd85f39fad742e5c742eeab16f5f1acaa9d9c48977767c7daa4708a47b7222ec").publicKey()) assertEquals(keyManager.revocationPoint(channelKeyPath).publicKey, PrivateKey.fromHex("ee211f583f3b1b1fb10dca7c82708d985fde641e83e28080f669eb496de85113").publicKey()) @@ -130,7 +130,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(channelKeys = keyManager.channelKeys(fundingKeyPath)) - val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelConfig.standard) assertEquals(fundingPub.publicKey, PrivateKey.fromHex("b3b3f1af2ef961ee7aa62451a93a1fd57ea126c81008e5d95ced822cca30da6e").publicKey()) assertEquals(keyManager.revocationPoint(channelKeyPath).publicKey, PrivateKey.fromHex("119ae90789c0b9a68e5cfa2eee08b62cc668b2cd758403dfa7eabde1dc0b6d0a").publicKey()) @@ -148,7 +148,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(channelKeys = keyManager.channelKeys(fundingKeyPath)) - val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelConfig.standard) assertEquals(fundingPub.publicKey, PrivateKey.fromHex("033880995016c275e725da625e4a78ea8c3215ab8ea54145fa3124bbb2e4a3d4").publicKey()) assertEquals(keyManager.revocationPoint(channelKeyPath).publicKey, PrivateKey.fromHex("16d8dd5e6a22de173288cdb7905cfbbcd9efab99471eb735ff95cb7fbdf43e45").publicKey()) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt b/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt index d6490cdca..852172e05 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt @@ -4,9 +4,9 @@ import fr.acinq.bitcoin.Block import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.PrivateKey import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.InvoiceDefaultRoutingFees import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey -import fr.acinq.lightning.InvoiceDefaultRoutingFees import fr.acinq.lightning.NodeUri import fr.acinq.lightning.channel.* import fr.acinq.lightning.db.InMemoryDatabases @@ -52,7 +52,8 @@ class PeerTest : LightningTestSuite() { randomKey().publicKey(), randomKey().publicKey(), randomKey().publicKey(), - 0.toByte() + 0.toByte(), + TlvStream(listOf(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve))) ) @Test @@ -139,7 +140,7 @@ class PeerTest : LightningTestSuite() { val syncState = syncChannels.first() val yourLastPerCommitmentSecret = ByteVector32.Zeroes - val channelKeyPath = peer.nodeParams.keyManager.channelKeyPath(syncState.state.commitments.localParams, syncState.state.commitments.channelVersion) + val channelKeyPath = peer.nodeParams.keyManager.channelKeyPath(syncState.state.commitments.localParams, syncState.state.commitments.channelConfig) val myCurrentPerCommitmentPoint = peer.nodeParams.keyManager.commitmentPoint(channelKeyPath, syncState.state.commitments.localCommit.index) val channelReestablish = ChannelReestablish( diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/CompatibilityTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/CompatibilityTestsCommon.kt index 3b2febce2..5098df558 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/CompatibilityTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/CompatibilityTestsCommon.kt @@ -4,10 +4,8 @@ import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Lightning import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK import fr.acinq.lightning.blockchain.WatchEventConfirmed -import fr.acinq.lightning.blockchain.WatchLost import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.TestsHelper.processEx -import fr.acinq.lightning.channel.findOutgoingMessage import fr.acinq.lightning.channel.states.WaitForFundingConfirmedTestsCommon import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.utils.UUID @@ -15,8 +13,6 @@ import fr.acinq.lightning.utils.msat import fr.acinq.lightning.wire.FundingLocked import fr.acinq.lightning.wire.UpdateAddHtlc import fr.acinq.secp256k1.Hex -import org.kodein.memory.file.FileSystem -import kotlin.jvm.JvmStatic import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertTrue @@ -25,7 +21,7 @@ class CompatibilityTestsCommon { @Ignore fun `generate data`() { // generate test data - val (alice, bob) = WaitForFundingConfirmedTestsCommon.init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat) + val (alice, bob) = WaitForFundingConfirmedTestsCommon.init(ChannelType.SupportedChannelType.AnchorOutputs, TestConstants.fundingAmount, TestConstants.pushMsat) val fundingTx = alice.fundingTx!! val bin = Serialization.encrypt(TestConstants.Bob.nodeParams.nodePrivateKey, bob) println("wait_for_funding_confirmed: ${bin.data}") @@ -82,19 +78,24 @@ class CompatibilityTestsCommon { fun `read compatibility test data`() { // data was generated with lightning-kmp v1.0-beta11 // README: it this test fails it means that we cannot decrypt and/or deserialize data created with on older version of the app !! - val wait_for_funding_confirmed = Hex.decode("2d0cef30160b00f8c206f1e0618f597b774b67338a1a52f4e1d13616a4032719bfb41817e7272b215afcc9d68e64fa46c94fa75f5b1c374832891a7ce4d326ec6cbd7cc3a9c876a9df0254b1306d02ef4755cf6a292dd643ee90cffa111b4a20b46c060d985ebb7618bf5c1d1d05b6c2e6535d3bf0f25e2bbae396dc2bca66e390d18ef46965d6986835cfa602728763adf74cde41924c609b41fef839b3eba6b514b5c70bbc5c58159122381407cce35643143aff68bef046a13a208cc4ee738b2b943e85d85004d94b014609c1876a48694e9b4ad92ceaa40094901c183103af1a25a35d6c38128921d53dfac1ed121ca6acd3dc66bdb90603d2f6cfb790303afcea3dd4b5a3f80c1ddb8abcac2fad767c91bd1f072bbf6269a79d210e2a8f16cd030ffc297c3ad746c21969c48a602acbc5b05aaf3875f91e0186428128f81a73bc872d144d36840c5dc51378911c06638d3d4b85193ce7e72a656f2592c862910b9e34f79e6173996e87b7a62f8637d62407da219823f6d5c3b13f30c0995f7a6eac5febfd16d4c7b8c389beef57ee9e44b731d25a3a9fb3fbfdd02884136bed9ee8c975b276c0dd9fb1b37dc4ce8a3c4fa674a6724384c5616b0c8b86f52bd66b0b5e1cc6d8c8e4fe0f818a4e5d9bc04f1fee5f2b63af5eee8340adea0654058058fcd16b6ef4d2c8154e2df4fa142fe289f26ddb77dc8da98401f4a7d47f8398977292858bc0092e9dfc1d3c1d621938f4d5fd4e36c7637a0773a0f1b546357e330c61fee88dd273e956f45133a765da823f2686b15ceadc84048a4dbaf0a9e83e6ff06bff586e6734b3a4e4a8511369c07f88f1efd548c1fff2d1d0aecfe1504232548cfae442249fc5d066aa93e442b511de05eb2be87595922be69e655c1048889ff10b1fc5047e7537f5db225a95049db32f885016e32699766b1e111040849122d2c0ff6600a349292a27022b3d1964bb5d8050362f3bf042641872e8d0dfb39c94bcf9068c190450ca4126efca8bbd926a8be65c9d07e4b2d19a226596a50fec795fc0b715a9cc324a13e15c76bdea9a0e4af8689dc95410638aa4b655f7bdac268284ef74c824d9626953ab202fa185715317f7621d602aae5e186d6b4820663c418d7374a2e8d378a306861ce1d775a4a8c5c06784abc084d45b001289b06bb39053e7777b6d691af5affb5548aa703715d41435a43e824beae0051ebf0189d1228f7e6065821925bef16de743a91b74396c39318611bc28484a7e96cf9066113ea27df045b09dfa89c880a08775cd209c6b6f543c742bd07e61e52b4aa41899a705646a790d2929db5e8610bdf75511f8b1a6d48cf3b691a0cf6a1ef6c92670b2c6dd97ad94077dc5408100244382b89bc452a17f5103aed6500a2a54bbd88e0ff08d9666270f0f3e015e11f6f34c177f7af4adfd04f8978f18a524cff48c70f1854f82f85049c1f9ed24155284dac43dae45fdb49aff52e9eb11af884044da9a312bc158142cc6e7aaad028e8260da39eab30483a7763cef83d73f12a98979f536a46deeda25b89383e963711349cc14e4defb2847bb0c61df705f465f0608dcebdf8bc1306d4910bfe438ef10b718fd0329f4f85cbceb37d853ff743a848b010b1e50b44edc47334600e664acebd48bd83cf8394be1c168c0cc4da41da83f13c6b879392fe035631cc038b8eda26c0ba07802480774ac013368b741634fd29784c38acb9fd5f17f84d62ae76579b392b97f200dcd73efaa7790a1943fb99d77589ab5c6598bf43bb9bd034179b01f869b5d430e4aec014f619edadb8fb8c1396f18f71b1feb200ee93a8ee70ef450e9f5bf9eb5d42987a4d34b698c9bf6f1c0de914fd3b896f523941a3929ea6e4654a33cbeb05babecdf0e5dfe003338caad556af72c9efa990ab802f8a3cdf15f05a6dd0c884eb58ce3d06958e57555a30fc8954edff727568c2a63b51dce9e03633211f7feb5b80ee4a595d6b10d97acb2dc0c5cc8a6153e2710d774ed50ebcf85556847969698a74c1b4a030774bb7edd355cca7d5de68c0a5b3c5f3fae02a92c0dfb052f7c9ffa970d9fb3bf79f088eddadeefbcfdaca8099cfaf3ed0a111efb962a5b893279ed5f624b36f887c6b244352dc946d112bed31a538022d27e1334d51bf4a4972e6fa5eec28094492bd5b5cb5a5665664f72d39337800dea153813f16518aaee105356aa9a401d8b65b4fbb951a71c33206bb9642f6d315d2cdd12287fbdc7ac7442ab75a2ae2506a0a6458377eb505acbcbdf4a5d8194ad44deb118baa21816609ee26b2df267796e22b3e531891a2596ecd696554c6d2110bf13f5241e2daa71a56fd124d4cb8b78c35d67722ac64d676b39bdad7490ce576a90c7682bfe161fe19fa95d742031777b4c80443f22f6d620f0256449555e72e5b382c43d739342adacdf6ce6501cd9b5b57a8a504aa4db64058ee0ca72f32b62900e74be6f855ebac216f54a6b9f1cd22cf03db900488b1dd5defc457fd06c670ad8ee9e7b5ad2a9c321b447d548ce00f8eaed63a8345de12eeb4525f21d8c5ef66bc3fde74a0bfb72369f49a92e6560a51c67ee4d1b1ce307daca36939cb35fad382a3ef6f4bd8315aa82e00467784740815cc89827926a7b06ed47c344a9e6c0077affe80dcc5c57ad12c53afc45064202011f94ac57e3073e901e168514a1fb5e35af3ce1fbd9aa04ee434d18bd784cdd9ad8fe2091790a70e39059a51a318e1dc0258663485df58f48b7474f348ccea202d4814c8c077497923667a84d2d5a809e6235486354bd36843f0f5c5cd915b4d0900482cf8912f595a064278eb83d3af361a8ff57cb1455e40142ff3ecb606154c8ab1eee031cef2144984bf9a6a0cdb751a9ccb8bcbc8c0ce2e2b339ece889a037a0e1745b22ec7b8779d18260357fb2f311e73234651bdb3fc651a8198d3310f815c9b2b02a55604436bf899863c84eb7f6d3a420323b92e4f3a3277c6a7ce4fccdc0f564981d14f44543682d93f6208ecd6188a119f95dd8507eeac3c6d058151e1a7f4f18a46a204f6453aaa0a2108089f4c7b12873a9ed9e262c54ff53df4df819b7929e5d528bc44af5b6f34b26d49bda7b4b849f08e09f1d5ae1338281e4717a0c7f150d419028c5a215811b3421644ecb5de2c1efd95d0bc74339a47a3c31e3ecfa873d9382f2f8cc4c566db19a0edb2ee36e6e7d8dc0aeae4f06cf30525c89fbfa844b6b7494d23fe3ad152f8e6da913e9960322d99267904fc8768106d40690c8a90dc01d8514ab1937c96edc16f0502fe81491b5c5c95f255d6e07722f5b39d376bd1c7a63339a5c6f6fccd6a0c6a6840723cac35da3b806c0924550461fb5fc93f30cee4d61cc167a256bd08d78c365299c53aecc8855e4c6ffffc33eb077427785b4b6a18368a238e496630a5c9ab16ff5aa4e84f86fd4d8f3fb13ca3903302f7adcfdcccc45ea945cb9636cb2d1bfe505c1270c0f747ac90024c3c958684c3c087bd54e762f6ea7466283d36cd879a5494dd382c37c61c418576a8635d784c8cdff9b399ea3b02347aab6fe5bc2ee96ab76199d81ff8e532feb7aa504567fd4470f0f8ec11d591ca15f53229c24a1a14bb4544f07a1f19f36af6617102a37836dbad9961e20f026d8e07001bdd09a3afee156ef68d92c938feb92f74656d5eef63783087989ee4f16ae7eaabab0714b5e272263c21ecd294eddad9671c7bd11ce44f23e5b0647fe64d1f3c734bb59a7dcf366df1a899018a2fa51a225a7112bd05fe3c2fb0e24eedb4922a92fb32a9361f9f65d67c765abd2c2a1ef25d4d615bcb9d773a21de944f083718f3cd0eede1826580433706816bb7bbd22fae3fde0b6eeabc8da690e8609034e973c99ca1b3f70f4a2b8f8343babbe1322c947752cdac45701b25c43e8cfde6d939801c2144dd326cec2890955992e5b0b9359390f14ac20801bce083302e3aaeec74371955a6cebb127223dcb3246b79f10a9e037f44c9610ae803f852100487e0f6a2fc52298b05dab2f3d742a43e61316d07de5851183560ecc79504f4747fdf6f64aaa48c97ed29e070a903da9e12176e125c6cbdc14150711a93569cb687477d3265ca281850e8b3dd39470eaf70fa5910785ae99aa3f3e6b90edbb7a03f634f59f1346f5668f1a329cabdc0ba2fc5258da0e761047b5e87a308ca6f48f38a806699efc3b8beb1e1b35e89b8ddfa53b8e466d0b3e82fac0ebdc78a86a6ac823b6f38b568dfaa1e3114b544bec784d13c43d33fa35c8580fac385477a39baaa0b79f68a9d38480b5b2ab5bf3cb19bc385fe973986070d6f3a7b2d764edff376d51dc4bc72bfaa17a09") + val wait_for_funding_confirmed = Hex.decode( + "2d0cef30160b00f8c206f1e0618f597b774b67338a1a52f4e1d13616a4032719bfb41817e7272b215afcc9d68e64fa46c94fa75f5b1c374832891a7ce4d326ec6cbd7cc3a9c876a9df0254b1306d02ef4755cf6a292dd643ee90cffa111b4a20b46c060d985ebb7618bf5c1d1d05b6c2e6535d3bf0f25e2bbae396dc2bca66e390d18ef46965d6986835cfa602728763adf74cde41924c609b41fef839b3eba6b514b5c70bbc5c58159122381407cce35643143aff68bef046a13a208cc4ee738b2b943e85d85004d94b014609c1876a48694e9b4ad92ceaa40094901c183103af1a25a35d6c38128921d53dfac1ed121ca6acd3dc66bdb90603d2f6cfb790303afcea3dd4b5a3f80c1ddb8abcac2fad767c91bd1f072bbf6269a79d210e2a8f16cd030ffc297c3ad746c21969c48a602acbc5b05aaf3875f91e0186428128f81a73bc872d144d36840c5dc51378911c06638d3d4b85193ce7e72a656f2592c862910b9e34f79e6173996e87b7a62f8637d62407da219823f6d5c3b13f30c0995f7a6eac5febfd16d4c7b8c389beef57ee9e44b731d25a3a9fb3fbfdd02884136bed9ee8c975b276c0dd9fb1b37dc4ce8a3c4fa674a6724384c5616b0c8b86f52bd66b0b5e1cc6d8c8e4fe0f818a4e5d9bc04f1fee5f2b63af5eee8340adea0654058058fcd16b6ef4d2c8154e2df4fa142fe289f26ddb77dc8da98401f4a7d47f8398977292858bc0092e9dfc1d3c1d621938f4d5fd4e36c7637a0773a0f1b546357e330c61fee88dd273e956f45133a765da823f2686b15ceadc84048a4dbaf0a9e83e6ff06bff586e6734b3a4e4a8511369c07f88f1efd548c1fff2d1d0aecfe1504232548cfae442249fc5d066aa93e442b511de05eb2be87595922be69e655c1048889ff10b1fc5047e7537f5db225a95049db32f885016e32699766b1e111040849122d2c0ff6600a349292a27022b3d1964bb5d8050362f3bf042641872e8d0dfb39c94bcf9068c190450ca4126efca8bbd926a8be65c9d07e4b2d19a226596a50fec795fc0b715a9cc324a13e15c76bdea9a0e4af8689dc95410638aa4b655f7bdac268284ef74c824d9626953ab202fa185715317f7621d602aae5e186d6b4820663c418d7374a2e8d378a306861ce1d775a4a8c5c06784abc084d45b001289b06bb39053e7777b6d691af5affb5548aa703715d41435a43e824beae0051ebf0189d1228f7e6065821925bef16de743a91b74396c39318611bc28484a7e96cf9066113ea27df045b09dfa89c880a08775cd209c6b6f543c742bd07e61e52b4aa41899a705646a790d2929db5e8610bdf75511f8b1a6d48cf3b691a0cf6a1ef6c92670b2c6dd97ad94077dc5408100244382b89bc452a17f5103aed6500a2a54bbd88e0ff08d9666270f0f3e015e11f6f34c177f7af4adfd04f8978f18a524cff48c70f1854f82f85049c1f9ed24155284dac43dae45fdb49aff52e9eb11af884044da9a312bc158142cc6e7aaad028e8260da39eab30483a7763cef83d73f12a98979f536a46deeda25b89383e963711349cc14e4defb2847bb0c61df705f465f0608dcebdf8bc1306d4910bfe438ef10b718fd0329f4f85cbceb37d853ff743a848b010b1e50b44edc47334600e664acebd48bd83cf8394be1c168c0cc4da41da83f13c6b879392fe035631cc038b8eda26c0ba07802480774ac013368b741634fd29784c38acb9fd5f17f84d62ae76579b392b97f200dcd73efaa7790a1943fb99d77589ab5c6598bf43bb9bd034179b01f869b5d430e4aec014f619edadb8fb8c1396f18f71b1feb200ee93a8ee70ef450e9f5bf9eb5d42987a4d34b698c9bf6f1c0de914fd3b896f523941a3929ea6e4654a33cbeb05babecdf0e5dfe003338caad556af72c9efa990ab802f8a3cdf15f05a6dd0c884eb58ce3d06958e57555a30fc8954edff727568c2a63b51dce9e03633211f7feb5b80ee4a595d6b10d97acb2dc0c5cc8a6153e2710d774ed50ebcf85556847969698a74c1b4a030774bb7edd355cca7d5de68c0a5b3c5f3fae02a92c0dfb052f7c9ffa970d9fb3bf79f088eddadeefbcfdaca8099cfaf3ed0a111efb962a5b893279ed5f624b36f887c6b244352dc946d112bed31a538022d27e1334d51bf4a4972e6fa5eec28094492bd5b5cb5a5665664f72d39337800dea153813f16518aaee105356aa9a401d8b65b4fbb951a71c33206bb9642f6d315d2cdd12287fbdc7ac7442ab75a2ae2506a0a6458377eb505acbcbdf4a5d8194ad44deb118baa21816609ee26b2df267796e22b3e531891a2596ecd696554c6d2110bf13f5241e2daa71a56fd124d4cb8b78c35d67722ac64d676b39bdad7490ce576a90c7682bfe161fe19fa95d742031777b4c80443f22f6d620f0256449555e72e5b382c43d739342adacdf6ce6501cd9b5b57a8a504aa4db64058ee0ca72f32b62900e74be6f855ebac216f54a6b9f1cd22cf03db900488b1dd5defc457fd06c670ad8ee9e7b5ad2a9c321b447d548ce00f8eaed63a8345de12eeb4525f21d8c5ef66bc3fde74a0bfb72369f49a92e6560a51c67ee4d1b1ce307daca36939cb35fad382a3ef6f4bd8315aa82e00467784740815cc89827926a7b06ed47c344a9e6c0077affe80dcc5c57ad12c53afc45064202011f94ac57e3073e901e168514a1fb5e35af3ce1fbd9aa04ee434d18bd784cdd9ad8fe2091790a70e39059a51a318e1dc0258663485df58f48b7474f348ccea202d4814c8c077497923667a84d2d5a809e6235486354bd36843f0f5c5cd915b4d0900482cf8912f595a064278eb83d3af361a8ff57cb1455e40142ff3ecb606154c8ab1eee031cef2144984bf9a6a0cdb751a9ccb8bcbc8c0ce2e2b339ece889a037a0e1745b22ec7b8779d18260357fb2f311e73234651bdb3fc651a8198d3310f815c9b2b02a55604436bf899863c84eb7f6d3a420323b92e4f3a3277c6a7ce4fccdc0f564981d14f44543682d93f6208ecd6188a119f95dd8507eeac3c6d058151e1a7f4f18a46a204f6453aaa0a2108089f4c7b12873a9ed9e262c54ff53df4df819b7929e5d528bc44af5b6f34b26d49bda7b4b849f08e09f1d5ae1338281e4717a0c7f150d419028c5a215811b3421644ecb5de2c1efd95d0bc74339a47a3c31e3ecfa873d9382f2f8cc4c566db19a0edb2ee36e6e7d8dc0aeae4f06cf30525c89fbfa844b6b7494d23fe3ad152f8e6da913e9960322d99267904fc8768106d40690c8a90dc01d8514ab1937c96edc16f0502fe81491b5c5c95f255d6e07722f5b39d376bd1c7a63339a5c6f6fccd6a0c6a6840723cac35da3b806c0924550461fb5fc93f30cee4d61cc167a256bd08d78c365299c53aecc8855e4c6ffffc33eb077427785b4b6a18368a238e496630a5c9ab16ff5aa4e84f86fd4d8f3fb13ca3903302f7adcfdcccc45ea945cb9636cb2d1bfe505c1270c0f747ac90024c3c958684c3c087bd54e762f6ea7466283d36cd879a5494dd382c37c61c418576a8635d784c8cdff9b399ea3b02347aab6fe5bc2ee96ab76199d81ff8e532feb7aa504567fd4470f0f8ec11d591ca15f53229c24a1a14bb4544f07a1f19f36af6617102a37836dbad9961e20f026d8e07001bdd09a3afee156ef68d92c938feb92f74656d5eef63783087989ee4f16ae7eaabab0714b5e272263c21ecd294eddad9671c7bd11ce44f23e5b0647fe64d1f3c734bb59a7dcf366df1a899018a2fa51a225a7112bd05fe3c2fb0e24eedb4922a92fb32a9361f9f65d67c765abd2c2a1ef25d4d615bcb9d773a21de944f083718f3cd0eede1826580433706816bb7bbd22fae3fde0b6eeabc8da690e8609034e973c99ca1b3f70f4a2b8f8343babbe1322c947752cdac45701b25c43e8cfde6d939801c2144dd326cec2890955992e5b0b9359390f14ac20801bce083302e3aaeec74371955a6cebb127223dcb3246b79f10a9e037f44c9610ae803f852100487e0f6a2fc52298b05dab2f3d742a43e61316d07de5851183560ecc79504f4747fdf6f64aaa48c97ed29e070a903da9e12176e125c6cbdc14150711a93569cb687477d3265ca281850e8b3dd39470eaf70fa5910785ae99aa3f3e6b90edbb7a03f634f59f1346f5668f1a329cabdc0ba2fc5258da0e761047b5e87a308ca6f48f38a806699efc3b8beb1e1b35e89b8ddfa53b8e466d0b3e82fac0ebdc78a86a6ac823b6f38b568dfaa1e3114b544bec784d13c43d33fa35c8580fac385477a39baaa0b79f68a9d38480b5b2ab5bf3cb19bc385fe973986070d6f3a7b2d764edff376d51dc4bc72bfaa17a09" + ) val state = Serialization.decrypt(TestConstants.Bob.nodeParams.nodePrivateKey, wait_for_funding_confirmed, TestConstants.Bob.nodeParams) assertTrue(state is WaitForFundingConfirmed) - val wait_for_funding_locked = Hex.decode("394227ce9e9a2d2cf9fc7b94d6d3f69df0a87f1472ac180fb9b56c72746763fa311169c07b484ad5968da95691cd3dc4ddc3e8a67c9d33ba0a263e6267c046cb0e19ca47e02cc39ef45c594acaaccfafcb2a7dd2e91b1d437c14ef992df3dcbcd30583938ca1706d1f8035003f95b2464e5305be4e73cfd87562b73cb75382f41b99ecbb5af39edde68d61bf935473a094a99b5b57692500ffa603a5ec049f63a4176ec7608e750e1d45c8224a6dcfc4fd134c9ec7861e7aa4dbc6edbeed919544080632f65bf57536d96d7c0e6750c0009842178b4ab97c166b443ee95d9fb2a604fddffcfc0cae338774ba4cd0573572542914440a0e80d1e79257e8c216db933b62d445abe2e56b6f56ea23f3629f5bf8ac3591d88947bb776a8672e13fc33cc53ba4805782169e9bd8c367cfa941bbca5621faf8f14e71f1b4550cd276350b70f8a94307efd3792cf07f0a4ff79ee7146dcc41910840720c37db66135edfc2f493a2a1133bc90e03ad0006c6c5feb797e7ccd8ba35a5af8b3a411c7994c6475a00662fe24e1c2720237f1a41ef45c1397c75d5703dacbc28a326643e240e1c0e688a761d650a27ba99ff2448bdf59cb379167ba07d8e099cbaa1419f75b9e4d705972cf62aeea9975acd73d65bd78c297cabd95a75604d46348c63ab7405fce17f154df98afef7b9aa7676ea46f2884a4ba1842514d1e3cf471db40053131451483d0d5d87e48694964859cc9a0611e531c4a6d44e76c5df2119b1a6689dfdf88faab0f4075baf7291531e53ffae41477e94517f013685e0aebae9d232dbfd91290c5f7a4de0a547e8e2ee9670cf6b21960c9c3bd429f133cf5c95a42fad0eb6c433816ff41b72452ffd580ff401fcfd7f3db13ece38d2f1c72bb4dffe6ffdebbffc65ee78d965a4946501727fa47404fe90334a52c6069d0174d907b7a79ee8ddd70be8a371f98b23e40ab7e90278a589c71a915e86b34ebd3709258749081e6cd676d5e5af520ca05297075cdfaed6cad55a00e036239350a6b1415a5ae385212d9444756948b178526e58a93769299e6f9277cf1d96c8e945151f93b38a5ed684669d0a6fc3ef01330f3883aeac1818ddcece8f1e5293effecfa5eda29e026a93163a0224a38636c01fb6bda4231134f551262dda0074774a61a64756f52e36765a1f4ba55e22c277d5a42fdb467244667897e97c832b0b710a338904c3391f1a9828431b3ff9c5cca2a8532c8409f36219093fc12d7051a7315148d3a46987bf0c93d3b4425abffc61e475b0189e88ad923d456eb8915c060ba18504db76e18c3cfb9195a96ec8d10550669528168190756eedbb0d5d49d6c2f59230a754fc27bbd34318b88b2e40378f3492b00243d6006a542d6062127074ba50293e3567701c1eb8d554886746cc21a203b2928d5405d60ba950fb1763a3725c5df73c7d201af3fa6e530574605b9e306703457ae1f41bacae3992c6e946eb2ce060ff057c2402051904ebc88e067ae221854055e0e7ed440cb414bffa23e03b66af1f695e8b429a3116e2a7191cb80582a5b1401cc1a4398590342c9e792b6b21997c28b4ba50a7402f7cc4eb84df6df5bde644c6b3b5b421687a2172144a280cc524b6e7ce4e502f6cd5623248c27435894e20f9a5f3eb8fb34a388c4e83b09167b16d66cf3537bd82baf01d53b43329387b2ad7fc43e3a0a85722f00ba480f041d499559782a5775512a1959317e1fefadaa235a31aeb3e65ef2ba7ad268b338efbb3e0cf47d9d45f7d1ea8cf41e474b17a7007a821239f176240a6a8147bbfb2e032e35a12c1d3e6b8361f2d492ca2c9097a1c079ccef51167d5962a4b1c1413a32e2a1a9fb00d7d9760c70f42a2832c98a83c69d211d47c1b66aeb68fa7055d239e9c34ba09056b33dea4df8a65b8b3ebd246e4d77d0f8d1dc5e090f7dab5e5e696ef9b3d8a5f627dbf311e0598fa89afd2e74c4ee3500e5c64c12f749d296e2a0fa73c7c6a4af5a529f7a572b4a3f173f58802d062500c67c44f6db44f9e11a38d6e986994979aef862b77096a23f5cc197a72586e0f3129c8cc92074cc4cd9fb4fd43673bf7eb544346866ff86fc62239c60ffc07f9930eaf5bc04b39f323224d13aeaeea78b2ba2ed5933880ffec7014a9b1802d3ca842f18e38c354c7851186a670d6ad34248b9d3128c1c7b038f426244cc01eaec9c2b2a6157757c3054a8f902d97d5f95c7b7a0594d0bdf8e8aa444201168391811b3ebd4d3b6a290c430ab00f63f685a11f8d9cd580e3ec71b63d5d5943bec74898b5204694e8ad183a21a0bc41e31bff9cd93c94d2aec16aaf8b28bac6f544b1fdc058318052ebfa923b8a644a020b3ce9706b17999c146125fed848c80d6a7d6c6a537312fca8b7c59a76ead2a108dca93755515b4ffc221ca2b70e665e37489ec5449f178c11a8e0b9799888946be1475bab24c7180567b4ca7696a5ee5ffde4b1cdfaaf0d23b2b29bc705cc1a6640d80f2847f06a930b26022c4b7c4543accdfa06b155a98262af01c70b757aa1723255deca1c2dd8e6319ece97e8c56071ef6437ba0f5a9746860133fd6f8d7f7c1dc704d36ee2e2dd187d5c5cfb0a9184e204859680b4b96b388ad9bcf8f3be4f289be7339079f0a1cd762cbcaa02e699adf4e841e1ad44b77ad4350c4d7d025af3b4fe835b686e6015d35cb6fe60b8870253fcccc387e26a648fab82f1bc2321c6407e3f1ae2b8057161678c0f0c9996108ae96f3c6d0795edce0ab691abdc11bd270d16632252c486194927f3e231870907769350052b3505dda1405308682ea5062db65aeedc1fb9ba50fcfbaca4b7cbda9ab6eb821e1180b3c747e7a9765e4a948e1bdd907794ea76d522f19a8b1388a1603ce41ff5c46e514e11bfc2f27daf35072971099422022af8f1d4e6b312a81f3330dacc2cf784baecd8512b30f37fc2c9b8ebf878b06b6ceec0e419f68006453a0f849cdf4a4afec06cec74245862c950c4f56b183d380d5e13857805103b376bb035360314385bf9626e68d7ff8e68bd5b6e72ca482e3965988d0e8ca932dfdc725a7e5b41b34c107fd9f2cb32968ed22b9e88b60d1d0719e7c56c193aa0a8f8eb919dbf6b20d095baa4feba29e6f5817a8ac2b29aa744021391efc3c461d9ca8806e43a99b65b5b0eb5cedad468bb97be871ab3eb32044430e7164944feb41d2bae03fc4558c76124d687bef90ecc7fb6e8bfa93df848a4ae8699fb13942e0f4869e84c05b00f347cbeb9ca4944616e1efc4e171a630dc11e45bfd7068e996fa1a573132f9bea9cba4953e26dca469d0f0e5e2895ddf37e0eba0599fe49dface166c3ed207da15b7854fa0500685fa99ff308894f7c47f84728cda0199456207c386608f52d33e77796bc3061adc4215a20b4284a41e77e51ffafb3545c06a887cd172d2abad60c243d3084c679322fdf03479018a6842e8d814510b5e9ffb80af2cb907be26c2b491356b682375b29282835cf238a7c6e2fe630300f7877e71ce3f3de99dcbeaa52f99e2c15a7dad33b2161fea2beca47fb00a1c619aad307318f2fb67248bff7fe13a5c342f182697e16f6b471b252dae9b36830eba884d4b5d3279b42adc35a1ad248b641f7d53f2475512f02b09115a21f62fcef7b56604fa76349a1ba4369c7392f498eaf8ff744d7f4b096218deec11e9d4007bf142994b47e6a7ce5add498de00e0c61863b2e4474d38969336765a8b478bd2b7be50577b992cc3e75120b9d564864faf24d1e702ed544ab629f149c94a3a42be5cc88e23acd9dbb8cc64993e1e5a8ffb2c4c44fc977628169564a052cd280ce649c5d528924e7912b9235e8f27c9f786da8e95cc34383c8d8975327c23c22530829cbaf8e33f31e969421b32af4664060396372d7b44bd67a5b38cb49ba79f4e8408e2cb7ccfeb56c8b5692d7839f079f82a409b614699b1eb143056a4af355f6840f0d81d826fabebab3112399ac741af439a8cba1afb9051b95b740cb2911f7304d758acb708c3631e81aac49528a9c781ffedeb7ad23a26316204be2427d0fd12b3d2a9d826b8f3c84ccf72f28e8bfd4f4799dedde2810961d62c2ba4b47988596da8c2c56e4c971875e663c0ec5e68c7e23b9abbdecf67655c023a76f4af34ed095a6956a6dc83504372a74324e5c224916aa0ee73d4514ba81231258e9f24c1a44e5f2e1cee021a1674d1516ddfc48092819ec4c2acdd0abd437adb21d8979d53faec974988190c8ed933d118558a73dbdb8f9b9131eb6a40d7af") + val wait_for_funding_locked = Hex.decode( + "394227ce9e9a2d2cf9fc7b94d6d3f69df0a87f1472ac180fb9b56c72746763fa311169c07b484ad5968da95691cd3dc4ddc3e8a67c9d33ba0a263e6267c046cb0e19ca47e02cc39ef45c594acaaccfafcb2a7dd2e91b1d437c14ef992df3dcbcd30583938ca1706d1f8035003f95b2464e5305be4e73cfd87562b73cb75382f41b99ecbb5af39edde68d61bf935473a094a99b5b57692500ffa603a5ec049f63a4176ec7608e750e1d45c8224a6dcfc4fd134c9ec7861e7aa4dbc6edbeed919544080632f65bf57536d96d7c0e6750c0009842178b4ab97c166b443ee95d9fb2a604fddffcfc0cae338774ba4cd0573572542914440a0e80d1e79257e8c216db933b62d445abe2e56b6f56ea23f3629f5bf8ac3591d88947bb776a8672e13fc33cc53ba4805782169e9bd8c367cfa941bbca5621faf8f14e71f1b4550cd276350b70f8a94307efd3792cf07f0a4ff79ee7146dcc41910840720c37db66135edfc2f493a2a1133bc90e03ad0006c6c5feb797e7ccd8ba35a5af8b3a411c7994c6475a00662fe24e1c2720237f1a41ef45c1397c75d5703dacbc28a326643e240e1c0e688a761d650a27ba99ff2448bdf59cb379167ba07d8e099cbaa1419f75b9e4d705972cf62aeea9975acd73d65bd78c297cabd95a75604d46348c63ab7405fce17f154df98afef7b9aa7676ea46f2884a4ba1842514d1e3cf471db40053131451483d0d5d87e48694964859cc9a0611e531c4a6d44e76c5df2119b1a6689dfdf88faab0f4075baf7291531e53ffae41477e94517f013685e0aebae9d232dbfd91290c5f7a4de0a547e8e2ee9670cf6b21960c9c3bd429f133cf5c95a42fad0eb6c433816ff41b72452ffd580ff401fcfd7f3db13ece38d2f1c72bb4dffe6ffdebbffc65ee78d965a4946501727fa47404fe90334a52c6069d0174d907b7a79ee8ddd70be8a371f98b23e40ab7e90278a589c71a915e86b34ebd3709258749081e6cd676d5e5af520ca05297075cdfaed6cad55a00e036239350a6b1415a5ae385212d9444756948b178526e58a93769299e6f9277cf1d96c8e945151f93b38a5ed684669d0a6fc3ef01330f3883aeac1818ddcece8f1e5293effecfa5eda29e026a93163a0224a38636c01fb6bda4231134f551262dda0074774a61a64756f52e36765a1f4ba55e22c277d5a42fdb467244667897e97c832b0b710a338904c3391f1a9828431b3ff9c5cca2a8532c8409f36219093fc12d7051a7315148d3a46987bf0c93d3b4425abffc61e475b0189e88ad923d456eb8915c060ba18504db76e18c3cfb9195a96ec8d10550669528168190756eedbb0d5d49d6c2f59230a754fc27bbd34318b88b2e40378f3492b00243d6006a542d6062127074ba50293e3567701c1eb8d554886746cc21a203b2928d5405d60ba950fb1763a3725c5df73c7d201af3fa6e530574605b9e306703457ae1f41bacae3992c6e946eb2ce060ff057c2402051904ebc88e067ae221854055e0e7ed440cb414bffa23e03b66af1f695e8b429a3116e2a7191cb80582a5b1401cc1a4398590342c9e792b6b21997c28b4ba50a7402f7cc4eb84df6df5bde644c6b3b5b421687a2172144a280cc524b6e7ce4e502f6cd5623248c27435894e20f9a5f3eb8fb34a388c4e83b09167b16d66cf3537bd82baf01d53b43329387b2ad7fc43e3a0a85722f00ba480f041d499559782a5775512a1959317e1fefadaa235a31aeb3e65ef2ba7ad268b338efbb3e0cf47d9d45f7d1ea8cf41e474b17a7007a821239f176240a6a8147bbfb2e032e35a12c1d3e6b8361f2d492ca2c9097a1c079ccef51167d5962a4b1c1413a32e2a1a9fb00d7d9760c70f42a2832c98a83c69d211d47c1b66aeb68fa7055d239e9c34ba09056b33dea4df8a65b8b3ebd246e4d77d0f8d1dc5e090f7dab5e5e696ef9b3d8a5f627dbf311e0598fa89afd2e74c4ee3500e5c64c12f749d296e2a0fa73c7c6a4af5a529f7a572b4a3f173f58802d062500c67c44f6db44f9e11a38d6e986994979aef862b77096a23f5cc197a72586e0f3129c8cc92074cc4cd9fb4fd43673bf7eb544346866ff86fc62239c60ffc07f9930eaf5bc04b39f323224d13aeaeea78b2ba2ed5933880ffec7014a9b1802d3ca842f18e38c354c7851186a670d6ad34248b9d3128c1c7b038f426244cc01eaec9c2b2a6157757c3054a8f902d97d5f95c7b7a0594d0bdf8e8aa444201168391811b3ebd4d3b6a290c430ab00f63f685a11f8d9cd580e3ec71b63d5d5943bec74898b5204694e8ad183a21a0bc41e31bff9cd93c94d2aec16aaf8b28bac6f544b1fdc058318052ebfa923b8a644a020b3ce9706b17999c146125fed848c80d6a7d6c6a537312fca8b7c59a76ead2a108dca93755515b4ffc221ca2b70e665e37489ec5449f178c11a8e0b9799888946be1475bab24c7180567b4ca7696a5ee5ffde4b1cdfaaf0d23b2b29bc705cc1a6640d80f2847f06a930b26022c4b7c4543accdfa06b155a98262af01c70b757aa1723255deca1c2dd8e6319ece97e8c56071ef6437ba0f5a9746860133fd6f8d7f7c1dc704d36ee2e2dd187d5c5cfb0a9184e204859680b4b96b388ad9bcf8f3be4f289be7339079f0a1cd762cbcaa02e699adf4e841e1ad44b77ad4350c4d7d025af3b4fe835b686e6015d35cb6fe60b8870253fcccc387e26a648fab82f1bc2321c6407e3f1ae2b8057161678c0f0c9996108ae96f3c6d0795edce0ab691abdc11bd270d16632252c486194927f3e231870907769350052b3505dda1405308682ea5062db65aeedc1fb9ba50fcfbaca4b7cbda9ab6eb821e1180b3c747e7a9765e4a948e1bdd907794ea76d522f19a8b1388a1603ce41ff5c46e514e11bfc2f27daf35072971099422022af8f1d4e6b312a81f3330dacc2cf784baecd8512b30f37fc2c9b8ebf878b06b6ceec0e419f68006453a0f849cdf4a4afec06cec74245862c950c4f56b183d380d5e13857805103b376bb035360314385bf9626e68d7ff8e68bd5b6e72ca482e3965988d0e8ca932dfdc725a7e5b41b34c107fd9f2cb32968ed22b9e88b60d1d0719e7c56c193aa0a8f8eb919dbf6b20d095baa4feba29e6f5817a8ac2b29aa744021391efc3c461d9ca8806e43a99b65b5b0eb5cedad468bb97be871ab3eb32044430e7164944feb41d2bae03fc4558c76124d687bef90ecc7fb6e8bfa93df848a4ae8699fb13942e0f4869e84c05b00f347cbeb9ca4944616e1efc4e171a630dc11e45bfd7068e996fa1a573132f9bea9cba4953e26dca469d0f0e5e2895ddf37e0eba0599fe49dface166c3ed207da15b7854fa0500685fa99ff308894f7c47f84728cda0199456207c386608f52d33e77796bc3061adc4215a20b4284a41e77e51ffafb3545c06a887cd172d2abad60c243d3084c679322fdf03479018a6842e8d814510b5e9ffb80af2cb907be26c2b491356b682375b29282835cf238a7c6e2fe630300f7877e71ce3f3de99dcbeaa52f99e2c15a7dad33b2161fea2beca47fb00a1c619aad307318f2fb67248bff7fe13a5c342f182697e16f6b471b252dae9b36830eba884d4b5d3279b42adc35a1ad248b641f7d53f2475512f02b09115a21f62fcef7b56604fa76349a1ba4369c7392f498eaf8ff744d7f4b096218deec11e9d4007bf142994b47e6a7ce5add498de00e0c61863b2e4474d38969336765a8b478bd2b7be50577b992cc3e75120b9d564864faf24d1e702ed544ab629f149c94a3a42be5cc88e23acd9dbb8cc64993e1e5a8ffb2c4c44fc977628169564a052cd280ce649c5d528924e7912b9235e8f27c9f786da8e95cc34383c8d8975327c23c22530829cbaf8e33f31e969421b32af4664060396372d7b44bd67a5b38cb49ba79f4e8408e2cb7ccfeb56c8b5692d7839f079f82a409b614699b1eb143056a4af355f6840f0d81d826fabebab3112399ac741af439a8cba1afb9051b95b740cb2911f7304d758acb708c3631e81aac49528a9c781ffedeb7ad23a26316204be2427d0fd12b3d2a9d826b8f3c84ccf72f28e8bfd4f4799dedde2810961d62c2ba4b47988596da8c2c56e4c971875e663c0ec5e68c7e23b9abbdecf67655c023a76f4af34ed095a6956a6dc83504372a74324e5c224916aa0ee73d4514ba81231258e9f24c1a44e5f2e1cee021a1674d1516ddfc48092819ec4c2acdd0abd437adb21d8979d53faec974988190c8ed933d118558a73dbdb8f9b9131eb6a40d7af" + ) val state1 = Serialization.decrypt(TestConstants.Bob.nodeParams.nodePrivateKey, wait_for_funding_locked, TestConstants.Bob.nodeParams) assertTrue(state1 is WaitForFundingLocked) - val normal = Hex.decode("c207f3309e3ec64ccd7d576521d7d0e65dc36292be30d7b84e557bfccdd8102480cbb97b1f9bfbe267ac1b09b8b6829d9153cc5f132bb951f8bfc3cc417faa9ecd6ad3491f65816571dd304d1d5851685ec689724cd8b57f6cdc8451641b85304cdddcdd080095ec5cebe0e1a2c64d8eddc84eed08af8d423e92229a9fcb00860a0c691ff526f7909ab1b8ce351efd7a206322b7177ef9fe064215d0f8d13fd2ce474893c6f8547ed45cc69f03943f328a81169b18fdbdf82f5fd7b1c1af5c1227ea231828fb6ed0467af0acbd8dcc57f0880aae72597f920330d5571b1e7f7641f7f64a7f534dd8c6099e5a5361ba35fdf8f472407dce5867f9ee75ad89ccead717ced8525a1db14c529612fcabceab951bb5a6e38421fe1bb7c092358105ed2ecf7293ad0f0cde3873dad70a6668e44a6563e059b3fc08e07e3265c0bc708ca75ba7a9c238a14a8eb125581e998fa8c489b8dfd720dacd5cc8de832fc0065b9aafa2761672a4ecbb69f24df95e5fe892b01623ce0093f816577a832c9482308bc9668ebf91a89739efc3c965f34dce28674ea45fd841e73c3ca1eee01b1cd0a7e30a0a65c8517a8192e0b582aaaf7a3c0fae8776b38aad7e38ce4e680c44563ce3c635ceaf3b0295e4ff113208da349df01ee5bffb6e357c11b09fc7471ea204f1efbb214f1640029abe7aaf7c97215728ad66dc1d6f7b339f935a6a2e1f9f1e5070acedcbc7a3f8a7289d6265664b21c013be3aada176cfec2dc51c5b7fc7c9e008a837d3ad48c7f4db82d29462646484b1173017cdb81b2bd274ec565b67276f53eca3a0980af205759c39e8605f1a171e34b38833f84f3c0d29d6467c6df1f1dfb8871aa91277153220c82bef0a70d10fe58a1b94ec011d5404f4423b25e471d0df9194dc6df5758b2ee41411ebc9e4b911f6803cf32356abc8181644608ca55233785ac60966b80e567533f849f76876a99bfbfa40f62b6169f6518badc1aab7b531b223f9282a723e6426ed310ea8e91c0a342dca9a160955577ce97e0447954000ef048cdcb0105b1a111409dd8a818c36cf2fd4cd8d32065ef8b6b8bc4270677c40bbac05a4becbdb1c1a5f88a845f2cdf1e39f71d5e668cbf44238dd6bd917112c1e963421c89ae84ae780acdeefa77131dfa4586811ed6f0cf88cc78974dca3d8b2cbc0f7d3d2266c5756b1faaa16587367d0e0a710db90b5318537601ba713e5d06ce5a631ae5e31b6744fb5ec39d7cdf65c5c2f265895b37f079acd6ed4ff0a9ea780899bf44bab32215c2440ba1e0c30089a5fc49abd3c8c3c847190be76d7f01a1d6b1e611823c58ea168926edf8511201183db5c7038b0d1d9c6aa3fe6eaf7c94f08e2119612e4e10cc92493c54d35e794c119619baef4e5492995f5e3c8f5ca002382def7f4ffb5a27ff8930969cb528fcd1713a9166ccbc6e105568c495148f5e35a64fa81d394d73ff08f7ea3ee21be2f828ec0151d6a6a9dcc34a9664677260635c675ff182974e072c67967cd08a019aa4d595467b77198adc9f43e1010a5d5382c99731b05a98e4fe600e57be72d29693196acd5c92641160cdffb4cc4bec84c1e57c9bcb3581ab98f1c3c2b2f30d79813c46b29aac84a78d66b37f2ccb63db5e650b87218401be534b54295f6772fd6ae74541326fb3c27a1d386b9dd9775b24996f7730387ab409ae65acc14d7bac0c236290e9531a833b0519f96a0f1149a4468e6020978f5a70531c73d8b15f043147302886a20334a57c181c34e386ed16fb69a27aaa791aadb3ffbe6b6cd0a86c33ace842fbb1009754b3f5ec33242ad0624679717eeef8422e6f520d320f0fa7e3f5cdf0414aa0f9670b0643bb38cec3c7050fad6bcb394e30e7b23d6e49a9a9978c9703c8a14f9bc8ed592c07e73b64992fe2b255ba5bd07581dc819b2b271aed6e15c90f003405b51ca45df390ddfb2824284a8f245f450f773af6455c9effda695a5281389da73f10d101cca7e484e6a0dd206c2206c6952acad6e593513f5cc085e546792285d26b6d21aaa8a516edf6d2e851e40a0815652e2ebf293f82cc796c15629fc53c706b46e8f591b10f360294e5f10430fa575dd4aab6893df157a75cf2cf05fffa20dd264701ecfbc34affe72e9594e55cbebf19f03c0ad3873b8c284bdfeb95f1c8f97e2a0df976a38a599c8c56110b215f5e3c8bf1aa854e06b0b1ac34bdd9d7c0cdd6f27b79e4b09613fdf20b50d4a4ff9de27bdcb559d184220a0f0415dafec69f7ff96df6d358433f7b6f8fe5748d51a6ecec3f90d2a316ec38f4a3a0abe39464b7a321e5395cee63eeecbaf2d88432b7df923dc88ff36a41bf374b49d791d2d659b94ed236e899886900aee1c93413705c03dc631909411839d6b1b9ccdfe9bfccb95bf67d421234ac702c247a340b103c5b16d7e4b13f17c25bfa747cade204743c7f3736016247f1d412a3f5d7efe4a0e2004f19334bf7b9b0489481cdb593a2cb482e0027022a795abb21afb1656f5a09083f40f2786d7e1ba53b3295b05f8b96790960fc348de406e604e5c0665aad131cd4e930bf2ed0fef354fd4971d62a9f3ac795c9d36c01e2e0bcb5db96bc946710909443e0630e39d9d268bb8d6d75325162cf0b3929de466194c3b054440ac54e1c2ab28b0b4711c418547ed5500e6981117f412316083e892a841d8a01550bac490d149ccefbcd4cad9ae5aa9205edb317f618b06acc59501ead750618994c793bea0423ae8e3513316282d3722baa1041509b0368c3230bb7e10ad9f19594374664ffa321a3439843a12d69fc84f206bb0711c67d5fcb7790f43b78917226c1db29423e05074a9ef178a720c649010475ec17b121864f0f615e9757d51c69ca8ff9a48e8fc54916182642c456d2c12fa4d94e1f4a6e916b32e342f9b3a7179225c517701bd917c6de4cdc3b8c7bda7a977bc34162f11c00ae0895b3331cf542fe3c689a9a235accfb4a28f18d822506c6c6352697eec7d7732b5818a3783b59427f75653469018736c3dd03eefdd565b83a878013bae85b5ebc40ef080d39935081b3ebbc629097cf1a141c9c0f64b08a3f196ae8ec4b6fc73757e17ca47f40d10a309f936e02598cc305facc421ad7545cb2f4341cacd42ffc28e5cb51e3b03e6f2f530339caea268fa415845477fff3c84166d7fb26a975fafe88180fe723527beaa633f2fb268f85f87fbd912d79fe539639ff6d3702f1fa08f6e03975a357b33a999a5c6186162c108efb32f16007c10664474d10afa814a9ceb593b5a2a6b1c37fc499584c984cc7f78cd80c1ee7959280784bcb85d902d5b96896ebeb6b14851be00c347cf863791f7454604e8a58c9509a56b82721ddd80144bedcf3053e091caa0b574d8414a8946ab326624a8914a5c7b36e002780e616b6066417d0c9e6d0de5825d760253ac938ab3aa2dfb73cf41d942cdc7c808bd400bfb0716f15bf8e8953850548b4b8d1c2cf4621d324e7207cbefb3f192062e79216fc8d71820e083bcc9285c51f94141b78fbc1aadced9c61f7fb9b7789fd17dd664d28e4b73b4d295d5dffd34636de62e7d449e046a66539da5a7ce71d38d757cd185e873f375bf5c34baaf78cfada8f8855040f82730fdc567c08767706515f4720eab0e5659ce49fb85ca40b0cd111e00ee27c6ea2592d0da518fb28010ce9fb36e3c1f02e4eab361aa9955193b00c846a891553e48559e03d93aae894bcb15db586b36f2914a3fc841b32e80e33d873254b6a18107f4385bf359a1ed6a649e2b4ef7b11af60af4fd1626c11324032ce71fe707b3e6b0ea320e6540f294acc36c0e77366f6f1f16f4683f9c73da6ae77d6dc36a34ed5c23e5d5e59a82b0cd2099510cfad3fd6be627a26d11f8d3a88e1f49f74fef089eed30d07123343229219f55c5bb15a5651fb95304282eb0757506de37cc95cad16449723a9bb7d9a8a087bdcfc15665c7b2787ab1478255ddc530cf327c22f2ef6e404c116045eaeb8be9a137e754c951a1fadb3829283107eed807e1ce7eea848600f4e35cb37ed21c11b3c6977b05d6a9575d17a5baeace3b036eb87f2fab175b6e3eb1ae6b22acb454162d257737e59a38e346abafba9ce1530f5a4a900fc6f7048f0aace10030c638fc205a1309e792dc37639f69f5d569a1d1b2c5bdd4d45112bdce3041b9a117a82585522854857dfc686bd3f6f4149dc0294e7ef93c21e9f006c1b9994388634d338c8aa4b10c7e2017a131bcf4a9f1f5a44e09b136b5df5f3b80cc2b4101859776e6f71d9e5a57e88d623d042fda6e8512c582479e12116993a6357908eee94e26e103ff2efb102b5a52ceddc323136ec21b2d87b743fc4ab8339cdcdbc717fe77641291f9934cb86ebf227f1214ad5072") + val normal = Hex.decode( + "c207f3309e3ec64ccd7d576521d7d0e65dc36292be30d7b84e557bfccdd8102480cbb97b1f9bfbe267ac1b09b8b6829d9153cc5f132bb951f8bfc3cc417faa9ecd6ad3491f65816571dd304d1d5851685ec689724cd8b57f6cdc8451641b85304cdddcdd080095ec5cebe0e1a2c64d8eddc84eed08af8d423e92229a9fcb00860a0c691ff526f7909ab1b8ce351efd7a206322b7177ef9fe064215d0f8d13fd2ce474893c6f8547ed45cc69f03943f328a81169b18fdbdf82f5fd7b1c1af5c1227ea231828fb6ed0467af0acbd8dcc57f0880aae72597f920330d5571b1e7f7641f7f64a7f534dd8c6099e5a5361ba35fdf8f472407dce5867f9ee75ad89ccead717ced8525a1db14c529612fcabceab951bb5a6e38421fe1bb7c092358105ed2ecf7293ad0f0cde3873dad70a6668e44a6563e059b3fc08e07e3265c0bc708ca75ba7a9c238a14a8eb125581e998fa8c489b8dfd720dacd5cc8de832fc0065b9aafa2761672a4ecbb69f24df95e5fe892b01623ce0093f816577a832c9482308bc9668ebf91a89739efc3c965f34dce28674ea45fd841e73c3ca1eee01b1cd0a7e30a0a65c8517a8192e0b582aaaf7a3c0fae8776b38aad7e38ce4e680c44563ce3c635ceaf3b0295e4ff113208da349df01ee5bffb6e357c11b09fc7471ea204f1efbb214f1640029abe7aaf7c97215728ad66dc1d6f7b339f935a6a2e1f9f1e5070acedcbc7a3f8a7289d6265664b21c013be3aada176cfec2dc51c5b7fc7c9e008a837d3ad48c7f4db82d29462646484b1173017cdb81b2bd274ec565b67276f53eca3a0980af205759c39e8605f1a171e34b38833f84f3c0d29d6467c6df1f1dfb8871aa91277153220c82bef0a70d10fe58a1b94ec011d5404f4423b25e471d0df9194dc6df5758b2ee41411ebc9e4b911f6803cf32356abc8181644608ca55233785ac60966b80e567533f849f76876a99bfbfa40f62b6169f6518badc1aab7b531b223f9282a723e6426ed310ea8e91c0a342dca9a160955577ce97e0447954000ef048cdcb0105b1a111409dd8a818c36cf2fd4cd8d32065ef8b6b8bc4270677c40bbac05a4becbdb1c1a5f88a845f2cdf1e39f71d5e668cbf44238dd6bd917112c1e963421c89ae84ae780acdeefa77131dfa4586811ed6f0cf88cc78974dca3d8b2cbc0f7d3d2266c5756b1faaa16587367d0e0a710db90b5318537601ba713e5d06ce5a631ae5e31b6744fb5ec39d7cdf65c5c2f265895b37f079acd6ed4ff0a9ea780899bf44bab32215c2440ba1e0c30089a5fc49abd3c8c3c847190be76d7f01a1d6b1e611823c58ea168926edf8511201183db5c7038b0d1d9c6aa3fe6eaf7c94f08e2119612e4e10cc92493c54d35e794c119619baef4e5492995f5e3c8f5ca002382def7f4ffb5a27ff8930969cb528fcd1713a9166ccbc6e105568c495148f5e35a64fa81d394d73ff08f7ea3ee21be2f828ec0151d6a6a9dcc34a9664677260635c675ff182974e072c67967cd08a019aa4d595467b77198adc9f43e1010a5d5382c99731b05a98e4fe600e57be72d29693196acd5c92641160cdffb4cc4bec84c1e57c9bcb3581ab98f1c3c2b2f30d79813c46b29aac84a78d66b37f2ccb63db5e650b87218401be534b54295f6772fd6ae74541326fb3c27a1d386b9dd9775b24996f7730387ab409ae65acc14d7bac0c236290e9531a833b0519f96a0f1149a4468e6020978f5a70531c73d8b15f043147302886a20334a57c181c34e386ed16fb69a27aaa791aadb3ffbe6b6cd0a86c33ace842fbb1009754b3f5ec33242ad0624679717eeef8422e6f520d320f0fa7e3f5cdf0414aa0f9670b0643bb38cec3c7050fad6bcb394e30e7b23d6e49a9a9978c9703c8a14f9bc8ed592c07e73b64992fe2b255ba5bd07581dc819b2b271aed6e15c90f003405b51ca45df390ddfb2824284a8f245f450f773af6455c9effda695a5281389da73f10d101cca7e484e6a0dd206c2206c6952acad6e593513f5cc085e546792285d26b6d21aaa8a516edf6d2e851e40a0815652e2ebf293f82cc796c15629fc53c706b46e8f591b10f360294e5f10430fa575dd4aab6893df157a75cf2cf05fffa20dd264701ecfbc34affe72e9594e55cbebf19f03c0ad3873b8c284bdfeb95f1c8f97e2a0df976a38a599c8c56110b215f5e3c8bf1aa854e06b0b1ac34bdd9d7c0cdd6f27b79e4b09613fdf20b50d4a4ff9de27bdcb559d184220a0f0415dafec69f7ff96df6d358433f7b6f8fe5748d51a6ecec3f90d2a316ec38f4a3a0abe39464b7a321e5395cee63eeecbaf2d88432b7df923dc88ff36a41bf374b49d791d2d659b94ed236e899886900aee1c93413705c03dc631909411839d6b1b9ccdfe9bfccb95bf67d421234ac702c247a340b103c5b16d7e4b13f17c25bfa747cade204743c7f3736016247f1d412a3f5d7efe4a0e2004f19334bf7b9b0489481cdb593a2cb482e0027022a795abb21afb1656f5a09083f40f2786d7e1ba53b3295b05f8b96790960fc348de406e604e5c0665aad131cd4e930bf2ed0fef354fd4971d62a9f3ac795c9d36c01e2e0bcb5db96bc946710909443e0630e39d9d268bb8d6d75325162cf0b3929de466194c3b054440ac54e1c2ab28b0b4711c418547ed5500e6981117f412316083e892a841d8a01550bac490d149ccefbcd4cad9ae5aa9205edb317f618b06acc59501ead750618994c793bea0423ae8e3513316282d3722baa1041509b0368c3230bb7e10ad9f19594374664ffa321a3439843a12d69fc84f206bb0711c67d5fcb7790f43b78917226c1db29423e05074a9ef178a720c649010475ec17b121864f0f615e9757d51c69ca8ff9a48e8fc54916182642c456d2c12fa4d94e1f4a6e916b32e342f9b3a7179225c517701bd917c6de4cdc3b8c7bda7a977bc34162f11c00ae0895b3331cf542fe3c689a9a235accfb4a28f18d822506c6c6352697eec7d7732b5818a3783b59427f75653469018736c3dd03eefdd565b83a878013bae85b5ebc40ef080d39935081b3ebbc629097cf1a141c9c0f64b08a3f196ae8ec4b6fc73757e17ca47f40d10a309f936e02598cc305facc421ad7545cb2f4341cacd42ffc28e5cb51e3b03e6f2f530339caea268fa415845477fff3c84166d7fb26a975fafe88180fe723527beaa633f2fb268f85f87fbd912d79fe539639ff6d3702f1fa08f6e03975a357b33a999a5c6186162c108efb32f16007c10664474d10afa814a9ceb593b5a2a6b1c37fc499584c984cc7f78cd80c1ee7959280784bcb85d902d5b96896ebeb6b14851be00c347cf863791f7454604e8a58c9509a56b82721ddd80144bedcf3053e091caa0b574d8414a8946ab326624a8914a5c7b36e002780e616b6066417d0c9e6d0de5825d760253ac938ab3aa2dfb73cf41d942cdc7c808bd400bfb0716f15bf8e8953850548b4b8d1c2cf4621d324e7207cbefb3f192062e79216fc8d71820e083bcc9285c51f94141b78fbc1aadced9c61f7fb9b7789fd17dd664d28e4b73b4d295d5dffd34636de62e7d449e046a66539da5a7ce71d38d757cd185e873f375bf5c34baaf78cfada8f8855040f82730fdc567c08767706515f4720eab0e5659ce49fb85ca40b0cd111e00ee27c6ea2592d0da518fb28010ce9fb36e3c1f02e4eab361aa9955193b00c846a891553e48559e03d93aae894bcb15db586b36f2914a3fc841b32e80e33d873254b6a18107f4385bf359a1ed6a649e2b4ef7b11af60af4fd1626c11324032ce71fe707b3e6b0ea320e6540f294acc36c0e77366f6f1f16f4683f9c73da6ae77d6dc36a34ed5c23e5d5e59a82b0cd2099510cfad3fd6be627a26d11f8d3a88e1f49f74fef089eed30d07123343229219f55c5bb15a5651fb95304282eb0757506de37cc95cad16449723a9bb7d9a8a087bdcfc15665c7b2787ab1478255ddc530cf327c22f2ef6e404c116045eaeb8be9a137e754c951a1fadb3829283107eed807e1ce7eea848600f4e35cb37ed21c11b3c6977b05d6a9575d17a5baeace3b036eb87f2fab175b6e3eb1ae6b22acb454162d257737e59a38e346abafba9ce1530f5a4a900fc6f7048f0aace10030c638fc205a1309e792dc37639f69f5d569a1d1b2c5bdd4d45112bdce3041b9a117a82585522854857dfc686bd3f6f4149dc0294e7ef93c21e9f006c1b9994388634d338c8aa4b10c7e2017a131bcf4a9f1f5a44e09b136b5df5f3b80cc2b4101859776e6f71d9e5a57e88d623d042fda6e8512c582479e12116993a6357908eee94e26e103ff2efb102b5a52ceddc323136ec21b2d87b743fc4ab8339cdcdbc717fe77641291f9934cb86ebf227f1214ad5072" + ) val state2 = Serialization.decrypt(TestConstants.Bob.nodeParams.nodePrivateKey, normal, TestConstants.Bob.nodeParams) assertTrue(state2 is Normal) - val normal_with_htlcs = - // @formatter:off - Hex.decode("9300cf64e81d29328c082f3b98a58b3e156ff01126416bd5a64d4722e4b67db83b692546b8f2d1801e2b2c3ccdb48e91029b424bafefbc7a97dd069cf8a287ab2ecd1a18a5dccb8fc1012fde3d3d65eebae83292d646405b4e696533fcaeab0f689a1a15de7aeab322f0c0e87a503bfae2165a514e9bb956217f12504e16297e1850038dbd4898c14e3da34bb6cbe01ab5aa73759234498ccfba5bc08438d5f8788c69bc1526731fb63949a020d3a8bdd50010369e4ff6b2177ee8793e35474b94ec647502fe5b3ca4e452e27d425a413e06d0ed63a908ec4e992601b5af34778d65d84162cb2d26f8793f47154e514eb6c5ba9e8b97712cf254185161ddba8d360d87ca469a32e6f20d5d91fb75229616156dfdbd955b92a28b288884f99ce0685310e8341b714173f0ec83d87de9502bf75d98ab298f400386c2b1804826794cd68859b2cde5fefb020e8d8117b2789b6f1d95f849c2dcd7dcf097207ae25ec65625bbe5a7465983a5a2197341c5e2542cf29576c9421c163bb69cd8f0f1be6246f96e62cbcb2398870469872ba1c1b10f2533c8321453ed2dee3b62dedccac47cf631568091add2f6408b138662a55ba9140b82d23759990884730ab6b9742c56b8b272944ab4365b8a9fdd67a6e6c3b86fe80c67a4b5e253b302695fb6163cdb1a4b118f16869ce40087cc3590faa07cd6d3c534fd5c91ae89ba1ed627ba0cba45c3a3ca9b49bb6d0fe7d57019a2504c601aacb5bb7deaf4149a6dbf1153da00f174d42f247c6e62b4d448f028900fd6f11fd308e402f5f71593c50320edab2e8e4e3569f6be00947bedb229f2e6cc0e34ffff19a3248f3e9d71b0a3f78d81d7bc6e1ce97cb7f127b65c7d08b5e7b15e9d44f28f590ad9e29e24a2d973e6fc79f2abd86d6915e7d37393c5e280e8fe1854639105bcbeef5c9ece791caa1423e6c66cc351ed4ae5f47d326e537910d786bf738529ab2b9a658b6f3d490103f4082e6021aed15b29f65cefefe0daf7e4f00a321d8d2bda5e3ff142e059b027da3f9c9bfaac311ea0c577a577a9d257adef445c966e301145dd57794aaedcdb1081c89ee86b65de8a515afc6b2514e64575919692ae764440d3e723fe1aeff40206dbd85d46cfe2aae6a9229f08f04bca768b7cdfa5d118cdf6de5c66d44d5d98e73cf593e029ded9b87ad381ab0ce0eab54ad98cf52160b75488943c9ff4e73778cbd60435dfcb4489c8c9081f9ebd650367a8f30eafa036a2357fce1f903eef0172f7ae42d7277e16ed1c095513955316167cd69f3f49dbeb022dffd8f5df8b44d9f78cbc47276a7de62d550854b2832a99b4a42a8ab709dadbe8a5c595105eb90895ae8e3daee024a28f12b4f98ee62f41687b486bd99211d478fe4780a301f79205c87e6ae0c7569721f517ca0fa0eab19727ed01a6ae12f1ae940ba86c4da24cb07aca3f3abe6d9146735b5fb45aaae34f9720ccf3d623635e9abd3408aa40ebdc3305df8624b947811a98e7ee11a3cc2294fd0e9506ecfdc936309cfb0c08b673aaaf139f2458d42b8d8895b72c697cd81462eb246122872b89a241993306fac6953da58965b290b65c47ba9a76c16aa80fbe05c49c410908a899cb0382977926f51e6e95b062e6edea331d8a946ce4ece90564c687e0cce1b64baaef7b8f874493a9437a50a0f333582472634ccd05fa577c6a0f4ad27589ce7ed5ea9dfad20381c2211a1da8b6da908e38a26191d216e9d060b7a288f4f695135678fa48a29b05672fc2d20eb211a4c7f71d7c07299c9262d21cde189390871a7d8cbbd0c170680c841a3640cd5a9c3ec1f63ee0bbb63a217aa980350f8ab052d5b4d9e6713ba353bded4c9f845150ef95165cc85973629e93fbe51b6b206996433e746fd5661dbd2b2a9cded9ce8ebd6703d2274c861a6123e130758832293f361962344ac2e892ec90a759b1ab15da2c768441b10fa68c10695dface2235ac3d498044f8d138d8b1a995195d71af31b4fadbf7899b2fb5f69f60df52139a76b27b61140d4eca6486a751c1b58c928a2126b91811b2818f5153c92d37aa998f2587d794e19adf4acb401b6e167de0e04c5798e566f70aeb92e1e5245fd534639188f345168d0be0606bab59a26bdb0565fcf5b5e95a4f2e08e0279b06888673e3cd851b86cc4eeb20e86e4d349ac6dfdfdffd9f3b045ef4699985f2c3d331133949c084f5e3cc7ada6c23ecd33ca20a6792833974997f5a8276a9240bf10285b8526b15aaa2e42345d7c10c5a947121b95356e6355840dca6d0373b41eae0c50e51c0436970449964a855f9d224173f961280e27db6a2d15ce63aee3d937a502f068ba31df25d45c6b100fb4d05a5d6b0a169b6abdbaa932d0134783af8cbf18180fde2f71ca5ecd4a02c752322eb564b65293c88283c706f01eb4660fe6bb7434b5d1ca1a153ccf03a57b9ee3e9bf4176af312723a5da58e5a265bad34c444c1d9eff74ec262977043e31d4aec08134dd788627903dbe08121089777be236ec4944310796b996abcb698f3fe271289482a0f1bb4c3a4684c6611f57d0d8fe86a1d362097184fffe7c5115f1b9a6762a3604bcef0ed98a8d7d93d5d9e2eefe9130c5eefda67f91208d26af61757950341c89bd612111c1fbf00bea19a5951a61023864ab7737c9b54062e0e03afb2d21333151bfdb9b241e4fff7f01465f5320e928a91313b4aebad1d8c062730eaa3bb921491fde2e3ca14b9365c42a96d679100d074d67fcae0cbef56af06dfa4525f6778cb3f182bbd355475fd505931d6e793a08825d93d9431bf3a2f4cc93aac5d12cef343071c92ba6cffcb22b682ecc69e0c227037a0d731e787f9d86db843cd8d410765411264c7098cff84751a5702d4ba181a86a5dc03b2d2866c48edee6ce2e87d8c2716f2f67a1418d12f50903f2a183f5f85eaef6da52bdecf3952ef82cf57d3e70158beaa0c2ca9a3932354f34cc8a1b85de7028cf05127e3ccde73b716875b2670f18d07a491a786732f3638b9a5225a7056b1944ab5b0d57a98e68b25a9bd76e13d0483f8bfe85d29740b74c3d7ee81716fc92798223df1333d728343ec572f405bf8b5a9dbcc823712114609c70ac2b22fba0ccd0f2eb86ab1d0a3f1ff6fd2c9678c09ce508fc481e8f70f5e9dee0482ba942397bccea74be1298c9ce855d9bfa0b2262e94a3256414e0c3b4f4de4d313d557c5b90312da459570aeac680d9a2f66a584bc190318654272b3526bc2d6d8957e70812855ab79509ea9d9651822490e58dc955946a256d01bf570f200de1936d7052e65b2389c4e2f1248202a0ef9a4fb3c5d208944b80240e42af270daf3a0f22b6d7d30aadc526f3b99206d4b7221625c6d4cc7e5631d2632dae992ec228b331c0e5c4c32c0ceb1f1ad26c34193ca1d3cce34fdbf32ce475b7eeca64e1d26a3631868fcfdded27cd4e6d2cec404d2a27236308664702e512cb7a7b6693bf9d43718b509b063a33171cc6e9a30fb341e39e4190e5f84f9fbf429ee938348dc15dd0e876a8dab35a938aade07ecc91ad0e28dfa06866e6e3f4202fcbc497232661251503d6cdc318ffc45f968cbb7e5e39035a2334b4a09cb7fb5b895fd44a4626c2f9cb7bee2154e9d4b97dd839b5a1eef2f0bb516df840331ac3b7b2d01eb7c0558e81fd48203f0896be1d776e1bf43631825082d3fd663258ad9b252022ddace0da71aef2a88e7ab9d3b87ba842f27a9c151838524180fde5c823a6a2a6234c90a9e0b5f9becb7f778d25ca9e444ea7af121c2b64530ec3e04650448e455a6c4df4d5d355099e3b0110aae59e422836e35a1b49ce206e6b0f4cce52303b84ebb8eeda742a8abd077bdc918042ba36cd5e78b30799029fc218af89a76e5fe92ac4afc7ca4f3dd52324f5bf5e21d6976a14c91ec9ae47a1c32760a5dcb7551c3bac653fdabf9a74c65ac6c2379603ca79e599a23ac88f1621965a343b310fa72e73f742a736e08ca4ca31a45bc4da026e3039a5e3ad1d43d87ba4a61da9ad373902dd823283d0224dd24065d240c14790987e247b27acc6ad4f601e0e7c4ed42325cf46052ea852d05fcef605335569a7994d3744d69e9e83402172cd040d457d55a9e72f95333bd8be0226b5e8c4dbcba64359ef2499e34ca2778acb3abec1d2071373f667f6d061c2b11a5c7958385309e9d3e8ecd586e3b78135a0ff3c15f69ea05b5853bede01211c1a428bcccc9ca7ec1768a02ee356c768323f21657a94a477ec7e4fae97b3cf8be54b28f8fc00dfe8bb8ea679e826d63f105c554723502d5c8ff8b9e5dbd885253d0fcd27a83dca721571ebb2aa63010fec634731728421f982c71adf8fa5df9286ae97fa8fa927a7fbd1f9efcbb77e37c8f4761e02db0370844ed521f95ab859987f8d4fc1d762d0914449d62ed0ae5f44f856004f0718c0b4171b6d09d74a76f97f26aa233f78c99949979fc0c4ad7ead223af590ab6cfb46cb15e46303b4139de7837f709bae68e8180d1e1ac151f575e56ac9ce619cebbfaf76d0c83708e41f1038b0134571cfb7673d4b6b2f6cdc053891f64b33ef9d604caacfeca17e6eb73ea96ea66f32cd85972db179401504b8df11d58281c5cf0ceb6e96644ce608ce4f5eba41d74431847d7f2da1f8a17c805788e726b65357d2db94363ec90f245029c423043deb36194e23679cc1da75714e5196aae121c48c7c176529399faeb6bff617297fee33c287d08f530a489a4eb97cebd47eb5448df69bc423f805e56b759fa4a27d8f161d457fe5e3adc662da391352fb3f567686d84b0d366e3ea506bead1cee37e2a4abba61e52243d975e3e5d9d685a5a08134614cd34a0d0930857e8146c240b4ef81083a0be988f66f81056272742e9326552fddf22e625a2497449e0fd89cfc3bcfb34168d8ff30cb03d732f0f0e29c7af0c5c35c87abffd5e8d4375574a7b9c70169ffae3a65ab40e4726aa3ba2626f3bb946bb051dd25757b5266c5ba92244023b64ccefaac806d59343efad15b3acdbb0db21e7fd1f9d0dd245529624256ef79f032982fd3f1841ecda72dced45768a4b95a28f1c9165c009336a32557c42109804694c309fcf1efa48850bbc62a098c1a1720ef5b69862d53c70fe4ba2e86aedbc1a291673c8c7375106fbd690dc44cc945d3346d0e1fa7e97cce996644e112d634bf0435310347d58cbe178f694e4c252ac16a623fb1f87060f17ffc2d0392e10ef157242a7eb082ad51dfcba72ffbad367e9b65da904d7d8dd2b2bd01cc71706842b8e2ab47d9fd592251ff986c87a5e92c1a666689e47cc756e9af10bf51eaf2a6bb1f3414000429320fbce800405b3a552d0fcbbd88ee280a3441115eb890a9b922bf004527856b32a735f5a3596a1f31cb127c00d3961e9ef0de7ea81b30aa1ce0d80b1475a24e4edb9cc6c2faf3836ff8e7f9e297e293822156c1866ee49fa76d33a984d93c642f7990b05aa46a651b6138a71f04f3761c97ee4b09cf8b34d26cb76a88397400eef52cdc12a5e2d57565d858db2124b81f556ddf83cecb3e053698b524bd391fcea0bc1cb5f97cd8d4d5a4100df9c2a9d2890d854cc7deee1523cfd43b2b34332137fa2ae8b57cc582a61587c3aa5c361726356db8922df56ec6333ddcd14476869edf76e23f327f55703762539ccaf99a72301675902bc0b23b46939b08054909455225b316a089ab25fec7c47d218c8e1d97fa009d71078254ee84ff688596d3d9ca44e7d153a1e77860047887b9c650f94cfef73907907b30460c175feb57dc872c1703596b9cae41fd24bff1c981ba5b0cf6d4ccd16e291764fdc38111cf8c77d1957cf6d90e97327644c798ad6da48ce93923293c99da477cc8f3249ebbdaba6640149a85961e7cd47ae77fe5a39338ace479f48f11e5b0affc32fb2870d76d41c99909111ad04bbda0127a12aeb7e6c319b95fe8cf0d7d21651f0c7b2191abd87a6ebb3324cbfc20c9771a27435167c53487657b0490d21bbdfb1eff1ba22cfdae62d4e4ac8ee1ca704fc1b3d61999601314b50eee4ea9f476b248bb7598862339d6b9dc328f79ea9c0f17ac5c885dc6fa529fcc61147ab2a04c9bfe9314baf4c63553a3f1f2bd125194c22c33bb91b382bb71e2e85a554a7c652c0aff788958b4a757507c3c07d30e8313ed5329dbf750673563bcaa88bd0e477bf9ce9d455ed687b9a1897cf325b90eddc733dfac1e28e0157ff6a0c323373e27e897417daf037aa14c00e51075091f566d5daf51b17080a8ebf0b7a5a15ecad308c4e97783fd6f59fe6f249553f223c208e55f2143bfdc2435347b36be1df9a9686608e373d26a07a0ddebcd49d8bca97a5daa377635304796174904f7154b25bbaf55663d58e03d965f7c9385132c1782fe5e6bd9b15eb2f09f9b130842577c5087fec5fae4b1f9061ce5d73210fc9c2b509bb0e309bedded725c247e7f25681ba2f487862b2e9b599f26e7d4d2e960451a8e023e46590368f4ee9b0fe9c7cb8929f483dbb15ea074487ff85241d2916b6c19e524c000b6585b05d588eca277489fdd0a24f3f95aed52d40d49913cada7f3297a35be8a5ff8294450a5e14b6a8ff82d40a473b719f118df5663a8eb53398e673a1bb7b3e061d68902b8442a495983023719f243279d9bdb3742a4fb66079840f73cfff42f1cacdfc3a80dc9f0d85d79acc338105293b7b69f942cff96b9633ef42586c61793d57379df777c39431e5f3ffee9b9621eaa2e54e0d4c496b3ade33482576b38e1b46f43ac41232b03ece0368d4e13807e4f130db7d4ab41d43dfda1fef57d3849f20595430cad32ceb875c3093e1a0b32a1a2b0699ac4201f8dab12309bfafdb4db01cf7542f959331a4da2cfba941c0c0d3fcccf774d0bf517bb3b94962c4b0342d3e0483b538b30bb5aa3aebb63fa11be3ea5e660e6aa38b44812e6801411b842fa6d3cc11abb47f5c95d1662e71d07dc6af6a89bdda86da188fc10dee7350efad5bbbdc97b035b955691b10a69db0d3ecf91ed844ca2e424c134487adda3fbf528616baec224a7d068fb02b1f9c93c0d914bb3cf3c52d2393464fe7d9f2ed5bdf2d582bfa7ad709db053ee3699d6b78c05b02d77e14353c642143aaea1a6ebc113cb72b300910496402ee87fe8565c80b9d82aabf5b4707527e40ec9775412060c04d7462c11c60fd03be80a999ec89d3c2d6aa3cdc410cf981e3cc48ade471bd28da5b8532c4b780c02b244019785c9580a87dd0b8d1c443e34d0dd454ab82267542cdb37421ea272565b9711e4ceef6aab8c8e5f75c4a5f0aa4c9072a66e34367cbc28ea29629b936584e5f6a805e42fc7437040302aaf98ee232413752dd6612995685e92b6b4d909cfb8f84155cd52d31d60c471a86b910c765426cdeffc8feaae147482dc831d3d8b75d98595b22268ddcaa5a3b6be75ab435ffab2ee866d622e9fcb0518eb5b76b95c9b30ff3f4fd7682a5a2ff4137652748f66825f030c8afe5a552cc58d08194a9eebf047cad68ab5a3d5d3a73af0a578ad6f3e6fa2b63c5e67543db0106691747b8c60a970efdaf3175fc13142dcfc3806380482bbc2600aefe1c47eaca8a4f99020f1682e56b204270d1b94b9dfce0eea858be60853835a6c4b19124bffaab9f1a22e1f8c16041ae612902b7be92750426d258a9f478a2cc4037783ea6128ec46f99d76431e3fd6c4957638c0628d4d455f66d8517696951b01a6a27b3c92fe01809108089b42b738baae28bab71aa7e6fed0ca8f465d269d9133c35f85f4578171ceaf0a1962ec852366d2a6c66627ed14170b5f3673233e68d7b03f9945eb8258e4ad6147929101a18a11ddc0c8884a043558a15fd7dfcd8a9375595193886d734c88358df2e0abb4998185b673ee8c6e639b534a6b233295f535efe67438451a32ad2309713f98102200dc86c850515a2c11c332811468b4a8e4ca4cfe0c0598270211fbea442ea0226d7a94ac1993afa438d7f3e04b373a73ed598e076171c8efb8b012c29843795c32723eb46172a8db3e447d38578b2f53b16ae7ced0ba74e0e3bba5e3e24aa154ecd0638da5b32f42b69325d1ce98a3e6c1af8d8053d1b00938a1a9f776c73a63bce40c3deaa6e6a3dabcb52044c57ec9fc4eb2f0c8e0c53b19a85863a83ebfaf17bb35474e9cfe860c49774753f0350f168a7f7407271a010ac930151e63eafbbf1543de37ea0df5ccb2863188a3fd01701dceba46d165ad4476e30acc8e7cf5e0457b2a6b201cc7b9454cdf33e68de422c104adc97f38cd44e756a4fd9711fd475aba536cdc0c5edf14b20f276f9a420910a7a1b7b6f851e8186a56c96e35f8f65ab7ee89baa42fdc4b97a7f8182ebc253667859694c1341f28c19027f552a224fc167eadae1dd376537d6e8847d209a675c9d51380d0fcad50e3054821e6fa62b65d8735ea5a465383528b7d1e67796ffd689a8e8b0298d5b3607c1ae27332a8a2ba2573bffe4600090a5cc740d68d6e8265e3c5d971e6f2acaab5a9bb79bb2f9093bbdfc92b3db9773fc142397e8e3cbcf21f383eece25e93cd8049eeed7ad945704b505a62179053fbad5d6e7a27693409d79b50da49e4a2c6aadf198d8af6b42772642daa8b9288f9af589d9ceafa61544862d96bb8f2059cdf7322b83a203c9506954b9771c38656a6fea8d93c9ca4612247df9511ca6d82ba6b8242e783106c26943e411763acad19b75a24c1bfd47388b73153c9a6903a48db00b67d134eea0196d860480318e06ba5e9a73f9efbd731732f8c85580b54eb2c97f130c19defeea37e224be09f61b2ec2d7760d77566f4b15a2c8a51ab0827b702e6a28f981b0d184b14b092b13261ea53aa225365df4e84a4098f0d00204b6c0e27f1e024306c0437878ccf5ae7810ac0326ab3134b4f8c7f471ccb4a49d565532c49b3e9a5c57a0b103240aac6bc26c3bb46de8fc7b621321c5480ac133d02cffbc95d65a9845220a5d2c54676ba01c79808da1b93986fbe944cf6ce01502f980f0d2297a977faf17c00989ffb39c550ebf8b3bb69a4b7ea0a3342d2ab3ee59147604a58a735ce57a867e8ccde55a09fbd1c1172442848264486bc359988fabd9285403e76a5eca2870edba993d6c9aea3107eb905e3e0a2bdb6bbc60fcd01788cc62a3c851365df274b8a3b543352f21af2b6d8bc473be423f7b414da42bb97f5c48f5a4881caa8d01c08d9c5d8236c91387e70c2379317b6f0971070ef026a36b5109c3bfaf37407ca253d87eac326f8970a8f8238972e0c9144c9c6c7808a24728f9627753d34c28455720a78f82a2b44b94422c5b17b2281878ef4ce6b5167ff06c79ad5d4c991b4bd34ad3e4fa6a5410305b4f15e28fb08ae8de542e8dcc4cdcf15521d065de83b5baea42503ff38ec3ba78dbacb29a04234b488f94bd3d37d49d89068a41bec52cdb42713a17429619a72744d38708d68172ef72f633fd4adb95a08e6f60bc7d225ecdc77e85289c767618eb8319dca4ec671a10e02df450a1535fa85055bb3aaf7da0959339ce1ac7b9444a4fdf1b6515146cd056d692de6ce0fef56b2e057cf3b414299af657c3bebe2f6d8b9507960b3dd866ee990d680c58a3cf48cff83fcb39ff4167aa606225e183d7ec967698e6718273c9be67596aa2f2a5ead45b877c0f8869cfcc976a2380125f7d0a0d239258278161f72ece7276da44ad42edc5e276afb7f3c06c2b9ac9a38e4a941a73678e7443e917095cae48685d81998f02df2ed8ac3a7212988ffbabfc4eba0c73c1063fb0f91be73b2b390031a6357d2a6146d56ac56c7e72dc226792e5fdf96a973db5aa1b4f76b4e53d2e407ca5d05bc0fa05d0d997c3aa05998cbef0d9558aca05eb0b6d011261d577d964478f4cfdf822b21b5d6dd6f7027cade680067e1c2933c510aea0146bd960433ce3f133a60c1a4147802460e008439f0344147244a34dd1a5ba1971e45ef4a7d3adfa4b49a9eee25502a7b428e49128e3cf9b8e52c19ca1218803dbc090629425909525a4f5c24bcd84f32f8216a72eee7065a6d5c1f50fdc06abb1d24fabeddd5e9087b931913ed13c2e689585e2aec022e6f92abf65d3b9dad5cbcc6d3173177dc6c46abfc9f135b28c11bf23807f4a3dd993ea8332b4d7774bf13253bf8ea9823b9037a8b858d99c17ed7a2665f1daddfa541b93278b34e4ffa14c875ac5a2d9a2e1800942f18b9006723dd971f3321bdc399761eb4e3e85ea36d950cebdff441553829a8d8de9db960d87d386f86a720d4ec897723662c173794255bee06ff176b27ebb2fb4b8aaec08f1f5636887cd54762b2446fb0752a1c996b5c8f14302e090b2b2591dfdad9d36e7fd93982f9004373ceedf19e95b3786761acc84b9710b352a535338e3b75447b22c0874137415e7b62ca21ad36df1ef388c27ab4dac2237a6830a85afe1ba4b679bb43ded918c54f4e2d19ddcacb977a5b1294f242306377547c021882b891492d680f4fdcb76432d64344e1ada4e718f9f1d9104192ee2a5b9344ce31b40cca8aeb2db5774617ee67d21ad549eb10e2e2b4a825d4f929418d660cc805be6a274384d2c903e5befba1fae82ff0a8da2ea2e0c74c948f67457e68c4468ce01d1dd348c9ed47a51ad3a70b9905ba0cea4b47d9f01924cad931e504f24291d9afd3a75b3a333fcb99fb965db06e03bbbe7fcf14020a025af7cf9ccfaeda8dc29a61fc6822f14ec1c4dbf6562b3843e3b96a95d1c65651282baaadda200a081e33b06ad5ef7508134c7c7b74b0814ef54a7c510a712e1685dd86ead0b7dfd46ae442918a5519156dd546b805d42b799b23daf585291f41e146a547101f87e1d981487875f38b11f1362a520bee82e69722638e618fbbc23cd0dd037c6cab1443a4f004ea59d2b6ffa8c825e6bb553cc03bfdd4631b17d660d827a99526bf0309da141b04f465606c7fcb033b84cff03c3b7d574bb5a207a8da6162c42819536e59a0166ff6d59592098b216b4cabed5e9a84ea8c7517c71630bc1e05b25134b97479631498495e849816c39a68c439a9dc0b717dc50dc136b3c7c84997306977b087a8ea3a4d45465ae640faf364fcdefc32d6aefbc8b2bc0530a3195fb38230f4071db3b1a1b002371f26c6b2fb6f019f15a41d7d99ed9ddfbc6ac2b889be3ac8aef74d40b4dc356a7c22c47d94b468a40209b87c5fb27da906cb2c0961df008ba4eb775a7a85def3deef816352962518fc778bb92be27f2da9aa218b453b4043b306fa3d73bde20baea092021fb3b24eefbb8d5ebfba0a80dee83a859352d2bbbd2b21b973e4bf27c6e41c516f24ec407c85897fcf1ef4fd54ef75d3c64eb6fee46948927891310aa8f4e50b1a2ceebc76710b4a18786b33084a1449d30794f938d44185492125bb0dc24a69856583809ba8336ac95ec3893cf65c508dee3820affe4661170818d26d5613a28be393242aa6cc666d9c95f5cbaa150499f418e1871ce824a2acaa802f83753dd83e5810fd4277cc6b92b13481c57e5826c7e2c6b46482879d856407cba843704e47047c8844efe68c215b02cfac454e82666cd926f174e64b37f5cc58338cbb89704c2328e4929e5a9c5b9928a4050920bc414692781afa5c67e6d53121f92863b880f6c91208723dae6bb3993d367aff4c3e42309186fcfb42bb70cfd399fb89a0b8894bcd0a9a15b16820e9ddb6d96eca84394c3ac28bd21c34ba064f55d49ed498317c92b10b0de0889d21f768cbe60e522e60a4ea5be0086e5a0e9cb1c9b8fe5eae51200875e3242791c9c98b5afe9eacd085b8c50da71640d2bf599dfe97e5dd7060ac81eae5ce681250875ad9a10f18603bf436174826fd1b2a6ec576e0f76098695559c5c10b731f8327dfa24423030d6b5a4e2606d973a101bbcab6a2c87f53f5b1fabfc2d57783311e1cf478f80b7a8c81485705c834a49c418592927a87417a14c455b28411b2bf5f82075b45294bb2aa90484be85df2f4d03a28c9acf415607f592f08d91f3e711fb4030382e64f8d8b574723c012ac45990cb98620046f98e7b6eb5ea1e8fea24c39f57442802b77158aa5c18723a974584e31257bdc17116f0710bf1b8e1435e6ce53205d051ff142e349b375dcc4bad57c6d8a413fcf91bc653624be9260a0b4768d364489b4d4bed78babdb9dfabf7a058d6085fab9d720853e807d5d9a3fba8e28d0d9aaeb735bf225aa11f0f2362813810556c1e44a9f32a0686e43ec64065dd9d81c92c563531907688c31cab1f5424312f3cd9c1a69664a1f63acfb87a5b3f98aa1aeb233b96538f1d65b4d07e4b904293bf2733c451fac502118848ff60d5f607060eeea21f0bb3397714761b2652dc7806f8e25b1a71cac99d1231deb4d2b1e1ffa510e2bc0f35837c877044f1e7f823859cf72d9718b3423656303859cd6e9a044cd184e0345b953362a5b32b5352a2ac514aa94795f19b90020d9f4e73fabb6d6bb4efd65e1a91922d3ddcb0ee13f0c336fabd1fd529f9d49be5f93ee81ff55e53ef316a0e307b95906b4e595511ed9fdd1868276341923eac54d60d5c4d2c26bf2777659117ad11b1daf4c42b78d8a7ad527a319d14f231968ad4b544dc937ff5b7f1f148bf00c4c34e62c2eabecf85cb4e012a7d03ae30eb58d5f2fc717deb7db40b8f4e2a87625b7e0fbcb5bed866702c0282f9638afd8fb9dd12a093b771b080d9be69b84d9d5d65277a84f0d0515671446a5d0e4cbb429c9bb7c33147d7a7a7bba9f76d92173b2eabea097bd05f6c590b8d27eb44046cd283b4b2452d63814bfa7992d568dc52f30a45061a480743699c6c59868bef5d1e25797297e7f91a42f50aa102129a69145199a8b8cdaa7f2acc8e0caafe496de211f362e309cc996fa1c158dea062a8a6e29f0d92b13f858c2ec8306275b8b8a15c849981668e7e4d13c9102748ecf917f4cffca2605282fb4936caf6d111422e72bb71704d96a0f0ca8746ce533b5229eb3c7f0ae09779106ccd636f448485d53a035cb87ed880113cc7a62ad724820b82c348f1d86b68e") - // @formatter:on + val normal_with_htlcs = Hex.decode( + "9300cf64e81d29328c082f3b98a58b3e156ff01126416bd5a64d4722e4b67db83b692546b8f2d1801e2b2c3ccdb48e91029b424bafefbc7a97dd069cf8a287ab2ecd1a18a5dccb8fc1012fde3d3d65eebae83292d646405b4e696533fcaeab0f689a1a15de7aeab322f0c0e87a503bfae2165a514e9bb956217f12504e16297e1850038dbd4898c14e3da34bb6cbe01ab5aa73759234498ccfba5bc08438d5f8788c69bc1526731fb63949a020d3a8bdd50010369e4ff6b2177ee8793e35474b94ec647502fe5b3ca4e452e27d425a413e06d0ed63a908ec4e992601b5af34778d65d84162cb2d26f8793f47154e514eb6c5ba9e8b97712cf254185161ddba8d360d87ca469a32e6f20d5d91fb75229616156dfdbd955b92a28b288884f99ce0685310e8341b714173f0ec83d87de9502bf75d98ab298f400386c2b1804826794cd68859b2cde5fefb020e8d8117b2789b6f1d95f849c2dcd7dcf097207ae25ec65625bbe5a7465983a5a2197341c5e2542cf29576c9421c163bb69cd8f0f1be6246f96e62cbcb2398870469872ba1c1b10f2533c8321453ed2dee3b62dedccac47cf631568091add2f6408b138662a55ba9140b82d23759990884730ab6b9742c56b8b272944ab4365b8a9fdd67a6e6c3b86fe80c67a4b5e253b302695fb6163cdb1a4b118f16869ce40087cc3590faa07cd6d3c534fd5c91ae89ba1ed627ba0cba45c3a3ca9b49bb6d0fe7d57019a2504c601aacb5bb7deaf4149a6dbf1153da00f174d42f247c6e62b4d448f028900fd6f11fd308e402f5f71593c50320edab2e8e4e3569f6be00947bedb229f2e6cc0e34ffff19a3248f3e9d71b0a3f78d81d7bc6e1ce97cb7f127b65c7d08b5e7b15e9d44f28f590ad9e29e24a2d973e6fc79f2abd86d6915e7d37393c5e280e8fe1854639105bcbeef5c9ece791caa1423e6c66cc351ed4ae5f47d326e537910d786bf738529ab2b9a658b6f3d490103f4082e6021aed15b29f65cefefe0daf7e4f00a321d8d2bda5e3ff142e059b027da3f9c9bfaac311ea0c577a577a9d257adef445c966e301145dd57794aaedcdb1081c89ee86b65de8a515afc6b2514e64575919692ae764440d3e723fe1aeff40206dbd85d46cfe2aae6a9229f08f04bca768b7cdfa5d118cdf6de5c66d44d5d98e73cf593e029ded9b87ad381ab0ce0eab54ad98cf52160b75488943c9ff4e73778cbd60435dfcb4489c8c9081f9ebd650367a8f30eafa036a2357fce1f903eef0172f7ae42d7277e16ed1c095513955316167cd69f3f49dbeb022dffd8f5df8b44d9f78cbc47276a7de62d550854b2832a99b4a42a8ab709dadbe8a5c595105eb90895ae8e3daee024a28f12b4f98ee62f41687b486bd99211d478fe4780a301f79205c87e6ae0c7569721f517ca0fa0eab19727ed01a6ae12f1ae940ba86c4da24cb07aca3f3abe6d9146735b5fb45aaae34f9720ccf3d623635e9abd3408aa40ebdc3305df8624b947811a98e7ee11a3cc2294fd0e9506ecfdc936309cfb0c08b673aaaf139f2458d42b8d8895b72c697cd81462eb246122872b89a241993306fac6953da58965b290b65c47ba9a76c16aa80fbe05c49c410908a899cb0382977926f51e6e95b062e6edea331d8a946ce4ece90564c687e0cce1b64baaef7b8f874493a9437a50a0f333582472634ccd05fa577c6a0f4ad27589ce7ed5ea9dfad20381c2211a1da8b6da908e38a26191d216e9d060b7a288f4f695135678fa48a29b05672fc2d20eb211a4c7f71d7c07299c9262d21cde189390871a7d8cbbd0c170680c841a3640cd5a9c3ec1f63ee0bbb63a217aa980350f8ab052d5b4d9e6713ba353bded4c9f845150ef95165cc85973629e93fbe51b6b206996433e746fd5661dbd2b2a9cded9ce8ebd6703d2274c861a6123e130758832293f361962344ac2e892ec90a759b1ab15da2c768441b10fa68c10695dface2235ac3d498044f8d138d8b1a995195d71af31b4fadbf7899b2fb5f69f60df52139a76b27b61140d4eca6486a751c1b58c928a2126b91811b2818f5153c92d37aa998f2587d794e19adf4acb401b6e167de0e04c5798e566f70aeb92e1e5245fd534639188f345168d0be0606bab59a26bdb0565fcf5b5e95a4f2e08e0279b06888673e3cd851b86cc4eeb20e86e4d349ac6dfdfdffd9f3b045ef4699985f2c3d331133949c084f5e3cc7ada6c23ecd33ca20a6792833974997f5a8276a9240bf10285b8526b15aaa2e42345d7c10c5a947121b95356e6355840dca6d0373b41eae0c50e51c0436970449964a855f9d224173f961280e27db6a2d15ce63aee3d937a502f068ba31df25d45c6b100fb4d05a5d6b0a169b6abdbaa932d0134783af8cbf18180fde2f71ca5ecd4a02c752322eb564b65293c88283c706f01eb4660fe6bb7434b5d1ca1a153ccf03a57b9ee3e9bf4176af312723a5da58e5a265bad34c444c1d9eff74ec262977043e31d4aec08134dd788627903dbe08121089777be236ec4944310796b996abcb698f3fe271289482a0f1bb4c3a4684c6611f57d0d8fe86a1d362097184fffe7c5115f1b9a6762a3604bcef0ed98a8d7d93d5d9e2eefe9130c5eefda67f91208d26af61757950341c89bd612111c1fbf00bea19a5951a61023864ab7737c9b54062e0e03afb2d21333151bfdb9b241e4fff7f01465f5320e928a91313b4aebad1d8c062730eaa3bb921491fde2e3ca14b9365c42a96d679100d074d67fcae0cbef56af06dfa4525f6778cb3f182bbd355475fd505931d6e793a08825d93d9431bf3a2f4cc93aac5d12cef343071c92ba6cffcb22b682ecc69e0c227037a0d731e787f9d86db843cd8d410765411264c7098cff84751a5702d4ba181a86a5dc03b2d2866c48edee6ce2e87d8c2716f2f67a1418d12f50903f2a183f5f85eaef6da52bdecf3952ef82cf57d3e70158beaa0c2ca9a3932354f34cc8a1b85de7028cf05127e3ccde73b716875b2670f18d07a491a786732f3638b9a5225a7056b1944ab5b0d57a98e68b25a9bd76e13d0483f8bfe85d29740b74c3d7ee81716fc92798223df1333d728343ec572f405bf8b5a9dbcc823712114609c70ac2b22fba0ccd0f2eb86ab1d0a3f1ff6fd2c9678c09ce508fc481e8f70f5e9dee0482ba942397bccea74be1298c9ce855d9bfa0b2262e94a3256414e0c3b4f4de4d313d557c5b90312da459570aeac680d9a2f66a584bc190318654272b3526bc2d6d8957e70812855ab79509ea9d9651822490e58dc955946a256d01bf570f200de1936d7052e65b2389c4e2f1248202a0ef9a4fb3c5d208944b80240e42af270daf3a0f22b6d7d30aadc526f3b99206d4b7221625c6d4cc7e5631d2632dae992ec228b331c0e5c4c32c0ceb1f1ad26c34193ca1d3cce34fdbf32ce475b7eeca64e1d26a3631868fcfdded27cd4e6d2cec404d2a27236308664702e512cb7a7b6693bf9d43718b509b063a33171cc6e9a30fb341e39e4190e5f84f9fbf429ee938348dc15dd0e876a8dab35a938aade07ecc91ad0e28dfa06866e6e3f4202fcbc497232661251503d6cdc318ffc45f968cbb7e5e39035a2334b4a09cb7fb5b895fd44a4626c2f9cb7bee2154e9d4b97dd839b5a1eef2f0bb516df840331ac3b7b2d01eb7c0558e81fd48203f0896be1d776e1bf43631825082d3fd663258ad9b252022ddace0da71aef2a88e7ab9d3b87ba842f27a9c151838524180fde5c823a6a2a6234c90a9e0b5f9becb7f778d25ca9e444ea7af121c2b64530ec3e04650448e455a6c4df4d5d355099e3b0110aae59e422836e35a1b49ce206e6b0f4cce52303b84ebb8eeda742a8abd077bdc918042ba36cd5e78b30799029fc218af89a76e5fe92ac4afc7ca4f3dd52324f5bf5e21d6976a14c91ec9ae47a1c32760a5dcb7551c3bac653fdabf9a74c65ac6c2379603ca79e599a23ac88f1621965a343b310fa72e73f742a736e08ca4ca31a45bc4da026e3039a5e3ad1d43d87ba4a61da9ad373902dd823283d0224dd24065d240c14790987e247b27acc6ad4f601e0e7c4ed42325cf46052ea852d05fcef605335569a7994d3744d69e9e83402172cd040d457d55a9e72f95333bd8be0226b5e8c4dbcba64359ef2499e34ca2778acb3abec1d2071373f667f6d061c2b11a5c7958385309e9d3e8ecd586e3b78135a0ff3c15f69ea05b5853bede01211c1a428bcccc9ca7ec1768a02ee356c768323f21657a94a477ec7e4fae97b3cf8be54b28f8fc00dfe8bb8ea679e826d63f105c554723502d5c8ff8b9e5dbd885253d0fcd27a83dca721571ebb2aa63010fec634731728421f982c71adf8fa5df9286ae97fa8fa927a7fbd1f9efcbb77e37c8f4761e02db0370844ed521f95ab859987f8d4fc1d762d0914449d62ed0ae5f44f856004f0718c0b4171b6d09d74a76f97f26aa233f78c99949979fc0c4ad7ead223af590ab6cfb46cb15e46303b4139de7837f709bae68e8180d1e1ac151f575e56ac9ce619cebbfaf76d0c83708e41f1038b0134571cfb7673d4b6b2f6cdc053891f64b33ef9d604caacfeca17e6eb73ea96ea66f32cd85972db179401504b8df11d58281c5cf0ceb6e96644ce608ce4f5eba41d74431847d7f2da1f8a17c805788e726b65357d2db94363ec90f245029c423043deb36194e23679cc1da75714e5196aae121c48c7c176529399faeb6bff617297fee33c287d08f530a489a4eb97cebd47eb5448df69bc423f805e56b759fa4a27d8f161d457fe5e3adc662da391352fb3f567686d84b0d366e3ea506bead1cee37e2a4abba61e52243d975e3e5d9d685a5a08134614cd34a0d0930857e8146c240b4ef81083a0be988f66f81056272742e9326552fddf22e625a2497449e0fd89cfc3bcfb34168d8ff30cb03d732f0f0e29c7af0c5c35c87abffd5e8d4375574a7b9c70169ffae3a65ab40e4726aa3ba2626f3bb946bb051dd25757b5266c5ba92244023b64ccefaac806d59343efad15b3acdbb0db21e7fd1f9d0dd245529624256ef79f032982fd3f1841ecda72dced45768a4b95a28f1c9165c009336a32557c42109804694c309fcf1efa48850bbc62a098c1a1720ef5b69862d53c70fe4ba2e86aedbc1a291673c8c7375106fbd690dc44cc945d3346d0e1fa7e97cce996644e112d634bf0435310347d58cbe178f694e4c252ac16a623fb1f87060f17ffc2d0392e10ef157242a7eb082ad51dfcba72ffbad367e9b65da904d7d8dd2b2bd01cc71706842b8e2ab47d9fd592251ff986c87a5e92c1a666689e47cc756e9af10bf51eaf2a6bb1f3414000429320fbce800405b3a552d0fcbbd88ee280a3441115eb890a9b922bf004527856b32a735f5a3596a1f31cb127c00d3961e9ef0de7ea81b30aa1ce0d80b1475a24e4edb9cc6c2faf3836ff8e7f9e297e293822156c1866ee49fa76d33a984d93c642f7990b05aa46a651b6138a71f04f3761c97ee4b09cf8b34d26cb76a88397400eef52cdc12a5e2d57565d858db2124b81f556ddf83cecb3e053698b524bd391fcea0bc1cb5f97cd8d4d5a4100df9c2a9d2890d854cc7deee1523cfd43b2b34332137fa2ae8b57cc582a61587c3aa5c361726356db8922df56ec6333ddcd14476869edf76e23f327f55703762539ccaf99a72301675902bc0b23b46939b08054909455225b316a089ab25fec7c47d218c8e1d97fa009d71078254ee84ff688596d3d9ca44e7d153a1e77860047887b9c650f94cfef73907907b30460c175feb57dc872c1703596b9cae41fd24bff1c981ba5b0cf6d4ccd16e291764fdc38111cf8c77d1957cf6d90e97327644c798ad6da48ce93923293c99da477cc8f3249ebbdaba6640149a85961e7cd47ae77fe5a39338ace479f48f11e5b0affc32fb2870d76d41c99909111ad04bbda0127a12aeb7e6c319b95fe8cf0d7d21651f0c7b2191abd87a6ebb3324cbfc20c9771a27435167c53487657b0490d21bbdfb1eff1ba22cfdae62d4e4ac8ee1ca704fc1b3d61999601314b50eee4ea9f476b248bb7598862339d6b9dc328f79ea9c0f17ac5c885dc6fa529fcc61147ab2a04c9bfe9314baf4c63553a3f1f2bd125194c22c33bb91b382bb71e2e85a554a7c652c0aff788958b4a757507c3c07d30e8313ed5329dbf750673563bcaa88bd0e477bf9ce9d455ed687b9a1897cf325b90eddc733dfac1e28e0157ff6a0c323373e27e897417daf037aa14c00e51075091f566d5daf51b17080a8ebf0b7a5a15ecad308c4e97783fd6f59fe6f249553f223c208e55f2143bfdc2435347b36be1df9a9686608e373d26a07a0ddebcd49d8bca97a5daa377635304796174904f7154b25bbaf55663d58e03d965f7c9385132c1782fe5e6bd9b15eb2f09f9b130842577c5087fec5fae4b1f9061ce5d73210fc9c2b509bb0e309bedded725c247e7f25681ba2f487862b2e9b599f26e7d4d2e960451a8e023e46590368f4ee9b0fe9c7cb8929f483dbb15ea074487ff85241d2916b6c19e524c000b6585b05d588eca277489fdd0a24f3f95aed52d40d49913cada7f3297a35be8a5ff8294450a5e14b6a8ff82d40a473b719f118df5663a8eb53398e673a1bb7b3e061d68902b8442a495983023719f243279d9bdb3742a4fb66079840f73cfff42f1cacdfc3a80dc9f0d85d79acc338105293b7b69f942cff96b9633ef42586c61793d57379df777c39431e5f3ffee9b9621eaa2e54e0d4c496b3ade33482576b38e1b46f43ac41232b03ece0368d4e13807e4f130db7d4ab41d43dfda1fef57d3849f20595430cad32ceb875c3093e1a0b32a1a2b0699ac4201f8dab12309bfafdb4db01cf7542f959331a4da2cfba941c0c0d3fcccf774d0bf517bb3b94962c4b0342d3e0483b538b30bb5aa3aebb63fa11be3ea5e660e6aa38b44812e6801411b842fa6d3cc11abb47f5c95d1662e71d07dc6af6a89bdda86da188fc10dee7350efad5bbbdc97b035b955691b10a69db0d3ecf91ed844ca2e424c134487adda3fbf528616baec224a7d068fb02b1f9c93c0d914bb3cf3c52d2393464fe7d9f2ed5bdf2d582bfa7ad709db053ee3699d6b78c05b02d77e14353c642143aaea1a6ebc113cb72b300910496402ee87fe8565c80b9d82aabf5b4707527e40ec9775412060c04d7462c11c60fd03be80a999ec89d3c2d6aa3cdc410cf981e3cc48ade471bd28da5b8532c4b780c02b244019785c9580a87dd0b8d1c443e34d0dd454ab82267542cdb37421ea272565b9711e4ceef6aab8c8e5f75c4a5f0aa4c9072a66e34367cbc28ea29629b936584e5f6a805e42fc7437040302aaf98ee232413752dd6612995685e92b6b4d909cfb8f84155cd52d31d60c471a86b910c765426cdeffc8feaae147482dc831d3d8b75d98595b22268ddcaa5a3b6be75ab435ffab2ee866d622e9fcb0518eb5b76b95c9b30ff3f4fd7682a5a2ff4137652748f66825f030c8afe5a552cc58d08194a9eebf047cad68ab5a3d5d3a73af0a578ad6f3e6fa2b63c5e67543db0106691747b8c60a970efdaf3175fc13142dcfc3806380482bbc2600aefe1c47eaca8a4f99020f1682e56b204270d1b94b9dfce0eea858be60853835a6c4b19124bffaab9f1a22e1f8c16041ae612902b7be92750426d258a9f478a2cc4037783ea6128ec46f99d76431e3fd6c4957638c0628d4d455f66d8517696951b01a6a27b3c92fe01809108089b42b738baae28bab71aa7e6fed0ca8f465d269d9133c35f85f4578171ceaf0a1962ec852366d2a6c66627ed14170b5f3673233e68d7b03f9945eb8258e4ad6147929101a18a11ddc0c8884a043558a15fd7dfcd8a9375595193886d734c88358df2e0abb4998185b673ee8c6e639b534a6b233295f535efe67438451a32ad2309713f98102200dc86c850515a2c11c332811468b4a8e4ca4cfe0c0598270211fbea442ea0226d7a94ac1993afa438d7f3e04b373a73ed598e076171c8efb8b012c29843795c32723eb46172a8db3e447d38578b2f53b16ae7ced0ba74e0e3bba5e3e24aa154ecd0638da5b32f42b69325d1ce98a3e6c1af8d8053d1b00938a1a9f776c73a63bce40c3deaa6e6a3dabcb52044c57ec9fc4eb2f0c8e0c53b19a85863a83ebfaf17bb35474e9cfe860c49774753f0350f168a7f7407271a010ac930151e63eafbbf1543de37ea0df5ccb2863188a3fd01701dceba46d165ad4476e30acc8e7cf5e0457b2a6b201cc7b9454cdf33e68de422c104adc97f38cd44e756a4fd9711fd475aba536cdc0c5edf14b20f276f9a420910a7a1b7b6f851e8186a56c96e35f8f65ab7ee89baa42fdc4b97a7f8182ebc253667859694c1341f28c19027f552a224fc167eadae1dd376537d6e8847d209a675c9d51380d0fcad50e3054821e6fa62b65d8735ea5a465383528b7d1e67796ffd689a8e8b0298d5b3607c1ae27332a8a2ba2573bffe4600090a5cc740d68d6e8265e3c5d971e6f2acaab5a9bb79bb2f9093bbdfc92b3db9773fc142397e8e3cbcf21f383eece25e93cd8049eeed7ad945704b505a62179053fbad5d6e7a27693409d79b50da49e4a2c6aadf198d8af6b42772642daa8b9288f9af589d9ceafa61544862d96bb8f2059cdf7322b83a203c9506954b9771c38656a6fea8d93c9ca4612247df9511ca6d82ba6b8242e783106c26943e411763acad19b75a24c1bfd47388b73153c9a6903a48db00b67d134eea0196d860480318e06ba5e9a73f9efbd731732f8c85580b54eb2c97f130c19defeea37e224be09f61b2ec2d7760d77566f4b15a2c8a51ab0827b702e6a28f981b0d184b14b092b13261ea53aa225365df4e84a4098f0d00204b6c0e27f1e024306c0437878ccf5ae7810ac0326ab3134b4f8c7f471ccb4a49d565532c49b3e9a5c57a0b103240aac6bc26c3bb46de8fc7b621321c5480ac133d02cffbc95d65a9845220a5d2c54676ba01c79808da1b93986fbe944cf6ce01502f980f0d2297a977faf17c00989ffb39c550ebf8b3bb69a4b7ea0a3342d2ab3ee59147604a58a735ce57a867e8ccde55a09fbd1c1172442848264486bc359988fabd9285403e76a5eca2870edba993d6c9aea3107eb905e3e0a2bdb6bbc60fcd01788cc62a3c851365df274b8a3b543352f21af2b6d8bc473be423f7b414da42bb97f5c48f5a4881caa8d01c08d9c5d8236c91387e70c2379317b6f0971070ef026a36b5109c3bfaf37407ca253d87eac326f8970a8f8238972e0c9144c9c6c7808a24728f9627753d34c28455720a78f82a2b44b94422c5b17b2281878ef4ce6b5167ff06c79ad5d4c991b4bd34ad3e4fa6a5410305b4f15e28fb08ae8de542e8dcc4cdcf15521d065de83b5baea42503ff38ec3ba78dbacb29a04234b488f94bd3d37d49d89068a41bec52cdb42713a17429619a72744d38708d68172ef72f633fd4adb95a08e6f60bc7d225ecdc77e85289c767618eb8319dca4ec671a10e02df450a1535fa85055bb3aaf7da0959339ce1ac7b9444a4fdf1b6515146cd056d692de6ce0fef56b2e057cf3b414299af657c3bebe2f6d8b9507960b3dd866ee990d680c58a3cf48cff83fcb39ff4167aa606225e183d7ec967698e6718273c9be67596aa2f2a5ead45b877c0f8869cfcc976a2380125f7d0a0d239258278161f72ece7276da44ad42edc5e276afb7f3c06c2b9ac9a38e4a941a73678e7443e917095cae48685d81998f02df2ed8ac3a7212988ffbabfc4eba0c73c1063fb0f91be73b2b390031a6357d2a6146d56ac56c7e72dc226792e5fdf96a973db5aa1b4f76b4e53d2e407ca5d05bc0fa05d0d997c3aa05998cbef0d9558aca05eb0b6d011261d577d964478f4cfdf822b21b5d6dd6f7027cade680067e1c2933c510aea0146bd960433ce3f133a60c1a4147802460e008439f0344147244a34dd1a5ba1971e45ef4a7d3adfa4b49a9eee25502a7b428e49128e3cf9b8e52c19ca1218803dbc090629425909525a4f5c24bcd84f32f8216a72eee7065a6d5c1f50fdc06abb1d24fabeddd5e9087b931913ed13c2e689585e2aec022e6f92abf65d3b9dad5cbcc6d3173177dc6c46abfc9f135b28c11bf23807f4a3dd993ea8332b4d7774bf13253bf8ea9823b9037a8b858d99c17ed7a2665f1daddfa541b93278b34e4ffa14c875ac5a2d9a2e1800942f18b9006723dd971f3321bdc399761eb4e3e85ea36d950cebdff441553829a8d8de9db960d87d386f86a720d4ec897723662c173794255bee06ff176b27ebb2fb4b8aaec08f1f5636887cd54762b2446fb0752a1c996b5c8f14302e090b2b2591dfdad9d36e7fd93982f9004373ceedf19e95b3786761acc84b9710b352a535338e3b75447b22c0874137415e7b62ca21ad36df1ef388c27ab4dac2237a6830a85afe1ba4b679bb43ded918c54f4e2d19ddcacb977a5b1294f242306377547c021882b891492d680f4fdcb76432d64344e1ada4e718f9f1d9104192ee2a5b9344ce31b40cca8aeb2db5774617ee67d21ad549eb10e2e2b4a825d4f929418d660cc805be6a274384d2c903e5befba1fae82ff0a8da2ea2e0c74c948f67457e68c4468ce01d1dd348c9ed47a51ad3a70b9905ba0cea4b47d9f01924cad931e504f24291d9afd3a75b3a333fcb99fb965db06e03bbbe7fcf14020a025af7cf9ccfaeda8dc29a61fc6822f14ec1c4dbf6562b3843e3b96a95d1c65651282baaadda200a081e33b06ad5ef7508134c7c7b74b0814ef54a7c510a712e1685dd86ead0b7dfd46ae442918a5519156dd546b805d42b799b23daf585291f41e146a547101f87e1d981487875f38b11f1362a520bee82e69722638e618fbbc23cd0dd037c6cab1443a4f004ea59d2b6ffa8c825e6bb553cc03bfdd4631b17d660d827a99526bf0309da141b04f465606c7fcb033b84cff03c3b7d574bb5a207a8da6162c42819536e59a0166ff6d59592098b216b4cabed5e9a84ea8c7517c71630bc1e05b25134b97479631498495e849816c39a68c439a9dc0b717dc50dc136b3c7c84997306977b087a8ea3a4d45465ae640faf364fcdefc32d6aefbc8b2bc0530a3195fb38230f4071db3b1a1b002371f26c6b2fb6f019f15a41d7d99ed9ddfbc6ac2b889be3ac8aef74d40b4dc356a7c22c47d94b468a40209b87c5fb27da906cb2c0961df008ba4eb775a7a85def3deef816352962518fc778bb92be27f2da9aa218b453b4043b306fa3d73bde20baea092021fb3b24eefbb8d5ebfba0a80dee83a859352d2bbbd2b21b973e4bf27c6e41c516f24ec407c85897fcf1ef4fd54ef75d3c64eb6fee46948927891310aa8f4e50b1a2ceebc76710b4a18786b33084a1449d30794f938d44185492125bb0dc24a69856583809ba8336ac95ec3893cf65c508dee3820affe4661170818d26d5613a28be393242aa6cc666d9c95f5cbaa150499f418e1871ce824a2acaa802f83753dd83e5810fd4277cc6b92b13481c57e5826c7e2c6b46482879d856407cba843704e47047c8844efe68c215b02cfac454e82666cd926f174e64b37f5cc58338cbb89704c2328e4929e5a9c5b9928a4050920bc414692781afa5c67e6d53121f92863b880f6c91208723dae6bb3993d367aff4c3e42309186fcfb42bb70cfd399fb89a0b8894bcd0a9a15b16820e9ddb6d96eca84394c3ac28bd21c34ba064f55d49ed498317c92b10b0de0889d21f768cbe60e522e60a4ea5be0086e5a0e9cb1c9b8fe5eae51200875e3242791c9c98b5afe9eacd085b8c50da71640d2bf599dfe97e5dd7060ac81eae5ce681250875ad9a10f18603bf436174826fd1b2a6ec576e0f76098695559c5c10b731f8327dfa24423030d6b5a4e2606d973a101bbcab6a2c87f53f5b1fabfc2d57783311e1cf478f80b7a8c81485705c834a49c418592927a87417a14c455b28411b2bf5f82075b45294bb2aa90484be85df2f4d03a28c9acf415607f592f08d91f3e711fb4030382e64f8d8b574723c012ac45990cb98620046f98e7b6eb5ea1e8fea24c39f57442802b77158aa5c18723a974584e31257bdc17116f0710bf1b8e1435e6ce53205d051ff142e349b375dcc4bad57c6d8a413fcf91bc653624be9260a0b4768d364489b4d4bed78babdb9dfabf7a058d6085fab9d720853e807d5d9a3fba8e28d0d9aaeb735bf225aa11f0f2362813810556c1e44a9f32a0686e43ec64065dd9d81c92c563531907688c31cab1f5424312f3cd9c1a69664a1f63acfb87a5b3f98aa1aeb233b96538f1d65b4d07e4b904293bf2733c451fac502118848ff60d5f607060eeea21f0bb3397714761b2652dc7806f8e25b1a71cac99d1231deb4d2b1e1ffa510e2bc0f35837c877044f1e7f823859cf72d9718b3423656303859cd6e9a044cd184e0345b953362a5b32b5352a2ac514aa94795f19b90020d9f4e73fabb6d6bb4efd65e1a91922d3ddcb0ee13f0c336fabd1fd529f9d49be5f93ee81ff55e53ef316a0e307b95906b4e595511ed9fdd1868276341923eac54d60d5c4d2c26bf2777659117ad11b1daf4c42b78d8a7ad527a319d14f231968ad4b544dc937ff5b7f1f148bf00c4c34e62c2eabecf85cb4e012a7d03ae30eb58d5f2fc717deb7db40b8f4e2a87625b7e0fbcb5bed866702c0282f9638afd8fb9dd12a093b771b080d9be69b84d9d5d65277a84f0d0515671446a5d0e4cbb429c9bb7c33147d7a7a7bba9f76d92173b2eabea097bd05f6c590b8d27eb44046cd283b4b2452d63814bfa7992d568dc52f30a45061a480743699c6c59868bef5d1e25797297e7f91a42f50aa102129a69145199a8b8cdaa7f2acc8e0caafe496de211f362e309cc996fa1c158dea062a8a6e29f0d92b13f858c2ec8306275b8b8a15c849981668e7e4d13c9102748ecf917f4cffca2605282fb4936caf6d111422e72bb71704d96a0f0ca8746ce533b5229eb3c7f0ae09779106ccd636f448485d53a035cb87ed880113cc7a62ad724820b82c348f1d86b68e" + ) val state3 = Serialization.decrypt(TestConstants.Bob.nodeParams.nodePrivateKey, normal_with_htlcs, TestConstants.Bob.nodeParams) assertTrue(state3 is Normal) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index 42838f448..3ca383a6b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -1,6 +1,7 @@ package fr.acinq.lightning.serialization import fr.acinq.bitcoin.Block +import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.channel.* @@ -8,11 +9,13 @@ import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.wire.CommitSig import fr.acinq.lightning.wire.LightningMessage -import fr.acinq.lightning.wire.RevokeAndAck import fr.acinq.secp256k1.Hex import kotlinx.serialization.ExperimentalSerializationApi import kotlin.math.max -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertTrue @OptIn(ExperimentalSerializationApi::class) class StateSerializationTestsCommon : LightningTestSuite() { @@ -63,13 +66,16 @@ class StateSerializationTestsCommon : LightningTestSuite() { ) val state = Serialization.deserialize(bin, TestConstants.Alice.nodeParams) assertTrue(state is Normal) + assertEquals(state.commitments.channelConfig, ChannelConfig.standard) + assertEquals(state.commitments.channelFeatures, ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.Wumbo, Feature.ZeroReserveChannels, Feature.ZeroConfChannels))) } @Test fun `maximum number of HTLCs that is safe to use`() { - val (alice, bob) = TestsHelper.reachNormal(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE) + val (alice, bob) = TestsHelper.reachNormal() + assertTrue(bob.commitments.localParams.features.hasFeature(Feature.ChannelBackupClient)) - tailrec fun addHtlcs(sender: Normal, receiver: Normal, amount: MilliSatoshi, count: Int) : Pair = if (count == 0) Pair(sender, receiver) else { + tailrec fun addHtlcs(sender: Normal, receiver: Normal, amount: MilliSatoshi, count: Int): Pair = if (count == 0) Pair(sender, receiver) else { val (p, _) = TestsHelper.addHtlc(amount, sender, receiver) val (alice1, bob1) = p assertTrue(alice1 is Normal && bob1 is Normal) @@ -88,7 +94,7 @@ class StateSerializationTestsCommon : LightningTestSuite() { val (_, actions2) = bob3.process(ChannelEvent.ExecuteCommand(commandSign0)) val commitSig1 = actions2.findOutgoingMessage() - val bina =LightningMessage.encode(commitSig0) + val bina = LightningMessage.encode(commitSig0) val binb = LightningMessage.encode(commitSig1) return max(bina.size, binb.size) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt index f80c22366..3d2d92e41 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt @@ -6,9 +6,11 @@ import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.* +import fr.acinq.lightning.channel.ChannelKeys +import fr.acinq.lightning.channel.Commitments +import fr.acinq.lightning.channel.LocalParams +import fr.acinq.lightning.channel.RemoteParams import fr.acinq.lightning.crypto.Generators -import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx @@ -28,8 +30,6 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class AnchorOutputsTestsCommon { - val channelVersion = ChannelVersion.STANDARD - val local_funding_privkey = PrivateKey.fromHex("30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f374901") val local_funding_pubkey = PublicKey.fromHex(" 023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb") val remote_funding_pubkey = PublicKey.fromHex("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1") @@ -57,7 +57,7 @@ class AnchorOutputsTestsCommon { val local_delayed_privkey = PrivateKey.fromHex("adf3464ce9c2f230fd2582fda4c6965e4993ca5524e8c9580e3df0cf226981ad01") val local_htlc_privkey = Generators.derivePrivKey(local_payment_basepoint_secret, local_per_commitment_point) - val local_payment_privkey = if (channelVersion.hasStaticRemotekey) local_payment_basepoint_secret else local_htlc_privkey + val local_payment_privkey = local_payment_basepoint_secret val local_delayed_payment_privkey = Generators.derivePrivKey(local_delayed_payment_basepoint_secret, local_per_commitment_point) val remote_htlc_privkey = Generators.derivePrivKey(remote_payment_basepoint_secret, local_per_commitment_point) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 851b809ae..4f8334134 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -13,6 +13,7 @@ import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelOrigin +import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.crypto.assertArrayEquals import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat @@ -263,27 +264,40 @@ class LightningCodecsTestsCommon : LightningTestSuite() { // To allow extending all messages with TLV streams, the upfront_shutdown_script was moved to a TLV stream extension // in https://github.com/lightningnetwork/lightning-rfc/pull/714 and made mandatory when including a TLV stream. // We don't make it mandatory at the codec level: it's the job of the actor creating the message to include it. - val defaultEncoded = - ByteVector("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000100000000000000010000000000000001000000000000000100000000000000010000000100010001031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00") + val defaultEncoded = ByteVector( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000100000000000000010000000000000001000000000000000100000000000000010000000100010001031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00" + ) val testCases = mapOf( // legacy encoding without upfront_shutdown_script defaultEncoded to defaultOpen, // empty upfront_shutdown_script - defaultEncoded + ByteVector("0000") to defaultOpen.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))), + defaultEncoded + ByteVector("0000") to defaultOpen.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))), // non-empty upfront_shutdown_script - defaultEncoded + ByteVector("0004 01abcdef") to defaultOpen.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScript(ByteVector("01abcdef"))))), + defaultEncoded + ByteVector("0004 01abcdef") to defaultOpen.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector("01abcdef"))))), // missing upfront_shutdown_script + unknown odd tlv records defaultEncoded + ByteVector("0302002a 050102") to defaultOpen.copy(tlvStream = TlvStream(listOf(), listOf(GenericTlv(3L, ByteVector("002a")), GenericTlv(5L, ByteVector("02"))))), // empty upfront_shutdown_script + unknown odd tlv records defaultEncoded + ByteVector("0000 0302002a 050102") to defaultOpen.copy( tlvStream = TlvStream( - listOf(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)), + listOf(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)), listOf(GenericTlv(3L, ByteVector("002a")), GenericTlv(5L, ByteVector("02"))) ) ), // non-empty upfront_shutdown_script + unknown odd tlv records - defaultEncoded + ByteVector("0002 1234 0303010203") to defaultOpen.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScript(ByteVector("1234"))), listOf(GenericTlv(3L, ByteVector("010203"))))), + defaultEncoded + ByteVector("0002 1234 0303010203") to defaultOpen.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector("1234"))), listOf(GenericTlv(3L, ByteVector("010203"))))), + // channel type + defaultEncoded + ByteVector("0000 0103101000") to defaultOpen.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs)))), + // channel type + channel version + defaultEncoded + ByteVector("0000 0103101000 fe47000001040000000e") to defaultOpen.copy( + tlvStream = TlvStream( + listOf( + ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), + ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), + ChannelTlv.ChannelVersionTlv(ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve) + ) + ) + ), // channel origin tlv records defaultEncoded + ByteVector("fe47000005 2a 0001 187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281 00000000000004d2") to defaultOpen.copy( tlvStream = TlvStream( @@ -320,6 +334,26 @@ class LightningCodecsTestsCommon : LightningTestSuite() { } } + @Test + fun `open_channel channel type fallback to channel version`() { + val defaultOpen = OpenChannel(ByteVector32.Zeroes, ByteVector32.Zeroes, 1.sat, 1.msat, 1.sat, 1L, 1.sat, 1.msat, FeeratePerKw(1.sat), CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte()) + val testCases = listOf( + defaultOpen.copy(tlvStream = TlvStream(listOf(ChannelTlv.ChannelVersionTlv(ChannelType.SupportedChannelType.StaticRemoteKey)))) to ChannelType.SupportedChannelType.StaticRemoteKey, + defaultOpen.copy(tlvStream = TlvStream(listOf(ChannelTlv.ChannelVersionTlv(ChannelType.SupportedChannelType.AnchorOutputs)))) to ChannelType.SupportedChannelType.AnchorOutputs, + defaultOpen.copy( + tlvStream = TlvStream( + listOf( + ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs), + ChannelTlv.ChannelVersionTlv(ChannelType.SupportedChannelType.StaticRemoteKey) + ) + ) + ) to ChannelType.SupportedChannelType.AnchorOutputs, + ) + testCases.forEach { + assertEquals(it.second, it.first.channelType) + } + } + @Test fun `decode invalid open_channel`() { val defaultEncoded = @@ -349,26 +383,26 @@ class LightningCodecsTestsCommon : LightningTestSuite() { ByteVector("000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000100000000000000010000000100010001031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a") val testCases = mapOf( defaultEncoded to defaultAccept, // legacy encoding without upfront_shutdown_script - defaultEncoded + ByteVector("0000") to defaultAccept.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))), // empty upfront_shutdown_script - defaultEncoded + ByteVector("0004 01abcdef") to defaultAccept.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScript(ByteVector("01abcdef"))))), // non-empty upfront_shutdown_script - defaultEncoded + ByteVector("0000 0102002a 030102") to defaultAccept.copy( + defaultEncoded + ByteVector("0000") to defaultAccept.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))), // empty upfront_shutdown_script + defaultEncoded + ByteVector("0004 01abcdef") to defaultAccept.copy(tlvStream = TlvStream(listOf(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector("01abcdef"))))), // non-empty upfront_shutdown_script + defaultEncoded + ByteVector("0000 0102012a 030102") to defaultAccept.copy( tlvStream = TlvStream( - listOf(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)), - listOf(GenericTlv(1L, ByteVector("002a")), GenericTlv(3L, ByteVector("02"))) + listOf(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelType.UnsupportedChannelType(Features(ByteVector("012a"))))), + listOf(GenericTlv(3L, ByteVector("02"))) ) - ), // empty upfront_shutdown_script + unknown odd tlv records + ), // empty upfront_shutdown_script + unknown channel_type + unknown odd tlv records defaultEncoded + ByteVector("0002 1234 0303010203") to defaultAccept.copy( tlvStream = TlvStream( - listOf(ChannelTlv.UpfrontShutdownScript(ByteVector("1234"))), + listOf(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector("1234"))), listOf(GenericTlv(3L, ByteVector("010203"))) ) ), // non-empty upfront_shutdown_script + unknown odd tlv records - defaultEncoded + ByteVector("0303010203 05020123") to defaultAccept.copy( + defaultEncoded + ByteVector("0103101000 0303010203 05020123") to defaultAccept.copy( tlvStream = TlvStream( - listOf(), + listOf(ChannelTlv.ChannelTypeTlv(ChannelType.SupportedChannelType.AnchorOutputs)), listOf(GenericTlv(3L, ByteVector("010203")), GenericTlv(5L, ByteVector("0123"))) ) - ) // no upfront_shutdown_script + unknown odd tlv records + ) // no upfront_shutdown_script + channel type + unknown odd tlv records ) testCases.forEach { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/OpenTlvTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/OpenTlvTestsCommon.kt index 18d0d3fda..54e5d9b79 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/OpenTlvTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/OpenTlvTestsCommon.kt @@ -2,37 +2,78 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.ByteVector32 import fr.acinq.lightning.channel.ChannelOrigin -import fr.acinq.lightning.channel.ChannelVersion +import fr.acinq.lightning.channel.ChannelType import fr.acinq.lightning.crypto.assertArrayEquals import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.sat import fr.acinq.secp256k1.Hex import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class OpenTlvTestsCommon : LightningTestSuite() { + @Test - fun `channel version TLV`() { + fun `channel type TLV`() { val testCases = listOf( - Pair(ChannelVersion.STANDARD, Hex.decode("fe47000001 04 00000007")), - Pair(ChannelVersion.STANDARD or ChannelVersion.ZERO_RESERVE, Hex.decode("fe47000001 04 0000000f")) + Pair(ChannelType.SupportedChannelType.Standard, Hex.decode("0100")), + Pair(ChannelType.SupportedChannelType.StaticRemoteKey, Hex.decode("01021000")), + Pair(ChannelType.SupportedChannelType.AnchorOutputs, Hex.decode("0103101000")), + Pair(ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, Hex.decode("01110500000000000000000000000000141000")), ) @Suppress("UNCHECKED_CAST") - val readers = mapOf( - ChannelTlv.ChannelVersionTlv.tag to ChannelTlv.ChannelVersionTlv.Companion as TlvValueReader + val readers = mapOf(ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader) + val tlvStreamSerializer = TlvStreamSerializer(false, readers) + + testCases.forEach { + val decoded = tlvStreamSerializer.read(it.second) + val encoded = tlvStreamSerializer.write(decoded) + assertArrayEquals(it.second, encoded) + val channelType = decoded.get()?.channelType + assertNotNull(channelType) + assertEquals(it.first, channelType) + } + } + + @Test + fun `invalid channel type TLV`() { + val testCases = listOf( + Hex.decode("010101"), + Hex.decode("01022000"), + Hex.decode("0103202000"), + Hex.decode("0103100000"), + Hex.decode("0103401000"), ) + + @Suppress("UNCHECKED_CAST") + val readers = mapOf(ChannelTlv.ChannelTypeTlv.tag to ChannelTlv.ChannelTypeTlv.Companion as TlvValueReader) + val tlvStreamSerializer = TlvStreamSerializer(false, readers) + + testCases.forEach { + val decoded = tlvStreamSerializer.read(it) + val channelType = decoded.get() + assertNotNull(channelType) + assertTrue(channelType.channelType is ChannelType.UnsupportedChannelType) + } + } + + @Test + fun `channel version TLV (legacy)`() { + val testCases = listOf( + Pair(ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, Hex.decode("fe47000001 04 00000007")), + Pair(ChannelType.SupportedChannelType.AnchorOutputsZeroConfZeroReserve, Hex.decode("fe47000001 04 0000000f")) + ) + + @Suppress("UNCHECKED_CAST") + val readers = mapOf(ChannelTlv.ChannelVersionTlv.tag to ChannelTlv.ChannelVersionTlv.Companion as TlvValueReader) val tlvStreamSerializer = TlvStreamSerializer(false, readers) testCases.forEach { val decoded = tlvStreamSerializer.read(it.second) - val version = decoded.records.mapNotNull { record -> - when (record) { - is ChannelTlv.ChannelVersionTlv -> record.channelVersion - else -> null - } - }.first() - assertEquals(it.first, version) + val channelType = decoded.get()?.channelType + assertEquals(it.first, channelType) } } @@ -50,22 +91,17 @@ class OpenTlvTestsCommon : LightningTestSuite() { ) @Suppress("UNCHECKED_CAST") - val readers = mapOf( - ChannelTlv.ChannelOriginTlv.tag to ChannelTlv.ChannelOriginTlv.Companion as TlvValueReader - ) + val readers = mapOf(ChannelTlv.ChannelOriginTlv.tag to ChannelTlv.ChannelOriginTlv.Companion as TlvValueReader) val tlvStreamSerializer = TlvStreamSerializer(false, readers) testCases.forEach { val decoded = tlvStreamSerializer.read(it.second) val encoded = tlvStreamSerializer.write(decoded) assertArrayEquals(it.second, encoded) - val channelOrigin = decoded.records.mapNotNull { record -> - when (record) { - is ChannelTlv.ChannelOriginTlv -> record.channelOrigin - else -> null - } - }.first() + val channelOrigin = decoded.get()?.channelOrigin + assertNotNull(channelOrigin) assertEquals(it.first, channelOrigin) } } + } diff --git a/src/jvmTest/kotlin/fr/acinq/lightning/Node.kt b/src/jvmTest/kotlin/fr/acinq/lightning/Node.kt index 2434617c0..7c76c9a19 100644 --- a/src/jvmTest/kotlin/fr/acinq/lightning/Node.kt +++ b/src/jvmTest/kotlin/fr/acinq/lightning/Node.kt @@ -147,6 +147,7 @@ object Node { Feature.Wumbo to FeatureSupport.Optional, Feature.StaticRemoteKey to FeatureSupport.Optional, Feature.AnchorOutputs to FeatureSupport.Optional, + Feature.ChannelType to FeatureSupport.Mandatory, Feature.TrampolinePayment to FeatureSupport.Optional, Feature.ZeroReserveChannels to FeatureSupport.Optional, Feature.ZeroConfChannels to FeatureSupport.Optional, diff --git a/src/jvmTest/kotlin/fr/acinq/lightning/db/sqlite/SqliteChannelsDbTestsJvm.kt b/src/jvmTest/kotlin/fr/acinq/lightning/db/sqlite/SqliteChannelsDbTestsJvm.kt index 2d896e80d..5fcb70a7b 100644 --- a/src/jvmTest/kotlin/fr/acinq/lightning/db/sqlite/SqliteChannelsDbTestsJvm.kt +++ b/src/jvmTest/kotlin/fr/acinq/lightning/db/sqlite/SqliteChannelsDbTestsJvm.kt @@ -1,6 +1,5 @@ package fr.acinq.lightning.db.sqlite -import fr.acinq.lightning.channel.ChannelVersion import fr.acinq.lightning.channel.TestsHelper import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite @@ -18,9 +17,9 @@ class SqliteChannelsDbTestsJvm : LightningTestSuite() { fun `basic tests`() { runBlocking { val db = SqliteChannelsDb(TestConstants.Alice.nodeParams, sqliteInMemory()) - val (alice, _) = TestsHelper.reachNormal(ChannelVersion.STANDARD, 1, 1000000.sat) + val (alice, _) = TestsHelper.reachNormal(currentHeight = 1, fundingAmount = 1_000_000.sat) db.addOrUpdateChannel(alice) - val (bob, _) = TestsHelper.reachNormal(ChannelVersion.STANDARD, 2, 2000000.sat) + val (bob, _) = TestsHelper.reachNormal(currentHeight = 2, fundingAmount = 2_000_000.sat) db.addOrUpdateChannel(bob) assertEquals(db.listLocalChannels(), listOf(alice, bob)) }