diff --git a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt index 37b5ab3e8..7d2fe24b0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/Features.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/Features.kt @@ -242,6 +242,13 @@ sealed class Feature { override val scopes: Set get() = setOf(FeatureScope.Init) } + @Serializable + object Quiescence : Feature() { + override val rfcName get() = "option_quiescence" + override val mandatory get() = 34 + override val scopes: Set get() = setOf(FeatureScope.Init, FeatureScope.Node) + } + } @Serializable @@ -320,6 +327,7 @@ data class Features(val activated: Map, val unknown: Se Feature.ChannelBackupClient, Feature.ChannelBackupProvider, Feature.ExperimentalSplice, + Feature.Quiescence ) operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray()) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt index eb6e7889f..1db24c29b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt @@ -203,6 +203,7 @@ data class NodeParams( Feature.PayToOpenClient to FeatureSupport.Optional, Feature.ChannelBackupClient to FeatureSupport.Optional, Feature.ExperimentalSplice to FeatureSupport.Optional, + Feature.Quiescence to FeatureSupport.Mandatory ), dustLimit = 546.sat, maxRemoteDustLimit = 600.sat, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt index e6db5d9a3..6b2d5aa08 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt @@ -129,5 +129,7 @@ sealed class ChannelAction { } data class EmitEvent(val event: ChannelEvents) : ChannelAction() + + object Disconnect : ChannelAction() // @formatter:on } \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt index b59615609..0a3f7643c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt @@ -62,10 +62,11 @@ sealed class ChannelCommand { data class WatchReceived(val watch: WatchEvent) : ChannelCommand() sealed interface ForbiddenDuringSplice + sealed interface ForbiddenDuringQuiescence sealed class Htlc : ChannelCommand() { - data class Add(val amount: MilliSatoshi, val paymentHash: ByteVector32, val cltvExpiry: CltvExpiry, val onion: OnionRoutingPacket, val paymentId: UUID, val commit: Boolean = false) : Htlc(), ForbiddenDuringSplice + data class Add(val amount: MilliSatoshi, val paymentHash: ByteVector32, val cltvExpiry: CltvExpiry, val onion: OnionRoutingPacket, val paymentId: UUID, val commit: Boolean = false) : Htlc(), ForbiddenDuringSplice, ForbiddenDuringQuiescence - sealed class Settlement : Htlc(), ForbiddenDuringSplice { + sealed class Settlement : Htlc(), ForbiddenDuringSplice, ForbiddenDuringQuiescence { abstract val id: Long data class Fulfill(override val id: Long, val r: ByteVector32, val commit: Boolean = false) : Settlement() @@ -81,8 +82,8 @@ sealed class ChannelCommand { sealed class Commitment : ChannelCommand() { object Sign : Commitment(), ForbiddenDuringSplice - data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice - data object CheckHtlcTimeout : Commitment() + data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice, ForbiddenDuringQuiescence + object CheckHtlcTimeout : Commitment() sealed class Splice : Commitment() { data class Request(val replyTo: CompletableDeferred, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, val feerate: FeeratePerKw, val origins: List = emptyList()) : Splice() { val pushAmount: MilliSatoshi = spliceIn?.pushAmount ?: 0.msat @@ -116,7 +117,8 @@ sealed class ChannelCommand { data object InsufficientFunds : Failure() data object InvalidSpliceOutPubKeyScript : Failure() data object SpliceAlreadyInProgress : Failure() - data object ChannelNotIdle : Failure() + data object ConcurrentRemoteSplice : Failure() + data object ChannelNotQuiescent : Failure() data class InvalidLiquidityAds(val reason: ChannelException) : Failure() data class FundingFailure(val reason: FundingContributionFailure) : Failure() data object CannotStartSession : Failure() @@ -130,7 +132,7 @@ sealed class ChannelCommand { } sealed class Close : ChannelCommand() { - data class MutualClose(val scriptPubKey: ByteVector?, val feerates: ClosingFeerates?) : Close() + data class MutualClose(val scriptPubKey: ByteVector?, val feerates: ClosingFeerates?) : Close(), ForbiddenDuringSplice, ForbiddenDuringQuiescence data object ForceClose : Close() } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt index 75ae81213..3f08d7cc2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt @@ -45,7 +45,7 @@ data class InvalidRbfNonInitiator (override val channelId: Byte data class InvalidRbfAttempt (override val channelId: ByteVector32) : ChannelException(channelId, "invalid rbf attempt") data class InvalidSpliceAlreadyInProgress (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice attempt: the current splice attempt must be completed or aborted first") data class InvalidSpliceAbortNotAcked (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice attempt: our previous tx_abort has not been acked") -data class InvalidSpliceChannelNotIdle (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice attempt: channel is not idle") +data class InvalidSpliceNotQuiescent (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice attempt: the channel is not quiescent") data class NoMoreHtlcsClosingInProgress (override val channelId: ByteVector32) : ChannelException(channelId, "cannot send new htlcs, closing in progress") data class ClosingAlreadyInProgress (override val channelId: ByteVector32) : ChannelException(channelId, "closing already in progress") data class CannotCloseWithUnsignedOutgoingHtlcs (override val channelId: ByteVector32) : ChannelException(channelId, "cannot close when there are unsigned outgoing htlc") @@ -89,4 +89,5 @@ data class InvalidFailureCode (override val channelId: Byte data class PleasePublishYourCommitment (override val channelId: ByteVector32) : ChannelException(channelId, "please publish your local commitment") data class CommandUnavailableInThisState (override val channelId: ByteVector32, val state: String) : ChannelException(channelId, "cannot execute command in state=$state") data class ForbiddenDuringSplice (override val channelId: ByteVector32, val command: String?) : ChannelException(channelId, "cannot process $command while splicing") +data class InvalidSpliceRequest (override val channelId: ByteVector32) : ChannelException(channelId, "invalid splice request") // @formatter:on diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt index 038b60313..9e97a062c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt @@ -93,12 +93,66 @@ data class HtlcTxAndSigs(val txinfo: HtlcTx, val localSig: ByteVector64, val rem data class PublishableTxs(val commitTx: CommitTx, val htlcTxsAndSigs: List) /** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */ -data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishableTxs: PublishableTxs) +data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishableTxs: PublishableTxs) { + companion object { + fun fromCommitSig(keyManager: KeyManager.ChannelKeys, params: ChannelParams, fundingTxIndex: Long, + remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, commit: CommitSig, + localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey, log: MDCLogger): Either { + val (localCommitTx, sortedHtlcTxs) = Commitments.makeLocalTxs( + keyManager, + commitTxNumber = localCommitIndex, + params.localParams, + params.remoteParams, + fundingTxIndex = fundingTxIndex, + remoteFundingPubKey = remoteFundingPubKey, + commitInput, + localPerCommitmentPoint = localPerCommitmentPoint, + spec + ) + val sig = Transactions.sign(localCommitTx, keyManager.fundingKey(fundingTxIndex)) + + // no need to compute htlc sigs if commit sig doesn't check out + val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPubKey(fundingTxIndex), remoteFundingPubKey, sig, commit.signature) + when (val check = Transactions.checkSpendable(signedCommitTx)) { + is Try.Failure -> { + log.error(check.error) { "remote signature $commit is invalid" } + return Either.Left(InvalidCommitmentSignature(params.channelId, signedCommitTx.tx.txid)) + } + else -> {} + } + if (commit.htlcSignatures.size != sortedHtlcTxs.size) { + return Either.Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) + } + val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, keyManager.htlcKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) } + val remoteHtlcPubkey = params.remoteParams.htlcBasepoint.deriveForCommitment(localPerCommitmentPoint) + // combine the sigs to make signed txs + val htlcTxsAndSigs = Triple(sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped().map { (htlcTx, localSig, remoteSig) -> + when (htlcTx) { + is HtlcTx.HtlcTimeoutTx -> { + if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isFailure) { + return Either.Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) + } + HtlcTxAndSigs(htlcTx, localSig, remoteSig) + } + is HtlcTx.HtlcSuccessTx -> { + // we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig + // which was created with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY + if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) { + return Either.Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) + } + HtlcTxAndSigs(htlcTx, localSig, remoteSig) + } + } + } + return Either.Right(LocalCommit(localCommitIndex, spec, PublishableTxs(signedCommitTx, htlcTxsAndSigs))) + } + } +} /** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxId, val remotePerCommitmentPoint: PublicKey) { fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo): CommitSig { - val (remoteCommitTx, htlcTxs) = Commitments.makeRemoteTxs( + val (remoteCommitTx, sortedHtlcsTxs) = Commitments.makeRemoteTxs( channelKeys, index, params.localParams, @@ -111,7 +165,6 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI ) val sig = Transactions.sign(remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - val sortedHtlcsTxs = htlcTxs.sortedBy { it.input.outPoint.index } val htlcSigs = sortedHtlcsTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } return CommitSig(params.channelId, sig, htlcSigs.toList()) } @@ -252,8 +305,6 @@ data class Commitment( return hasNoPendingHtlcs() && hasNoPendingFeeUpdate } - fun isIdle(changes: CommitmentChanges): Boolean = hasNoPendingHtlcs() && changes.localChanges.all.isEmpty() && changes.remoteChanges.all.isEmpty() - fun timedOutOutgoingHtlcs(blockHeight: Long): Set { fun expired(add: UpdateAddHtlc) = blockHeight >= add.cltvExpiry.toLong() @@ -411,7 +462,7 @@ data class Commitment( fun sendCommit(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, log: MDCLogger): Pair { // remote commitment will include all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) - val (remoteCommitTx, htlcTxs) = Commitments.makeRemoteTxs( + val (remoteCommitTx, sortedHtlcTxs) = Commitments.makeRemoteTxs( channelKeys, commitTxNumber = remoteCommit.index + 1, params.localParams, @@ -424,7 +475,6 @@ data class Commitment( ) val sig = Transactions.sign(remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) - val sortedHtlcTxs: List = htlcTxs.sortedBy { it.input.outPoint.index } // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remoteNextPerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } @@ -469,62 +519,15 @@ data class Commitment( // receiving money i.e its commit tx has one output for them val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommit.index + 1) - val (localCommitTx, htlcTxs) = Commitments.makeLocalTxs( - channelKeys, - commitTxNumber = localCommit.index + 1, - params.localParams, - params.remoteParams, - fundingTxIndex = fundingTxIndex, - remoteFundingPubKey = remoteFundingPubkey, - commitInput, - localPerCommitmentPoint = localPerCommitmentPoint, - spec - ) - val sig = Transactions.sign(localCommitTx, channelKeys.fundingKey(fundingTxIndex)) - - log.info { - val htlcsIn = spec.htlcs.incomings().map { it.id }.joinToString(",") - val htlcsOut = spec.htlcs.outgoings().map { it.id }.joinToString(",") - "built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txId=${localCommitTx.tx.txid} fundingTxId=$fundingTxId" - } - - // no need to compute htlc sigs if commit sig doesn't check out - val signedCommitTx = Transactions.addSigs(localCommitTx, channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey, sig, commit.signature) - when (val check = Transactions.checkSpendable(signedCommitTx)) { - is Try.Failure -> { - log.error(check.error) { "remote signature $commit is invalid" } - return Either.Left(InvalidCommitmentSignature(params.channelId, signedCommitTx.tx.txid)) - } - else -> {} - } - val sortedHtlcTxs: List = htlcTxs.sortedBy { it.input.outPoint.index } - if (commit.htlcSignatures.size != sortedHtlcTxs.size) { - return Either.Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) - } - val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) } - val remoteHtlcPubkey = params.remoteParams.htlcBasepoint.deriveForCommitment(localPerCommitmentPoint) - // combine the sigs to make signed txs - val htlcTxsAndSigs = Triple(sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped().map { (htlcTx, localSig, remoteSig) -> - when (htlcTx) { - is HtlcTx.HtlcTimeoutTx -> { - if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isFailure) { - return Either.Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) - } - HtlcTxAndSigs(htlcTx, localSig, remoteSig) - } - is HtlcTx.HtlcSuccessTx -> { - // we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig - // which was created with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) { - return Either.Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) - } - HtlcTxAndSigs(htlcTx, localSig, remoteSig) - } + return LocalCommit.fromCommitSig(channelKeys, params, fundingTxIndex, remoteFundingPubkey, commitInput, commit, localCommit.index + 1, spec, localPerCommitmentPoint, log).map { localCommit1 -> + log.info { + val htlcsIn = spec.htlcs.incomings().map { it.id }.joinToString(",") + val htlcsOut = spec.htlcs.outgoings().map { it.id }.joinToString(",") + "built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txid=${localCommit1.publishableTxs.commitTx.tx.txid} fundingTxId=$fundingTxId" } + copy(localCommit = localCommit1) } - val localCommit1 = LocalCommit(localCommit.index + 1, spec, PublishableTxs(signedCommitTx, htlcTxsAndSigs)) - return Either.Right(copy(localCommit = localCommit1)) } } @@ -599,8 +602,10 @@ data class Commitments( } // @formatter:off + fun localIsQuiescent(): Boolean = changes.localChanges.all.isEmpty() + fun remoteIsQuiescent(): Boolean = changes.remoteChanges.all.isEmpty() + fun isQuiescent(): Boolean = localIsQuiescent() && remoteIsQuiescent() // HTLCs and pending changes are the same for all active commitments, so we don't need to loop through all of them. - fun isIdle(): Boolean = active.first().isIdle(changes) fun hasNoPendingHtlcsOrFeeUpdate(): Boolean = active.first().hasNoPendingHtlcsOrFeeUpdate(changes) fun timedOutOutgoingHtlcs(currentHeight: Long): Set = active.first().timedOutOutgoingHtlcs(currentHeight) fun almostTimedOutIncomingHtlcs(currentHeight: Long, fulfillSafety: CltvExpiryDelta): Set = active.first().almostTimedOutIncomingHtlcs(currentHeight, fulfillSafety, changes) @@ -608,6 +613,21 @@ data class Commitments( fun getIncomingHtlcCrossSigned(htlcId: Long): UpdateAddHtlc? = active.first().getIncomingHtlcCrossSigned(htlcId) // @formatter:on + /** + * Whenever we're not sure the `IncomingPaymentHandler` has received our previous `ChannelAction.ProcessIncomingHtlcs`, + * or when we may have ignored the responses from the `IncomingPaymentHandler` (eg. while quiescent or disconnected), + * we need to reprocess those incoming HTLCs. + */ + fun reprocessIncomingHtlcs(): List { + // We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been forwarded to the payment handler). + // They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when we subsequently sign it. + // That's why we need to look in *their* commitment with direction=OUT. + // + // We also need to filter out htlcs that we already settled and signed (the settlement messages are being retransmitted). + val alreadySettled = changes.localChanges.signed.filterIsInstance().map { it.id }.toSet() + return latest.remoteCommit.spec.htlcs.outgoings().filter { !alreadySettled.contains(it.id) }.map { ChannelAction.ProcessIncomingHtlc(it) } + } + fun sendAdd(cmd: ChannelCommand.Htlc.Add, paymentId: UUID, blockHeight: Long): Either> { val maxExpiry = Channel.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(blockHeight) // we don't want to use too high a refund timeout, because our funds will be locked during that time if the payment is never fulfilled diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt index 253510421..d526c5f70 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt @@ -277,14 +277,14 @@ object Helpers { ) } - data class PairOfCommitTxs(val localSpec: CommitmentSpec, val localCommitTx: Transactions.TransactionWithInputInfo.CommitTx, val remoteSpec: CommitmentSpec, val remoteCommitTx: Transactions.TransactionWithInputInfo.CommitTx) + data class PairOfCommitTxs(val localSpec: CommitmentSpec, val localCommitTx: Transactions.TransactionWithInputInfo.CommitTx, val localHtlcTxs: List, val remoteSpec: CommitmentSpec, val remoteCommitTx: Transactions.TransactionWithInputInfo.CommitTx, val remoteHtlcTxs: List) /** * Creates both sides' first commitment transaction. * * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) */ - fun makeCommitTxsWithoutHtlcs( + fun makeCommitTxs( channelKeys: KeyManager.ChannelKeys, channelId: ByteVector32, localParams: LocalParams, @@ -292,6 +292,7 @@ object Helpers { fundingAmount: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi, + localHtlcs: Set, localCommitmentIndex: Long, remoteCommitmentIndex: Long, commitTxFeerate: FeeratePerKw, @@ -301,8 +302,8 @@ object Helpers { remoteFundingPubkey: PublicKey, remotePerCommitmentPoint: PublicKey ): Either { - val localSpec = CommitmentSpec(setOf(), commitTxFeerate, toLocal = toLocal, toRemote = toRemote) - val remoteSpec = CommitmentSpec(setOf(), commitTxFeerate, toLocal = toRemote, toRemote = toLocal) + val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote) + val remoteSpec = CommitmentSpec(localHtlcs.map{ it.opposite() }.toSet(), commitTxFeerate, toLocal = toRemote, toRemote = toLocal) if (!localParams.isInitiator) { // They initiated the channel open, therefore they pay the fee: we need to make sure they can afford it! @@ -319,7 +320,7 @@ object Helpers { val fundingPubKey = channelKeys.fundingPubKey(fundingTxIndex) val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, fundingPubKey, remoteFundingPubkey) val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommitmentIndex) - val localCommitTx = Commitments.makeLocalTxs( + val (localCommitTx, localHtlcTxs) = Commitments.makeLocalTxs( channelKeys, commitTxNumber = localCommitmentIndex, localParams, @@ -329,8 +330,8 @@ object Helpers { commitmentInput, localPerCommitmentPoint = localPerCommitmentPoint, localSpec - ).first - val remoteCommitTx = Commitments.makeRemoteTxs( + ) + val (remoteCommitTx, remoteHtlcTxs) = Commitments.makeRemoteTxs( channelKeys, commitTxNumber = remoteCommitmentIndex, localParams, @@ -340,9 +341,9 @@ object Helpers { commitmentInput, remotePerCommitmentPoint = remotePerCommitmentPoint, remoteSpec - ).first + ) - return Either.Right(PairOfCommitTxs(localSpec, localCommitTx, remoteSpec, remoteCommitTx)) + return Either.Right(PairOfCommitTxs(localSpec, localCommitTx, localHtlcTxs, remoteSpec, remoteCommitTx, remoteHtlcTxs)) } } @@ -961,6 +962,10 @@ object Helpers { // NB: from the point of view of the remote, their incoming htlcs are our outgoing htlcs htlcsInRemoteCommit.incomings().toSet() - localCommit.spec.htlcs.outgoings().toSet() } + revokedCommitPublished.map { it.commitTx.txid }.contains(tx.txid) -> { + // a revoked commitment got confirmed: we will claim its outputs, but we also need to fail htlcs that are pending in the latest commitment + (nextRemoteCommit ?: remoteCommit).spec.htlcs.incomings().toSet() + } remoteCommit.txid == tx.txid -> when (nextRemoteCommit) { null -> emptySet() // their last commitment got confirmed, so no htlcs will be overridden, they will timeout or be fulfilled on chain else -> { @@ -969,10 +974,6 @@ object Helpers { nextRemoteCommit.spec.htlcs.incomings().toSet() - localCommit.spec.htlcs.outgoings().toSet() } } - revokedCommitPublished.map { it.commitTx.txid }.contains(tx.txid) -> { - // a revoked commitment got confirmed: we will claim its outputs, but we also need to fail htlcs that are pending in the latest commitment - (nextRemoteCommit ?: remoteCommit).spec.htlcs.incomings().toSet() - } else -> emptySet() } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 0917d7ef2..8ffff0891 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -5,8 +5,10 @@ import fr.acinq.bitcoin.Script.tail import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForCommitment import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.transactions.CommitmentSpec +import fr.acinq.lightning.transactions.DirectedHtlc import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.* @@ -46,8 +48,8 @@ sealed class SharedFundingInput { } /** The current balances of a [[SharedFundingInput]]. */ -data class SharedFundingInputBalances(val toLocal: MilliSatoshi, val toRemote: MilliSatoshi) { - val fundingAmount: Satoshi = (toLocal + toRemote).truncateToSatoshi() +data class SharedFundingInputBalances(val toLocal: MilliSatoshi, val toRemote: MilliSatoshi, val toHtlcs: MilliSatoshi) { + val fundingAmount: Satoshi = (toLocal + toRemote + toHtlcs).truncateToSatoshi() } /** @@ -155,9 +157,9 @@ sealed class InteractiveTxOutput { data class Remote(override val serialId: Long, override val amount: Satoshi, override val pubkeyScript: ByteVector) : InteractiveTxOutput(), Incoming /** The shared output can be added by us or by our peer, depending on who initiated the protocol. */ - data class Shared(override val serialId: Long, override val pubkeyScript: ByteVector, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi) : InteractiveTxOutput(), Incoming, Outgoing { + data class Shared(override val serialId: Long, override val pubkeyScript: ByteVector, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi, val htlcAmount: MilliSatoshi) : InteractiveTxOutput(), Incoming, Outgoing { // Note that the truncation is a no-op: the sum of balances in a channel must be a satoshi amount. - override val amount: Satoshi = (localAmount + remoteAmount).truncateToSatoshi() + override val amount: Satoshi = (localAmount + remoteAmount + htlcAmount).truncateToSatoshi() } } @@ -232,7 +234,7 @@ data class FundingContributions(val inputs: List, v return Either.Left(FundingContributionFailure.InvalidFundingBalances(params.fundingAmount, nextLocalBalance, nextRemoteBalance)) } - val sharedOutput = listOf(InteractiveTxOutput.Shared(0, fundingPubkeyScript, nextLocalBalance, nextRemoteBalance)) + val sharedOutput = listOf(InteractiveTxOutput.Shared(0, fundingPubkeyScript, nextLocalBalance, nextRemoteBalance, sharedUtxo?.second?.toHtlcs ?: 0.msat)) val nonChangeOutputs = localOutputs.map { o -> InteractiveTxOutput.Local.NonChange(0, o.amount, o.publicKeyScript) } val changeOutput = when (changePubKey) { null -> listOf() @@ -462,6 +464,7 @@ data class InteractiveTxSession( val previousFunding: SharedFundingInputBalances, val toSend: List>, val previousTxs: List = listOf(), + val localHtlcs: Set, val localInputs: List = listOf(), val remoteInputs: List = listOf(), val localOutputs: List = listOf(), @@ -492,15 +495,17 @@ data class InteractiveTxSession( fundingParams: InteractiveTxParams, previousLocalBalance: MilliSatoshi, previousRemoteBalance: MilliSatoshi, + localHtlcs: Set, fundingContributions: FundingContributions, previousTxs: List = listOf() ) : this( channelKeys, swapInKeys, fundingParams, - SharedFundingInputBalances(previousLocalBalance, previousRemoteBalance), + SharedFundingInputBalances(previousLocalBalance, previousRemoteBalance, localHtlcs.map { it.add.amountMsat }.sum()), fundingContributions.inputs.map { i -> Either.Left(i) } + fundingContributions.outputs.map { o -> Either.Right(o) }, - previousTxs + previousTxs, + localHtlcs ) val isComplete: Boolean = txCompleteSent && txCompleteReceived @@ -593,7 +598,7 @@ data class InteractiveTxSession( } else if (message.pubkeyScript == fundingParams.fundingPubkeyScript(channelKeys)) { val localAmount = previousFunding.toLocal + fundingParams.localContribution.toMilliSatoshi() val remoteAmount = previousFunding.toRemote + fundingParams.remoteContribution.toMilliSatoshi() - Either.Right(InteractiveTxOutput.Shared(message.serialId, message.pubkeyScript, localAmount, remoteAmount)) + Either.Right(InteractiveTxOutput.Shared(message.serialId, message.pubkeyScript, localAmount, remoteAmount, previousFunding.toHtlcs)) } else { Either.Right(InteractiveTxOutput.Remote(message.serialId, message.amount, message.pubkeyScript)) } @@ -772,27 +777,28 @@ data class InteractiveTxSigningSession( fun receiveCommitSig(channelKeys: KeyManager.ChannelKeys, channelParams: ChannelParams, remoteCommitSig: CommitSig, currentBlockHeight: Long, logger: MDCLogger): Pair { return when (localCommit) { is Either.Left -> { - val fundingKey = channelKeys.fundingKey(fundingTxIndex) - val localSigOfLocalTx = Transactions.sign(localCommit.value.commitTx, fundingKey) - val signedLocalCommitTx = Transactions.addSigs(localCommit.value.commitTx, fundingKey.publicKey(), fundingParams.remoteFundingPubkey, localSigOfLocalTx, remoteCommitSig.signature) - when (Transactions.checkSpendable(signedLocalCommitTx)) { - is Try.Failure -> { + val localCommitIndex = localCommit.value.index + val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex) + when (val signedLocalCommit = LocalCommit.fromCommitSig(channelKeys, channelParams, fundingTxIndex, fundingParams.remoteFundingPubkey, commitInput, remoteCommitSig, localCommitIndex, localCommit.value.spec, localPerCommitmentPoint, logger)) { + is Either.Left -> { + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val localSigOfLocalTx = Transactions.sign(localCommit.value.commitTx, fundingKey) + val signedLocalCommitTx = Transactions.addSigs(localCommit.value.commitTx, fundingKey.publicKey(), fundingParams.remoteFundingPubkey, localSigOfLocalTx, remoteCommitSig.signature) logger.info { "interactiveTxSession=$this" } logger.info { "channelParams=$channelParams" } logger.info { "fundingKey=${fundingKey.publicKey()}" } logger.info { "localSigOfLocalTx=$localSigOfLocalTx" } logger.info { "signedLocalCommitTx=$signedLocalCommitTx" } - Pair(this, InteractiveTxSigningSessionAction.AbortFundingAttempt(InvalidCommitmentSignature(fundingParams.channelId, signedLocalCommitTx.tx.txid))) + Pair(this, InteractiveTxSigningSessionAction.AbortFundingAttempt(signedLocalCommit.value)) } - is Try.Success -> { - val signedLocalCommit = LocalCommit(localCommit.value.index, localCommit.value.spec, PublishableTxs(signedLocalCommitTx, listOf())) + is Either.Right -> { if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) { val fundingStatus = LocalFundingStatus.UnconfirmedFundingTx(fundingTx, fundingParams, currentBlockHeight) - val commitment = Commitment(fundingTxIndex, fundingParams.remoteFundingPubkey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, nextRemoteCommit = null) + val commitment = Commitment(fundingTxIndex, fundingParams.remoteFundingPubkey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit.value, remoteCommit, nextRemoteCommit = null) val action = InteractiveTxSigningSessionAction.SendTxSigs(fundingStatus, commitment, fundingTx.localSigs) - Pair(this.copy(localCommit = Either.Right(signedLocalCommit)), action) + Pair(this.copy(localCommit = Either.Right(signedLocalCommit.value)), action) } else { - Pair(this.copy(localCommit = Either.Right(signedLocalCommit)), InteractiveTxSigningSessionAction.WaitForTxSigs) + Pair(this.copy(localCommit = Either.Right(signedLocalCommit.value)), InteractiveTxSigningSessionAction.WaitForTxSigs) } } } @@ -831,19 +837,21 @@ data class InteractiveTxSigningSession( localCommitmentIndex: Long, remoteCommitmentIndex: Long, commitTxFeerate: FeeratePerKw, - remotePerCommitmentPoint: PublicKey + remotePerCommitmentPoint: PublicKey, + localHtlcs: Set ): Either> { val channelKeys = channelParams.localParams.channelKeys(keyManager) val unsignedTx = sharedTx.buildUnsignedTx() val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) } val liquidityFees = liquidityLease?.fees?.total?.toMilliSatoshi() ?: 0.msat - return Helpers.Funding.makeCommitTxsWithoutHtlcs( + return Helpers.Funding.makeCommitTxs( channelKeys, channelParams.channelId, channelParams.localParams, channelParams.remoteParams, fundingAmount = sharedTx.sharedOutput.amount, toLocal = sharedTx.sharedOutput.localAmount - localPushAmount + remotePushAmount - liquidityFees, toRemote = sharedTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount + liquidityFees, + localHtlcs = localHtlcs, localCommitmentIndex = localCommitmentIndex, remoteCommitmentIndex = remoteCommitmentIndex, commitTxFeerate, @@ -851,24 +859,32 @@ data class InteractiveTxSigningSession( remoteFundingPubkey = fundingParams.remoteFundingPubkey, remotePerCommitmentPoint = remotePerCommitmentPoint ).map { firstCommitTx -> - val localSigOfRemoteTx = Transactions.sign(firstCommitTx.remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) - val alternativeSigs = Commitments.alternativeFeerates.map { feerate -> - val alternativeSpec = firstCommitTx.remoteSpec.copy(feerate = feerate) - val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( - channelKeys, - remoteCommitmentIndex, - channelParams.localParams, - channelParams.remoteParams, - fundingTxIndex, - fundingParams.remoteFundingPubkey, - firstCommitTx.remoteCommitTx.input, - remotePerCommitmentPoint, - alternativeSpec - ) - val alternativeSig = Transactions.sign(alternativeRemoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) - CommitSigTlv.AlternativeFeerateSig(feerate, alternativeSig) + val localSigOfRemoteCommitTx = Transactions.sign(firstCommitTx.remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } + + val alternativeSigs = if (firstCommitTx.remoteHtlcTxs.isEmpty()) { + val commitSigTlvs = Commitments.alternativeFeerates.map { feerate -> + val alternativeSpec = firstCommitTx.remoteSpec.copy(feerate = feerate) + val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( + channelKeys, + remoteCommitmentIndex, + channelParams.localParams, + channelParams.remoteParams, + fundingTxIndex, + fundingParams.remoteFundingPubkey, + firstCommitTx.remoteCommitTx.input, + remotePerCommitmentPoint, + alternativeSpec + ) + val sig = Transactions.sign(alternativeRemoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + CommitSigTlv.AlternativeFeerateSig(feerate, sig) + } + TlvStream(CommitSigTlv.AlternativeFeerateSigs(commitSigTlvs) as CommitSigTlv) + } else { + TlvStream.empty() } - val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteTx, listOf(), TlvStream(CommitSigTlv.AlternativeFeerateSigs(alternativeSigs))) + val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, alternativeSigs) + // We haven't received the remote commit_sig: we don't have local htlc txs yet. val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf()) val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint) val signedFundingTx = sharedTx.sign(keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) @@ -900,9 +916,30 @@ sealed class RbfStatus { data object RbfAborted : RbfStatus() } +/** We're waiting for the channel to be quiescent. */ +sealed class QuiescenceNegotiation : SpliceStatus() { + abstract class Initiator : QuiescenceNegotiation() { + abstract val command: ChannelCommand.Commitment.Splice.Request + } + abstract class NonInitiator : QuiescenceNegotiation() +} + +/** The channel is quiescent and a splice attempt was initiated. */ +sealed class QuiescentSpliceStatus : SpliceStatus() + sealed class SpliceStatus { data object None : SpliceStatus() - data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : SpliceStatus() + /** We stop sending new updates and wait for our updates to be added to the local and remote commitments. */ + data class QuiescenceRequested(override val command: ChannelCommand.Commitment.Splice.Request) : QuiescenceNegotiation.Initiator() + /** Our updates have been added to the local and remote commitments, we wait for our peer to do the same. */ + data class InitiatorQuiescent(override val command: ChannelCommand.Commitment.Splice.Request) : QuiescenceNegotiation.Initiator() + /** Our peer has asked us to stop sending new updates and wait for our updates to be added to the local and remote commitments. */ + data class ReceivedStfu(val stfu: Stfu) : QuiescenceNegotiation.NonInitiator() + /** Our updates have been added to the local and remote commitments, we wait for our peer to use the now quiescent channel. */ + object NonInitiatorQuiescent : QuiescentSpliceStatus() + /** We told our peer we want to splice funds in the channel. */ + data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : QuiescentSpliceStatus() + /** We both agreed to splice and are building the splice transaction. */ data class InProgress( val replyTo: CompletableDeferred?, val spliceSession: InteractiveTxSession, @@ -910,7 +947,9 @@ sealed class SpliceStatus { val remotePushAmount: MilliSatoshi, val liquidityLease: LiquidityAds.Lease?, val origins: List - ) : SpliceStatus() - data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List) : SpliceStatus() - data object Aborted : SpliceStatus() + ) : QuiescentSpliceStatus() + /** The splice transaction has been negotiated, we're exchanging signatures. */ + data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List) : QuiescentSpliceStatus() + /** The splice attempt was aborted by us, we're waiting for our peer to ack. */ + data object Aborted : QuiescentSpliceStatus() } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index ef8f10e83..d26cb1bb0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -31,9 +31,18 @@ data class Normal( override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input) override fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> { - if (cmd is ChannelCommand.ForbiddenDuringSplice && spliceStatus !is SpliceStatus.None) { - val error = ForbiddenDuringSplice(channelId, cmd::class.simpleName) - return handleCommandError(cmd, error, channelUpdate) + val forbiddenPreSplice = cmd is ChannelCommand.ForbiddenDuringQuiescence && spliceStatus is QuiescenceNegotiation + val forbiddenDuringSplice = cmd is ChannelCommand.ForbiddenDuringSplice && spliceStatus is QuiescentSpliceStatus + if (forbiddenPreSplice || forbiddenDuringSplice) { + return when (cmd) { + is ChannelCommand.Htlc.Settlement -> { + // Htlc settlement commands are ignored and will be replayed when the splice completes. + // This could create issues if we're keeping htlcs that should be settled pending for too long, as they could timeout. + logger.warning { "ignoring ${cmd::class.simpleName} for htlc #${cmd.id} during splice: will be replayed once splice is complete" } + Pair(this@Normal, listOf()) + } + else -> handleCommandError(cmd, ForbiddenDuringSplice(channelId, cmd::class.simpleName), channelUpdate) + } } return when (cmd) { is ChannelCommand.Htlc.Add -> { @@ -100,45 +109,10 @@ data class Normal( is ChannelCommand.Funding.BumpFundingFee -> unhandled(cmd) is ChannelCommand.Commitment.Splice.Request -> when (spliceStatus) { is SpliceStatus.None -> { - if (commitments.isIdle()) { - val parentCommitment = commitments.active.first() - val fundingContribution = FundingContributions.computeSpliceContribution( - isInitiator = true, - commitment = parentCommitment, - walletInputs = cmd.spliceIn?.walletInputs ?: emptyList(), - localOutputs = cmd.spliceOutputs, - targetFeerate = cmd.feerate - ) - if (fundingContribution < 0.sat && parentCommitment.localCommit.spec.toLocal + fundingContribution.toMilliSatoshi() < parentCommitment.localChannelReserve(commitments.params)) { - logger.warning { "cannot do splice: insufficient funds" } - cmd.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds) - Pair(this@Normal, emptyList()) - } else if (cmd.spliceOut?.scriptPubKey?.let { Helpers.Closing.isValidFinalScriptPubkey(it, allowAnySegwit = true) } == false) { - logger.warning { "cannot do splice: invalid splice-out script" } - cmd.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript) - Pair(this@Normal, emptyList()) - } else if (cmd.requestRemoteFunding?.let { r -> r.rate.fees(cmd.feerate, r.fundingAmount, r.fundingAmount).total <= parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } == false) { - val missing = cmd.requestRemoteFunding.let { r -> r.rate.fees(cmd.feerate, r.fundingAmount, r.fundingAmount).total - parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } - logger.warning { "cannot do splice: balance is too low to pay for inbound liquidity (missing=$missing)" } - cmd.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds) - Pair(this@Normal, emptyList()) - } else { - val spliceInit = SpliceInit( - channelId, - fundingContribution = fundingContribution, - lockTime = currentBlockHeight.toLong(), - feerate = cmd.feerate, - fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), - pushAmount = cmd.pushAmount, - requestFunds = cmd.requestRemoteFunding?.requestFunds, - ) - logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution} local.push=${spliceInit.pushAmount} requesting ${cmd.requestRemoteFunding?.fundingAmount ?: 0.sat} from our peer" } - Pair(this@Normal.copy(spliceStatus = SpliceStatus.Requested(cmd, spliceInit)), listOf(ChannelAction.Message.Send(spliceInit))) - } + if (commitments.localIsQuiescent()) { + Pair(this@Normal.copy(spliceStatus = SpliceStatus.InitiatorQuiescent(cmd)), listOf(ChannelAction.Message.Send(Stfu(channelId, initiator = true)))) } else { - logger.warning { "cannot initiate splice, channel not idle" } - cmd.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotIdle) - Pair(this@Normal, emptyList()) + Pair(this@Normal.copy(spliceStatus = SpliceStatus.QuiescenceRequested(cmd)), emptyList()) } } else -> { @@ -148,11 +122,18 @@ data class Normal( } } is ChannelCommand.MessageReceived -> when { - cmd.message is ForbiddenMessageDuringSplice && spliceStatus !is SpliceStatus.None && spliceStatus !is SpliceStatus.Requested -> { - // In case of a race between our splice_init and a forbidden message from our peer, we accept their message, because - // we know they are going to reject our splice attempt - val error = ForbiddenDuringSplice(channelId, cmd.message::class.simpleName) - handleLocalError(cmd, error) + cmd.message is ForbiddenMessageDuringSplice && spliceStatus is QuiescentSpliceStatus -> { + logger.warning { "received forbidden message ${cmd::class.simpleName} during splicing with status ${spliceStatus::class.simpleName}" } + // Instead of force-closing (which would cost us on-chain fees), we try to resolve this issue by disconnecting. + // This will abort the splice attempt if it hasn't been signed yet, and restore the channel to a clean state. + // If the splice attempt was signed, it gives us an opportunity to re-exchange signatures on reconnection before + // the forbidden message. It also provides the opportunity for our peer to update their node to get rid of that + // bug and resume normal execution. + val actions = buildList { + add(ChannelAction.Message.Send(Warning(channelId, ForbiddenDuringSplice(channelId, cmd.message::class.simpleName).message))) + add(ChannelAction.Disconnect) + } + Pair(this@Normal, actions) } else -> when (cmd.message) { is UpdateAddHtlc -> when (val result = commitments.receiveAdd(cmd.message)) { @@ -216,13 +197,25 @@ data class Normal( is List -> when (val result = commitments.receiveCommit(sigs, channelKeys(), logger)) { is Either.Left -> handleLocalError(cmd, result.value) is Either.Right -> { - val nextState = this@Normal.copy(commitments = result.value.first) + val commitments1 = result.value.first + val spliceStatus1 = when { + spliceStatus is SpliceStatus.QuiescenceRequested && commitments1.localIsQuiescent() -> SpliceStatus.InitiatorQuiescent(spliceStatus.command) + spliceStatus is SpliceStatus.ReceivedStfu && commitments1.localIsQuiescent() -> SpliceStatus.NonInitiatorQuiescent + else -> spliceStatus + } + val nextState = this@Normal.copy(commitments = commitments1, spliceStatus = spliceStatus1) val actions = mutableListOf() actions.add(ChannelAction.Storage.StoreState(nextState)) actions.add(ChannelAction.Message.Send(result.value.second)) - if (result.value.first.changes.localHasChanges()) { + if (commitments1.changes.localHasChanges()) { actions.add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign)) } + // If we're now quiescent, we may send our stfu message. + when { + spliceStatus is SpliceStatus.QuiescenceRequested && commitments1.localIsQuiescent() -> actions.add(ChannelAction.Message.Send(Stfu(channelId, initiator = true))) + spliceStatus is SpliceStatus.ReceivedStfu && commitments1.localIsQuiescent() -> actions.add(ChannelAction.Message.Send(Stfu(channelId, initiator = false))) + else -> {} + } Pair(nextState, actions) } } @@ -353,9 +346,113 @@ data class Normal( } } } + is Stfu -> when { + localShutdown != null -> { + logger.warning { "our peer sent stfu but we sent shutdown first" } + // We don't need to do anything, they should accept our shutdown. + Pair(this@Normal, listOf()) + } + !commitments.remoteIsQuiescent() -> { + logger.warning { "our peer sent stfu but is not quiescent" } + val actions = buildList { + add(ChannelAction.Message.Send(Warning(channelId, InvalidSpliceNotQuiescent(channelId).message))) + add(ChannelAction.Disconnect) + } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions) + } + else -> when (spliceStatus) { + is SpliceStatus.None -> { + if (commitments.localIsQuiescent()) { + Pair(this@Normal.copy(spliceStatus = SpliceStatus.NonInitiatorQuiescent), listOf(ChannelAction.Message.Send(Stfu(channelId, initiator = false)))) + } else { + Pair(this@Normal.copy(spliceStatus = SpliceStatus.ReceivedStfu(cmd.message)), emptyList()) + } + } + is SpliceStatus.QuiescenceRequested -> { + // We could keep track of our splice attempt and merge it with the remote splice instead of cancelling it. + // But this is an edge case that should rarely occur, so it's probably not worth the additional complexity. + logger.warning { "our peer initiated quiescence before us, cancelling our splice attempt" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice) + Pair(this@Normal.copy(spliceStatus = SpliceStatus.ReceivedStfu(cmd.message)), emptyList()) + } + is SpliceStatus.InitiatorQuiescent -> { + // if both sides send stfu at the same time, the quiescence initiator is the channel initiator + if (!cmd.message.initiator || commitments.params.localParams.isInitiator) { + if (commitments.isQuiescent()) { + val parentCommitment = commitments.active.first() + val fundingContribution = FundingContributions.computeSpliceContribution( + isInitiator = true, + commitment = parentCommitment, + walletInputs = spliceStatus.command.spliceIn?.walletInputs ?: emptyList(), + localOutputs = spliceStatus.command.spliceOutputs, + targetFeerate = spliceStatus.command.feerate + ) + val commitTxFees = when { + commitments.params.localParams.isInitiator -> Transactions.commitTxFee(commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec) + else -> 0.sat + } + if (parentCommitment.localCommit.spec.toLocal + fundingContribution.toMilliSatoshi() < parentCommitment.localChannelReserve(commitments.params).max(commitTxFees)) { + logger.warning { "cannot do splice: insufficient funds" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds) + val actions = buildList { + add(ChannelAction.Message.Send(Warning(channelId, InvalidSpliceRequest(channelId).message))) + add(ChannelAction.Disconnect) + } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions) + } else if (spliceStatus.command.spliceOut?.scriptPubKey?.let { Helpers.Closing.isValidFinalScriptPubkey(it, allowAnySegwit = true) } == false) { + logger.warning { "cannot do splice: invalid splice-out script" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript) + val actions = buildList { + add(ChannelAction.Message.Send(Warning(channelId, InvalidSpliceRequest(channelId).message))) + add(ChannelAction.Disconnect) + } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions) + } else if (spliceStatus.command.requestRemoteFunding?.let { r -> r.rate.fees(spliceStatus.command.feerate, r.fundingAmount, r.fundingAmount).total <= parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } == false) { + val missing = spliceStatus.command.requestRemoteFunding.let { r -> r.rate.fees(spliceStatus.command.feerate, r.fundingAmount, r.fundingAmount).total - parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } + logger.warning { "cannot do splice: balance is too low to pay for inbound liquidity (missing=$missing)" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds) + Pair(this@Normal, emptyList()) + } else { + val spliceInit = SpliceInit( + channelId, + fundingContribution = fundingContribution, + lockTime = currentBlockHeight.toLong(), + feerate = spliceStatus.command.feerate, + fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), + pushAmount = spliceStatus.command.pushAmount, + requestFunds = spliceStatus.command.requestRemoteFunding?.requestFunds, + ) + logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution} local.push=${spliceInit.pushAmount}" } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.Requested(spliceStatus.command, spliceInit)), listOf(ChannelAction.Message.Send(spliceInit))) + } + } else { + logger.warning { "cannot initiate splice, channel not quiescent" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotQuiescent) + val actions = buildList { + add(ChannelAction.Message.Send(Warning(channelId, InvalidSpliceNotQuiescent(channelId).message))) + add(ChannelAction.Disconnect) + } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions) + } + } else { + logger.warning { "concurrent stfu received and our peer is the channel initiator, cancelling our splice attempt" } + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice) + Pair(this@Normal.copy(spliceStatus = SpliceStatus.NonInitiatorQuiescent), emptyList()) + } + } + else -> { + logger.warning { "ignoring duplicate stfu" } + Pair(this@Normal, emptyList()) + } + } + } is SpliceInit -> when (spliceStatus) { - is SpliceStatus.None -> - if (commitments.isIdle()) { + is SpliceStatus.None -> { + logger.warning { "rejecting splice attempt: quiescence not negotiated" } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidSpliceNotQuiescent(channelId).message)))) + } + is SpliceStatus.NonInitiatorQuiescent -> + if (commitments.isQuiescent()) { logger.info { "accepting splice with remote.amount=${cmd.message.fundingContribution} remote.push=${cmd.message.pushAmount}" } val parentCommitment = commitments.active.first() val spliceAck = SpliceAck( @@ -383,6 +480,7 @@ data class Normal( fundingParams, previousLocalBalance = parentCommitment.localCommit.spec.toLocal, previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, + localHtlcs = parentCommitment.localCommit.spec.htlcs, fundingContributions = FundingContributions(emptyList(), emptyList()), // as non-initiator we don't contribute to this splice for now previousTxs = emptyList() ) @@ -398,8 +496,8 @@ data class Normal( ) Pair(nextState, listOf(ChannelAction.Message.Send(spliceAck))) } else { - logger.info { "rejecting splice attempt: channel is not idle" } - Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidSpliceChannelNotIdle(channelId).message)))) + logger.warning { "rejecting splice attempt: channel is not quiescent" } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidSpliceNotQuiescent(channelId).message)))) } is SpliceStatus.Aborted -> { logger.info { "rejecting splice attempt: our previous tx_abort was not acked" } @@ -446,7 +544,7 @@ data class Normal( channelKeys = channelKeys(), swapInKeys = keyManager.swapInOnChainWallet, params = fundingParams, - sharedUtxo = Pair(sharedInput, SharedFundingInputBalances(toLocal = parentCommitment.localCommit.spec.toLocal, toRemote = parentCommitment.localCommit.spec.toRemote)), + sharedUtxo = Pair(sharedInput, SharedFundingInputBalances(toLocal = parentCommitment.localCommit.spec.toLocal, toRemote = parentCommitment.localCommit.spec.toRemote, toHtlcs = parentCommitment.localCommit.spec.htlcs.map { it.add.amountMsat }.sum())), walletInputs = spliceStatus.command.spliceIn?.walletInputs ?: emptyList(), localOutputs = spliceStatus.command.spliceOutputs, changePubKey = null // we don't want a change output: we're spending every funds available @@ -464,6 +562,7 @@ data class Normal( fundingParams, previousLocalBalance = parentCommitment.localCommit.spec.toLocal, previousRemoteBalance = parentCommitment.localCommit.spec.toRemote, + localHtlcs = parentCommitment.localCommit.spec.htlcs, fundingContributions.value, previousTxs = emptyList() ).send() when (interactiveTxAction) { @@ -515,7 +614,8 @@ data class Normal( localCommitmentIndex = parentCommitment.localCommit.index, remoteCommitmentIndex = parentCommitment.remoteCommit.index, parentCommitment.localCommit.spec.feerate, - parentCommitment.remoteCommit.remotePerCommitmentPoint + parentCommitment.remoteCommit.remotePerCommitmentPoint, + localHtlcs = parentCommitment.localCommit.spec.htlcs ) when (signingSession) { is Either.Left -> { @@ -615,42 +715,55 @@ data class Normal( is SpliceStatus.Requested -> { logger.info { "our peer rejected our splice request: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data}" } spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer(cmd.message.toAscii())) - Pair( - this@Normal.copy(spliceStatus = SpliceStatus.None), - listOf(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message))) - ) + val actions = buildList { + add(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message))) + addAll(endQuiescence()) + } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions) } is SpliceStatus.InProgress -> { logger.info { "our peer aborted the splice attempt: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data}" } spliceStatus.replyTo?.complete(ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer(cmd.message.toAscii())) - Pair( - this@Normal.copy(spliceStatus = SpliceStatus.None), - listOf(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message))) - ) + val actions = buildList { + add(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message))) + addAll(endQuiescence()) + } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions) } is SpliceStatus.WaitingForSigs -> { logger.info { "our peer aborted the splice attempt: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data}" } val nextState = this@Normal.copy(spliceStatus = SpliceStatus.None) - val actions = listOf( - ChannelAction.Storage.StoreState(nextState), - ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message)) - ) + val actions = buildList { + add(ChannelAction.Storage.StoreState(nextState)) + add(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message))) + addAll(endQuiescence()) + } Pair(nextState, actions) } is SpliceStatus.Aborted -> { logger.info { "our peer acked our previous tx_abort" } - Pair( - this@Normal.copy(spliceStatus = SpliceStatus.None), - emptyList() - ) + Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), endQuiescence()) } is SpliceStatus.None -> { logger.info { "our peer wants to abort the splice, but we've already negotiated a splice transaction: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data}" } // We ack their tx_abort but we keep monitoring the funding transaction until it's confirmed or double-spent. - Pair( - this@Normal, - listOf(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message))) - ) + Pair(this@Normal, listOf(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message)))) + } + is SpliceStatus.NonInitiatorQuiescent -> { + logger.info { "our peer aborted their own splice attempt: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data}" } + val actions = buildList { + add(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message))) + addAll(endQuiescence()) + } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions) + } + is QuiescenceNegotiation -> { + logger.info { "our peer aborted the splice during quiescence negotiation, disconnecting: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data}" } + val actions = buildList { + add(ChannelAction.Message.Send(Warning(channelId, UnexpectedInteractiveTxMessage(channelId, cmd.message).message))) + add(ChannelAction.Disconnect) + } + Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions) } } is SpliceLocked -> { @@ -707,6 +820,12 @@ data class Normal( SpliceStatus.None } is SpliceStatus.WaitingForSigs -> spliceStatus + is SpliceStatus.NonInitiatorQuiescent -> SpliceStatus.None + is QuiescenceNegotiation.NonInitiator -> SpliceStatus.None + is QuiescenceNegotiation.Initiator -> { + spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.Disconnected) + SpliceStatus.None + } } // reset the commit_sig batch sigStash = emptyList() @@ -780,6 +899,7 @@ data class Normal( val spliceLocked = SpliceLocked(channelId, action.fundingTx.txId) add(ChannelAction.Message.Send(spliceLocked)) } + addAll(endQuiescence()) } return Pair(nextState, actions) } @@ -813,4 +933,8 @@ data class Normal( } } } + + private fun endQuiescence(): List { + return commitments.reprocessIncomingHtlcs() + } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt index f7cc88be7..6ff1ec5e4 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt @@ -4,7 +4,6 @@ import fr.acinq.lightning.ShortChannelId import fr.acinq.lightning.blockchain.* import fr.acinq.lightning.channel.* import fr.acinq.lightning.crypto.KeyManager -import fr.acinq.lightning.transactions.outgoings import fr.acinq.lightning.utils.Either import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* @@ -428,16 +427,9 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // When a channel is reestablished after a wallet restarts, we need to reprocess incoming HTLCs that may have been only partially processed // (either because they didn't reach the payment handler, or because the payment handler response didn't reach the channel). // Otherwise these HTLCs will stay in our commitment until they timeout and our peer closes the channel. - // - // We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been forwarded to the payment handler). - // They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when we subsequently sign it. - // That's why we need to look in *their* commitment with direction=OUT. - // - // We also need to filter out htlcs that we already settled and signed (the settlement messages are being retransmitted). - val alreadySettled = commitments1.changes.localChanges.signed.filterIsInstance().map { it.id }.toSet() - val htlcsToReprocess = commitments1.latest.remoteCommit.spec.htlcs.outgoings().filter { !alreadySettled.contains(it.id) } - logger.info { "re-processing signed incoming HTLCs: ${htlcsToReprocess.map { it.id }.joinToString(", ")}" } - sendQueue.addAll(htlcsToReprocess.map { ChannelAction.ProcessIncomingHtlc(it) }) + val htlcsToReprocess = commitments1.reprocessIncomingHtlcs() + logger.info { "re-processing signed IN: ${htlcsToReprocess.map { it.add.id }.joinToString()}" } + sendQueue.addAll(htlcsToReprocess) return Pair(commitments1, sendQueue) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt index 02cf439ad..df4a37c19 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt @@ -59,7 +59,7 @@ data class WaitForAcceptChannel( } is Either.Right -> { // The channel initiator always sends the first interactive-tx message. - val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, fundingContributions.value).send() + val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value).send() when (interactiveTxAction) { is InteractiveTxSessionAction.SendMessage -> { val nextState = WaitForFundingCreated( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt index 5730f9604..32c1e6fda 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -107,7 +107,7 @@ data class WaitForFundingConfirmed( addAll(latestFundingTx.sharedTx.tx.localInputs.map { Either.Left(it) }) addAll(latestFundingTx.sharedTx.tx.localOutputs.map { Either.Right(it) }) } - val session = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, SharedFundingInputBalances(0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx }) + val session = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, SharedFundingInputBalances(0.msat, 0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx }, commitments.latest.localCommit.spec.htlcs) val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session)) Pair(nextState, listOf(ChannelAction.Message.Send(TxAckRbf(channelId, fundingParams.localContribution)))) } @@ -142,7 +142,7 @@ data class WaitForFundingConfirmed( Pair(this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message)))) } is Either.Right -> { - val (session, action) = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, contributions.value, previousFundingTxs.map { it.sharedTx }).send() + val (session, action) = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), contributions.value, previousFundingTxs.map { it.sharedTx }).send() when (action) { is InteractiveTxSessionAction.SendMessage -> { val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session)) @@ -180,7 +180,8 @@ data class WaitForFundingConfirmed( localCommitmentIndex = replacedCommitment.localCommit.index, remoteCommitmentIndex = replacedCommitment.remoteCommit.index, replacedCommitment.localCommit.spec.feerate, - replacedCommitment.remoteCommit.remotePerCommitmentPoint + replacedCommitment.remoteCommit.remotePerCommitmentPoint, + replacedCommitment.localCommit.spec.htlcs ) when (signingSession) { is Either.Left -> { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt index 7facc1db0..d67627a67 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -66,7 +66,8 @@ data class WaitForFundingCreated( localCommitmentIndex = 0, remoteCommitmentIndex = 0, commitTxFeerate, - remoteFirstPerCommitmentPoint + remoteFirstPerCommitmentPoint, + emptySet() ) when (signingSession) { is Either.Left -> { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt index 62c47578e..d9a3d6e42 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt @@ -86,7 +86,7 @@ data class WaitForOpenChannel( Pair(Aborted, listOf(ChannelAction.Message.Send(Error(temporaryChannelId, ChannelFundingError(temporaryChannelId).message)))) } is Either.Right -> { - val interactiveTxSession = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, fundingContributions.value) + val interactiveTxSession = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value) val nextState = WaitForFundingCreated( localParams, remoteParams, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index e3b183037..0b48c30fd 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -782,6 +782,11 @@ class Peer( } is ChannelAction.EmitEvent -> nodeParams._nodeEvents.emit(action.event) + + is ChannelAction.Disconnect -> { + logger.warning { "channel disconnected due to a protocol error" } + disconnect() + } } } } 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 0236186c2..90c1e8974 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v2/ChannelState.kt @@ -54,6 +54,7 @@ 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.msat import fr.acinq.lightning.wire.* import kotlinx.serialization.* import kotlinx.serialization.descriptors.SerialDescriptor @@ -403,7 +404,7 @@ internal data class Commitments( // We will put a WatchConfirmed when starting, which will return the confirmed transaction. fr.acinq.lightning.channel.LocalFundingStatus.UnconfirmedFundingTx( fr.acinq.lightning.channel.PartiallySignedSharedTransaction( - fr.acinq.lightning.channel.SharedTransaction(null, InteractiveTxOutput.Shared(0, commitInput.txOut.publicKeyScript, localCommit.spec.toLocal, localCommit.spec.toRemote), listOf(), listOf(), listOf(), listOf(), 0), + fr.acinq.lightning.channel.SharedTransaction(null, InteractiveTxOutput.Shared(0, commitInput.txOut.publicKeyScript, localCommit.spec.toLocal, localCommit.spec.toRemote, 0.msat), listOf(), listOf(), listOf(), listOf(), 0), // We must correctly set the txId here. TxSignatures(channelId, commitInput.outPoint.txid, listOf()), ), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt index dc7becec6..0a548543d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v3/ChannelState.kt @@ -54,6 +54,7 @@ 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.msat import fr.acinq.lightning.wire.* import kotlinx.serialization.* import kotlinx.serialization.descriptors.SerialDescriptor @@ -396,7 +397,7 @@ internal data class Commitments( // We will put a WatchConfirmed when starting, which will return the confirmed transaction. fr.acinq.lightning.channel.LocalFundingStatus.UnconfirmedFundingTx( fr.acinq.lightning.channel.PartiallySignedSharedTransaction( - fr.acinq.lightning.channel.SharedTransaction(null, InteractiveTxOutput.Shared(0, commitInput.txOut.publicKeyScript, localCommit.spec.toLocal, localCommit.spec.toRemote), listOf(), listOf(), listOf(), listOf(), 0), + fr.acinq.lightning.channel.SharedTransaction(null, InteractiveTxOutput.Shared(0, commitInput.txOut.publicKeyScript, localCommit.spec.toLocal, localCommit.spec.toRemote, 0.msat), listOf(), listOf(), listOf(), listOf(), 0), // We must correctly set the txId here. TxSignatures(channelId, commitInput.outPoint.txid, listOf()), ), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index c825933f0..3de5553f3 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -189,7 +189,7 @@ object Deserialization { 0x01 -> SharedFundingInput.Multisig2of2( info = readInputInfo(), fundingTxIndex = readNumber(), - remoteFundingPubkey = readPublicKey(), + remoteFundingPubkey = readPublicKey() ) else -> error("unknown discriminator $discriminator for class ${SharedFundingInput::class}") } @@ -213,7 +213,7 @@ object Deserialization { outPoint = readOutPoint(), sequence = readNumber().toUInt(), localAmount = readNumber().msat, - remoteAmount = readNumber().msat, + remoteAmount = readNumber().msat ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Shared::class}") } @@ -262,6 +262,14 @@ object Deserialization { pubkeyScript = readDelimitedByteArray().toByteVector(), localAmount = readNumber().msat, remoteAmount = readNumber().msat, + htlcAmount = 0.msat + ) + 0x02 -> InteractiveTxOutput.Shared( + serialId = readNumber(), + pubkeyScript = readDelimitedByteArray().toByteVector(), + localAmount = readNumber().msat, + remoteAmount = readNumber().msat, + htlcAmount = readNumber().msat ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxOutput.Shared::class}") } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index eede7f3cc..d2c124804 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -303,11 +303,12 @@ object Serialization { } private fun Output.writeSharedInteractiveTxOutput(o: InteractiveTxOutput.Shared) = o.run { - write(0x01) + write(0x02) writeNumber(serialId) writeDelimited(pubkeyScript.toByteArray()) writeNumber(localAmount.toLong()) writeNumber(remoteAmount.toLong()) + writeNumber(htlcAmount.toLong()) } private fun Output.writeLocalInteractiveTxOutput(o: InteractiveTxOutput.Local) = when (o) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index 016d8a42b..2231a627e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -480,7 +480,7 @@ object Transactions { } .mapNotNull { (it as? TxResult.Success)?.result } - return htlcTimeoutTxs + htlcSuccessTxs + return (htlcTimeoutTxs + htlcSuccessTxs).sortedBy { it.input.outPoint.index } } fun makeClaimHtlcSuccessTx( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index b3d969b63..c81acd705 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -861,6 +861,29 @@ data class ChannelReady( } } +data class Stfu( + override val channelId: ByteVector32, + val initiator: Boolean +) : SetupMessage, HasChannelId { + override val type: Long get() = Stfu.type + + override fun write(out: Output) { + LightningCodecs.writeBytes(channelId, out) + LightningCodecs.writeByte(if (initiator) 1 else 0, out) + } + + companion object : LightningMessageReader { + const val type: Long = 2 + + override fun read(input: Input): Stfu { + return Stfu( + ByteVector32(LightningCodecs.bytes(input, 32)), + LightningCodecs.byte(input) == 1 + ) + } + } +} + data class SpliceInit( override val channelId: ByteVector32, val fundingContribution: Satoshi, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index 7a18a8014..f9906150e 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -32,8 +32,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) assertEquals(0xfffffffdU, inputA1.sequence) @@ -116,8 +116,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Even though the initiator isn't contributing, they're paying the fees for the common parts of the transaction. // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) @@ -177,8 +177,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA + fundingB) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -227,8 +227,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createFixture(fundingA, utxosA, 0.sat, listOf(), targetFeerate, 330.sat, 0) assertEquals(f.fundingParamsA.fundingAmount, fundingA) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -298,8 +298,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -375,8 +375,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -448,8 +448,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -526,8 +526,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -595,7 +595,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { fun `remove input - output`() { val f = createFixture(100_000.sat, listOf(150_000.sat), 0.sat, listOf(), FeeratePerKw(2500.sat), 330.sat, 0) // In this flow we introduce dummy inputs/outputs from Bob to Alice that are then removed. - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -682,7 +682,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxAddInput(f.channelId, 9, previousTx, 2, 0xffffffffU) to InteractiveTxSessionAction.NonReplaceableInput(f.channelId, 9, previousTx.txid, 2, 0xffffffff), ) testCases.forEach { (input, expected) -> - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -702,7 +702,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxAddOutput(f.channelId, 1, 25_000.sat, Script.write(listOf(OP_1)).byteVector()), ) testCases.forEach { output -> - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -722,7 +722,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxAddOutput(f.channelId, 3, 329.sat, validScript) to InteractiveTxSessionAction.OutputBelowDust(f.channelId, 3, 329.sat, 330.sat), ) testCases.forEach { (output, expected) -> - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -743,7 +743,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { TxRemoveInput(f.channelId, 57) to InteractiveTxSessionAction.UnknownSerialId(f.channelId, 57), ) testCases.forEach { (msg, expected) -> - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) // Alice --- tx_add_input --> Bob val (alice1, _) = sendMessage(alice0) // Alice <-- tx_remove_(in|out)put --- Bob @@ -756,7 +756,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { fun `too many protocol rounds`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() - var (alice, _) = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() (1..InteractiveTxSession.MAX_INPUTS_OUTPUTS_RECEIVED).forEach { i -> // Alice --- tx_message --> Bob val (alice1, _) = alice.receive(TxAddOutput(f.channelId, 2 * i.toLong() + 1, 2500.sat, validScript)) @@ -769,7 +769,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `too many inputs`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - var (alice, _) = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() (1..252).forEach { i -> // Alice --- tx_message --> Bob val (alice1, _) = alice.receive(createTxAddInput(f.channelId, 2 * i.toLong() + 1, 5000.sat)) @@ -785,7 +785,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `too many outputs`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - var (alice, _) = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA).send() val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() (1..252).forEach { i -> // Alice --- tx_message --> Bob @@ -804,7 +804,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { fun `missing funding output`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob @@ -817,7 +817,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `multiple funding outputs`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob @@ -835,7 +835,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val spliceOutputA = TxOut(20_000.sat, Script.pay2wpkh(randomKey().publicKey())) val subtractedFundingA = 25_000.sat val f = createSpliceFixture(balanceA, -subtractedFundingA, listOf(), listOf(spliceOutputA), 0.msat, 0.sat, listOf(), listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, balanceA, f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_output --> Bob val (bob1, _) = receiveMessage(bob0, TxAddOutput(f.channelId, 0, 75_000.sat, f.fundingParamsB.fundingPubkeyScript(f.channelKeysB))) // Alice --- tx_add_output --> Bob @@ -848,8 +848,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `swap-in input missing user key`() { val f = createFixture(100_000.sat, listOf(150_000.sat), 0.sat, listOf(), FeeratePerKw(2500.sat), 330.sat, 0) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -878,7 +878,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `invalid funding amount`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) // Alice --- tx_add_output --> Bob @@ -899,8 +899,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -939,7 +939,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `missing previous tx`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_output --> Bob val failure = receiveInvalidMessage(bob0, TxAddInput(f.channelId, 0, null, 3, 0u)) assertIs(failure) @@ -948,7 +948,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `total input amount too low`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) @@ -964,7 +964,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `minimum fee not met`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, createTxAddInput(f.channelId, 0, 150_000.sat)) @@ -981,7 +981,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `previous attempts not double-spent`() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) - val sharedOutput = InteractiveTxOutput.Shared(0, f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), 100_000_000.msat, 0.msat) + val sharedOutput = InteractiveTxOutput.Shared(0, f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), 100_000_000.msat, 0.msat, 0.msat) val previousTx1 = Transaction(2, listOf(), listOf(TxOut(150_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) val previousTx2 = Transaction(2, listOf(), listOf(TxOut(160_000.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(175_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() @@ -995,7 +995,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { SharedTransaction(null, sharedOutput, listOf(), firstAttempt.tx.remoteInputs + listOf(InteractiveTxInput.RemoteOnly(4, OutPoint(previousTx2, 1), TxOut(150_000.sat, validScript), 0u)), listOf(), listOf(), 0), TxSignatures(f.channelId, TxId(randomBytes32()), listOf()), ) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, f.fundingContributionsB, listOf(firstAttempt, secondAttempt)) + val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB, listOf(firstAttempt, secondAttempt)) // Alice --- tx_add_input --> Bob val (bob1, _) = receiveMessage(bob0, TxAddInput(f.channelId, 4, previousTx2, 1, 0u)) // Alice --- tx_add_output --> Bob @@ -1013,7 +1013,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val parentTx = Transaction.read( "02000000000101f86fd1d0db3ac5a72df968622f31e6b5e6566a09e29206d7c7a55df90e181de800000000171600141fb9623ffd0d422eacc450fd1e967efc477b83ccffffffff0580b2e60e00000000220020fd89acf65485df89797d9ba7ba7a33624ac4452f00db08107f34257d33e5b94680b2e60e0000000017a9146a235d064786b49e7043e4a042d4cc429f7eb6948780b2e60e00000000160014fbb4db9d85fba5e301f4399e3038928e44e37d3280b2e60e0000000017a9147ecd1b519326bc13b0ec716e469b58ed02b112a087f0006bee0000000017a914f856a70093da3a5b5c4302ade033d4c2171705d387024730440220696f6cee2929f1feb3fd6adf024ca0f9aa2f4920ed6d35fb9ec5b78c8408475302201641afae11242160101c6f9932aeb4fcd1f13a9c6df5d1386def000ea259a35001210381d7d5b1bc0d7600565d827242576d9cb793bfe0754334af82289ee8b65d137600000000" ) - val sharedOutput = InteractiveTxOutput.Shared(44, ByteVector("0020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec5"), 200_000_000_000.msat, 200_000_000_000.msat) + val sharedOutput = InteractiveTxOutput.Shared(44, ByteVector("0020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec5"), 200_000_000_000.msat, 200_000_000_000.msat, 0.msat) val initiatorTx = run { val initiatorInput = InteractiveTxInput.LocalOnly(20, parentTx, 0, 4294967293u) @@ -1140,7 +1140,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val sharedInputA = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, channelKeysB.fundingPubKey(fundingTxIndex)) val nextFundingPubkeyB = channelKeysB.fundingPubKey(fundingTxIndex + 1) val fundingParamsA = InteractiveTxParams(channelId, true, fundingContributionA, fundingContributionB, sharedInputA, nextFundingPubkeyB, outputsA, lockTime, dustLimit, targetFeerate) - return FundingContributions.create(channelKeysA, swapInKeysA, fundingParamsA, Pair(sharedInputA, SharedFundingInputBalances(balanceA, balanceB)), listOf(), outputsA, randomKey().publicKey()) + return FundingContributions.create(channelKeysA, swapInKeysA, fundingParamsA, Pair(sharedInputA, SharedFundingInputBalances(balanceA, balanceB, 0.msat)), listOf(), outputsA, randomKey().publicKey()) } private fun createSpliceFixture( @@ -1178,10 +1178,10 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingParamsA = InteractiveTxParams(channelId, true, fundingContributionA, fundingContributionB, sharedInputA, nextFundingPubkeyB, outputsA, lockTime, dustLimit, targetFeerate) val fundingParamsB = InteractiveTxParams(channelId, false, fundingContributionB, fundingContributionA, sharedInputB, nextFundingPubkeyA, outputsB, lockTime, dustLimit, targetFeerate) val walletA = createWallet(swapInKeysA, utxosA) - val contributionsA = FundingContributions.create(channelKeysA, swapInKeysA, fundingParamsA, Pair(sharedInputA, SharedFundingInputBalances(balanceA, balanceB)), walletA, outputsA, randomKey().publicKey()) + val contributionsA = FundingContributions.create(channelKeysA, swapInKeysA, fundingParamsA, Pair(sharedInputA, SharedFundingInputBalances(balanceA, balanceB, 0.msat)), walletA, outputsA, randomKey().publicKey()) assertNotNull(contributionsA.right) val walletB = createWallet(swapInKeysB, utxosB) - val contributionsB = FundingContributions.create(channelKeysB, swapInKeysB, fundingParamsB, Pair(sharedInputB, SharedFundingInputBalances(balanceB, balanceA)), walletB, outputsB, randomKey().publicKey()) + val contributionsB = FundingContributions.create(channelKeysB, swapInKeysB, fundingParamsB, Pair(sharedInputB, SharedFundingInputBalances(balanceB, balanceA, 0.msat)), walletB, outputsB, randomKey().publicKey()) assertNotNull(contributionsB.right) return Fixture(channelId, TestConstants.Alice.keyManager, channelKeysA, localParamsA, fundingParamsA, contributionsA.right!!, TestConstants.Bob.keyManager, channelKeysB, localParamsB, fundingParamsB, contributionsB.right!!) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index 12b2b07ba..dc61e58bb 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -108,19 +108,23 @@ data class LNChannel( // we check that serialization works by checking that deserialize(serialize(state)) == state private fun checkSerialization(state: PersistedChannelState) { - // We don't persist unsigned funding RBF attempts. - fun removeRbfAttempt(state: PersistedChannelState): PersistedChannelState = when (state) { + // We don't persist unsigned funding RBF or splice attempts. + fun removeTemporaryStatuses(state: PersistedChannelState): PersistedChannelState = when (state) { is WaitForFundingConfirmed -> when (state.rbfStatus) { is RbfStatus.WaitingForSigs -> state else -> state.copy(rbfStatus = RbfStatus.None) } + is Normal -> when (state.spliceStatus) { + is SpliceStatus.WaitingForSigs -> state + else -> state.copy(spliceStatus = SpliceStatus.None) + } else -> state } val serialized = Serialization.serialize(state) val deserialized = Serialization.deserialize(serialized).value - assertEquals(removeRbfAttempt(state), deserialized, "serialization error") + assertEquals(removeTemporaryStatuses(state), deserialized, "serialization error") } private fun checkSerialization(actions: List) { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt new file mode 100644 index 000000000..6d92d0c9b --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt @@ -0,0 +1,591 @@ +package fr.acinq.lightning.channel.states + +import fr.acinq.bitcoin.* +import fr.acinq.lightning.CltvExpiry +import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Lightning +import fr.acinq.lightning.blockchain.electrum.WalletState +import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.* +import fr.acinq.lightning.channel.TestsHelper.htlcTimeoutTxs +import fr.acinq.lightning.channel.TestsHelper.reachNormal +import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.tests.TestConstants +import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.tests.utils.runSuspendTest +import fr.acinq.lightning.utils.UUID +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.wire.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.withTimeout +import kotlin.test.* + +class QuiescenceTestsCommon : LightningTestSuite() { + + @Test + fun `send stfu after pending local changes have been added`() { + // we have an unsigned htlc in our local changes + val (alice, bob) = reachNormal() + val (nodes1, _, _) = TestsHelper.addHtlc(50_000_000.msat, alice, bob) + val (alice1, bob1) = nodes1 + val (alice2, actionsAlice2) = alice1.process(createSpliceCommand(alice1)) + assertIs>(alice2) + assertNull(actionsAlice2.findOutgoingMessageOpt()) + val (_, _, stfu) = crossSignForStfu(alice2, bob1) + assertTrue(stfu.initiator) + } + + @Test + fun `recv stfu when there are pending local changes`() { + val (alice, bob) = reachNormal() + val (alice1, actionsAlice1) = alice.process(createSpliceCommand(alice)) + val stfuAlice = actionsAlice1.findOutgoingMessage() + assertTrue(stfuAlice.initiator) + // we're holding the stfu from alice so that bob can add a pending local change + val (nodes2, _, _) = TestsHelper.addHtlc(50_000_000.msat, bob, alice1) + val (bob2, alice2) = nodes2 + // bob will not reply to alice's stfu until bob has no pending local commitment changes + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(stfuAlice)) + assertTrue(actionsBob3.isEmpty()) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.Commitment.Sign) + val commitSigBob = actionsBob4.findOutgoingMessage() + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(commitSigBob)) + val revAlice = actionsAlice3.findOutgoingMessage() + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.Commitment.Sign) + val commitSigAlice = actionsAlice4.findOutgoingMessage() + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(revAlice)) + assertNull(actionsBob5.findOutgoingMessageOpt()) + val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(commitSigAlice)) + val revBob = actionsBob6.findOutgoingMessage() + val stfuBob = actionsBob6.findOutgoingMessage() + assertFalse(stfuBob.initiator) + val (alice5, _) = alice4.process(ChannelCommand.MessageReceived(revBob)) + val (_, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(stfuBob)) + // when both nodes are quiescent, alice can start the splice + val spliceInit = actionsAlice6.findOutgoingMessage() + val (_, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(spliceInit)) + actionsBob7.findOutgoingMessage() + } + + @Test + fun `recv forbidden non-settlement commands while initiator is awaiting stfu from remote`() { + val (alice, _) = reachNormal() + val (alice1, actionsAlice1) = alice.process(createSpliceCommand(alice)) + actionsAlice1.findOutgoingMessage() + // Alice should reject commands that change the commitment once it became quiescent. + val cmds = listOf( + ChannelCommand.Htlc.Add(1_000_000.msat, Lightning.randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(alice.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket, UUID.randomUUID()), + ChannelCommand.Commitment.UpdateFee(FeeratePerKw(100.sat)), + ChannelCommand.Close.MutualClose(null, null), + ) + cmds.forEach { + alice1.process(it).second.findCommandError() + } + } + + @Test + fun `recv forbidden non-settlement commands while quiescent`() { + val (alice, bob) = reachNormal() + val (alice1, bob1, _) = exchangeStfu(createSpliceCommand(alice), alice, bob) + // both should reject commands that change the commitment while quiescent + val cmds = listOf( + ChannelCommand.Htlc.Add(1_000_000.msat, Lightning.randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(alice.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket, UUID.randomUUID()), + ChannelCommand.Commitment.UpdateFee(FeeratePerKw(100.sat)), + ChannelCommand.Close.MutualClose(null, null) + ) + cmds.forEach { + alice1.process(it).second.findCommandError() + } + cmds.forEach { + bob1.process(it).second.findCommandError() + } + } + + @Test + fun `recv settlement command while initiator is awaiting stfu from remote`() { + val (alice, bob) = reachNormal() + val (nodes1, preimage, htlc) = TestsHelper.addHtlc(50_000_000.msat, bob, alice) + val (bob1, alice1) = nodes1 + val (alice2, actionsAlice2) = alice1.process(createSpliceCommand(alice1)) + assertIs>(alice2) + val stfuAlice = actionsAlice2.findOutgoingMessage() + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(stfuAlice)) + assertIs>(bob2) + assertTrue(actionsBob2.isEmpty()) + val (_, alice3, stfuBob) = crossSignForStfu(bob2, alice2) + listOf( + ChannelCommand.Htlc.Settlement.Fulfill(htlc.id, preimage), + ChannelCommand.Htlc.Settlement.Fail(htlc.id, ChannelCommand.Htlc.Settlement.Fail.Reason.Failure(TemporaryNodeFailure)) + ).forEach { cmd -> + // Alice simply ignores the settlement command. + val (alice4, actionsAlice4) = alice3.process(cmd) + assertTrue(actionsAlice4.isEmpty()) + // But she replays the HTLC once splicing is complete. + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(stfuBob)) + actionsAlice5.findOutgoingMessage() + val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, null))) + assertIs(alice6.state) + assertEquals(2, actionsAlice6.size) + assertEquals(htlc, actionsAlice6.find().add) + actionsAlice6.findOutgoingMessage() + // She can now process the command. + val (alice7, actionsAlice7) = alice6.process(cmd) + assertIs(alice7.state) + assertEquals(htlc.id, actionsAlice7.findOutgoingMessage().id) + } + } + + @Test + fun `recv settlement commands while initiator is awaiting stfu from remote and channel disconnects`() { + val (alice, bob) = reachNormal() + val (nodes1, preimage, htlc) = TestsHelper.addHtlc(50_000_000.msat, bob, alice) + val (bob1, alice1) = nodes1 + val (alice2, actionsAlice2) = alice1.process(createSpliceCommand(alice1)) + assertIs>(alice2) + val stfuAlice = actionsAlice2.findOutgoingMessage() + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(stfuAlice)) + assertIs>(bob2) + assertTrue(actionsBob2.isEmpty()) + val (bob3, alice3, _) = crossSignForStfu(bob2, alice2) + listOf( + ChannelCommand.Htlc.Settlement.Fulfill(htlc.id, preimage), + ChannelCommand.Htlc.Settlement.Fail(htlc.id, ChannelCommand.Htlc.Settlement.Fail.Reason.Failure(TemporaryNodeFailure)) + ).forEach { cmd -> + // Alice simply ignores the settlement command. + val (alice4, actionsAlice4) = alice3.process(cmd) + assertTrue(actionsAlice4.isEmpty()) + // Alice and Bob disconnect and reconnect, which aborts the quiescence negotiation. + val (aliceOffline, bobOffline) = disconnect(alice4, bob3) + val (alice5, _, actionsAlice5, _) = reconnect(aliceOffline, bobOffline) + assertIs(alice5.state) + assertEquals(1, actionsAlice5.size) + assertEquals(htlc, actionsAlice5.find().add) + // She can now process the command. + val (alice6, actionsAlice6) = alice5.process(cmd) + assertIs(alice6.state) + assertEquals(htlc.id, actionsAlice6.findOutgoingMessage().id) + } + } + + @Test + fun `recv settlement commands while quiescent`() { + val (alice, bob) = reachNormal() + // Alice initiates quiescence with an outgoing HTLC to Bob. + val (nodes1, preimageBob, htlcBob) = TestsHelper.addHtlc(50_000_000.msat, alice, bob) + val (alice1, bob1) = nodes1 + val (alice2, actionsAlice2) = alice1.process(createSpliceCommand(alice1)) + assertIs>(alice2) + assertTrue(actionsAlice2.isEmpty()) + val (alice3, bob3, stfuAlice) = crossSignForStfu(alice2, bob1) + // Bob sends an outgoing HTLC to Alice before going quiescent. + val (nodes4, preimageAlice, htlcAlice) = TestsHelper.addHtlc(40_000_000.msat, bob3, alice3) + val (bob4, alice4) = nodes4 + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(stfuAlice)) + assertIs>(bob5) + assertTrue(actionsBob5.isEmpty()) + val (bob6, alice6, stfuBob) = crossSignForStfu(bob5, alice4) + val (alice7, actionsAlice7) = alice6.process(ChannelCommand.MessageReceived(stfuBob)) + val spliceInit = actionsAlice7.findOutgoingMessage() + val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(spliceInit)) + actionsBob7.findOutgoingMessage() + // Alice receives settlement commands. + run { + listOf( + ChannelCommand.Htlc.Settlement.Fulfill(htlcAlice.id, preimageAlice), + ChannelCommand.Htlc.Settlement.Fail(htlcAlice.id, ChannelCommand.Htlc.Settlement.Fail.Reason.Failure(TemporaryNodeFailure)) + ).forEach { cmd -> + // Alice simply ignores the settlement command. + val (alice8, actionsAlice8) = alice7.process(cmd) + assertTrue(actionsAlice8.isEmpty()) + // But she replays the HTLC once splicing is complete. + val (alice9, actionsAlice9) = alice8.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, null))) + assertIs(alice9.state) + assertEquals(htlcAlice, actionsAlice9.find().add) + // She can now process the command. + val (alice10, actionsAlice10) = alice9.process(cmd) + assertIs(alice10.state) + assertEquals(htlcAlice.id, actionsAlice10.findOutgoingMessage().id) + } + } + // Bob receives settlement commands. + run { + listOf( + ChannelCommand.Htlc.Settlement.Fulfill(htlcBob.id, preimageBob), + ChannelCommand.Htlc.Settlement.Fail(htlcBob.id, ChannelCommand.Htlc.Settlement.Fail.Reason.Failure(TemporaryNodeFailure)) + ).forEach { cmd -> + // Bob simply ignores the settlement command. + val (bob8, actionsBob8) = bob7.process(cmd) + assertTrue(actionsBob8.isEmpty()) + // But he replays the HTLC once splicing is complete. + val (bob9, actionsBob9) = bob8.process(ChannelCommand.MessageReceived(TxAbort(bob.channelId, null))) + assertIs(bob9.state) + assertEquals(htlcBob, actionsBob9.find().add) + // He can now process the command. + val (bob10, actionsBob10) = bob9.process(cmd) + assertIs(bob10.state) + assertEquals(htlcBob.id, actionsBob10.findOutgoingMessage().id) + } + } + } + + @Test + fun `recv settlement commands while quiescent and channel disconnects`() { + val (alice, bob) = reachNormal() + // Alice initiates quiescence with an outgoing HTLC to Bob. + val (nodes1, preimageBob, htlcBob) = TestsHelper.addHtlc(50_000_000.msat, alice, bob) + val (alice1, bob1) = nodes1 + val (alice2, actionsAlice2) = alice1.process(createSpliceCommand(alice1)) + assertIs>(alice2) + assertTrue(actionsAlice2.isEmpty()) + val (alice3, bob3, stfuAlice) = crossSignForStfu(alice2, bob1) + // Bob sends an outgoing HTLC to Alice before going quiescent. + val (nodes4, preimageAlice, htlcAlice) = TestsHelper.addHtlc(40_000_000.msat, bob3, alice3) + val (bob4, alice4) = nodes4 + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(stfuAlice)) + assertIs>(bob5) + assertTrue(actionsBob5.isEmpty()) + val (bob6, alice6, stfuBob) = crossSignForStfu(bob5, alice4) + val (alice7, actionsAlice7) = alice6.process(ChannelCommand.MessageReceived(stfuBob)) + val spliceInit = actionsAlice7.findOutgoingMessage() + val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(spliceInit)) + actionsBob7.findOutgoingMessage() + // Alice receives settlement commands. + run { + listOf( + ChannelCommand.Htlc.Settlement.Fulfill(htlcAlice.id, preimageAlice), + ChannelCommand.Htlc.Settlement.Fail(htlcAlice.id, ChannelCommand.Htlc.Settlement.Fail.Reason.Failure(TemporaryNodeFailure)) + ).forEach { cmd -> + // Alice simply ignores the settlement command. + val (alice8, actionsAlice8) = alice7.process(cmd) + assertTrue(actionsAlice8.isEmpty()) + // Alice and Bob disconnect and reconnect, which aborts the quiescence negotiation. + val (aliceOffline, bobOffline) = disconnect(alice8, bob7) + val (alice9, _, actionsAlice9, _) = reconnect(aliceOffline, bobOffline) + assertIs(alice9.state) + assertEquals(1, actionsAlice9.size) + assertEquals(htlcAlice, actionsAlice9.find().add) + // She can now process the command. + val (alice10, actionsAlice10) = alice9.process(cmd) + assertIs(alice10.state) + assertEquals(htlcAlice.id, actionsAlice10.findOutgoingMessage().id) + } + } + // Bob receives settlement commands. + run { + listOf( + ChannelCommand.Htlc.Settlement.Fulfill(htlcBob.id, preimageBob), + ChannelCommand.Htlc.Settlement.Fail(htlcBob.id, ChannelCommand.Htlc.Settlement.Fail.Reason.Failure(TemporaryNodeFailure)) + ).forEach { cmd -> + // Bob simply ignores the settlement command. + val (bob8, actionsBob8) = bob7.process(cmd) + assertTrue(actionsBob8.isEmpty()) + // Alice and Bob disconnect and reconnect, which aborts the quiescence negotiation. + val (aliceOffline, bobOffline) = disconnect(alice7, bob8) + val (_, bob9, _, actionsBob9) = reconnect(aliceOffline, bobOffline) + assertIs(bob9.state) + assertEquals(htlcBob, actionsBob9.find().add) + // He can now process the command. + val (bob10, actionsBob10) = bob9.process(cmd) + assertIs(bob10.state) + assertEquals(htlcBob.id, actionsBob10.findOutgoingMessage().id) + } + } + } + + @Test + fun `recv second stfu while non-initiator is waiting for local commitment to be signed`() { + val (alice, bob) = reachNormal() + val (alice1, actionsAlice1) = alice.process(createSpliceCommand(alice)) + val stfu = actionsAlice1.findOutgoingMessage() + val (nodes2, _, _) = TestsHelper.addHtlc(50_000_000.msat, bob, alice1) + val (bob2, _) = nodes2 + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(stfu)) + assertTrue(actionsBob3.isEmpty()) + // second stfu to bob is ignored + val (_, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(stfu)) + assertTrue(actionsBob4.isEmpty()) + } + + @Test + fun `recv Shutdown message before initiator receives stfu from remote`() { + val (alice, bob) = reachNormal() + // Alice initiates quiescence. + val (alice1, actionsAlice1) = alice.process(createSpliceCommand(alice)) + val stfuAlice = actionsAlice1.findOutgoingMessage() + // But Bob is concurrently initiating a mutual close, which should "win". + val (bob1, actionsBob1) = bob.process(ChannelCommand.Close.MutualClose(null, null)) + val shutdownBob = actionsBob1.hasOutgoingMessage() + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(stfuAlice)) + assertNull(actionsBob2.findOutgoingMessageOpt()) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(shutdownBob)) + assertIs(alice2.state) + val shutdownAlice = actionsAlice2.findOutgoingMessage() + actionsAlice2.findOutgoingMessage() + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(shutdownAlice)) + assertIs(bob3.state) + actionsBob3.has() + } + + @Test + fun `recv forbidden settlement messages while quiescent`() { + val (alice, bob) = reachNormal() + val (nodes1, preimage, htlc) = TestsHelper.addHtlc(50_000_000.msat, bob, alice) + val (bob1, alice1) = nodes1 + val (bob2, alice2) = TestsHelper.crossSign(bob1, alice1) + val (alice3, bob3, _) = exchangeStfu(createSpliceCommand(alice2), alice2, bob2) + listOf( + UpdateFulfillHtlc(bob3.channelId, htlc.id, preimage), + UpdateFailHtlc(bob3.channelId, htlc.id, Lightning.randomBytes32()), + UpdateFee(bob3.channelId, FeeratePerKw(500.sat)), + UpdateAddHtlc(Lightning.randomBytes32(), htlc.id + 1, 50000000.msat, Lightning.randomBytes32(), CltvExpiry(alice.currentBlockHeight.toLong()), TestConstants.emptyOnionPacket), + Shutdown(alice.channelId, alice.commitments.params.localParams.defaultFinalScriptPubKey), + ).forEach { + // both parties will respond to a forbidden msg while quiescent with a warning (and disconnect) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(it)) + assertEquals(alice3, alice4) + actionsAlice4.findOutgoingMessage() + actionsAlice4.has() + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(it)) + assertEquals(bob3, bob4) + actionsBob4.findOutgoingMessage() + actionsBob4.has() + } + } + + @Test + fun `recv stfu from splice initiator that is not quiescent`() { + val (alice, bob) = reachNormal() + val (nodes1, _, _) = TestsHelper.addHtlc(50_000_000.msat, alice, bob) + val (alice1, bob1) = nodes1 + val (nodes2, _, _) = TestsHelper.addHtlc(40_000_000.msat, bob1, alice1) + val (bob2, alice2) = nodes2 + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(Stfu(alice.channelId, initiator = true))) + assertEquals(bob2, bob3) + actionsBob3.findOutgoingMessage() + actionsBob3.find() + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(Stfu(alice.channelId, initiator = true))) + assertEquals(alice2, alice3) + actionsAlice3.findOutgoingMessage() + actionsAlice3.find() + } + + @Test + fun `recv stfu from splice non-initiator that is not quiescent`() { + val (alice, bob) = reachNormal() + val (nodes1, _, _) = TestsHelper.addHtlc(50_000_000.msat, bob, alice) + val (_, alice1) = nodes1 + val (alice2, actionsAlice2) = alice1.process(createSpliceCommand(alice1)) + assertIs(alice2.state) + actionsAlice2.findOutgoingMessage() + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(Stfu(bob.channelId, initiator = false))) + assertIs(alice3.state) + assertEquals(alice2.state.copy(spliceStatus = SpliceStatus.None), alice3.state) + actionsAlice3.findOutgoingMessage() + actionsAlice3.find() + } + + @Test + fun `initiate quiescence concurrently with no pending changes`() = runSuspendTest { + val (alice, bob) = reachNormal() + val cmdAlice = createSpliceCommand(alice) + val cmdBob = createSpliceCommand(bob) + val (alice1, actionsAlice1) = alice.process(cmdAlice) + val stfuAlice = actionsAlice1.findOutgoingMessage() + assertTrue(stfuAlice.initiator) + val (bob1, actionsBob1) = bob.process(cmdBob) + val stfuBob = actionsBob1.findOutgoingMessage() + assertTrue(stfuBob.initiator) + // Alice is the channel initiator, so she has precedence and remains the splice initiator. + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(stfuBob)) + val spliceInit = actionsAlice2.findOutgoingMessage() + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(stfuAlice)) + assertTrue(actionsBob2.isEmpty()) + val (_, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(spliceInit)) + val spliceAck = actionsBob3.findOutgoingMessage() + val (_, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(spliceAck)) + actionsAlice3.hasOutgoingMessage() + withTimeout(100) { + assertIs(cmdBob.replyTo.await()) + } + } + + @Test + fun `initiate quiescence concurrently with pending changes on one side`() = runSuspendTest { + val (alice, bob) = reachNormal() + val (nodes1, _, _) = TestsHelper.addHtlc(50_000_000.msat, alice, bob) + val (alice1, bob1) = nodes1 + val cmdAlice = createSpliceCommand(alice1) + val cmdBob = createSpliceCommand(bob1) + val (alice2, actionsAlice2) = alice1.process(cmdAlice) + assertTrue(actionsAlice2.isEmpty()) // alice isn't quiescent yet + val (bob2, actionsBob2) = bob1.process(cmdBob) + val stfuBob = actionsBob2.findOutgoingMessage() + assertTrue(stfuBob.initiator) + val (alice3, _) = alice2.process(ChannelCommand.MessageReceived(stfuBob)) + assertIs>(alice3) + assertIs>(bob2) + val (alice4, bob3, stfuAlice) = crossSignForStfu(alice3, bob2) + assertFalse(stfuAlice.initiator) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(stfuAlice)) + val spliceInit = actionsBob4.findOutgoingMessage() + val (_, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(spliceInit)) + val spliceAck = actionsAlice5.findOutgoingMessage() + val (_, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(spliceAck)) + actionsBob5.hasOutgoingMessage() + withTimeout(100) { + assertIs(cmdAlice.replyTo.await()) + } + } + + @Test + fun `outgoing htlc timeout during quiescence negotiation`() { + val (alice, bob) = reachNormal() + val (nodes1, _, add) = TestsHelper.addHtlc(50_000_000.msat, alice, bob) + val (alice1, bob1) = nodes1 + val (alice2, bob2) = TestsHelper.crossSign(alice1, bob1) + val (alice3, _, _) = exchangeStfu(createSpliceCommand(alice2), alice2, bob2) + // The outgoing HTLC from Alice has timed out: she should force-close to avoid an on-chain race. + val (alice4, actionsAlice4) = run { + val tmp = alice3.copy(ctx = alice3.ctx.copy(currentBlockHeight = add.cltvExpiry.toLong().toInt())) + tmp.process(ChannelCommand.Commitment.CheckHtlcTimeout) + } + assertIs(alice4.state) + val lcp = alice4.state.localCommitPublished + assertNotNull(lcp) + assertEquals(1, lcp.htlcTxs.size) + val htlcTimeoutTxs = lcp.htlcTimeoutTxs() + assertEquals(1, htlcTimeoutTxs.size) + actionsAlice4.hasPublishTx(lcp.commitTx) + actionsAlice4.hasPublishTx(lcp.htlcTimeoutTxs().first().tx) + } + + @Test + fun `incoming htlc timeout during quiescence negotiation`() { + val (alice, bob) = reachNormal() + val (nodes1, preimage, add) = TestsHelper.addHtlc(50_000_000.msat, bob, alice) + val (bob1, alice1) = nodes1 + val (bob2, alice2) = TestsHelper.crossSign(bob1, alice1) + val (alice3, _, _) = exchangeStfu(createSpliceCommand(alice2), alice2, bob2) + listOf( + ChannelCommand.Htlc.Settlement.Fail(add.id, ChannelCommand.Htlc.Settlement.Fail.Reason.Failure(TemporaryNodeFailure)), + ChannelCommand.Htlc.Settlement.Fulfill(add.id, preimage) + ).forEach { cmd -> + // Alice simply ignores the settlement command during quiescence. + val (alice4, actionsAlice4) = alice3.process(cmd) + assertTrue(actionsAlice4.isEmpty()) + // The incoming HTLC to Alice has timed out: it is Bob's responsibility to force-close. + // If Bob doesn't force-close, Alice will fulfill or fail the HTLC when they reconnect. + val (alice5, actionsAlice5) = run { + val tmp = alice4.copy(ctx = alice4.ctx.copy(currentBlockHeight = add.cltvExpiry.toLong().toInt())) + tmp.process(ChannelCommand.Commitment.CheckHtlcTimeout) + } + assertTrue(actionsAlice5.isEmpty()) + // Alice replays the HTLC once splicing is complete. + val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(TxAbort(alice5.channelId, "deadbeef"))) + assertIs(alice6.state) + assertEquals(add, actionsAlice6.find().add) + // She can now process the command. + val (alice7, actionsAlice7) = alice6.process(cmd) + assertIs(alice7.state) + assertEquals(add.id, actionsAlice7.findOutgoingMessage().id) + } + } + + @Test + fun `receive SpliceInit when channel is not quiescent`() { + val (alice, bob) = reachNormal() + val (_, _, spliceInit) = exchangeStfu(createSpliceCommand(alice), alice, bob) + // If we send splice_init to Bob's before reaching quiescence, he simply rejects it. + val (bob2, actionsBob2) = bob.process(ChannelCommand.MessageReceived(spliceInit)) + assertEquals(bob.state.copy(spliceStatus = SpliceStatus.Aborted), bob2.state) + actionsBob2.hasOutgoingMessage() + } + + companion object { + private fun createWalletWithFunds(keyManager: KeyManager, utxos: List): List { + val script = keyManager.swapInOnChainWallet.pubkeyScript + return utxos.map { amount -> + val txIn = listOf(TxIn(OutPoint(TxId(Lightning.randomBytes32()), 2), 0)) + val txOut = listOf(TxOut(amount, script), TxOut(150.sat, Script.pay2wpkh(Lightning.randomKey().publicKey()))) + val parentTx = Transaction(2, txIn, txOut, 0) + WalletState.Utxo(parentTx.txid, 0, 42, parentTx) + } + } + + fun createSpliceCommand(sender: LNChannel, spliceIn: List = listOf(500_000.sat), spliceOut: Satoshi? = 100_000.sat): ChannelCommand.Commitment.Splice.Request { + return ChannelCommand.Commitment.Splice.Request( + replyTo = CompletableDeferred(), + spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(createWalletWithFunds(sender.staticParams.nodeParams.keyManager, spliceIn)), + spliceOut = spliceOut?.let { ChannelCommand.Commitment.Splice.Request.SpliceOut(it, Script.write(Script.pay2wpkh(Lightning.randomKey().publicKey())).byteVector()) }, + feerate = FeeratePerKw(253.sat), + requestRemoteFunding = null + ) + } + + /** Use this function when both nodes are already quiescent and want to exchange stfu. */ + fun exchangeStfu(cmd: ChannelCommand.Commitment.Splice.Request, sender: LNChannel, receiver: LNChannel): Triple, LNChannel, SpliceInit> { + val (sender1, sActions1) = sender.process(cmd) + val stfu1 = sActions1.findOutgoingMessage() + assertTrue(stfu1.initiator) + val (receiver1, rActions1) = receiver.process(ChannelCommand.MessageReceived(stfu1)) + val stfu2 = rActions1.findOutgoingMessage() + assertFalse(stfu2.initiator) + val (sender2, sActions2) = sender1.process(ChannelCommand.MessageReceived(stfu2)) + val spliceInit = sActions2.findOutgoingMessage() + assertIs>(sender2) + assertIs>(receiver1) + return Triple(sender2, receiver1, spliceInit) + } + + /** Use this function when the sender has pending changes that need to be cross-signed before sending stfu. */ + fun crossSignForStfu(sender: LNChannel, receiver: LNChannel): Triple, LNChannel, Stfu> { + val (sender2, sActions2) = sender.process(ChannelCommand.Commitment.Sign) + val sCommitSig = sActions2.findOutgoingMessage() + val (receiver2, rActions2) = receiver.process(ChannelCommand.MessageReceived(sCommitSig)) + val rRev = rActions2.findOutgoingMessage() + val (receiver3, rActions3) = receiver2.process(ChannelCommand.Commitment.Sign) + val rCommitSig = rActions3.findOutgoingMessage() + val (sender3, sActions3) = sender2.process(ChannelCommand.MessageReceived(rRev)) + assertNull(sActions3.findOutgoingMessageOpt()) + val (sender4, sActions4) = sender3.process(ChannelCommand.MessageReceived(rCommitSig)) + val sRev = sActions4.findOutgoingMessage() + val stfu = sActions4.findOutgoingMessage() + val (receiver4, _) = receiver3.process(ChannelCommand.MessageReceived(sRev)) + assertIs>(sender4) + assertIs>(receiver4) + return Triple(sender4, receiver4, stfu) + } + + fun disconnect(alice: LNChannel, bob: LNChannel): Pair, LNChannel> { + val (alice1, actionsAlice1) = alice.process(ChannelCommand.Disconnected) + val (bob1, actionsBob1) = bob.process(ChannelCommand.Disconnected) + assertIs(alice1.state) + assertTrue(actionsAlice1.isEmpty()) + assertIs(bob1.state) + assertTrue(actionsBob1.isEmpty()) + assertIs>(alice1) + assertIs>(bob1) + return Pair(alice1, bob1) + } + + data class PostReconnectionState(val alice: LNChannel, val bob: LNChannel, val actionsAlice: List, val actionsBob: List) + + fun reconnect(alice: LNChannel, bob: LNChannel): PostReconnectionState { + val aliceInit = Init(alice.commitments.params.localParams.features) + val bobInit = Init(bob.commitments.params.localParams.features) + val (alice1, actionsAlice1) = alice.process(ChannelCommand.Connected(aliceInit, bobInit)) + assertIs>(alice1) + val channelReestablishA = actionsAlice1.findOutgoingMessage() + val (bob1, _) = bob.process(ChannelCommand.Connected(bobInit, aliceInit)) + assertIs>(bob1) + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(channelReestablishA)) + val channelReestablishB = actionsBob2.findOutgoingMessage() + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(channelReestablishB)) + assertIs>(alice2) + assertIs>(bob2) + return PostReconnectionState(alice2, bob2, actionsAlice2, actionsBob2) + } + } + +} diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index 96dfae18e..edf6bc3b3 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -14,6 +14,9 @@ import fr.acinq.lightning.channel.TestsHelper.reachNormal import fr.acinq.lightning.channel.TestsHelper.useAlternativeCommitSig import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.transactions.Transactions +import fr.acinq.lightning.transactions.incomings +import fr.acinq.lightning.transactions.outgoings import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.sum @@ -22,6 +25,7 @@ import fr.acinq.lightning.wire.* import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking +import kotlin.math.abs import kotlin.test.* class SpliceTestsCommon : LightningTestSuite() { @@ -38,6 +42,56 @@ class SpliceTestsCommon : LightningTestSuite() { spliceIn(alice, bob, listOf(50_000.sat)) } + @Test + fun `splice funds in and out with pending htlcs`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) + val (alice2, bob2) = spliceInAndOut(alice1, bob1, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) + + // Bob sends an HTLC that is applied to both commitments. + val (nodes3, preimage, add) = addHtlc(10_000_000.msat, bob2, alice2) + val (bob4, alice4) = crossSign(nodes3.first, nodes3.second, commitmentsCount = 2) + + alice4.commitments.active.forEach { c -> + val commitTx = c.localCommit.publishableTxs.commitTx.tx + Transaction.correctlySpends(commitTx, mapOf(c.commitInput.outPoint to c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + bob4.state.commitments.active.forEach { c -> + val commitTx = c.localCommit.publishableTxs.commitTx.tx + Transaction.correctlySpends(commitTx, mapOf(c.commitInput.outPoint to c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + // Alice fulfills that HTLC in both commitments. + val (bob5, alice5) = fulfillHtlc(add.id, preimage, bob4, alice4) + val (alice6, bob6) = crossSign(alice5, bob5, commitmentsCount = 2) + + alice6.state.commitments.active.forEach { c -> + val commitTx = c.localCommit.publishableTxs.commitTx.tx + Transaction.correctlySpends(commitTx, mapOf(c.commitInput.outPoint to c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + bob6.state.commitments.active.forEach { c -> + val commitTx = c.localCommit.publishableTxs.commitTx.tx + Transaction.correctlySpends(commitTx, mapOf(c.commitInput.outPoint to c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + resolveHtlcs(alice6, bob6, htlcs, commitmentsCount = 2) + } + + @Test + fun `splice funds in and out with pending htlcs resolved after splice locked`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) + val (alice2, bob2) = spliceInAndOut(alice1, bob1, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) + val spliceTx = alice2.commitments.latest.localFundingStatus.signedTx!! + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice2.channelId, BITCOIN_FUNDING_DEPTHOK, 42, 0, spliceTx))) + val (bob3, _) = bob2.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.WatchReceived(WatchEventConfirmed(bob3.channelId, BITCOIN_FUNDING_DEPTHOK, 42, 0, spliceTx))) + val (alice4, _) = alice3.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) + assertIs>(alice4) + assertIs>(bob4) + resolveHtlcs(alice4, bob4, htlcs, commitmentsCount = 1) + } + @Test fun `splice funds in -- non-initiator`() { val (alice, bob) = reachNormal() @@ -89,23 +143,47 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(1, bob7.commitments.remoteCommitIndex) Pair(Pair(alice7, bob7), Pair(preimage1, preimage2)) } + val (alice1, bob1) = spliceIn(nodes.first, nodes.second, listOf(500_000.sat)) + val (alice2, bob2) = fulfillHtlc(0, preimages.first, alice1, bob1) + val (bob3, alice3) = fulfillHtlc(0, preimages.second, bob2, alice2) + val (alice4, _) = crossSign(alice3, bob3, commitmentsCount = 2) + assertEquals(2, alice4.commitments.localCommitIndex) + assertEquals(4, alice4.commitments.remoteCommitIndex) + } - // TODO: once we support quiescence, fulfill those HTLCs after the splice instead of before. - val (alice1, bob1) = fulfillHtlc(0, preimages.first, nodes.first, nodes.second) - val (bob2, alice2) = fulfillHtlc(0, preimages.second, bob1, alice1) - val (alice3, bob3) = crossSign(alice2, bob2) - assertEquals(2, alice3.commitments.localCommitIndex) - assertEquals(4, alice3.commitments.remoteCommitIndex) - - spliceIn(alice3, bob3, listOf(500_000.sat)) + @Test + fun `splice funds out -- would go below reserve`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice1, bob1, _) = setupHtlcs(alice, bob) + val cmd = createSpliceOutRequest(760_000.sat) + val (alice2, actionsAlice2) = alice1.process(cmd) + + val aliceStfu = actionsAlice2.findOutgoingMessage() + val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(aliceStfu)) + val bobStfu = actionsBob2.findOutgoingMessage() + val (_, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(bobStfu)) + actionsAlice3.findOutgoingMessage() + runBlocking { + val response = cmd.replyTo.await() + assertIs(response) + } } @Test fun `splice cpfp`() { val (alice, bob) = reachNormal() - spliceIn(alice, bob, listOf(50_000.sat)) - spliceOut(alice, bob, 50_000.sat) - spliceCpfp(alice, bob) + val (nodes, preimage, _) = addHtlc(15_000_000.msat, alice, bob) + val (alice0, bob0) = crossSign(nodes.first, nodes.second) + val (alice1, bob1) = spliceIn(alice0, bob0, listOf(50_000.sat)) + val fee1 = spliceFee(alice1, capacity = 1_050_000.sat) + val (alice2, bob2) = spliceOut(alice1, bob1, 50_000.sat) + val fee2 = spliceFee(alice2, capacity = 1_000_000.sat - fee1) + val (alice3, bob3) = spliceCpfp(alice2, bob2) + val (alice4, bob4) = fulfillHtlc(0, preimage, alice3, bob3) + val (_, alice5) = crossSign(bob4, alice4, commitmentsCount = 4) + val fee3 = spliceFee(alice5, capacity = 1_000_000.sat - fee1 - fee2) + assertEquals(alice5.state.commitments.latest.localCommit.spec.toLocal, 800_000_000.msat - (fee1 + fee2 + fee3).toMilliSatoshi() - 15_000_000.msat) + assertEquals(alice5.state.commitments.latest.localCommit.spec.toRemote, 200_000_000.msat + 15_000_000.msat) } @Test @@ -114,13 +192,12 @@ class SpliceTestsCommon : LightningTestSuite() { val leaseRate = LiquidityAds.LeaseRate(0, 250, 250 /* 2.5% */, 10.sat, 200, 100.msat) val liquidityRequest = LiquidityAds.RequestRemoteFunding(200_000.sat, alice.currentBlockHeight, leaseRate) val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) - val (alice1, actionsAlice1) = alice.process(cmd) - val spliceInit = actionsAlice1.findOutgoingMessage() + val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) assertEquals(spliceInit.requestFunds, liquidityRequest.requestFunds) // Alice's contribution is negative: she needs to pay on-chain fees for the splice. assertTrue(spliceInit.fundingContribution < 0.sat) // We haven't implemented the seller side, so we mock it. - val (_, actionsBob2) = bob.process(ChannelCommand.MessageReceived(spliceInit)) + val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) val defaultSpliceAck = actionsBob2.findOutgoingMessage() assertNull(defaultSpliceAck.willFund) val fundingScript = Helpers.Funding.makeFundingPubKeyScript(spliceInit.fundingPubkey, defaultSpliceAck.fundingPubkey) @@ -155,14 +232,18 @@ class SpliceTestsCommon : LightningTestSuite() { @Test @OptIn(ExperimentalCoroutinesApi::class) fun `splice to purchase inbound liquidity -- not enough funds`() { - val (_, bob) = reachNormal(aliceFundingAmount = 100_000.sat, bobFundingAmount = 10_000.sat, alicePushAmount = 0.msat, bobPushAmount = 0.msat) + val (alice, bob) = reachNormal(aliceFundingAmount = 100_000.sat, bobFundingAmount = 10_000.sat, alicePushAmount = 0.msat, bobPushAmount = 0.msat) val leaseRate = LiquidityAds.LeaseRate(0, 0, 100 /* 5% */, 1.sat, 200, 100.msat) run { val liquidityRequest = LiquidityAds.RequestRemoteFunding(1_000_000.sat, bob.currentBlockHeight, leaseRate) assertEquals(10_001.sat, liquidityRequest.rate.fees(FeeratePerKw(1000.sat), liquidityRequest.fundingAmount, liquidityRequest.fundingAmount).total) val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) - val (_, actions1) = bob.process(cmd) - assertTrue(actions1.isEmpty()) + val (bob1, actionsBob1) = bob.process(cmd) + val bobStfu = actionsBob1.findOutgoingMessage() + val (_, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(bobStfu)) + val aliceStfu = actionsAlice1.findOutgoingMessage() + val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(aliceStfu)) + assertTrue(actionsBob2.isEmpty()) assertTrue(cmd.replyTo.isCompleted) assertEquals(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds, cmd.replyTo.getCompleted()) } @@ -170,17 +251,22 @@ class SpliceTestsCommon : LightningTestSuite() { val liquidityRequest = LiquidityAds.RequestRemoteFunding(1_000_000.sat, bob.currentBlockHeight, leaseRate.copy(leaseFeeBase = 0.sat)) assertEquals(10_000.sat, liquidityRequest.rate.fees(FeeratePerKw(1000.sat), liquidityRequest.fundingAmount, liquidityRequest.fundingAmount).total) val cmd = ChannelCommand.Commitment.Splice.Request(CompletableDeferred(), null, null, liquidityRequest, FeeratePerKw(1000.sat)) - val (_, actions1) = bob.process(cmd) - actions1.hasOutgoingMessage() + val (bob1, actionsBob1) = bob.process(cmd) + val bobStfu = actionsBob1.findOutgoingMessage() + val (_, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(bobStfu)) + val aliceStfu = actionsAlice1.findOutgoingMessage() + val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(aliceStfu)) + actionsBob2.hasOutgoingMessage() } } @Test fun `reject splice_init`() { val cmd = createSpliceOutRequest(25_000.sat) - val (alice, _) = reachNormal() - val (alice1, actionsAlice1) = alice.process(cmd) - actionsAlice1.hasOutgoingMessage() + val (alice, bob) = reachNormal() + val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) + val (alice0, bob0) = crossSign(nodes.first, nodes.second) + val (alice1, _, _) = reachQuiescent(cmd, alice0, bob0) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "thanks but no thanks"))) assertIs(alice2.state) assertEquals(alice2.state.spliceStatus, SpliceStatus.None) @@ -192,25 +278,30 @@ class SpliceTestsCommon : LightningTestSuite() { fun `reject splice_ack`() { val cmd = createSpliceOutRequest(25_000.sat) val (alice, bob) = reachNormal() - val (_, actionsAlice1) = alice.process(cmd) - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(actionsAlice1.hasOutgoingMessage())) - actionsBob1.hasOutgoingMessage() - val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "changed my mind"))) - assertIs(bob2.state) - assertEquals(bob2.state.spliceStatus, SpliceStatus.None) - assertEquals(actionsBob2.size, 1) - actionsBob2.hasOutgoingMessage() + val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) + val (alice0, bob0) = crossSign(nodes.first, nodes.second) + val (_, bob1, spliceInit) = reachQuiescent(cmd, alice0, bob0) + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) + actionsBob2.hasOutgoingMessage() + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "changed my mind"))) + assertIs(bob3.state) + assertEquals(bob3.state.spliceStatus, SpliceStatus.None) + assertEquals(actionsBob3.size, 2) + actionsBob3.hasOutgoingMessage() + actionsBob3.has() } @Test fun `abort before tx_complete`() { val cmd = createSpliceOutRequest(20_000.sat) val (alice, bob) = reachNormal() - val (alice1, actionsAlice1) = alice.process(cmd) - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(actionsAlice1.findOutgoingMessage())) - val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(actionsBob1.findOutgoingMessage())) - val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) - val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) + val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) + val (alice0, bob0) = crossSign(nodes.first, nodes.second) + val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice0, bob0) + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) actionsAlice3.hasOutgoingMessage() run { val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "internal error"))) @@ -220,11 +311,12 @@ class SpliceTestsCommon : LightningTestSuite() { actionsAlice4.hasOutgoingMessage() } run { - val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "internal error"))) - assertIs(bob3.state) - assertEquals(bob3.state.spliceStatus, SpliceStatus.None) - assertEquals(actionsBob3.size, 1) - actionsBob3.hasOutgoingMessage() + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "internal error"))) + assertIs(bob4.state) + assertEquals(bob4.state.spliceStatus, SpliceStatus.None) + assertEquals(actionsBob4.size, 2) + actionsBob4.hasOutgoingMessage() + actionsBob4.has() } } @@ -232,18 +324,20 @@ class SpliceTestsCommon : LightningTestSuite() { fun `abort after tx_complete`() { val cmd = createSpliceOutRequest(31_000.sat) val (alice, bob) = reachNormal() - val (alice1, actionsAlice1) = alice.process(cmd) - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(actionsAlice1.findOutgoingMessage())) - val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(actionsBob1.findOutgoingMessage())) - val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) - val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) - val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) - val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) - val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) - val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) + val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) + val (alice0, bob0) = crossSign(nodes.first, nodes.second) + val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice0, bob0) + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(actionsBob5.findOutgoingMessage())) actionsAlice5.hasOutgoingMessage() - val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(actionsAlice5.findOutgoingMessage())) - actionsBob5.hasOutgoingMessage() + val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(actionsAlice5.findOutgoingMessage())) + actionsBob6.hasOutgoingMessage() run { val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "internal error"))) assertIs(alice6.state) @@ -253,12 +347,13 @@ class SpliceTestsCommon : LightningTestSuite() { actionsAlice6.has() } run { - val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "internal error"))) - assertIs(bob6.state) - assertEquals(bob6.state.spliceStatus, SpliceStatus.None) - assertEquals(actionsBob6.size, 2) - actionsBob6.hasOutgoingMessage() - actionsBob6.has() + val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(TxAbort(alice.channelId, "internal error"))) + assertIs(bob7.state) + assertEquals(bob7.state.spliceStatus, SpliceStatus.None) + assertEquals(actionsBob7.size, 3) + actionsBob7.hasOutgoingMessage() + actionsBob7.has() + actionsBob7.has() } } @@ -266,33 +361,36 @@ class SpliceTestsCommon : LightningTestSuite() { fun `abort after tx_complete then receive commit_sig`() { val cmd = createSpliceOutRequest(50_000.sat) val (alice, bob) = reachNormal() - val (alice1, actionsAlice1) = alice.process(cmd) - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(actionsAlice1.findOutgoingMessage())) - val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(actionsBob1.findOutgoingMessage())) - val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) - val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) + val (nodes, _, _) = addHtlc(15_000_000.msat, alice, bob) + val (alice0, bob0) = crossSign(nodes.first, nodes.second) + val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice0, bob0) + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) val txOut1 = actionsAlice3.findOutgoingMessage() - val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(txOut1)) - val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(txOut1)) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) // Instead of relaying the second output, we duplicate the first one, which will make Bob abort after receiving tx_complete. actionsAlice4.hasOutgoingMessage() - val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(txOut1.copy(serialId = 100))) - val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(txOut1.copy(serialId = 100))) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(actionsBob5.findOutgoingMessage())) val commitSigAlice = actionsAlice5.findOutgoingMessage() - val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(actionsAlice5.findOutgoingMessage())) - val txAbortBob = actionsBob5.findOutgoingMessage() + val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(actionsAlice5.findOutgoingMessage())) + val txAbortBob = actionsBob6.findOutgoingMessage() val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(txAbortBob)) assertIs(alice6.state) assertEquals(1, alice6.commitments.active.size) assertEquals(SpliceStatus.None, alice6.state.spliceStatus) val txAbortAlice = actionsAlice6.findOutgoingMessage() - val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(commitSigAlice)) - assertTrue(actionsBob6.isEmpty()) - val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(txAbortAlice)) - assertIs(bob7.state) - assertEquals(1, bob7.commitments.active.size) - assertEquals(SpliceStatus.None, bob7.state.spliceStatus) + val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(commitSigAlice)) assertTrue(actionsBob7.isEmpty()) + val (bob8, actionsBob8) = bob7.process(ChannelCommand.MessageReceived(txAbortAlice)) + assertIs(bob8.state) + assertEquals(1, bob8.commitments.active.size) + assertEquals(SpliceStatus.None, bob8.state.spliceStatus) + assertEquals(1, actionsBob8.size) + actionsBob8.has() } @Test @@ -484,7 +582,9 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- commit_sig not received`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, _, bob1, _) = spliceOutWithoutSigs(alice, bob, 20_000.sat) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, _, bob1, _) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) + val spliceStatus = alice1.state.spliceStatus assertIs(spliceStatus) @@ -492,46 +592,54 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(channelReestablishAlice.nextFundingTxId, spliceStatus.session.fundingTx.txId) val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(channelReestablishAlice)) assertIs>(bob3) - assertEquals(actionsBob3.size, 2) + assertEquals(actionsBob3.size, 4) val channelReestablishBob = actionsBob3.findOutgoingMessage() val commitSigBob = actionsBob3.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob3.filterIsInstance().map { it.add }.toSet()) assertEquals(channelReestablishBob.nextFundingTxId, spliceStatus.session.fundingTx.txId) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) assertIs>(alice3) - assertEquals(actionsAlice3.size, 1) + assertEquals(actionsAlice3.size, 3) val commitSigAlice = actionsAlice3.findOutgoingMessage() - exchangeSpliceSigs(alice3, commitSigAlice, bob3, commitSigBob) + val (alice4, bob4) = exchangeSpliceSigs(alice3, commitSigAlice, bob3, commitSigBob) + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice3.filterIsInstance().map { it.add }.toSet()) + resolveHtlcs(alice4, bob4, htlcs, commitmentsCount = 2) } @Test fun `disconnect -- commit_sig received by alice`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, _, bob1, commitSigBob1) = spliceOutWithoutSigs(alice, bob, 20_000.sat) - val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(commitSigBob1)) - assertIs>(alice2) - assertTrue(actionsAlice2.isEmpty()) - val spliceStatus = alice2.state.spliceStatus + val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) + val (alice2, _, bob2, commitSigBob1) = spliceInAndOutWithoutSigs(alice1, bob1, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(commitSigBob1)) + assertIs>(alice3) + assertTrue(actionsAlice3.isEmpty()) + val spliceStatus = alice3.state.spliceStatus assertIs(spliceStatus) - val (alice3, bob2, channelReestablishAlice) = disconnect(alice2, bob1) + val (alice4, bob3, channelReestablishAlice) = disconnect(alice3, bob2) assertEquals(channelReestablishAlice.nextFundingTxId, spliceStatus.session.fundingTx.txId) - val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertIs>(bob3) - assertEquals(actionsBob3.size, 2) - val channelReestablishBob = actionsBob3.findOutgoingMessage() - val commitSigBob2 = actionsBob3.findOutgoingMessage() + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) + assertIs>(bob4) + assertEquals(actionsBob4.size, 4) + val channelReestablishBob = actionsBob4.findOutgoingMessage() + val commitSigBob2 = actionsBob4.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) assertEquals(channelReestablishBob.nextFundingTxId, spliceStatus.session.fundingTx.txId) - val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(channelReestablishBob)) - assertIs>(alice4) - assertEquals(actionsAlice4.size, 1) - val commitSigAlice = actionsAlice4.findOutgoingMessage() - exchangeSpliceSigs(alice4, commitSigAlice, bob3, commitSigBob2) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(channelReestablishBob)) + assertIs>(alice5) + assertEquals(actionsAlice5.size, 3) + val commitSigAlice = actionsAlice5.findOutgoingMessage() + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice5.filterIsInstance().map { it.add }.toSet()) + val (alice6, bob5) = exchangeSpliceSigs(alice5, commitSigAlice, bob4, commitSigBob2) + resolveHtlcs(alice6, bob5, htlcs, commitmentsCount = 2) } @Test fun `disconnect -- tx_signatures sent by bob`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, commitSigAlice1, bob1, _) = spliceOutWithoutSigs(alice, bob, 20_000.sat) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, commitSigAlice1, bob1, _) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(80_000.sat), outAmount = 50_000.sat) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSigAlice1)) assertIs>(bob2) val spliceTxId = actionsBob2.hasOutgoingMessage().txId @@ -540,13 +648,15 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice2, bob3, channelReestablishAlice) = disconnect(alice1, bob2) assertEquals(channelReestablishAlice.nextFundingTxId, spliceTxId) val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(actionsBob4.size, 3) + assertEquals(actionsBob4.size, 5) val channelReestablishBob = actionsBob4.findOutgoingMessage() val commitSigBob2 = actionsBob4.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) val txSigsBob = actionsBob4.findOutgoingMessage() assertEquals(channelReestablishBob.nextFundingTxId, spliceTxId) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) - assertEquals(actionsAlice3.size, 1) + assertEquals(actionsAlice3.size, 3) + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice3.filterIsInstance().map { it.add }.toSet()) val commitSigAlice2 = actionsAlice3.findOutgoingMessage() val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(commitSigBob2)) @@ -554,8 +664,9 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(txSigsBob)) assertIs>(alice5) assertEquals(alice5.state.commitments.active.size, 2) - assertEquals(actionsAlice5.size, 5) + assertEquals(actionsAlice5.size, 8) assertEquals(actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, spliceTxId) + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice5.filterIsInstance().map { it.add }.toSet()) actionsAlice5.hasWatchConfirmed(spliceTxId) actionsAlice5.has() actionsAlice5.has() @@ -574,7 +685,8 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- tx_signatures sent by bob -- zero-conf`() { val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) - val (alice1, commitSigAlice1, bob1, _) = spliceOutWithoutSigs(alice, bob, 20_000.sat) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, commitSigAlice1, bob1, _) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(75_000.sat), outAmount = 120_000.sat) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSigAlice1)) assertIs>(bob2) val spliceTxId = actionsBob2.hasOutgoingMessage().txId @@ -584,17 +696,19 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice2, bob3, channelReestablishAlice) = disconnect(alice1, bob2) assertEquals(channelReestablishAlice.nextFundingTxId, spliceTxId) val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(actionsBob4.size, 4) + assertEquals(actionsBob4.size, 6) val channelReestablishBob = actionsBob4.findOutgoingMessage() val commitSigBob2 = actionsBob4.findOutgoingMessage() val txSigsBob = actionsBob4.findOutgoingMessage() // splice_locked must always be sent *after* tx_signatures assertIs(actionsBob4.filterIsInstance().last().message) val spliceLockedBob = actionsBob4.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) assertEquals(channelReestablishBob.nextFundingTxId, spliceTxId) val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob)) - assertEquals(actionsAlice3.size, 1) + assertEquals(actionsAlice3.size, 3) val commitSigAlice2 = actionsAlice3.findOutgoingMessage() + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice3.filterIsInstance().map { it.add }.toSet()) assertEquals(commitSigAlice1.signature, commitSigAlice2.signature) val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(commitSigBob2)) @@ -602,7 +716,7 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(txSigsBob)) assertIs>(alice5) assertEquals(alice5.state.commitments.active.size, 2) - assertEquals(actionsAlice5.size, 6) + assertEquals(actionsAlice5.size, 9) assertEquals(actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, spliceTxId) actionsAlice5.hasWatchConfirmed(spliceTxId) actionsAlice5.has() @@ -610,6 +724,7 @@ class SpliceTestsCommon : LightningTestSuite() { val txSigsAlice = actionsAlice5.findOutgoingMessage() assertIs(actionsAlice5.filterIsInstance().last().message) val spliceLockedAlice = actionsAlice5.findOutgoingMessage() + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice5.filterIsInstance().map { it.add }.toSet()) val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(spliceLockedBob)) assertIs>(alice6) assertEquals(alice6.state.commitments.active.size, 1) @@ -631,12 +746,14 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(actionsBob7.size, 2) actionsBob7.find().also { assertEquals(it.txId, spliceTxId) } actionsBob7.has() + resolveHtlcs(alice6, bob7, htlcs, commitmentsCount = 1) } @Test fun `disconnect -- tx_signatures sent by alice -- confirms while bob is offline`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, commitSigAlice1, bob1, commitSigBob1) = spliceOutWithoutSigs(alice, bob, 20_000.sat) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, commitSigAlice1, bob1, commitSigBob1) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(70_000.sat, 60_000.sat), outAmount = 150_000.sat) // Bob completes the splice, but is missing Alice's tx_signatures. val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSigAlice1)) @@ -664,15 +781,17 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice5, bob3, channelReestablishAlice) = disconnect(alice4, bob2) assertNull(channelReestablishAlice.nextFundingTxId) val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(1, actionsBob4.size) + assertEquals(3, actionsBob4.size) val channelReestablishBob = actionsBob4.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) assertEquals(channelReestablishBob.nextFundingTxId, spliceTx.txid) val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(channelReestablishBob)) assertIs>(alice6) assertEquals(alice6.state.spliceStatus, SpliceStatus.None) - assertEquals(2, actionsAlice6.size) + assertEquals(4, actionsAlice6.size) val txSigsAlice = actionsAlice6.hasOutgoingMessage() actionsAlice6.hasOutgoingMessage() + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice6.filterIsInstance().map { it.add }.toSet()) // Bob receives tx_signatures, which completes the splice. val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(txSigsAlice)) @@ -681,12 +800,14 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(2, actionsBob5.size) actionsBob5.hasPublishTx(spliceTx) actionsBob5.has() + resolveHtlcs(alice6, bob5, htlcs, commitmentsCount = 2) } @Test fun `disconnect -- tx_signatures received by alice`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, commitSigAlice, bob1, commitSigBob) = spliceOutWithoutSigs(alice, bob, 20_000.sat) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, commitSigAlice, bob1, commitSigBob) = spliceInAndOutWithoutSigs(alice0, bob0, inAmounts = listOf(315_000.sat), outAmount = 25_000.sat) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(commitSigBob)) assertTrue(actionsAlice2.isEmpty()) val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSigAlice)) @@ -701,14 +822,16 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice4, bob3, channelReestablishAlice) = disconnect(alice3, bob2) assertNull(channelReestablishAlice.nextFundingTxId) val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(actionsBob4.size, 1) + assertEquals(actionsBob4.size, 3) val channelReestablishBob = actionsBob4.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) assertEquals(channelReestablishBob.nextFundingTxId, spliceTx.txid) val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(channelReestablishBob)) assertIs>(alice5) assertEquals(alice5.state.commitments.active.size, 2) - assertEquals(actionsAlice5.size, 1) + assertEquals(actionsAlice5.size, 3) val txSigsAlice = actionsAlice5.findOutgoingMessage() + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice5.filterIsInstance().map { it.add }.toSet()) val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(txSigsAlice)) assertIs>(bob5) @@ -716,6 +839,7 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(actionsBob5.size, 2) assertEquals(actionsBob5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, spliceTx.txid) actionsBob5.has() + resolveHtlcs(alice5, bob5, htlcs, commitmentsCount = 2) } @Test @@ -744,7 +868,8 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `disconnect -- splice_locked sent`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, bob1) = spliceOut(alice, bob, 70_000.sat) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceInAndOut(alice0, bob0, inAmounts = listOf(150_000.sat, 25_000.sat, 15_000.sat), outAmount = 250_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! val (alice2, actionsAlice2) = alice1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_FUNDING_DEPTHOK, 100, 0, spliceTx))) @@ -758,13 +883,15 @@ class SpliceTestsCommon : LightningTestSuite() { // Alice disconnects before receiving Bob's splice_locked. val (alice3, bob4, channelReestablishAlice) = disconnect(alice2, bob3) val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(actionsBob5.size, 2) + assertEquals(actionsBob5.size, 4) val channelReestablishBob = actionsBob5.findOutgoingMessage() val spliceLockedBob = actionsBob5.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob5.filterIsInstance().map { it.add }.toSet()) val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(channelReestablishBob)) - assertEquals(actionsAlice4.size, 1) + assertEquals(actionsAlice4.size, 3) val spliceLockedAlice2 = actionsAlice4.hasOutgoingMessage() + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice4.filterIsInstance().map { it.add }.toSet()) val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(spliceLockedBob)) assertIs>(alice5) assertEquals(alice5.state.commitments.active.size, 1) @@ -777,13 +904,15 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(bob6.state.commitments.active.size, 1) assertEquals(actionsBob6.size, 1) actionsBob6.has() + resolveHtlcs(alice5, bob6, htlcs, commitmentsCount = 1) } @Test fun `disconnect -- latest commitment locked remotely and locally -- zero-conf`() { val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) - val (alice1, bob1) = spliceOut(alice, bob, 40_000.sat) - val (alice2, bob2) = spliceOut(alice1, bob1, 30_000.sat) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceIn(alice0, bob0, listOf(50_000.sat)) + val (alice2, bob2) = spliceOut(alice1, bob1, 100_000.sat) // Alice and Bob have not received any remote splice_locked yet. assertEquals(alice2.commitments.active.size, 3) @@ -794,14 +923,16 @@ class SpliceTestsCommon : LightningTestSuite() { // On reconnection, Alice and Bob only send splice_locked for the latest commitment. val (alice3, bob3, channelReestablishAlice) = disconnect(alice2, bob2) val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(actionsBob4.size, 2) + assertEquals(actionsBob4.size, 4) val channelReestablishBob = actionsBob4.findOutgoingMessage() val spliceLockedBob = actionsBob4.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob4.filterIsInstance().map { it.add }.toSet()) assertEquals(spliceLockedBob.fundingTxId, bob2.commitments.latest.fundingTxId) val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(channelReestablishBob)) - assertEquals(actionsAlice4.size, 1) + assertEquals(actionsAlice4.size, 3) val spliceLockedAlice = actionsAlice4.hasOutgoingMessage() + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice4.filterIsInstance().map { it.add }.toSet()) assertEquals(spliceLockedAlice.fundingTxId, spliceLockedBob.fundingTxId) val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(spliceLockedBob)) assertEquals(actionsAlice5.size, 3) @@ -818,14 +949,18 @@ class SpliceTestsCommon : LightningTestSuite() { actionsBob5.has() assertContains(actionsBob5, ChannelAction.Storage.SetLocked(bob1.commitments.latest.fundingTxId)) assertContains(actionsBob5, ChannelAction.Storage.SetLocked(bob2.commitments.latest.fundingTxId)) + assertIs>(alice5) + assertIs>(bob5) + resolveHtlcs(alice5, bob5, htlcs, commitmentsCount = 1) } @Test fun `disconnect -- latest commitment locked remotely but not locally`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, bob1) = spliceOut(alice, bob, 40_000.sat) + val (alice0, bob0, htlcs) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceIn(alice0, bob0, listOf(50_000.sat)) val spliceTx1 = alice1.commitments.latest.localFundingStatus.signedTx!! - val (alice2, bob2) = spliceOut(alice1, bob1, 30_000.sat) + val (alice2, bob2) = spliceOut(alice1, bob1, 100_000.sat) val spliceTx2 = alice2.commitments.latest.localFundingStatus.signedTx!! assertNotEquals(spliceTx1.txid, spliceTx2.txid) @@ -855,14 +990,16 @@ class SpliceTestsCommon : LightningTestSuite() { // On reconnection, the latest commitment is still unlocked by Bob so they have two active commitments. val (alice4, bob4, channelReestablishAlice) = disconnect(alice3, bob3) val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(channelReestablishAlice)) - assertEquals(actionsBob5.size, 2) + assertEquals(actionsBob5.size, 4) val channelReestablishBob = actionsBob5.findOutgoingMessage() val spliceLockedBob = actionsBob5.findOutgoingMessage() + assertEquals(htlcs.aliceToBob.map { it.second }.toSet(), actionsBob5.filterIsInstance().map { it.add }.toSet()) assertEquals(spliceLockedBob.fundingTxId, spliceTx1.txid) val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(channelReestablishBob)) - assertEquals(actionsAlice5.size, 1) + assertEquals(actionsAlice5.size, 3) val spliceLockedAlice = actionsAlice5.hasOutgoingMessage() + assertEquals(htlcs.bobToAlice.map { it.second }.toSet(), actionsAlice5.filterIsInstance().map { it.add }.toSet()) assertEquals(spliceLockedAlice.fundingTxId, spliceTx2.txid) val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(spliceLockedBob)) assertEquals(actionsAlice6.size, 2) @@ -875,12 +1012,16 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(bob6.commitments.active.map { it.fundingTxId }, listOf(spliceTx2.txid, spliceTx1.txid)) actionsBob6.has() actionsBob6.contains(ChannelAction.Storage.SetLocked(spliceTx1.txid)) + assertIs>(alice6) + assertIs>(bob6) + resolveHtlcs(alice6, bob6, htlcs, commitmentsCount = 2) } @Test fun `disconnect -- splice tx published`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, bob1) = spliceOut(alice, bob, 40_000.sat) + val (alice0, bob0, _) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceOut(alice0, bob0, 40_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! val (alice2, _) = alice1.process(ChannelCommand.Disconnected) @@ -899,13 +1040,14 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- latest active commitment`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, bob1) = spliceOut(alice, bob, 75_000.sat) + val (alice0, bob0, _) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceOut(alice0, bob0, 75_000.sat) // Bob force-closes using the latest active commitment. val bobCommitTx = bob1.commitments.active.first().localCommit.publishableTxs.commitTx.tx val (bob2, actionsBob2) = bob1.process(ChannelCommand.Close.ForceClose) assertIs(bob2.state) - assertEquals(actionsBob2.size, 7) + assertEquals(actionsBob2.size, 17) assertEquals(actionsBob2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx).txid, bobCommitTx.txid) val claimMain = actionsBob2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) actionsBob2.hasWatchConfirmed(bobCommitTx.txid) @@ -940,7 +1082,8 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- previous active commitment`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, bob1) = spliceOut(alice, bob, 75_000.sat) + val (alice0, bob0, _) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceOut(alice0, bob0, 75_000.sat) // Bob force-closes using an older active commitment. assertEquals(bob1.commitments.active.map { it.localCommit.publishableTxs.commitTx.tx }.toSet().size, 2) @@ -965,7 +1108,8 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- previous inactive commitment`() { val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) - val (alice1, bob1) = spliceOut(alice, bob, 50_000.sat) + val (alice0, bob0, _) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! val (alice2, _) = alice1.process(ChannelCommand.MessageReceived(SpliceLocked(alice.channelId, spliceTx.txid))) assertEquals(alice2.commitments.active.size, 1) @@ -983,7 +1127,8 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- revoked latest active commitment`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, bob1) = spliceOut(alice, bob, 50_000.sat) + val (alice0, bob0, _) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val bobCommitTx = bob1.commitments.active.first().localCommit.publishableTxs.commitTx.tx // Alice sends an HTLC to Bob, which revokes the previous commitment. @@ -1016,7 +1161,8 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- revoked previous active commitment`() { val (alice, bob) = reachNormalWithConfirmedFundingTx() - val (alice1, bob1) = spliceOut(alice, bob, 50_000.sat) + val (alice0, bob0, _) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val bobCommitTx = bob1.commitments.active.last().localCommit.publishableTxs.commitTx.tx // Alice sends an HTLC to Bob, which revokes the previous commitment. @@ -1034,7 +1180,8 @@ class SpliceTestsCommon : LightningTestSuite() { @Test fun `force-close -- revoked previous inactive commitment`() { val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) - val (alice1, bob1) = spliceOut(alice, bob, 50_000.sat) + val (alice0, bob0, _) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! val (alice2, _) = alice1.process(ChannelCommand.MessageReceived(SpliceLocked(alice.channelId, spliceTx.txid))) assertIs>(alice2) @@ -1056,20 +1203,80 @@ class SpliceTestsCommon : LightningTestSuite() { handlePreviousRevokedRemoteClose(alice6, bobCommitTx) } + @Test + fun `force-close -- revoked previous inactive commitment after two splices`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true) + val (alice0, bob0, _) = setupHtlcs(alice, bob) + val (alice1, bob1) = spliceOut(alice0, bob0, 50_000.sat) + val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! + val (alice2, _) = alice1.process(ChannelCommand.MessageReceived(SpliceLocked(alice.channelId, spliceTx.txid))) + assertIs>(alice2) + assertEquals(alice2.commitments.active.size, 1) + assertEquals(alice2.commitments.inactive.size, 1) + val (bob2, _) = bob1.process(ChannelCommand.MessageReceived(SpliceLocked(bob.channelId, spliceTx.txid))) + assertIs>(bob2) + assertEquals(bob2.commitments.active.size, 1) + assertEquals(bob2.commitments.inactive.size, 1) + val bobCommitTx = bob2.commitments.inactive.first().localCommit.publishableTxs.commitTx.tx + + // Alice sends an HTLC to Bob, which revokes the inactive commitment. + val (nodes3, preimage, htlc) = addHtlc(25_000_000.msat, alice2, bob2) + val (alice4, bob4) = crossSign(nodes3.first, nodes3.second, commitmentsCount = 1) + val (alice5, bob5) = fulfillHtlc(htlc.id, preimage, alice4, bob4) + val (bob6, alice6) = crossSign(bob5, alice5, commitmentsCount = 1) + + val (alice7, bob7) = spliceOut(alice6, bob6, 50_000.sat) + val spliceTx1 = alice7.commitments.latest.localFundingStatus.signedTx!! + val (alice8, _) = alice7.process(ChannelCommand.MessageReceived(SpliceLocked(alice.channelId, spliceTx1.txid))) + assertIs>(alice8) + assertEquals(alice8.commitments.active.size, 1) + assertEquals(alice8.commitments.inactive.size, 2) + val (bob8, _) = bob7.process(ChannelCommand.MessageReceived(SpliceLocked(bob.channelId, spliceTx1.txid))) + assertIs>(bob8) + assertEquals(bob8.commitments.active.size, 1) + assertEquals(bob8.commitments.inactive.size, 2) + + // Alice sends an HTLC to Bob, which revokes the inactive commitment. + val (nodes9, preimage1, htlc1) = addHtlc(25_000_000.msat, alice8, bob8) + val (alice10, bob10) = crossSign(nodes9.first, nodes9.second, commitmentsCount = 1) + val (alice11, bob11) = fulfillHtlc(htlc1.id, preimage1, alice10, bob10) + val (_, alice12) = crossSign(bob11, alice11, commitmentsCount = 1) + + // Bob force-closes using the revoked commitment. + handlePreviousRevokedRemoteClose(alice12, bobCommitTx) + } + + @Test + fun `recv invalid htlc signatures during splice`() { + val (alice, bob) = reachNormalWithConfirmedFundingTx() + val (alice1, bob1, htlcs) = setupHtlcs(alice, bob) + val (alice2, commitSigAlice, bob2, commitSigBob) = spliceInAndOutWithoutSigs(alice1, bob1, inAmounts = listOf(50_000.sat), outAmount = 100_000.sat) + assertEquals(commitSigAlice.htlcSignatures.size, 4) + assertEquals(commitSigBob.htlcSignatures.size, 4) + + val (alice3, _) = alice2.process(ChannelCommand.MessageReceived(commitSigBob)) + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(commitSigAlice.copy(htlcSignatures = commitSigAlice.htlcSignatures.reversed()))) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) + assertIs>(alice4) + assertEquals(SpliceStatus.None, alice4.state.spliceStatus) + val (bob4, _) = bob3.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) + assertIs>(bob4) + assertEquals(SpliceStatus.None, bob4.state.spliceStatus) + // resolve pre-splice HTLCs after aborting the splice attempt + resolveHtlcs(alice4, bob4, htlcs, commitmentsCount = 1) + } + companion object { + private val spliceFeerate = FeeratePerKw(253.sat) + private fun reachNormalWithConfirmedFundingTx(zeroConf: Boolean = false): Pair, LNChannel> { val (alice, bob) = reachNormal(zeroConf = zeroConf) val fundingTx = alice.commitments.latest.localFundingStatus.signedTx!! val (alice1, _) = alice.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_FUNDING_DEPTHOK, 42, 3, fundingTx))) val (bob1, _) = bob.process(ChannelCommand.WatchReceived(WatchEventConfirmed(bob.channelId, BITCOIN_FUNDING_DEPTHOK, 42, 3, fundingTx))) - val (nodes2, preimage, htlc) = addHtlc(5_000.msat, alice1, bob1) - val (alice2, bob2) = nodes2 - assertIs>(alice2) - assertIs>(bob2) - val (alice3, bob3) = crossSign(alice2, bob2, commitmentsCount = 1) - val (alice4, bob4) = fulfillHtlc(htlc.id, preimage, alice3, bob3) - val (bob5, alice5) = crossSign(bob4, alice4, commitmentsCount = 1) - return Pair(alice5, bob5) + assertIs>(alice1) + assertIs>(bob1) + return Pair(alice1, bob1) } private fun createSpliceOutRequest(amount: Satoshi): ChannelCommand.Commitment.Splice.Request = ChannelCommand.Commitment.Splice.Request( @@ -1077,7 +1284,7 @@ class SpliceTestsCommon : LightningTestSuite() { spliceIn = null, spliceOut = ChannelCommand.Commitment.Splice.Request.SpliceOut(amount, Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector()), requestRemoteFunding = null, - feerate = FeeratePerKw(253.sat) + feerate = spliceFeerate ) private fun spliceOut(alice: LNChannel, bob: LNChannel, amount: Satoshi): Pair, LNChannel> { @@ -1091,29 +1298,28 @@ class SpliceTestsCommon : LightningTestSuite() { val parentCommitment = alice.commitments.active.first() val cmd = createSpliceOutRequest(amount) // Negotiate a splice transaction where Alice is the only contributor. - val (alice1, actionsAlice1) = alice.process(cmd) - val spliceInit = actionsAlice1.findOutgoingMessage() + val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) // Alice takes more than the spliced out amount from her local balance because she must pay on-chain fees. assertTrue(-amount - 500.sat < spliceInit.fundingContribution && spliceInit.fundingContribution < -amount) - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(spliceInit)) - val spliceAck = actionsBob1.findOutgoingMessage() + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) + val spliceAck = actionsBob2.findOutgoingMessage() assertEquals(spliceAck.fundingContribution, 0.sat) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) - val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) - val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) - val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) - val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) - val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) - val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(actionsBob5.findOutgoingMessage())) assertIs>(alice5) val commitSigAlice = actionsAlice5.findOutgoingMessage() actionsAlice5.has() - val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(actionsAlice5.findOutgoingMessage())) - assertIs>(bob5) - val commitSigBob = actionsBob5.findOutgoingMessage() - actionsBob5.has() + val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(actionsAlice5.findOutgoingMessage())) + assertIs>(bob6) + val commitSigBob = actionsBob6.findOutgoingMessage() + actionsBob6.has() checkCommandResponse(cmd.replyTo, parentCommitment, spliceInit) - return UnsignedSpliceFixture(alice5, commitSigAlice, bob5, commitSigBob) + return UnsignedSpliceFixture(alice5, commitSigAlice, bob6, commitSigBob) } fun spliceIn(alice: LNChannel, bob: LNChannel, amounts: List): Pair, LNChannel> { @@ -1123,37 +1329,34 @@ class SpliceTestsCommon : LightningTestSuite() { spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(createWalletWithFunds(alice.staticParams.nodeParams.keyManager, amounts)), spliceOut = null, requestRemoteFunding = null, - feerate = FeeratePerKw(253.sat) + feerate = spliceFeerate ) - // Negotiate a splice transaction where Alice is the only contributor. - val (alice1, actionsAlice1) = alice.process(cmd) - val spliceInit = actionsAlice1.findOutgoingMessage() + val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) // Alice adds slightly less than her wallet amount because she must pay on-chain fees. assertTrue(amounts.sum() - 500.sat < spliceInit.fundingContribution && spliceInit.fundingContribution < amounts.sum()) - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(spliceInit)) - val spliceAck = actionsBob1.findOutgoingMessage() + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) + val spliceAck = actionsBob2.findOutgoingMessage() assertEquals(spliceAck.fundingContribution, 0.sat) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) // Alice adds the shared input and one input per wallet utxo. - val (alice3, actionsAlice3, bob2) = (0 until amounts.size + 1).fold(Triple(alice2, actionsAlice2, bob1)) { triple, _ -> + val (alice3, actionsAlice3, bob3) = (0 until amounts.size + 1).fold(Triple(alice2, actionsAlice2, bob2)) { triple, _ -> val (alicePrev, actionsAlicePrev, bobPrev) = triple val (bobNext, actionsBobNext) = bobPrev.process(ChannelCommand.MessageReceived(actionsAlicePrev.findOutgoingMessage())) val (aliceNext, actionsAliceNext) = alicePrev.process(ChannelCommand.MessageReceived(actionsBobNext.findOutgoingMessage())) Triple(aliceNext, actionsAliceNext, bobNext) } - val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) - val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) assertIs>(alice4) val commitSigAlice = actionsAlice4.findOutgoingMessage() actionsAlice4.has() - val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) - assertIs>(bob4) - val commitSigBob = actionsBob4.findOutgoingMessage() - actionsBob4.has() - + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) + assertIs>(bob5) + val commitSigBob = actionsBob5.findOutgoingMessage() + actionsBob5.has() checkCommandResponse(cmd.replyTo, parentCommitment, spliceInit) - return exchangeSpliceSigs(alice4, commitSigAlice, bob4, commitSigBob) + return exchangeSpliceSigs(alice4, commitSigAlice, bob5, commitSigBob) } private fun spliceCpfp(alice: LNChannel, bob: LNChannel): Pair, LNChannel> { @@ -1163,40 +1366,80 @@ class SpliceTestsCommon : LightningTestSuite() { spliceIn = null, spliceOut = null, requestRemoteFunding = null, - feerate = FeeratePerKw(253.sat) + feerate = spliceFeerate ) - // Negotiate a splice transaction with no contribution. - val (alice1, actionsAlice1) = alice.process(cmd) - val spliceInit = actionsAlice1.findOutgoingMessage() + val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) // Alice's contribution is negative: that amount goes to on-chain fees. assertTrue(spliceInit.fundingContribution < 0.sat) - val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(spliceInit)) - val spliceAck = actionsBob1.findOutgoingMessage() + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) + val spliceAck = actionsBob2.findOutgoingMessage() assertEquals(spliceAck.fundingContribution, 0.sat) val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) // Alice adds one shared input and one shared output - val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) - val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob2.findOutgoingMessage())) - val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) - val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) + val (bob3, actionsBob3) = bob2.process(ChannelCommand.MessageReceived(actionsAlice2.findOutgoingMessage())) + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(actionsBob3.findOutgoingMessage())) + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) assertIs>(alice4) val commitSigAlice = actionsAlice4.findOutgoingMessage() actionsAlice4.has() - val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) - assertIs>(bob4) - val commitSigBob = actionsBob4.findOutgoingMessage() - actionsBob4.has() + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) + assertIs>(bob5) + val commitSigBob = actionsBob5.findOutgoingMessage() + actionsBob5.has() + checkCommandResponse(cmd.replyTo, parentCommitment, spliceInit) + return exchangeSpliceSigs(alice4, commitSigAlice, bob5, commitSigBob) + } + fun spliceInAndOutWithoutSigs(alice: LNChannel, bob: LNChannel, inAmounts: List, outAmount: Satoshi): UnsignedSpliceFixture { + val parentCommitment = alice.commitments.active.first() + val cmd = ChannelCommand.Commitment.Splice.Request( + replyTo = CompletableDeferred(), + spliceIn = ChannelCommand.Commitment.Splice.Request.SpliceIn(createWalletWithFunds(alice.staticParams.nodeParams.keyManager, inAmounts)), + spliceOut = ChannelCommand.Commitment.Splice.Request.SpliceOut(outAmount, Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector()), + feerate = spliceFeerate, + requestRemoteFunding = null + ) + val (alice1, bob1, spliceInit) = reachQuiescent(cmd, alice, bob) + val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(spliceInit)) + val spliceAck = actionsBob2.findOutgoingMessage() + assertEquals(spliceAck.fundingContribution, 0.sat) + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(spliceAck)) + // Alice adds the shared input and one input per wallet utxo. + val (alice3, actionsAlice3, bob3) = (0 until inAmounts.size + 1).fold(Triple(alice2, actionsAlice2, bob2)) { triple, _ -> + val (alicePrev, actionsAlicePrev, bobPrev) = triple + val (bobNext, actionsBobNext) = bobPrev.process(ChannelCommand.MessageReceived(actionsAlicePrev.findOutgoingMessage())) + val (aliceNext, actionsAliceNext) = alicePrev.process(ChannelCommand.MessageReceived(actionsBobNext.findOutgoingMessage())) + Triple(aliceNext, actionsAliceNext, bobNext) + } + // Alice adds the shared output. + val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(actionsAlice3.findOutgoingMessage())) + val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(actionsBob4.findOutgoingMessage())) + // Alice adds the splice-out output. + val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(actionsAlice4.findOutgoingMessage())) + val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(actionsBob5.findOutgoingMessage())) + assertIs>(alice5) + val commitSigAlice = actionsAlice5.findOutgoingMessage() + actionsAlice5.has() + val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(actionsAlice5.findOutgoingMessage())) + assertIs>(bob6) + val commitSigBob = actionsBob6.findOutgoingMessage() + actionsBob6.has() checkCommandResponse(cmd.replyTo, parentCommitment, spliceInit) - return exchangeSpliceSigs(alice4, commitSigAlice, bob4, commitSigBob) + return UnsignedSpliceFixture(alice5, commitSigAlice, bob6, commitSigBob) + } + + private fun spliceInAndOut(alice: LNChannel, bob: LNChannel, inAmounts: List, outAmount: Satoshi): Pair, LNChannel> { + val (alice1, commitSigAlice, bob1, commitSigBob) = spliceInAndOutWithoutSigs(alice, bob, inAmounts, outAmount) + return exchangeSpliceSigs(alice1, commitSigAlice, bob1, commitSigBob) } private fun checkCommandResponse(replyTo: CompletableDeferred, parentCommitment: Commitment, spliceInit: SpliceInit): TxId = runBlocking { val response = replyTo.await() assertIs(response) assertEquals(response.capacity, parentCommitment.fundingAmount + spliceInit.fundingContribution) - assertEquals(response.balance, parentCommitment.localCommit.spec.toLocal + spliceInit.fundingContribution.toMilliSatoshi()) + assertEquals(response.balance, parentCommitment.localCommit.spec.toLocal + spliceInit.fundingContribution.toMilliSatoshi() - spliceInit.pushAmount) assertEquals(response.fundingTxIndex, parentCommitment.fundingTxIndex + 1) response.fundingTxId } @@ -1210,9 +1453,14 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice1, actionsAlice1) = alice.process(ChannelCommand.MessageReceived(commitSigBob)) assertTrue(actionsAlice1.isEmpty()) val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(commitSigAlice)) + val incomingHtlcsBob = bob1.commitments.latest.localCommit.spec.htlcs.incomings() when { - bob1.staticParams.useZeroConf -> assertEquals(actionsBob1.size, 4) - else -> assertEquals(actionsBob1.size, 3) + bob1.staticParams.useZeroConf -> assertEquals(actionsBob1.size, 4 + incomingHtlcsBob.size) + else -> assertEquals(actionsBob1.size, 3 + incomingHtlcsBob.size) + } + incomingHtlcsBob.forEach { htlc -> + // Bob re-processes incoming HTLCs, which may trigger a fulfill now that the splice has been created. + assertNotNull(actionsBob1.filterIsInstance().find { it.add == htlc }) } val txSigsBob = actionsBob1.findOutgoingMessage() assertEquals(txSigsBob.swapInServerSigs.size, aliceSpliceStatus.session.fundingTx.tx.localInputs.size) @@ -1222,25 +1470,27 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(actionsBob1.hasOutgoingMessage().fundingTxId, txSigsBob.txId) } val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(txSigsBob)) - if (alice1.staticParams.useZeroConf) { - actionsAlice2.hasOutgoingMessage() - } else { - assertTrue { actionsAlice2.filterIsInstance().none { it.message is SpliceLocked } } - } val txSigsAlice = actionsAlice2.findOutgoingMessage() assertEquals(txSigsAlice.swapInServerSigs.size, bobSpliceStatus.session.fundingTx.tx.localInputs.size) assertEquals(actionsAlice2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, txSigsAlice.txId) actionsAlice2.hasWatchConfirmed(txSigsAlice.txId) + if (alice1.staticParams.useZeroConf) { + assertEquals(actionsAlice2.hasOutgoingMessage().fundingTxId, txSigsAlice.txId) + } else { + assertNull(actionsAlice2.findOutgoingMessageOpt()) + } actionsAlice2.has() + val incomingHtlcsAlice = alice1.commitments.latest.localCommit.spec.htlcs.incomings() + incomingHtlcsAlice.forEach { htlc -> + // Alice re-processes incoming HTLCs, which may trigger a fulfill now that the splice has been created. + assertNotNull(actionsAlice2.filterIsInstance().find { it.add == htlc }) + } when { aliceSpliceStatus.session.fundingParams.localContribution > 0.sat -> actionsAlice2.has() aliceSpliceStatus.session.fundingParams.localContribution < 0.sat && aliceSpliceStatus.session.fundingParams.localOutputs.isNotEmpty() -> actionsAlice2.has() aliceSpliceStatus.session.fundingParams.localContribution < 0.sat && aliceSpliceStatus.session.fundingParams.localOutputs.isEmpty() -> actionsAlice2.has() else -> {} } - if (alice1.staticParams.useZeroConf) { - assertEquals(actionsAlice2.hasOutgoingMessage().fundingTxId, txSigsAlice.txId) - } val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(txSigsAlice)) assertEquals(actionsBob2.size, 2) assertEquals(actionsBob2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, txSigsBob.txId) @@ -1248,9 +1498,8 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(alice.commitments.active.size + 1, alice2.commitments.active.size) assertEquals(bob.commitments.active.size + 1, bob2.commitments.active.size) - - assertTrue { alice2.commitments.isMoreRecent(alice.commitments) } - assertTrue { bob2.commitments.isMoreRecent(bob.commitments) } + assertTrue(alice2.commitments.isMoreRecent(alice.commitments)) + assertTrue(bob2.commitments.isMoreRecent(bob.commitments)) assertIs>(alice2) assertIs>(bob2) @@ -1289,7 +1538,6 @@ class SpliceTestsCommon : LightningTestSuite() { /** Full remote commit resolution from tx detection to channel close */ private fun handleRemoteClose(channel1: LNChannel, actions1: List, commitment: Commitment, remoteCommitTx: Transaction) { assertIs(channel1.state) - assertEquals(0, commitment.remoteCommit.spec.htlcs.size, "this helper only supports remote-closing without htlcs") // Spend our outputs from the remote commitment. actions1.has() @@ -1297,6 +1545,8 @@ class SpliceTestsCommon : LightningTestSuite() { Transaction.correctlySpends(claimRemoteDelayedOutputTx, remoteCommitTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) actions1.hasWatchConfirmed(remoteCommitTx.txid) actions1.hasWatchConfirmed(claimRemoteDelayedOutputTx.txid) + assertEquals(commitment.localCommit.spec.htlcs.outgoings().size, actions1.filterIsInstance().filter { it.txType == ChannelAction.Blockchain.PublishTx.Type.ClaimHtlcTimeoutTx }.size) + assertEquals(commitment.localCommit.spec.htlcs.size, actions1.findWatches().size) // Remote commit confirms. val (channel2, actions2) = channel1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(channel1.channelId, BITCOIN_TX_CONFIRMED(remoteCommitTx), channel1.currentBlockHeight, 42, remoteCommitTx))) @@ -1306,10 +1556,17 @@ class SpliceTestsCommon : LightningTestSuite() { // Claim main output confirms. val (channel3, actions3) = channel2.process(ChannelCommand.WatchReceived(WatchEventConfirmed(channel1.channelId, BITCOIN_TX_CONFIRMED(claimRemoteDelayedOutputTx), channel2.currentBlockHeight, 43, claimRemoteDelayedOutputTx))) - assertIs(channel3.state) - assertEquals(actions3.size, 2) - actions3.has() - actions3.has() + if (commitment.remoteCommit.spec.htlcs.isEmpty()) { + assertIs(channel3.state) + assertEquals(actions3.size, 2) + actions3.has() + actions3.has() + } else { + // Htlc outputs must be resolved before channel is closed. + assertIs(channel2.state) + assertEquals(actions3.size, 1) + actions3.has() + } } private fun handlePreviousRemoteClose(alice1: LNChannel, bobCommitTx: Transaction) { @@ -1317,13 +1574,17 @@ class SpliceTestsCommon : LightningTestSuite() { val (alice2, actionsAlice2) = alice1.process(ChannelCommand.WatchReceived(WatchEventSpent(alice1.channelId, BITCOIN_FUNDING_SPENT, bobCommitTx))) assertIs(alice2.state) // Alice attempts to force-close and in parallel puts a watch on the remote commit. - assertEquals(actionsAlice2.size, 7) val localCommit = actionsAlice2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx) assertEquals(localCommit.txid, alice1.commitments.active.first().localCommit.publishableTxs.commitTx.tx.txid) val claimMain = actionsAlice2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) + assertEquals( + alice1.commitments.active.first().localCommit.spec.htlcs.outgoings().size, + actionsAlice2.filterIsInstance().filter { it.txType == ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx }.size + ) actionsAlice2.hasWatchConfirmed(localCommit.txid) actionsAlice2.hasWatchConfirmed(claimMain.txid) assertEquals(actionsAlice2.hasWatchConfirmed(bobCommitTx.txid).event, BITCOIN_ALTERNATIVE_COMMIT_TX_CONFIRMED) + assertEquals(alice1.commitments.active.first().localCommit.spec.htlcs.size, actionsAlice2.findWatches().size) actionsAlice2.has() actionsAlice2.has() @@ -1378,14 +1639,16 @@ class SpliceTestsCommon : LightningTestSuite() { // Alice detects that the remote force-close is not based on the latest funding transaction. val (alice2, actionsAlice2) = alice1.process(ChannelCommand.WatchReceived(WatchEventSpent(alice1.channelId, BITCOIN_FUNDING_SPENT, bobCommitTx))) assertIs(alice2.state) - assertEquals(actionsAlice2.size, 7) // Alice attempts to force-close and in parallel puts a watch on the remote commit. val localCommit = actionsAlice2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.CommitTx) assertEquals(localCommit.txid, alice1.commitments.active.first().localCommit.publishableTxs.commitTx.tx.txid) val claimMain = actionsAlice2.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.ClaimLocalDelayedOutputTx) + val pendingHtlcs = alice1.commitments.active.first().localCommit.spec.htlcs + assertEquals(pendingHtlcs.outgoings().size, actionsAlice2.filterIsInstance().filter { it.txType == ChannelAction.Blockchain.PublishTx.Type.HtlcTimeoutTx }.size) actionsAlice2.hasWatchConfirmed(localCommit.txid) actionsAlice2.hasWatchConfirmed(claimMain.txid) assertEquals(actionsAlice2.hasWatchConfirmed(bobCommitTx.txid).event, BITCOIN_ALTERNATIVE_COMMIT_TX_CONFIRMED) + assertEquals(pendingHtlcs.size, actionsAlice2.findWatches().size) actionsAlice2.has() actionsAlice2.has() @@ -1408,8 +1671,11 @@ class SpliceTestsCommon : LightningTestSuite() { // Alice's transactions confirm. val (alice4, actionsAlice4) = alice3.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice3.channelId, BITCOIN_TX_CONFIRMED(bobCommitTx), alice3.currentBlockHeight, 43, bobCommitTx))) - assertEquals(actionsAlice4.size, 1) actionsAlice4.has() + assertEquals( + pendingHtlcs.outgoings().toSet(), + actionsAlice4.filterIsInstance().map { it.htlc }.toSet() + ) val (alice5, actionsAlice5) = alice4.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice4.channelId, BITCOIN_TX_CONFIRMED(claimMainPenalty), alice4.currentBlockHeight, 44, claimMainPenalty))) assertEquals(actionsAlice5.size, 1) actionsAlice5.has() @@ -1418,6 +1684,72 @@ class SpliceTestsCommon : LightningTestSuite() { actionsAlice6.has() actionsAlice6.has() } + + private fun reachQuiescent(cmd: ChannelCommand.Commitment.Splice.Request, alice: LNChannel, bob: LNChannel): Triple, LNChannel, SpliceInit> { + val (alice1, actionsAlice1) = alice.process(cmd) + val aliceStfu = actionsAlice1.findOutgoingMessage() + val (bob1, actionsBob1) = bob.process(ChannelCommand.MessageReceived(aliceStfu)) + val bobStfu = actionsBob1.findOutgoingMessage() + val (alice2, actionsAlice2) = alice1.process(ChannelCommand.MessageReceived(bobStfu)) + val spliceInit = actionsAlice2.findOutgoingMessage() + return Triple(alice2, bob1, spliceInit) + } + + private fun spliceFee(alice: LNChannel, capacity: Satoshi): Satoshi { + // The splice initiator always pays fees from their local balance; this reduces the funding amount. + assertIs(alice.state) + val fundingTx = alice.commitments.latest.localFundingStatus.signedTx!! + val expectedMiningFee = Transactions.weight2fee(spliceFeerate, fundingTx.weight()) + val actualMiningFee = capacity - alice.state.commitments.latest.fundingAmount + // Fee computation is approximate (signature size isn't constant). + assertTrue(actualMiningFee >= 0.sat && abs(actualMiningFee.toLong() - expectedMiningFee.toLong()) < 100) + return actualMiningFee + } + + data class TestHtlcs(val aliceToBob: List>, val bobToAlice: List>) + + private fun setupHtlcs(alice: LNChannel, bob: LNChannel): Triple, LNChannel, TestHtlcs> { + val (nodes1, preimage1, add1) = addHtlc(15_000_000.msat, alice, bob) + val (nodes2, preimage2, add2) = addHtlc(15_000_000.msat, nodes1.first, nodes1.second) + val (alice3, bob3) = crossSign(nodes2.first, nodes2.second) + val (nodes3, preimage3, add3) = addHtlc(20_000_000.msat, bob3, alice3) + val (nodes4, preimage4, add4) = addHtlc(15_000_000.msat, nodes3.first, nodes3.second) + val (bob5, alice5) = crossSign(nodes4.first, nodes4.second) + + assertIs(alice5.state) + assertEquals(1_000_000.sat, alice5.state.commitments.latest.fundingAmount) + assertEquals(770_000_000.msat, alice5.state.commitments.latest.localCommit.spec.toLocal) + assertEquals(165_000_000.msat, alice5.state.commitments.latest.localCommit.spec.toRemote) + + val aliceToBob = listOf(Pair(preimage1, add1), Pair(preimage2, add2)) + val bobToAlice = listOf(Pair(preimage3, add3), Pair(preimage4, add4)) + return Triple(alice5, bob5, TestHtlcs(aliceToBob, bobToAlice)) + } + + private fun resolveHtlcs(alice: LNChannel, bob: LNChannel, htlcs: TestHtlcs, commitmentsCount: Int): Pair, LNChannel> { + // resolve pre-splice HTLCs after splice + val (preimage1a, htlc1a) = htlcs.aliceToBob.first() + val (preimage2a, htlc2a) = htlcs.aliceToBob.last() + val (preimage1b, htlc1b) = htlcs.bobToAlice.first() + val (preimage2b, htlc2b) = htlcs.bobToAlice.last() + val nodes1 = fulfillHtlc(htlc1a.id, preimage1a, alice, bob) + val nodes2 = fulfillHtlc(htlc2a.id, preimage2a, nodes1.first, nodes1.second) + val nodes3 = fulfillHtlc(htlc1b.id, preimage1b, nodes2.second, nodes2.first) + val nodes4 = fulfillHtlc(htlc2b.id, preimage2b, nodes3.first, nodes3.second) + val nodes5 = crossSign(nodes4.first, nodes4.second, commitmentsCount) + val aliceFinal = nodes5.second.commitments.latest + val bobFinal = nodes5.first.commitments.latest + assertTrue(aliceFinal.localCommit.spec.htlcs.isEmpty()) + assertTrue(aliceFinal.remoteCommit.spec.htlcs.isEmpty()) + assertTrue(bobFinal.localCommit.spec.htlcs.isEmpty()) + assertTrue(bobFinal.remoteCommit.spec.htlcs.isEmpty()) + assertEquals(alice.commitments.latest.localCommit.spec.toLocal + htlc1b.amountMsat + htlc2b.amountMsat, aliceFinal.localCommit.spec.toLocal) + assertEquals(alice.commitments.latest.localCommit.spec.toRemote + htlc1a.amountMsat + htlc2a.amountMsat, aliceFinal.localCommit.spec.toRemote) + assertEquals(bob.commitments.latest.localCommit.spec.toLocal + htlc1a.amountMsat + htlc2a.amountMsat, bobFinal.localCommit.spec.toLocal) + assertEquals(bob.commitments.latest.localCommit.spec.toRemote + htlc1b.amountMsat + htlc2b.amountMsat, bobFinal.localCommit.spec.toRemote) + return Pair(nodes5.second, nodes5.first) + } + } } 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 de788e393..e696c284d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SyncingTestsCommon.kt @@ -331,6 +331,22 @@ class SyncingTestsCommon : LightningTestSuite() { actions1.hasOutgoingMessage() } + @Test + fun `recv Disconnect after adding htlc but before processing settlement`() { + val (alice, bob) = init() + val (nodes1, _, add) = TestsHelper.addHtlc(55_000_000.msat, payer = bob, payee = alice) + val (bob1, alice1) = nodes1 + val (bob2, alice2) = TestsHelper.crossSign(bob1, alice1) + + // Disconnect before Alice's payment handler processes the htlc. + val (alice3, _, reestablish) = disconnect(alice2, bob2) + + // After reconnecting, Alice forwards the htlc again to her payment handler. + val (_, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(reestablish.second)) + val processIncomingHtlc = actionsAlice4.find() + assertEquals(processIncomingHtlc.add, add) + } + companion object { fun init(): Pair, LNChannel> { // NB: we disable channel backups to ensure Bob sends his channel_reestablish on reconnection. diff --git a/src/commonTest/kotlin/fr/acinq/lightning/io/peer/ConnectionTest.kt b/src/commonTest/kotlin/fr/acinq/lightning/io/peer/ConnectionTest.kt index ca2e86f5a..ac3ce5c52 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/io/peer/ConnectionTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/io/peer/ConnectionTest.kt @@ -1,7 +1,7 @@ package fr.acinq.lightning.io.peer -import fr.acinq.lightning.channel.states.Offline import fr.acinq.lightning.channel.TestsHelper.reachNormal +import fr.acinq.lightning.channel.states.Offline import fr.acinq.lightning.io.Disconnected import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.io.peer.newPeer diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index 4dace9e8d..f15a933fc 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -7,14 +7,15 @@ import fr.acinq.lightning.Lightning.randomBytes64 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.channel.* +import fr.acinq.lightning.channel.TestsHelper.crossSign import fr.acinq.lightning.channel.states.Normal import fr.acinq.lightning.channel.states.PersistedChannelState +import fr.acinq.lightning.channel.states.SpliceTestsCommon import fr.acinq.lightning.serialization.Encryption.from import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.value -import fr.acinq.lightning.wire.CommitSig import fr.acinq.lightning.wire.EncryptedChannelData import fr.acinq.lightning.wire.LightningMessage import fr.acinq.lightning.wire.LiquidityAds @@ -60,6 +61,28 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertEquals(state.commitments.params.channelFeatures, ChannelFeatures(setOf(Feature.Wumbo, Feature.StaticRemoteKey, Feature.AnchorOutputs, Feature.ZeroReserveChannels, Feature.ZeroConfChannels))) } + @Test + fun `backward compatibility test for a pending splice before htlc support`() { + val bin = Hex.decode( + "0402c1f79aa27b2c20c16f93544460bab4d8bf6dcefae621ed5ca5d07fedb52f897c01010410101000037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c09feccb3d240fe25a2f723fef3917edbfe27192dacfe54da43b0fe10038f88fe22e53af1fe6aeb1607fe80000001fd044cfe59682f000090640116001405e0104aa726e34ff5cd3a6320d05c0862b5b01c13280880000000000000000000001000101a51020362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06fd03e8fe59682f00fd03e8906402a12f9846fd62439531cf93f728b64b9e1b2e15ca851bf219325fe194e0fb7998022eed8d9faf0f854b21712a20e2ceb6e4354f768c2de6b8d8911794d508a6817c031a2b78b8502fe54333b880de820dce58a054467c229d02d9686df318475a1f24025ccb72f8bcfbbb459502be992e635dab69bdcf7d8cb9630c84bc44c6190ae7261408220222000000000000000000001004142a510200000000000000000000010003b0a19bf13175e9a28f6a571226071ea9cfeaeb26f9276e789be2ad2cd6a4d21502fd0249020000000001026f4bf7f5d389c6f0a055e69f55a1bf87d8bc0351a3c49ce048760f0f24ecccc00000000000fdffffffb9fa3e79ed827f9121f75543300ea6721a061f2b31c2fea681eb75ce986f0c190000000000fdffffff0140420f0000000000220020987b715dbb90ef5ab15add01c20bea31144bbb968599350c9dc20967164e66b00347304402207ab85e6e292ae514650997620a569c83008b5839330ca9dc5794b0eab6f5ba1302205959f036c04adffc83be6a373b22fbda7f2670f942ef7651eac5a2e5654b0b0901483045022100d4cc35848035c51fee4e8d114fbe66c6de91af4002e4246f15239815a4d0f42d022038688f60010a3d6af945eba5d48f22f2f3d5f3c7069f51aa577da62982f8e234014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b26803473044022053d90787db0dd8466d96e71b759315306a8c273fae801c1831e32eb2478cf96302201137f23667039c9ffbe99b72a48a8d4d26af486a14782340daf8d13ec63d489401483045022100d32e6d7bfeeb96e1572e3a34ec2e313994ce3f58e679c2735dabcc5ac226224c02207db8631ef9d424ae6fdf9b3843b0d065dace67ddf873660b8b192b9ada4e9be0014d21020bc21d9cb5ecc60fc2002195429d55ff4dfd0888e377a1b3226b4dc1ee7cedf3ad2102d8c2f4fe8a017ff3a30eb2a4477f3ebe64ae930f67f907270712a70b18cb8951ac7364024065b268801a0600fd1388cc0047c1f79aa27b2c20c16f93544460bab4d8bf6dcefae621ed5ca5d07fedb52f897cef4cb34b037ee2213ea0a7da496cd5a8f93455d9b273702c4b66a41635596c4e0000fd025b40d4cc35848035c51fee4e8d114fbe66c6de91af4002e4246f15239815a4d0f42d38688f60010a3d6af945eba5d48f22f2f3d5f3c7069f51aa577da62982f8e234fd025d4053d90787db0dd8466d96e71b759315306a8c273fae801c1831e32eb2478cf9631137f23667039c9ffbe99b72a48a8d4d26af486a14782340daf8d13ec63d4894000000fd1388fe2faf0800fe0bebc2000024ef4cb34b037ee2213ea0a7da496cd5a8f93455d9b273702c4b66a41635596c4e000000002b40420f0000000000220020987b715dbb90ef5ab15add01c20bea31144bbb968599350c9dc20967164e66b047522102a3581e881ac04f995c115c616ad184734426d3c28d2eb450f79b8aae214c44b52103b0a19bf13175e9a28f6a571226071ea9cfeaeb26f9276e789be2ad2cd6a4d21552aefd01bc02000000000101ef4cb34b037ee2213ea0a7da496cd5a8f93455d9b273702c4b66a41635596c4e0000000000998a3d80044a010000000000002200208ef5af3d2328c42cab14b324e96b66466edcb7371bf1636a5c542f2a5d8b75804a01000000000000220020b4631c529878505e750560c858330b874b8a98c7291b3ffe480a0281cff23d76400d030000000000220020cfc4f0603c66c6176b9ca453f0e65fd62c1dfacc95b8c13c25c69a335e0dbb64781c0c00000000002200204177bf2f0557fb0ec3bd7223059bda698a9e4d5736a803c2c3efe532bf9977c20400483045022100c8ce40afa3dd248ed3461db107b73c525e6f68828dd3400f5d21fb217811265f0220349361c7d01bbcbeeea46710f56c5b13a207161d39c6d4d44521b8650dc44cbc014730440220023d629e2eef013faa92a1865e3f86f63a0e5c5746b41f53091a9f5afa96831702206acde577e38bb6b73a7bfa19b07be63c8e84e9a3223c4e5b7be89633583fb91d0147522102a3581e881ac04f995c115c616ad184734426d3c28d2eb450f79b8aae214c44b52103b0a19bf13175e9a28f6a571226071ea9cfeaeb26f9276e789be2ad2cd6a4d21552aeec50ce20000000fd1388fe0bebc200fe2faf080035ee4fda2e1781bca6a32971f005395d5f6db59dcff90ae914cf1e1b88f3fe4d025e5f1c7c10edaa1fd989a4dfa548860e7efb37240fa4d7d72d85e6a38eefe18200000001025e9a4845223667a1eaabe5eee4e715817a8b2319a950e0d1d5131a32ce774b5f0000fd075f0400346e42fc793f976fe7151b268b331c6ee2bffe31b78cabe05c81a482346757a1d8476732c27acc6b7e5084298755db43b72ac0b79bef27ad159a08ac8d52bb8f7b2786b6f4b8a28bd40a0e4911ef633e87cebb206475cd40bdaf7328d93cfe33a1205441b8c1aac465d78913f4aefc7027463b0f7552d50a6bc2fa72eb1e2e806db02ebfd0ab3876ecceaba58b4ba7b2f70983ef67fc5cf539c2267efdf6697ddeb17cad9ffcf60f71391a703a54cb1d33c43373112304f82b147ddcbabfe3bf26f96ec86b44a43f10e952310a97d2f4f28c8ec5f19f1218714384ccede114d290af9ba57d6605bed4e023e156811ff2183fb65fb26db24aa190b273787eeaa680874711752d7f614e8589c1a6370b28cc1bcc74bec773e793be93b6b859db3cba78d6e1069faaeac2fb5bec034b229759ee4b150535d452d11b4dce82ef82facf7bec853b2f6865ced290106aec0b7ff3b4b765eafff2fd66f0c73dd66c4379da62dee48cafa22abfcc580f52b8219e4f50474c12102381f503a4e6725972ce25bcf6f08b611c3cef63214a313738dbfcdb4587af602ce58cf9ded063f47c2ad7b39a87a7cd336eb06276b6d12f6d084b1fdb6dabea62d30d15c2e5e710577ad497c14318e7869dc69c2ba0b3352229a6caf7c225d66f5743f925bf8617fd790aed1a88859afc95e43f43895e9650ccd482768407c4b98c1929e103fff7522b1e066e30e6b38a34856f9b45584aa2bf3450819f518ec8a03e7f41f585d7baad25f43024e9042db8e8942d3e7754d93d2fca73202670890a8a1d5e7d89030ec8a2126728df945617b1758be8f87a29a58ec32d77a80a3217d8e8c12ca8ccd3a4a62947cb285be0ca80af7d0fd9ffc52f7c87a289a40b90db417a46c31b9444416869e2165e580cf87dd149f38ab82c3ed892b635c5db67c707913daa35d342dca8f84d8b9ffa368544580322c9f2d05e611b76d40d1239afd37bb221377fdda627021b4cf5386536be73b39c2603e4fcf5cc8a4173cec48ac5ed3287107d0a3190cf9f5e79006b1bd6392970dde110952642c8fc0e385ea4cd8fa716c56d336c17ef39f4eb019d1e461bb9afd1b29b9cefde33dde901c6fe6fc53da370be2b2a46c4f259514ea3a34b03c24722d02708b5ee1827dbdf12abfe07987b45dae444cde32344851d8197d2063b158d4b7eae1b4f310843bf2e413125d9443a08b249deac493e45895a5e7869ac2f90a04d47ee58ac429b6d3c1b0cb30f7ed15aece49076f92fd2bc7ec95c74c4ecaa94cb00003bcce97b13336ddce76de1b990f303f32563a1065e590d1e2ed65498eda3af11657c9e7bf40838f05fdd0b6b758c98d7c6de9691b07ba82249e6fe941d910e8ca1a5dc069cda1145122386d871b797af6d4b16c489a7ddccd68ae5c29c4858660603c6cd9b940f42ffc7797ef324a36b0f02a231d79c834bb9d02cc199e8253579fb456e47415bbe09c9c0d6a537f56c2ce3504c3978822fa4f193a4d51eda7d2d81938fcd099707b9daa381bb2d0e08ad4617ebf26d1205e23229a812fbbd297f50580f1aabe14b259818cfd877a4c014e7a3d766d10f2d2b822f2532352634b89720d884d25479272d1684e74627869153c999062f396c47af4b244778ec0b1b2c09746fa6e71e65f6914a5523cbcaf0e11cb989eb2054fab4938ca84963959d626ccf4b4bf4f7ee8979e8c9c449274e988c8130922bf977ba7650673b6795601470702a3a5aaec49c0f2d91ceb6c8cc954cc350ea0c0463fa2694594febbf2b0519906175f5b9a5786be05b92a270d3e6d5a77739f5a325ff9d4a92a314930bf28b54647ed3ce0d4393474d062ed207d7b8a3a2720dc25d17f0f4ac88479c3ef70530c9a9f9ea23019eb4ac26c1846327244756ec448a99d4f9ebb671601f6e15f0225d1283cf4a678432c8c28af35ae5ec6a646b01acb14d30cd14efe564d4d7559b4d79f02a5f6fed52b8cfdca692bd733832ece06001ed07eea18e28dc8f29e3012d312542fa4aa30dc5419d6970f882b422253878b59a92502e39507cd0bcdae25618b1910502171b7c8e322caa62d3c6676737760bca3e24a6b601982e166b0a1f79d5f4dbc940d3ffe6879586af16d880e3f16c4c84d22152f26663de7b86927df4a500b769204f1cb55b7b2fc744509eecb76fdb4b130d1bd0a196a70fd85e3a8cdb149957703ad8d43981e457f7abaaf2c58b65ecf41f116b4c3dd9fb3e1e2efbe60b4054a3d1baaa0f3091a6b39cd6a61909b1f73d02a610091c8fdb8fbeb303cf4290d79baba0e0700e70548ac3e9af9884d32023712f674802932b6394369489fc722259654e080c4efd9f4e7bf0aa665374b290359ffb36375ed2f5ad1ecc062827831319f6ae8160f2df8bc2a170375b3f21553763034ceffa9f13f8227fa272f40b67444baf7b6f84a9eaf7854fc070ecc581a07ddcfc4a81e8b7d85a6cec1bb5f2f5dd15b5f2c7b85e4b441cabedb2892dee0d5abf0955d665e2aef96892b71be26d753ad4f7d56a54aa1e192161c21659d9ecafe9f728d0658ec5bc6c006e43f76d2e01fc0a9ced289df9915ef488107d5903848537aa6c80878f0b2b1760ea3a1a55146c235688eb8f5d3bdeb2d06f07ec0d1cd4a2cb7ee8d371307c2313acb3e187ff00002a00000000008a010225183d0761088563c7654df562ee91481fe56d13b357c58dc62073fb242f2e96633893ff89d00a475a4fe4c8cc61dc5fd23855763588f939f6cc797fa137b94506226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00002a0000000000658058210101009000000000000003e8000000640000000a000000003b9aca000000000001c1f79aa27b2c20c16f93544460bab4d8bf6dcefae621ed5ca5d07fedb52f897c01ffffffffffffff3b9600010124ef4cb34b037ee2213ea0a7da496cd5a8f93455d9b273702c4b66a41635596c4e000000002b40420f0000000000220020987b715dbb90ef5ab15add01c20bea31144bbb968599350c9dc20967164e66b047522102a3581e881ac04f995c115c616ad184734426d3c28d2eb450f79b8aae214c44b52103b0a19bf13175e9a28f6a571226071ea9cfeaeb26f9276e789be2ad2cd6a4d21552ae0003b0a19bf13175e9a28f6a571226071ea9cfeaeb26f9276e789be2ad2cd6a4d215039bb975b3bec65c671f38d9082c7a30837e95b87b1056ccb2b162564dfaaa8033011fa086010000000000160014dd82e90bd16748f535cab75db1e63b394f5d540dfe00061a80fd044cfd00fd010101010024ef4cb34b037ee2213ea0a7da496cd5a8f93455d9b273702c4b66a41635596c4e00000000fefffffffdfe2faf0800fe0bebc2000106220020c2cb46474d69d1707ef340d590cda687ed2f50467f7f9580b78593470021296ffe2cafc9f0fe0bebc2000102027d0200000001c6d2fa41d053e76a2410e6709e2e0c7feaea76e48948b273a167ff23a193bd540200000000000000000250c3000000000000220020778f86e213a518e8336ec940874ccf4b92910332f5b3c0265374a86dbd34e6a59600000000000000160014ba1b25f56373c6c9c7ba6458e90de5778bb4e12a0000000000fefffffffd03d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc49190256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdfd654000010204fe000186a0160014dd82e90bd16748f535cab75db1e63b394f5d540d00fe00061a80cc0047c1f79aa27b2c20c16f93544460bab4d8bf6dcefae621ed5ca5d07fedb52f897cd73a3879973c5dc3bfe5e8e3890fe57407a8263b3c9b144c5eb1807e3cb9fa230000fd0259407f8b07adf18cc2b1917d8d9ccf4e17cd7464f44016e70b99078e6e9736d479c32ee526ef9ed72607a34fb32cc21f4699da166df973a53c862bac3fcce458a231fd025b4056801e797df204c5c004d382548c3d826b5d4c6975df99262791a7a94b902a6376ad439e527c2f51e2e610e982d81932581123464bad7b6cb9522edd0f4c3f4c000000fd1388fe2cafc9f0fe0bebc2000024d73a3879973c5dc3bfe5e8e3890fe57407a8263b3c9b144c5eb1807e3cb9fa23010000002bd67d0e0000000000220020c2cb46474d69d1707ef340d590cda687ed2f50467f7f9580b78593470021296f47522102068d2c5f0ec2fb9442825b02dcb3f7caa3b28c72c8c6f5fa5bc4c85d93687d1021039bb975b3bec65c671f38d9082c7a30837e95b87b1056ccb2b162564dfaaa803352aedf0200000001d73a3879973c5dc3bfe5e8e3890fe57407a8263b3c9b144c5eb1807e3cb9fa230100000000998a3d80044a010000000000002200203ab9d87665f5b3b6fecf157415d5fe24fe2577da64e5191eb0f789f659f514584a01000000000000220020718752806a51509a24b7bc75f0a8a275710ae0fefad0a1569ec53796d77f4d1e400d030000000000220020cfc4f0603c66c6176b9ca453f0e65fd62c1dfacc95b8c13c25c69a335e0dbb640e580b00000000002200204177bf2f0557fb0ec3bd7223059bda698a9e4d5736a803c2c3efe532bf9977c2ec50ce20000000fd1388fe0bebc200fe2cafc9f039baaba55836c454a3198555fe3ef8c18ff7679288503580cf18b887fb626f4b025e5f1c7c10edaa1fd989a4dfa548860e7efb37240fa4d7d72d85e6a38eefe18200" + ) + val state = Serialization.deserialize(bin).value + assertIs(state) + assertIs(state.spliceStatus) + assertEquals((state.spliceStatus as SpliceStatus.WaitingForSigs).session.fundingTx.tx.sharedOutput.htlcAmount, 0.msat) + } + + @Test + fun `backward compatibility test for a pending splice after htlc support`() { + val bin = Hex.decode( + "0402c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4101010410101000037108815ff0128f7ed22640485c226d9ad64a9fd6d8b41b6623565aed6b34812c09feb944a462fefea53b9dfe1171a760fe02058e56fefc6d42e3fe00a9cffafe2e095d7afeb16b16fdfe80000001fd044cfe59682f000090640116001405e0104aa726e34ff5cd3a6320d05c0862b5b01c13280880000000000000000000001000101a51020362b19a83930389b4468be40308efb3f352b23142ae25e6aba0465a8220f95b06fd03e8fe59682f00fd03e8906403d504f9e2c0bc22e45093511d4a50694e1bc637175ebebbd68acef755175fb5bb033dde0adb2eadabe2487b45793516d4ed124b109c436fb4e36fcf8e5bc614658702ca6c7d92c1d21d66e5cc73a36572f9b64b276e36904f4908b17d4a3b263416d0021c95a2b55b030d80c09661109ad798d6d839feb26064643b920009b5b341f90a1408220222000000000000000000001004142a51020000000000000002020401fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000000000000000e4e1c0fce4d408665c5cd9eac99ce7babb852909bdbbfab979d981d95fa274af872c8200061b1000029a8bab32cfd069273baf8a1bfc003d975249c1797cf1d1f41ff75d17dc778623721f87c8fe614e47f3b432a325d8879389bd672c368bdf045b5f33527ea7ad5aae4ec7736bad1eefd197b234bc0268689ce2399ae3e8b19330491228efca2f62ce2d75d6f14117171ffb89cc19aefee4747dae2d943b061ec2b80f726a3de455c3f9c88f3291911dfd8f792c8cba419f6062b0031680cb6d73d5b18338311be0a26bae61ee4a73a89287deb7a2da9db0e2247bbdb3b064d1277566cb5838cf1888a8cb21950624b94891fdf592aa9bcad7ce1bd3680dab343bcd023543a34c634ff4ef85ed6177e8ab68296313530bcfc0d82a090a7651649fb08ab02df9b5b03b8787d3b667d0172f938f50352b3e9fea859c623be7a7a881ae34b6eefac2fc9d6fa0b85e707fb5c66816d32c6abb49a3ad15f8e0827b84f4b19e1f5a44178e442a415d0cf8a012e9e76296df8cfea71807aaad372991257ad52fa350ce272c27e25afcb6bc914f6926a5fbb8051e2f7e94735e00d8e781d51be4907f190fdce6570e4597169119a48eaa5660ba8207c40328a8ced33f8f381cdca7cd98ebe9fc38adbc734520e9075153e42c8802fa9f25d11fd5cde6fc194dc57fba5d391e4e85aeaab58a5a8e18396601c806d070b419cfa8e622776f4a81604cd7eadc9569b14d66767986d6836c10b6ddd04df141938431e423bb96f33bc5cf6d9ec496903f88b030216a01d07f7ea9a5bef992e6253b8a85b16419c4591694c45156bc67d8db0159ba0850d74f137061b9c917c5d3a45927be102a3934070292059c3d049954546b729bc9da4102c4e48157a9095f2567a06a4da5b15751ae492118f49312e9fba0dbc4170597a5da0e17da41320d99b385731c89bb1264b351f1355da52c39c59449a429bfe3ab7623b883bd3ea8fc01992f8874fce447ceff85120579919835660dc38d81388997ce3c7a99caebb32148e5a234ec21a994bcbd9c53f83bf8a78146069ef724e63201e25e9a357a3411eba4e03d09fe498ed23161028304036ec418fdbaf991afd5bc947e7957168574d5587c7e3b024f8f14b08ef7f07fcb287383a7def2e357a5d1cee69e2c691cd8105c54edaf746987e21ed529cc86f48ade1ff95c9d652549e2d1294c31342de7f815bf4fc0b6318ddf41d68f53c171bd0759b672466be594f316f3354e046be1dc0782e08c9f59eeb41a654a7122291c93cdf53f0dbbce659f3050edef293229818f54e36dad64ae5b297337254f456834d2338d1bb8e73f6bd2b1108e34e622514e9cf86227683a8a24bb1d3854dea043ff5ee770eef0ef300e34b994d6939fe6581d0b5c2f47d52482451e78024d092630796ad29411f379ec2a22efd7cc70ee9397d89f09b2b42d0371ddff55a85df1da314050c26e44c1617e7651ecfdd3ff5a79a3285aed565691119fae1802a75f156f21c9c7026ad75d60aa021fffa4fd5e60b86146fff244e8f1db52ab5c97d157d528d5a29ac3ba4dbddbf976a83caf7c2a9fbd3fe3869dc4a1ac9b641ab58ebd0e4b6ad97fddaf36e730d37713010679e594a13b2d56a028509bf85967e04f254258ba7be8c789db448cf65802755527b33efd2cbe310f972df46e7c425e99442faef2e7cd9134d0e05dfa3b83f18228cfbb0daa43dcd08ae572b497f403298871f679b235c16862d144a81336fb5e99d1037064f901c3235f03823b9ca44685669c3dd969dabcee9a50f36161c4772b17f7dd32ebe0e095433b46aa41a12014c602fd014176c4399521e4ef16f0cd58976ddc504eec915fa13fcfe366d203afbd5e1a63eb3c9f05ac9bb31542fd8a174a27d1237226d554df928ddde9a53849f0361b5d667e5bc4a063ac12bb7c29e4022be347c919626a23465368af050e9061af56f784962ae9bcb3d902b01401fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000010000000000e4e1c085748b11066ba78c7a4783329c29fafda6a601e9a52fdf6da215c053494c09c900061b1000030aa45d94b0c31aa64ba3bb2578325dd1a14262334ecf1685f9bbefa73e7e521961719f11427cde91873bad602f7f47bd3b5f1e5c853db889695f7136f2f98e19aa3aeda6c06b2858a3a2cf634768015c113e8ec16fc75a4e95a95fd9425ea134729c355a37fd7878467225bd1a6585f23edbab470ad8846aa820e36c3e28aff2ba86ef4d814373bee10cc51a5d5aac8b576e6dd34472d1d7397341bf6972fce46e1cc49e656a5f667f4c30fd0ffc0444366be23fab79135f7bf47b449f0aa77bc7669af5307f1c890fd047282f74c5f15aaadea00d2288ed30d1ccbeb93c946aacc0ff186414603ac0b9cf943e533c10bbc2c1ca8376c8ad1dd2af606e9ada581c7aef350b8b5332227b32b4b5e285443770c2ebbf4690ce908e76e6bc701c1744300aa148275ca197816c36cbfe483b4f67870d9ffdd63a97c2a49fee181ff9397d74a94235facb2d005b8af3377fc38a1b4be655b10e2daf89b468398e1a91ff021ddfd3d23855bad28b440b237a9c647dd70bc270167ae39ef9d9ac2ddfac13de12d919be6d43970d6ecaab2345e09f918a6f12a93e3887ec087f310de610aa1ed3430677b2de0eea41fd9bde0dde551025476d5c5563df74ea40090d72c00c8170f5138d62c6176637b3ac298e0896bc32512f5fb2d560fd54189273f23155b2092f68ec2e67a08b80c397736c6d7c179842cff7f00d0780e7c078b17021b1f7a570d0123fee296559607960b353cbfe72c502602614fe020206cb6a99269cde4b60f58225ee04e289eaa435279a49759402dddc767ffdede72a38b71c077f6e5154ee1a5fab12171ef4dcc7aac8ced38260e217d48aeabf06ae719d2c8b0a4fcfa4d982f4da6142e3f643000ac8a5d5ae553c36022ed7b89988d65d389e80676142111798b13ed1a8a7f9e097cbb5c36e6b2d3b35dcb0e2007ecb870c2137ea3c7f27e7a6aec807bd1056fc26b901bc54da7a593ebdde261340fe18bf3fbf6601dd032d40cc57122de5318c45ea3470290c710fa77162eeb6c29b4b136fb6a88cdc2fc3deec720a8bd4324cdf4b73c90b62a3883ed1ff0a105860e012fdb00a5853bf4227b105855a0131d9a334d7b88dacbc9f92675a39bb1793dcd22d41c9037345fb71e3548adf118025cbdcaf7cbfdcdeb80d9cdf4424f37475b1b51cce27780f857730d22f38768d44b9905416cad1c0056a03366bd2f8363e72fd56c4d02bee5c256daa9f68d1e63aa500bfd2583706ffdcc6bb95b5649d630bba92f2b3651eda32177b009389dae7334c6f3ad67299fab21bae4f1a7a2aab9a1c6d7f38fc230944005dd6819b621404af8ad74b080a4948b7fc272f65035e688f6ce04c498393415feeaa2c1801d11b126bf1e8315eb7f65457b731da70515fa52790580b547cbd3f27048b29ad5adc2b6a7610a473113aa4625776928fcd5a4dfa7134b4c6c3fb8dd2c72d412d272592ed1bb4909e1fc149ae88ad9eb038b0bb9077de52b41a31f7b3f42f3651f31c96f11ce307071973461139c9536c619fe30c6026dc9e78a28b31d3e39fc370304f8f077024b3eb3d8085641c256bda6f7f8b01dce9ec8898a77a526d5a6faff4c55a6b592560474d8e26406f8c8077dfd41b00b58c42295521dfba5d54ebdbe76a868946f9a2c10516161613eb618aa427502f80c6bcec6138c1f32806a849000ef09bd4967781762ecb3fee037a82f0103777a86c374891ab923a0c6de2d1c0400247eff31abb781e19ee270a47d471ca810cb68db4fe2a35d16bda49c67f6f5bc684bd997ef9f76142a341cecb025f497750c430d71a1e31232689025a8b445d689089811ea6405704ff71b2ef39a1ed570d0bf380132d32a1ea961f51e421f592279f1506912c838dbcf126ce3d1582e32c70fe932df599eab3b2bfedad100ca874de1300fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000000000000001312d005381a6ac452c017023e22a18f5ed62a25fd93aa69a22399468e13222eaa3d46b00061b1000033a7ec4227c55fb74e3f439ac42fc25dbc44c3ad1ac517799fe573c3ba00f5c55639abcbf5faf18cf8065550c6502770190c12dc467b10870e4bb490dfa0042db551a5f228affb013c8d4663623b0584d07d39e553591fa6edecf12e89d5a18c1f633bfac3aa31e1ea1ff324ddcb39e22449ab3c340f2c059c30223d47a82ad7e64fbc6bfc64dbccc3b991e555b3aabbd279489b3f8b6fe0e01792d523678b6a0f60ffeae359b593f7e1e4a5c768e9fcdabc423cb19e5e15be2ffdb9353ac19602e5dc94465d372abc0495c645b1823b1e84dbb3023f9d6cb8470d98d5845f501d804bac7ab6735e6ad1efa3ba05069ad8aa59ce7b53c46361639220a40ee40ac523d1e520bec94918fb25571a3d20083af06431ffe50fe9f2949714b125151bbc44dd5d8e1ca415eecfcd1530ef94d5a1c39f2f8dfb1b4b6a3cdb0804c2bea088e522cfb10378b637662afd1bffd17498c7dc8a7fb8d4ab97960d61e7ed5c3cfc5df77efc6cbf3c74b2438fb3e81ec2a779183fad1487ca527221941c1f59a3029efe088d1f37e7b03ab10a3336faee43c71d4a7f2b952271ac0b9709e4c87050c082ea961cc5c141d9455918f60aff12d2be1c51cad5e00ccfd4552baf41220a46ea93cdef5a77af9ca9a248cec71ac16f726ee4a5961533bed5837f24e87e1cd871b32ae6812b945594471a1533fa1c9345975a9c0ee94a39466fc7b7b9ef3822a3f0bedffbd5c8b01b428ff266afd28167b9d261d23dd398ecd4e32704ae7ea71a002c8514261b8e7fbcc6a1d2d66e140a468f5e4967edd85b741654279c0a5957524a8824b29469ba9f1cd72686a3e57fe08f894c7144bd513948e13538fba50234926364ce4c2360c88b6064e5680df71aedf695066e4cb5ccb2d5f0024b40b737d9b8b1f3e0f84c0fb693b4d1341c1780bc65802de7a81c4080fe3edadabc3627a255edd405098041389112f6a38202241204dd6f2d949c7f05d0f4d71d1b45e0d63f5ab6e57f4e8e85480b6b249d90dde207f332298f1a0fb9be0577b48bcaf5c2c8c413601037252005d6bf809363f192d6d6e9f976dae6ff1a836e985dc6c9b36a23de343e41c1f6415bbb3d9e321457cb7cebc687940db24e28e95b207bd2f6b47c815e6eabc11f4d75f81ab3d66fd20e1900607166d5b9f3f7ea697b1409a48f2d14aa1bcb6e8e73cc30e895edbabbc3ae9294a10f14c8d20cb4adbbe4dbb65cc41896f9582f3bfaf09c05c996e86892c24dac3ffe6ee7abfc5a60d81e249e534e065e61300f9088d6706f694c6f182516afc1b798f264c2831f434b25980b2ca445b356077478eabd275431ec974dabf203f274f7ca40a1eee8beaffefef469b9ab2377904375bdcf0cd8ce7117a835784e4f70cc048f04c62a4cb9fcb058597022b7523ff66abc1f93c140289fb758e7f2bee700e3f115ee503e76be983c5a8dd3e7f5c5e6029ee14af2b940851287ba5104994206c888a5e5f4dbca06127dea382cd0e368da52b924c967f5dd10b7021628292e34105d191a8aeb1e246be83685f553cb858c87f69f66f704c7ce0ffd1adbb610b2fdd562f26d86ffb333573e8b9a5a34d0998153f7c8d8426f9fb215c97e75530eab3ae0189d5930ae58a86b98a6cf59612fd09cb58ad4d1bd415cc041f8a1ecb5b8eab711700b3f2f7b63b57545efcc3bc2abf6224e6ce725926679daf1de033471f5a38d7e1ce9ad0349cd486102c1384956a469d83cc740e3ee1fddcbb9315e4b3a8489bc29168a1a823c1dd8bd0086d6eab123e0165c7d13d121158badb991c2401bde86d30233d275946acab7b0645f91e13676a32ee047b41ab51e9581d5ec9e61079c2292dbaf6ebb827b5d9955329b1251db24b9e3d78a02ef71b3c0595372ff50bc7c89cca6c93f78f07c3b285dca3d18ef671c11d00fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000010000000000e4e1c0963c4f67ddbf9158cf994c9b800bca63e3850f605bc85fb20901bc321fea49f100061b100002d3be755d19eb3f22830ed19c131bdded376e979d5d5caf48590ce4dd49ca28156027dc02b5ea32642693dff80fabd9bedfd1d30fb63cb956af2faae4482ddcb7a566f3efb99ceb035bdf6486629c5394e9ef47d4f522cc5ffb26d34a3d8bed1b9a1e15fdce639b4ea4b04dadb725000400d1c7cbb731a896e1bb11374ccfd7eeb67ada369e7796072ea24cdd27d343574409eca54527f9362f64ef9f9048a17a575eb292a5f56953f51e1a982af8b7855bbdaac0c9c2c05cb51a8e761d91af07a5083da19b9c1b270679649270f09b79971200344783f71c9c69744679fe44472148163ee09933f2a98f39a695a1e005282143c78cee3b59348d74c679a7cdf423106d24d9fc8468388591d0dba05a2e17cfdd1a0e59025feed98597794ebde5114ea049f3791591815d7dbe45844dcc47d1ed84df345558a55dc63b4927eaac22700f0900db5ee173c1e33d12f04891bb6f2e8efdbecdd8e0027f09e57570458ae13dc7af76bb7bacfff1ba21c6a079132df3b5c174ff324aa6c8798677d8eb3695975141cbf666fedf7689f624b0c64ebf0c8cb240b1a6426af1be3d5fa490b59845a405b2f8857093200deca6363ddfb0de045d2e78d66972640175a71efb88d7a099c20c4170b6bede458422b7232c069ac55ec6cda7eba9967de4ded2d3aa34732bd3fca4881fcfc160dbfdc87cd3ee205eb947286ff869387ff28ddaf96ab9d5a5f4133fe54b40b5f474a1f3d8f0ff7c38a19aeeacc31f7a6fa881d9bc86c150e768c61c6edc0a9e44fdab89245a8396263841335d4b9bfc9a0742f588dcfa2caad92d628157bb8b00afcb7c58c98ef0021742134e54d76b45d562f7b491f52db40327c47291e9b95c621368cc0ab4b8f4900ba33a2c6e7a6bc550bedc38003392be68ca6d00a8f2e67d2257c93c17728420e748f5994597d3a3fb5d5cfe069c774f2dfc3165d2fb5fdac1ecee75b59dcaf2d90cad5fba836d4b77c6b7610074b5dd32c18261d2e6ba08c53f9dd83e38f52b861e721d66144843563435c5381f12726de5e71d83db4c146f9b3b9b61690eaeeff6ec8d81cf33632c19618d6570d38e0221b86bf6dc75b18b97605f70428639d7d7e27610f180728284d139a4ad6f4d9620001ecd91e36e03c13dd97da08ec7fb0fba109d38f5c1a379e5e7ae605f6a5b5621c21f7a8512e85667a09f9e1a002cd96922c52efc56df06aebcaec109ba7efe52ba802a1f70d5118e0f791912e4eba469511f0c4da0aa0fb389aaaabf1a4a88cb501de945ef4bc435569af2ff6c9a4f71d0cba8fedf66cae778aa9d736d6b569ea018db2d809aabf49c8b0b1c36f8328b309107ad48185f58343262055308bac59d799b353e4ccc751e70cf732a01b5ae6067754ca3c3e7fa41f87072c7cf1e6c245c268a467cfba367c977a3729d0ab3c6684ce8cdad117ca2b8845a7edaf6d448ccc99fe021b37624f26184262edb6490e54c1ebeb5a02d9855c55bfb75127dc68f7b30821ddb4063dd1bb122919618f6bda7f84b6e1ac23e4b2945c96c982dc21630b38cf9b26ad12ef6ba5183954edcf5be015c5e5e6a89be58985585d06d7b652fc41069457148c9163d0685bb3d3107ba2593a1e50f75f3cc6852d0c92fe05598fa4114269c093c8673363a96b94afe3814d8760ad35e42c1dda1ad7745cc8e7f283deed2ce70a47c1b946beadb30b9ead8871b4a28722c59a95b191507ece52e2303066202d140d465347a3886a7581a65cc7d5d19631fc101bab643775437e32614882e9c8cf73c9290399ab2cafb04d8e4e7209012309364432879c1d3c55b34857bd73ad00ffc509a841c3f5be33474c3746ac56cd6e80fdf4f8d394ec43028647506a75b60f1987c44f58ebc2884e2da28f978950ec4bb467b122d03731ee94eca2b1649f5694a010002f09665e2eeca7660da13f8da413103422dcc969353cc879117a19cb8015b6b3f02fd024802000000000102cab049a8b896ae4f3bbd3f186da0c70e072450f28ac8feba10397825531643030000000000fdffffffe640308a43bc2e97b2fcd4fde5544b218b1ba7a4c97d8d7ad4a53d095fda879b0000000000fdffffff0140420f00000000002200203c6449397ab890ddb2c688ba0f997711958f86348171c384cdf6de73262964d3034830450221009cc6d358989f2fc83be197704d30ef2bb389444026a9ece3a02eb46c31a5e05902205498f3bb1fecd8fc4110db13e0ccf917527a40ab05ae5ca29bcd0ee045691f4b01473044022052f42b542e5fea664190bb155a4faf8125625a7ccde1b062dad20ff9997d116c02206f61450f1a15abcf98ec36b7d409e8cf9e52a07ba4b62d854f09d3c0491dbbe5014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b2680347304402201332a587a4c89f7f8c462679065571cff321cc91b89c41c39becd9b1e7d0155e022025766dde4256e3702065ade2527137bfd0585e8c6ec0fba96efa5c8f1231bc410147304402201c7a612bfdf94b18ded6b38ca16f2eeb59c8a666c2be0f7ccb7fd0974dad8ee5022011d51c60156a90b664a0990b72be492d56c1f05a2f01d8f004c1d61f094e88fe014d21020bc21d9cb5ecc60fc2002195429d55ff4dfd0888e377a1b3226b4dc1ee7cedf3ad2102d8c2f4fe8a017ff3a30eb2a4477f3ebe64ae930f67f907270712a70b18cb8951ac7364024065b268801a0600fd1388cc0047c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee41e518fbfcedefdf243f55a8dfd74720db1151aa67d2e20eff6769b88e090b8dfe0000fd025b4052f42b542e5fea664190bb155a4faf8125625a7ccde1b062dad20ff9997d116c6f61450f1a15abcf98ec36b7d409e8cf9e52a07ba4b62d854f09d3c0491dbbe5fd025d401332a587a4c89f7f8c462679065571cff321cc91b89c41c39becd9b1e7d0155e25766dde4256e3702065ade2527137bfd0585e8c6ec0fba96efa5c8f1231bc410002040100010100000001fd1388fe2de54480fe09d5b3400024e518fbfcedefdf243f55a8dfd74720db1151aa67d2e20eff6769b88e090b8dfe000000002b40420f00000000002200203c6449397ab890ddb2c688ba0f997711958f86348171c384cdf6de73262964d347522102f09665e2eeca7660da13f8da413103422dcc969353cc879117a19cb8015b6b3f21032416cc1fe1b31abaa3e22a41d5c54e1983effa7bc938d125ebd2c76e76fb5ff952aefd026802000000000101e518fbfcedefdf243f55a8dfd74720db1151aa67d2e20eff6769b88e090b8dfe0000000000cda2a880084a0100000000000022002082016b313812b58d8f06353fc06d888a1ad9cc1128541a5be2664563f46adcc04a0100000000000022002084ffb24b0b4addf353cd00c446f32115e9b8a41d247a16e8beb43aa2dee0edfe983a0000000000002200204651e3ec6c4fed0b1f605b5741a6a8625f493f781e2c819ec8c3c6e7348af71f983a0000000000002200205f4ed602c54e242b68a69a4f2c3e5705a822cfc99ebe09c773d3ebe9e664dc07983a000000000000220020b2f26c100af28a13b2244425b39ef455cfc0f65631dae208ef8b78cc442e93f6204e000000000000220020cb5de3c3c0e6f31526a01918323d64ca6ab611d9748354850c7f3a9bfd35668388840200000000002200205ba45f7fb84eccb104b280ec6ea4c261794c0ccef55d48cd24306c2dbd668511d8990b0000000000220020d5c5a151e77693d1e9dfa22cf95627caf2e07d6ab6302dd43ad6642261e572d70400473044022076d2f4b970d507aab4b5ba0941c8376d51703b6f22975b4db539505c37fb02e8022021573e5ab6a3884b6affad3553542fb413f26fa712427cd0db05a0e01ad9393d01483045022100fce0ae4376792dece9e818223b842d848dc3a36c22b4048a39d8abcbf50df4c502204fc3f71c9867047e39334a9a9da8bcfde19f4af549c08d32e4a3160305a352070147522102f09665e2eeca7660da13f8da413103422dcc969353cc879117a19cb8015b6b3f21032416cc1fe1b31abaa3e22a41d5c54e1983effa7bc938d125ebd2c76e76fb5ff952aee42c1320040124564801f75d10fc0d3ebc96546ff55fc0a8222c11d6b3fe1fed139eec78773665020000002b983a0000000000002200204651e3ec6c4fed0b1f605b5741a6a8625f493f781e2c819ec8c3c6e7348af71f8e76a914c7b8ea9abe20031e831bfb6b272dc61bc28625eb8763ac67210370b06f7de39f797875f856b65bd0335702eeecd89da5152b613f911e7e7a81817c8201208763a914a3368d0f95a9d5a5213fbbd902b0bb41799bf59288527c2103bd7bc3b1e792135ac5b037ba642544d943efa07aa79fd13f28ea1cc7a812559a52ae677503101b06b175ac6851b275685e0200000001564801f75d10fc0d3ebc96546ff55fc0a8222c11d6b3fe1fed139eec7877366502000000000100000001ce2c000000000000220020d5c5a151e77693d1e9dfa22cf95627caf2e07d6ab6302dd43ad6642261e572d700000000963c4f67ddbf9158cf994c9b800bca63e3850f605bc85fb20901bc321fea49f1014a0b2f50bd9bea034c555d19bad8debcfc9c69e02fd491e1e523047fdfbf874d74527b613acaff90958abb93377a9f1884bc35bb22bc45168794311faf6b921f8846104b4eede36bcfad5a1f08612b179c06e8b807a4b9c7041a9577820d53b0397d395b5c151d1c7c4c98aef4202850bb374ca166e588d77172ea4059280f370224564801f75d10fc0d3ebc96546ff55fc0a8222c11d6b3fe1fed139eec78773665030000002b983a0000000000002200205f4ed602c54e242b68a69a4f2c3e5705a822cfc99ebe09c773d3ebe9e664dc078876a914c7b8ea9abe20031e831bfb6b272dc61bc28625eb8763ac67210370b06f7de39f797875f856b65bd0335702eeecd89da5152b613f911e7e7a81817c820120876475527c2103bd7bc3b1e792135ac5b037ba642544d943efa07aa79fd13f28ea1cc7a812559a52ae67a914beb2665f75b44846b376692599ac1136d1e92e4e88ac6851b275685e0200000001564801f75d10fc0d3ebc96546ff55fc0a8222c11d6b3fe1fed139eec7877366503000000000100000001962d000000000000220020d5c5a151e77693d1e9dfa22cf95627caf2e07d6ab6302dd43ad6642261e572d7101b0600001431a6b6215917bd621896989020ff4bfb3f267942eab5b542d165cd66fef42b66370463a52f27c8c81bec913c97973eda1b3d0900cce2056d7895a7642d7aa87757fd82d0a1b0e0369e1b95151287173b281ad19f00f6ed8eeeebd05168ad6f139c75e0891d839246ff479eba4f930b2097544b7c7bb121a129e7eb143de75a0224564801f75d10fc0d3ebc96546ff55fc0a8222c11d6b3fe1fed139eec78773665040000002b983a000000000000220020b2f26c100af28a13b2244425b39ef455cfc0f65631dae208ef8b78cc442e93f68876a914c7b8ea9abe20031e831bfb6b272dc61bc28625eb8763ac67210370b06f7de39f797875f856b65bd0335702eeecd89da5152b613f911e7e7a81817c820120876475527c2103bd7bc3b1e792135ac5b037ba642544d943efa07aa79fd13f28ea1cc7a812559a52ae67a9146f273f74b2d5924977cb3721e8432ae43df3d0c288ac6851b275685e0200000001564801f75d10fc0d3ebc96546ff55fc0a8222c11d6b3fe1fed139eec7877366504000000000100000001962d000000000000220020d5c5a151e77693d1e9dfa22cf95627caf2e07d6ab6302dd43ad6642261e572d7101b060001768792af3889d1076719874fb43cdaa418a8eaf86bf879e4900799a0befd5e0a38eb7a275b4bec988c16fcc5c43e8a9b0f0d75f096d59bc2f82d002841be3cee2029c1b2fe76651a85309af3096fe23c7ad19a8d65812047859555a49f8d86bd6f14ff9d12f79c850b8b28ef47302d671aa016767092ef91c645d64c8a83ed1d0124564801f75d10fc0d3ebc96546ff55fc0a8222c11d6b3fe1fed139eec78773665050000002b204e000000000000220020cb5de3c3c0e6f31526a01918323d64ca6ab611d9748354850c7f3a9bfd3566838e76a914c7b8ea9abe20031e831bfb6b272dc61bc28625eb8763ac67210370b06f7de39f797875f856b65bd0335702eeecd89da5152b613f911e7e7a81817c8201208763a914d121d2b5e94b468755bf4211ed3b6eb4d5f6f9a088527c2103bd7bc3b1e792135ac5b037ba642544d943efa07aa79fd13f28ea1cc7a812559a52ae677503101b06b175ac6851b275685e0200000001564801f75d10fc0d3ebc96546ff55fc0a8222c11d6b3fe1fed139eec78773665050000000001000000015640000000000000220020d5c5a151e77693d1e9dfa22cf95627caf2e07d6ab6302dd43ad6642261e572d7000000005381a6ac452c017023e22a18f5ed62a25fd93aa69a22399468e13222eaa3d46b00e97dfb9628f68d13d8a6ab83baea6486910e8caa1519e68f0291502538cf90091d09cc8ea4aedd4ecec8f3a1125daf3488c41c67783e24f9755f9d2d4de561f2473faf22e3a8f8bb78949a6fd53e79782f99f7174790f225bf89debc64997f7c50020e29cc3a9c3de852ac8fa4d91db21542ce6128856e07a9cbaeb2fcde331502040000000101000101fd1388fe09d5b340fe2de54480d80016448094c8d21e6a4154217520915a3986a202c12b88c1a75d71e01dd0ed033ff4187890461554b5d54904615de7aacadb4fab6a0fc4f1069b5c5018d76578000002002464646433376264342d383933612d343033632d396563342d356330383965373034366333012465353639643533342d303764612d343934352d613634372d3161633764306438636330340102712dd19f3afc7b852f3d1d6d89c3e002b1ec428628f08cfa7d49db9caf4deef5013f00000000000000000000000000000000010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010144bc5b4d741e2299b98674759ea81f53bde096eabb77c2afeb9940f4551fe70401ff0000fffffffffffefd27c0040280c4bc523117bc0fa8dbcc773a861ea98bdeb7412f1c389e4c590b7d9665e04569ebdfc88231b9d794c33bf46e109a80ff878f46661bdb7497923eded70c88654a2b23a951428eea8b6d7a11e6ba0f59e173d056db544c0555a076202ade02d186423c9ea1cd3c17086c71381752a4977c67376298d4f4029fc8faeb2910fbf95457a115ea65afbfbc2385cac7e488726d17b9006026a4123f4f8750286728f338472df150782d9ceafbc3497217e1222852248d0a22f811cbcf2e6fb08d1a4fff7ce47be649e082744fd5a371cb064df1d563b9a5fbab6d38a093de5d51d59c45975320ee02f151adaebfb94cf1f18b9a42e609a7d8132398906317d45aefc231bcf606f2f7acf3cdeae1cd92f93b45c514d28692328c591a375a1e0a9d6c9bfa7b9b5252fb1cab318218ff2f74e3aac621620d1012ecad5be5d3e7fe83995647ac2d824a00bbd175a50095183d2c47850d371b52344721b68b25c8b385e596d46ebf26224b9e1f295f62026176b3a6254287dc99f70a9cc8f9088d17cbeaf6ce5846029581d2a2486fe8227ce1dde0e7bc4e02b73c3ca9e6ee0ae0b783eb9d39675f16797c5be1745cff6df077c3f4ca3cb9b285c6cfdf068607287a24b16e8cb717b3d2e48701a35f3126d4aad2cd52a894272d2c308d1843dee80fb0d0ed39f8da0c1c7eaad94a77f586265db4ea617f6eff9ca3408ab66f56691e214a722a7d6f74d8be9de87f6b38251d6cd95d5e7a0608135336112362030e4cabbe0a36779f713aa6d4896dc844a80b66dcec1741914f92f449098cfa44010a843afeadba3adbf833dec0972f2b272bb1bc6b170a514c432afa41af7ecf2f31d52b56ad397df86c5d73042335ceddccd78bb508ea66a9f701abbc1f7289085c87358c5960a27170aca5773662f5ea572fa2ab37162770f0e404dff7a3e7e913015653c10c8301b46fac15864cf40dc8b61288456870d9221ce552b20e58cddeadb3a1cd6489fdf7b726f8541fc13adfc7a8b49294bc70fd0f90f46b6ec01805d24dd1d57addb9be256603f5e8b19ab624cbdc3bcac784a53a2339864b547be7d554f1eaae6cb48aa9f832b48e230240d234c5de92c5fee2d8b2f27e87e9f15ef22d7deed2124cb40433edab718fe9b13eec70c1235c3455e4dbea337cf234ce6c471e6a13cbd293afe20f0f43232e947c6d35f626c28ac4f3f985f3eb3a763d3ce5ad2c2d56e5d083dc651332f9140ae0f1dd3e1a3c5b132904db5715bdaba8b2c274091d601ab604e6869cb4c20b31223ac7e5473cc1d8da94e5bbb10ec03c9793794586cdf657b274dd4d1f5606837744a419434590a5c419b4d076e3e615280e15715359af752928a818c27e3b36fc3728c0b90b872fc04b64a3a40dbc4a142ec57a3019a3db8cd4bbdb8f2aecf54c7cf635fa029339f4e79db677b823fc3a2522f33829414ca982aa2b209203ae653f2f6ba42a16b70044b2735718a0c9e5018e4c894c5cc816a68b6637e403ac7233884bea0987c40e7dd2687ef91fdf21836018ce3c15e5f8a7272916b747cc26d9a4a8d5e2345d7ea4dbf913d2440e3f640eb1f2d76c611a7daa59e14c78da06c450150233a4da94b889fde218b70a8ae534172d371e50ec9cf6a9de7161e884b2af88e9e77a4b654c8a12ae507663d1faac78154cbf2343266a3423ed4f038994036907fcff5482d7a243d1ae31dc46ab506c9943cb93c7ef520233ad3c4e097e6330ec06b6f9c4705403e4402ca83ed5654e0ddb2c1f7ea69f4e8b168ef74e68724ee29220d68ae0045d706ccde4e66932e50eae480f2aa0c7ad93dafaa2822aa7d4c84194f66c1ed4159efe82e04359a5c875c24b13d3fdd5ddfdba405088040222b0988a92cfde6c41d095037545ee9475798b182d99da1151ad0bf6f2bda03e43748c0e8c81b6affcbde82604b9adea628491f9b45d10b4c9ff906b66e8bfafb549ae9f859bbe99f06133ddb330513e4b8da8218d907f1fa64b6150592c9247395b50297688fd9f92b46d78a871667a93969d5d51668acd1d756632ac31cb5e4f26b55d239fbc553518dfef90364345714119590a5ea040210c5f08e468dea52d6622adf97c768611a4161a72c376edbbfdf73ab1c5471486ee7e165f98e87e6036da0b479fdb2b86fb02b935770e693c39ab193aff9e908ab009966c4a77730801a2eb6ac1caaa8026d36354d8243590764960ad5d2a80c3f3d1bd5054eca0f2c400c77482c68888c60078bb97348368917325a6c89d7ecaeb1f35ce75f0f7207d7e199f56e36b8708e79de2de2bd7da2af2d50e89c7e777df49ce6883ef57c0da080f8ff5fba0fc6018841e74e5fbbb6aa85ca320480a2b6d148f7a9ff83630447d49d01d6d8807921ebcb39e9264d159197272b637f3cdc6cdf5ca9dfca61d2309f8e21b834c0a04c7c6eb7307c1e278dd62f9b3165a4d29f9406c540905d49ac68370bfe313970e5d828a729af41dbd157e71f97b8a225762419f2fb534ee8308b2a2e739cf07fd7e2643490b09019d756587b961d06a3714410aac541a978c847117f5a6a79e0577921f6228aff7740b650ff86e6f868ef91e6c75237ced93f2eb45bde3dbdf78e327e9e8fb29f593e7b0d1cfafa90a5ce5300b72c49b95c4ae07a48c439a8e26fa4862aa70fe419a5259a3ea0fe0e8a71d955cd302d9d25b8ee5b589b53f118f8b650cfdc15df09edd9a0ba10286a62a01e18f8450035e323088aeb519cc5aee65b43a342ac891c0e9d39d6e55605ccffb0e1fcc02ca82d5decd6a3bb9b3162ca302345439481170607a6ed975e49e24e102443d00087869b680c7d40da9ee7ec5d2b9d52906d058854990ae10a9fb9320a7cd1c171b429d0dbecb7b05e89f4e858d3137d3e0d85d35232c3bcfbc81928608bf11a033ac1900419ca2bd01b895aa6a4a0820603f79644cb42d3b2c49189f20fe615101acbaa52cb43b76a8e408967a9f81920c0bc210d27f60a1d013bb4c3ea4c690eefeed2ce1b019cce9a351533cd2e6a8431a5e909b5f7a4dec760aa836be08005727fa69ad36ca2a15e65f91e9f69c74efbd239f2c736a5037b5e53775c81d58bd4108cebcff5e233e10d8f4ca75e14fab61e1585fd56b42f145f8d1e46266ee69a1fc61a4e18186702ac19cff7764b34c83a391643ab068f74cbba256a7b39c2581969d82e59bfa503e76019830fe20308597670cd71137dd1247a18f4fa79b5a06f2f17907ed9e6597d0b7d98a2d1ecef1479399d887e35535a3c83f8e14dc7c56abd6cf16866ab60d4820d544fb1e18d84749347a73ca2eba764e81641c0a83fd8f176fb4287173e0e988dc4a64b21b09d95300ec9d2a81ee5dae9c35f24ab5b0be5f9afaaf2987c721f3e4caecaa965a23b8402ba56c7ddc13b18f32ce6aef8ff0c7eea1b55b4eecaf3b9cb0dc8c879324c488e5a299985a61e616561f8a51687b22b407aec99c00c052441dc32ad6064d9c700a90791ca908fb73f6f4113293d908088006c768b53c32b5ed54572290c299b1add38d2f885d395a2043f35b1792ea4f2a997b07e19c463ad76c0dd9ad461572eaeab32cf2d20890a5f1ac0e28fa261afe36b579b6ccdf996a56e17373511a834fcc64c48c2132334bb7670fa1d5536191d9cf74734278f821b585b5ee36c69c04fd88cf5663cd0233cb140426b4206fc6972e176dc8af86d51ccc4159ecc685902c52f582f725af84891b4ee7d8a05f30e544e044b36907ecc5d997c8c5e6452d05607a06d022925bb9b6e02d69f3dea4a97c8b121a371f328f8fad7f98b4a3929efbb13ddaf07f48bd6cd7655c89438b19d8877e718d6620846b65e7216a99f92ea249bd263e8c3f1cde944b474e65bf539e49f20f89375a8d417fb5def1578405174f9b69bc293dbd9d93cf093bf113bd8f50dfc8d8ba121c5a3b4ceb1bcbd190f8d0e5b04bc65870c1674fa3a65155fc3b524f3de4342902a5d91d8c33fb9ffc0677969b2fd2eecf2499f6fb40088ccaea6ae7c872437e38ed39fef0f6b2d54e666b795abec5c9dd1f761c211976e10e4ba62295bc801e298fad10bd6881f8592da63d68f62e1ed353ba127859be6b6bf1d72e75a40f8e2c0b6cafcf3480398fcc61271d5bb03ac370eaf7219ed598f7b9f0f5eecc01be8b4c32505c511e9f80a1f7da5884af89f046f7403d3a94bffef8b72751da4e30b5af854266bff2ba3882459038733bae80fe8d20b2d3a25bf3c9bbc1e9d2a9033849db3147aff86b0a5c52213a2c1e738484e4eacda508fe58f9c3bc478c8493d41be483ea2e6a99e7083f1f362ad5326ff37817b75bdce06191f76947bee9278485c9ea6fb86213a031c22c4932e301abe20bdad9bdb8780588f51871b6cd6ba9ad219a862c6b57b8e3a8d7e56efbd7dad914209eef9c70843b733b81ae59e8a2b6411b19b8af62c92173cac7decce39fb4cc5827d9194842fa0ac865f737cbd30c8b4e9d8a5d782f317a066aec85a04e5bf7771998b4d1dbe2fac2dd53384908086c0a9b665d18b55892f17c4bbee535521292732bc3469db1280dcbb13b2e4d9430c1c4c9298f679984ee6a0af68bf819439ca5ade2116f338d84908dbb0bbeedefc382056dd23a7b8e91cb91bc613139ae4593ba7aa435fd4f461f788ef50f8fbe793d2d082784daf694b7caa422e29e67372ee1cc70c88517fb29653b675b4befd4551e8ee8f10fa8645b52322e0751296cc507dc247cf5302145dce251709b333582e7a8cb7551e42a88c325afac093265ced134fecaa20142ec9dbe7f54e89b100c46e9e89144409587fb6251c060ea2c1b07da4a80d5cd0fe1e4db694990d936a0d6c8c0b229d5b8e6549d218e39cdfcfda78e87f2f76673f7bba67992da71c25176ec4daef78ef63191f78195d52cb3229dad7f0aaf5d1910c049bca228ace9c55fb07a571b7300982c62e1e00188e00ff6ea97f7082bc1aaf6bff9d3261859579e0bd24373b98decbc19733d474d0c03f26d45d8227fac750633ab7be4c2f8b881c4f324b8e988271cd100b76913937194c24f41f61c1a7e8d27cd3c0df036e50cbc352cb0af816db1cf89595c62284c637d482b9d095aa4d5c4e2194ddfb5c47db729d8c6625500db1cc8093a1c30dc279acaeb82e217462f4ced82ee153af4b434cf8af38524acf61153d92e62f097b1a493c5b4d086e87e7d3f275e298410520f23ce32655c77a781609eed9c25286903713d1479ed379a91a423e2d6b56a4891cf155c17484cfa39da7695f9d449af2c90a25fb2b06948b5bc8ad020edcac8b6cec3cc5b6eba51a87d9ce5f301b0eff126a961a12b84dfa5e994dd5eaf681310b14b33320ee28b94e12877a5b99eff8da9fb9c88a6f86c9743816d2e766ba26967335986bd402dbcd5c077857a8ddca7587eca0a9036efb898240d0b41d9d498f579b842febf24bb875f9e95df507ca03fd8310f22c5f0f3e082c80079b9bef493a587c88e26409d46af9e4b7bee1b4996f669f3b962b12ba29ebbb9bdec9a2b328480d806418e65dceb939dbbb135053e002d26ef0869983442ec2a9dd172b880f40ef383359b6229477df8485dad4ea6c93bc3cad39aec49cbf91e384e1e30d798d08f4f50f1ce22803927d1439b973cc57a6664bf37ba4c7b7dfdcf879fd9d2254195090dcc99286d3af00b37b3aada95275d4994c2885b833c0a0b16e075db4c4d24aa114ac34a6eac6ec2752830d4043078dc95a2d9f37ccef5b67f02732c91c30e606bdc263cec50496c2aed25d63360708180dc037306861e97adda088a86638e36529e47e6ee595be71c50d09f8376623d63711cdce2e96e665be78856c0740c1c2f4b1dd6ba51b969877edfef977fe8835ccf96a360ae124ad5f1c2ce6b785f4a0ee3d127b5337cf49a579ea965a88ce38626d7848a1e0cbde3faf11221834239d21305449caa0085adf151e4aeb848b7f4f83fc3b0c6ec9687db5200f3965765b905887f7e6e6eca7306f3362a3506eb25b47f3484124e50d77b89e014ecb4cd791b3af822c57e70b1cb481956261b7ff30460c2d834fe1600d463ef46192dc6ad9e2d62dfa60e934b8d5d116154f3cb609276e8e0ebf24eb772bb0b5f7b9899c8555ef339691d46cac89493ed0d9c14bb5463fc034cd3f1d89446ab823d0d4c6abdb8413b4b7dc8f80d66b56183a30aa704e79d1a6d5df1fe3b1cf04f6f29dd57936a226a50ab15bd599eb4d64c26708f6cb5390e999ab959b4ceb606730c6a235adb15ac3389e9a198fe3ce5ef36d4bda57aa6268e6bf9b74ba83d02e750dc7d13fec27c86c1582bd5f3cdc6fd73bf78dec71b31b30a5ebdbd29a21c6b88982d321a68c116af3004ed48be80242383c7818524dbd4a9b81c0a174dce900c1ee158b63abb4b2e048e33ea61f400af286985c52aeb3e36a72a27fb1b256d268b0d7e278c8598862dc15d05bcca682c1d61b986ac4cda488e71c7337a92bdacc1bcf00030154ac42cad8312ca30e99c6ae1c0649171a1c1fd7a4f962603597f13b0f6290d254b4643ce17551ffb3d4426d360a3bcbafd936ff6b8c324ba9f4a01d5c59efd61aae1a521d13fb5756af01f2373679d6d50e67458eeae5e12e4cc8f81656f01db6294fbb01066ca0be250391b56ebddaa569b6fad1fd43c11cd53d494fdaf91616752a0d030919c7f166674cd4dc6f73a49243650e14499e591c4ca96b8a8f445435e2032f1f7cdf0096de37ea194b4b7645e560efa0cb1c7bdb3a305e3325693889503a63c6e6cedecd032e589fe0e7721c3d0ffb725cc6d6c23ae3cb0ca97ab6538236ce77f0ef11a49634cc23ea634716d0fb39f830938c5f9f57a03e90e1ad47a7e8ac2ad7d00100b746929625e52328393bf9c5f52321632ae2b6fd3163159ee1da2b4e939e5f08cfd78781369bf1691252ed777b465ab6c9079920f6967dc661559341f68882fc5ff81b5a7e7a5398ed401be65a07b31bba71e22aaade4b2f86f18b7acb34df19a1479eec1a484518aa5fbf769c17b6a3599ec9214a1a34c732cf928af31873e1ee3e930e3af8c05fa086e5a27361322b1f547c2ccf033cecfe6b0130d992c487680fa6f476661e25445a3934aff4dd0423d5f4c4e0cc308a726aba05f7580fd612b6d84c7adba5456285bd4be53ca0ed9cab9cb56e3151799d258d877872a3335415d135b9e894af0128e1092657b9864867ec37aef11c9e07a9aa9876d2f7face519a60e6059dd06c3f320c88b16150fb1bda6d65b0f47460c6d4265e0585dd7c94892d510a60c6cbd6abf9d8c28e91846bc913315a688c4f25a84cd99a96511bfda98be198b6fbf98d7445c297660256625fcb2a57658d74fdd74e2eddf57af8704bc1df7187c3951a513fe8ff1c2bd4687f6a954b7e9034033b37787e37094afa0c44303c58cc730cd6b1ade7b70c16b80ef2bf1e91a591073205c675a9d17b7c0310e563a3cbdc519eac2b8da519de386c83d739e667f40fb09ab288ef43a83d06cdbbe057a8ed7806f171762a6f17c633b723bd87b2d6a2baa71b7a63014cc3b114906a15199a6a95ee9562d94eb59134459e23974e59cf758cb28c7e19d9b6fff9fb6c1817998b09abb5606e055454671a842c8dc385008c5966aa60dc5000c53d7f41b6f432690d1d868573fc02329f6b478f10e9fffa6a06abf758c0709e60cfe28ef2acf97043a2acea8029d9daa48aeb1a89164274b92663be6e1655ed74ed43511093b4414617999944ff750be7594239148bda4cac936290d118b2cbdbe6b9bdcde77c280dfdf0dedf86ce12c2f90355270b8867bba8844cab26107bcee99694c8834244c0c8084a01cbb6bbd9ac0281216f3dec42e1914d0ec036c55db13aba59e241f4287129eae2f3a796cc2791b57ee57f2a4e433f59ab7f0a20c35f726b6b24fd59f1892daf7a50688d320a1d0b3688777ae529b87e94474e7852b18988c313ef96fccdb7f24970cd8e898e569b665e99e6aae51745e10e55c033c5d505e4ba41d1fc258e1d36ec89768ce2c95d55fff9b774a28e7b63434a5c1b1a6e6f7cfbabf4e2f64e41d91e53c6606c7750491b71c9ab40d385df37a96feb0e0ba1529411e270c83f7878fa96c6ce79ab500eb2e5ca6ee84f7cd2e71b16198a3ba3eeb43c8e89ac60ac5615a2ff44a67408d383f1d6f53be9ba86b2229659294bfd99014097ff8d4c215045a18225c71873c9f8d386f97731a8848fa0816d74e67c299ae5361d3e62234af992b41688adbdec1698b65c8168eb315990da33bc1fe84b08c1a8f852260f4b9859fc1f7b305c1f99f158721075525fa7f0721f3f5c8b5aed34d339bfc7fe36ecf3dd65a9fcabac7b65a078682d2571760da7afa18d144e45f1564528935190ac08d9f933d7de045f893897e2962592d9f87e2ea0538ec96888ce5140191265f88063edbf186189747e74a6e30161f08d028326f0be29ac62d771d9241ebff9c63263a0c5421ef6d54b88efe9e0066128cfd9efe0575f339ea69f064c5da1d922e7fba8c1bbf029cfd19d5058aeaadb580e024f3dddf597346eca92d9fe5778ce41302287adf8ce2cda357809d2e7e612d4e4ccce0ba7f778a16cdbf2c3c6cbe98e2abf0f1027bd983442a957ddcbdffcde7044ae872834a8ef4522578bcad20c0f610f99e5153ce6f50f76b03da46e8badcab195a71be260f3f6325e3fe557b49121e596f269ff92a7d8cde9234a68c34f3f1521c9a948425c87b6c9089267fbcbeb7dbe0e0784653af74b8cc2dead5cc683422e4c03411169d25cb9d29d359d5326b1695f3b86b69a9cd6900ee2f91f98a718806e637d031865acac09c39551a0c8e0778665505e46dde3209ddfa7a66d552255bef42b1cd3e2bd7d055704d44678bcb98cf1721b607483b7618d83da557ac42ac860fa617804d547df45ea1852e282523b631d6370f5a73db2405cdc434d63ca7a0a4b857b702621cfc37b5698a245c314cc203cf7251ef84fc594e397f9c30dfd578482e4fe74a08aa0243dd7867c2d85ab4a895c4dafc629b616fc99ab51c6452a43f798a37d3a152f9586527cd6b86bc861e9c77f9fef6a4388e0c012a15933de57bf38d613c623a485249fcb43d466b2669951b47deb1f9640e291fba179a01e081840baa9609b5153dc438b96f87635aea6d5898a640fa2d7a99fb579a858409533d9cc4f3cca2635a9e44a63e9ab88119c6a2ad2232b9ea571e57631e5987412c5f14c18151ff8d4ba98c0a8348c1a76a228de5e7ed016460ef82cf470a351c66b3609f1b77b75c258ab4a65026a4ed1c5a3a1eb21a7834e1565224dc5f11857e0608e433135e7f57c5b64313714154eeae87342f8502d34bb09f295cb37ddca6e19e7ba26d605b30a4465a5b27797818320a9d0c1f821bf6ec7b07568aaf26486cf1425ec32fed928b10ab6913568942b6e41a11c2be5a1cdf1a9effa50a5ca17bc74ec6e356ce9f933303b4a1c38fe34552cbc03cd75cbb4cb71ded491423ef92599dcf00bd3cca89d2bb8de73e2e418878d797f0bd54ace8fc81a4527587421d7d1c336f96d646883e290ca384d20c832728eb8958df7db2c95c1c596d69b1e11615a5117be102ec993995cad0db5481073cd55432c6c5b3142b033ef9fb9052c25a30203f95c490aa382ca7c26e499cafb32ec78c49aa643d0623516b7d6d2c43f9646fb6d6f8a0e771e5db270cd8b24985fdd97281509a9b247a967c920cbd0ab142e61e8f827af5829b323431c8d82ec39ad9a8b4a61055c1a911812d7af2a7327effaea73bbffb939e4ae78df2fd5f8eb07350e0f65c6d3e012d6d8f0ab17318f7304040a3e303d83a3c28961323f2d5f669975a738d988c5c948840751bf66c62d01f8b2f79050991c2ebf46724bf0a12e1059e58ed129e6235678ff62d88236ce6be8589cb530e3fc58b35d9156d061511bf866988737a4b270c8706ca2a9f6c65719f1f967f354634c6998cb40c7cc45b9243ca74041e22152bf5e435ab2bb760a38b0634681ff8bd1d817378635a111bd30c576b0b708d0da10db1e1623987de0193e9df7f157b804b1e7df3209f3fe984cd799ecf82847f2a6adeb76beab58fbdab043aa53145ca77b7253a3fdd01fe49c2d82fbdde2a5acff245b168eecdbdc6dc0be639556620948020755f77a8c76b88740871cbb711ea4dba9dc2c4b8d2264c7067c2ff452477ad6a8ec2821f7cdc40ee259c74024c4d1adbbe705c317ca8418908d7e3101905719cd2b1e45578e94a7f455a7c8071b2fd03a75dbff6b2a428671b4e2c0dc69d98479e192e22148238c67f0c5ed53fbd89770a55fb9e66fdead21fe9920c029e573dc12a5c455126d3eced22f88cde0c53ef7571140e46acca341235fcc356d95625b7b175f3a7ef558b496724c2c02d067b3e1ff2ff18cfba15b87752b0cdb7e9d3be32592162c9e9d2d289a576a5804cd4f3d723d80b95d28c53ead518f0dbd485c8598251bbc653805a9727c43863c3844537249e70806f635e5fa065f25f54a1921d9cbd04a2e6eb101128a81665d3caa811af66af1a90e1428d33ce62984f86f9c4bf741e44e2cd19b4d4dddfcc3ffbf0a9ea79b7b9af77badea1c50f88ab6b2e50df3ab6ec1f6ae0f9f1b037b939ffdaab4ddb7850ffbe2705dea1c320c3175c2cf2c8cc12d36341a4f78a377a3c862308c4ffcf133385adc279da6603df83e839b8baa4fbb9aa38c012787859427046d1ea2bf5da3a7ab19c8295ea0a0cb7f4351f6fcdd4fd0bccf0e93bbd7c3999293cd2e5a3f16a516690ec09e9da2f5da9e562448217b8c369c9ae4ef8fbb20ecb62971aa10a2b05976d1469d9879e2597746b012df197f927e23801929d22d54a32218c4ba3dd02badf90c07fc78feefc4e299533fb6b6ddbe7d4cafb0841b3d478a8d3b60ae7a287bf79dc1e9dc02244844c8a2cddd7c227fd8d6bdca01ba50b259e915674f030964395b7420c99a4a9d96f67933c7d87da1dc2d37cb3af1889f148c495172e2bbbf446cb6b25e67371db616e69f8624f5e019901177e07fb30e2f0fad8857d7a68f53a52cc69538a3eb5fd3d9d8e8693887ef4731c6d722ed9a220b86262d17213a272e162287fa32a78e4fd4f53afb4eff192c3ba43b728033d7e7906f3954a093a18ca31635ff548d89c7d6549c2484ea4096675f016c122346cec34341bd80f400665b7ec56fe64b02379ae4f4aa3bb4487beaae4b0628e836bdce05d63f867212568ccb5b54a22643bffcf83cfb0f5ce0fd0308ad40c823eec1eedb52b0c501c9c682593cc5bad971e34918b1ef5bfc07fbfb21442f4848313be49047d853181c412eb0fef7f37d1d29c6c7f73e485d64539799d1a08e1360454b57e3a935a431bbf1826cc00cfd5e3869b7dde0e98ef4173a2e1952f8b59674fa2d23bb1e0bd49d96c56d657ae1e13764d02e08d59973e3801f47db310a0c8e30895dfdf0092a211275f6614ab060b5d90240741bbc1e8505d73c07670875a5068fbd2aa8f7f42519e605894bfb8fb8bdeedba4f14b308219cf568884d913ba470680798b3235af0f19cf0038edad0604b8153a6e9b6159133b4711d39d41fd5bcb47d86166a7718f5f5a37cca75e97ac127fceb39627cd4803bdda23314215eb4ad55ae6bd20845b04ea5dd3547fcfb985d7a560b33ff68c9b9051be8832d29dbe9bfb9fd6242d8b69e68f644a4030af3b7ea69f9af7bbde858722fe65dcbed9885bda0fe18638575fced02d4072c6026adead36f6b71dd3a68369ec1605798ff3e413adde00291569c7ae319f634c9a9c023ea7aedc74e3f88354b44b1975fb09285b58222a40f92725828415d0ccf42e4f5f64bbaf79ea0e828cd2cb94556e4bb64798866c6da6b6359737804593f374ea1def8b839bddad7fbe1b0dd111a5b9bfd36eea222321b08d7c112aafbf145fdb35448318ffb3d7cc6546b70319e9ff7ea4b7fb0273598b8587e9f488a801c6f0b7633d5e4c1fa23ff951c359516eeec572a049546d120b3af336830d5891fa3b1cf90a6b9d5a40f483781aae73395394cca900b9e2fe6860acc24168a3dd7b6825bd22558f632bac19b6f6605542ffc04d01dc1d3dfa9632decaeb73baca16ce6a569df8ceaf5f0a1c666417e96e2894117802de7ca4e2f45f269b3161b5851ade8e1022c1372a16d23ebf81081fa8b6dac83590279222ed685acd8151233c64555d0c6ed87c9c4710998c96d06e5ea9b85172616b884076b37a67cb1593f9f758343889a8296acac5417694c2641ba13c0a672c77042e8a62bb1fe904f80c47e617d11bcbf72ce4d2f46d9b59d317c86a8ced95f473212f1696b7b83e3b153652a0388ed20e5b3f94c21b31c4c976bc911c0a15a406ab6ed9c00a2dc544042ee63c77f52f0d5bfeb7b9409c80eac183282820ff4d5c2bc45a54372799cce14c72a3f7fe28037608fd211e1e0b7ab6fc67cdd552285aeb13d8ea5467ed472c33d1a51428871d67f5a52585ed9095d879438ee8648f1c890553169d7960d3ec0f081807ee030d36866eb9db7ee598edc8c8823683b694258e40de0aaadcc34f5ce29af284ff01723370052ac337ae6d7dc8bf89b8e481a82bf68affb0ae6ca3792e1ab27b356885ea8121d14526da0461e12a323dc4feee9db6b7b92a57c5ee077a1830eb102b0306fb7ca7566f230fc450d8323bbefaa904507b6a6952611f18db6e965491f7615c77afbc4e664df4d8960406ecc42fc15c69088f4878ecb0615f95ae25f9c1e8b4795b1383422be464804dffb5ad285d3c12ebca97235b66c1bdecc71ca68c603b9b667e17606d075c3ac534e159ecc2a0a57c04528f1e3a017ccd03475af48ffc9f9f923d2711afa0b866c45cb3a0ce61856195a2bc1780be45691b4f70a21d9422dc57ad3f6ce73f2cab2299e3c906bc01527d920e942a255795903274bddb61bb094d3020020ad6cceaf4c13688002901b1260e9af07afdaf3e38dd962f196ac279b166515b6575c585be17f4bb3fe25891a882436aa5d627231c72266aa9ee88e0b618dfde3002450bb1cf7bb0087e104ba64fcc1c771aba7a07b64db9d2c11e283b22303dd5193e7e85f9b6b2b6b0c06810e1116eaf1294b6aad5f4c16972de6f167c63eda0cf250b7c468d0175bdd5e4ad3453732f20258731f3601f621759949df0c764212f30bc7b094d6e94db35166aa61a21242aa2a1f3fc162588f96463de3b0e601d468058c60f87054e38eb9e6f39becd0bbbdb1f4f9563d97d731eb2bfb276fb86f8c45ad64c44e16be495a6f89a256016f10df9c64412b6e3caa68a96f6a96fcb1ab037aa57608922bfa3b81e880fb28029f86838e3d6f3de6c990e2cc722f22526469376fd5a3ac6b1782185af31870eb64cbf3845a20ac5f3597461504d240d9af7116cbd05ea7e22a09335ec9251068ac80a721f913ff6f62456934e5be723c61e3b16040c269443c20f6056db689ccb01c4c06e1a437bbf053fb0e1320ec9c5d663c73411232563d9a829923734e4e6d9bea30eb780421681493ea9a72bb40625cf4a11d08e3f88dd990c346146b12ecf4bdbedadc48728bee4822ed35127ff2b627fc03030776af0482e8247367df46e290c16bd0f77e6d88dfa45a8e6f77fd0d6128101cf169cf62ee321a80f0ff72cbae05fae3ad3d1698fdd35cb5a852eafa664c97b668766cf16f3a067bed41d5b141d4fc8ffc2f32bbce98f1f11148b61111c2f488b53a24313e008455f51222fc5b9d28f18606387904fa2ac4850e82e6e2576183aef5b566f76b85140aee90a562595bd764503ebdc4f3a873badafcf8e7bac41ed0f1e499ac29c0607ac5e3721cc253e26f1fdd0c4ca7f7300e65b914c939fc576e5ae58ea83c7d30dc2986b3065782659126b02f3e56ec0fc27f43dc9a3d7b01a9df81afd4d8c45a55e95397fd118f19642d4a5b14aed8efb4c9d86f8dd141a70ef1bc7a59487b9c2170cbf5c7fc266ae2c75ba862d7ca57dc34fc8f2aa5b115fc4893f761d32e1785b9dcd7b698b3ab627b69343c29f71ad5826283ca4a32bbcd6e14547c0ade38ea22f84a70f5898ae2311e86b9cc0bc8edabb0e73006295d218d1b165c63391b659f4560b1a97d7d7f2b38a426448df0f155ca628970c2ba3fce926e7a4ee535e90991a88a8fe97586c40564a9fd8eb197f43c61b25020315099cfadf44f1889ce8dea13db1e5a35517e049e9a9a9784f2610da72405098c99de3c5da1544e6f2d92a624f548f331fa60a19e0e83b0880f1654569fb2c2593a52d2496547e7fd4556bd3721eadabb998556319997792cad12c5ce573396add4a095f8997f53553a0c03b9ad4162f0ebdfc4a91417f031bf2c7e1de09621f336c49bb17dc0a4fd16fb634ce2a8b1f0c6d9ddff00002a00000000008a010211df97adaded62e819c2f7e295f647e8d8ca808dadb6a3b76cc29d00856232f3420b1c96cdcfbd30c2775434a2474a054eab2bff0d42fff88e35445d99202ece06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00002a000000000065805e470101009000000000000003e8000000640000000a000000003b9aca000000000001c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4101ffffffffffffff3b9600010124e518fbfcedefdf243f55a8dfd74720db1151aa67d2e20eff6769b88e090b8dfe000000002b40420f00000000002200203c6449397ab890ddb2c688ba0f997711958f86348171c384cdf6de73262964d347522102f09665e2eeca7660da13f8da413103422dcc969353cc879117a19cb8015b6b3f21032416cc1fe1b31abaa3e22a41d5c54e1983effa7bc938d125ebd2c76e76fb5ff952ae0002f09665e2eeca7660da13f8da413103422dcc969353cc879117a19cb8015b6b3f03e668793b9c4635c532f3e2e2819c9166e181ea47e25aa7aea0934bc424cb3d0c011fa0860100000000001600146562e6ed88a3a31c2e18b88ddedf4f0c1680cfe8fe00061a80fd044cfd00fd010101010224e518fbfcedefdf243f55a8dfd74720db1151aa67d2e20eff6769b88e090b8dfe00000000fefffffffdfe2de54480fe09d5b340020622002023e496feeaf508bb034b35a7e0e128c117b14803364e4fa9eab8a82f6a91cd41fe2ae60670fe09d5b340fe03dfd2400102007d0200000001660488daddb90acdd259e69256ca64c1c1db9d8b9a80c03ebd6ae13b6b818aba0200000000000000000250c3000000000000220020778f86e213a518e8336ec940874ccf4b92910332f5b3c0265374a86dbd34e6a596000000000000001600147a895d5a23eb91b234159b07f459d77e93c459540000000000fefffffffd03d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc49190256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdfd654000010204fe000186a01600146562e6ed88a3a31c2e18b88ddedf4f0c1680cfe800fe00061a80cc0047c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee41298f74ea20dfd9553593a0552e2f49865f68fd90ff42a62a22eae9968954a13b0000fd02594066d92888217cdf1ce9eb3bd4478bf61738b4fce7c4ef28f857e293b3ac470dac3355955a31a42f913a7d3af01f621053224e37cc2600cc3a001caa648343fe90fd025b40dcb7c004e1fcc7767796adbddda8eb9c3aa9f1b1480c16735e66eea225a9872c4c2ec9105f94c0876ce3015711e9a378da419205de6223ca3fc957d09045e6f900020401fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000000000000000e4e1c0fce4d408665c5cd9eac99ce7babb852909bdbbfab979d981d95fa274af872c8200061b1000029a8bab32cfd069273baf8a1bfc003d975249c1797cf1d1f41ff75d17dc778623721f87c8fe614e47f3b432a325d8879389bd672c368bdf045b5f33527ea7ad5aae4ec7736bad1eefd197b234bc0268689ce2399ae3e8b19330491228efca2f62ce2d75d6f14117171ffb89cc19aefee4747dae2d943b061ec2b80f726a3de455c3f9c88f3291911dfd8f792c8cba419f6062b0031680cb6d73d5b18338311be0a26bae61ee4a73a89287deb7a2da9db0e2247bbdb3b064d1277566cb5838cf1888a8cb21950624b94891fdf592aa9bcad7ce1bd3680dab343bcd023543a34c634ff4ef85ed6177e8ab68296313530bcfc0d82a090a7651649fb08ab02df9b5b03b8787d3b667d0172f938f50352b3e9fea859c623be7a7a881ae34b6eefac2fc9d6fa0b85e707fb5c66816d32c6abb49a3ad15f8e0827b84f4b19e1f5a44178e442a415d0cf8a012e9e76296df8cfea71807aaad372991257ad52fa350ce272c27e25afcb6bc914f6926a5fbb8051e2f7e94735e00d8e781d51be4907f190fdce6570e4597169119a48eaa5660ba8207c40328a8ced33f8f381cdca7cd98ebe9fc38adbc734520e9075153e42c8802fa9f25d11fd5cde6fc194dc57fba5d391e4e85aeaab58a5a8e18396601c806d070b419cfa8e622776f4a81604cd7eadc9569b14d66767986d6836c10b6ddd04df141938431e423bb96f33bc5cf6d9ec496903f88b030216a01d07f7ea9a5bef992e6253b8a85b16419c4591694c45156bc67d8db0159ba0850d74f137061b9c917c5d3a45927be102a3934070292059c3d049954546b729bc9da4102c4e48157a9095f2567a06a4da5b15751ae492118f49312e9fba0dbc4170597a5da0e17da41320d99b385731c89bb1264b351f1355da52c39c59449a429bfe3ab7623b883bd3ea8fc01992f8874fce447ceff85120579919835660dc38d81388997ce3c7a99caebb32148e5a234ec21a994bcbd9c53f83bf8a78146069ef724e63201e25e9a357a3411eba4e03d09fe498ed23161028304036ec418fdbaf991afd5bc947e7957168574d5587c7e3b024f8f14b08ef7f07fcb287383a7def2e357a5d1cee69e2c691cd8105c54edaf746987e21ed529cc86f48ade1ff95c9d652549e2d1294c31342de7f815bf4fc0b6318ddf41d68f53c171bd0759b672466be594f316f3354e046be1dc0782e08c9f59eeb41a654a7122291c93cdf53f0dbbce659f3050edef293229818f54e36dad64ae5b297337254f456834d2338d1bb8e73f6bd2b1108e34e622514e9cf86227683a8a24bb1d3854dea043ff5ee770eef0ef300e34b994d6939fe6581d0b5c2f47d52482451e78024d092630796ad29411f379ec2a22efd7cc70ee9397d89f09b2b42d0371ddff55a85df1da314050c26e44c1617e7651ecfdd3ff5a79a3285aed565691119fae1802a75f156f21c9c7026ad75d60aa021fffa4fd5e60b86146fff244e8f1db52ab5c97d157d528d5a29ac3ba4dbddbf976a83caf7c2a9fbd3fe3869dc4a1ac9b641ab58ebd0e4b6ad97fddaf36e730d37713010679e594a13b2d56a028509bf85967e04f254258ba7be8c789db448cf65802755527b33efd2cbe310f972df46e7c425e99442faef2e7cd9134d0e05dfa3b83f18228cfbb0daa43dcd08ae572b497f403298871f679b235c16862d144a81336fb5e99d1037064f901c3235f03823b9ca44685669c3dd969dabcee9a50f36161c4772b17f7dd32ebe0e095433b46aa41a12014c602fd014176c4399521e4ef16f0cd58976ddc504eec915fa13fcfe366d203afbd5e1a63eb3c9f05ac9bb31542fd8a174a27d1237226d554df928ddde9a53849f0361b5d667e5bc4a063ac12bb7c29e4022be347c919626a23465368af050e9061af56f784962ae9bcb3d902b01401fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000010000000000e4e1c085748b11066ba78c7a4783329c29fafda6a601e9a52fdf6da215c053494c09c900061b1000030aa45d94b0c31aa64ba3bb2578325dd1a14262334ecf1685f9bbefa73e7e521961719f11427cde91873bad602f7f47bd3b5f1e5c853db889695f7136f2f98e19aa3aeda6c06b2858a3a2cf634768015c113e8ec16fc75a4e95a95fd9425ea134729c355a37fd7878467225bd1a6585f23edbab470ad8846aa820e36c3e28aff2ba86ef4d814373bee10cc51a5d5aac8b576e6dd34472d1d7397341bf6972fce46e1cc49e656a5f667f4c30fd0ffc0444366be23fab79135f7bf47b449f0aa77bc7669af5307f1c890fd047282f74c5f15aaadea00d2288ed30d1ccbeb93c946aacc0ff186414603ac0b9cf943e533c10bbc2c1ca8376c8ad1dd2af606e9ada581c7aef350b8b5332227b32b4b5e285443770c2ebbf4690ce908e76e6bc701c1744300aa148275ca197816c36cbfe483b4f67870d9ffdd63a97c2a49fee181ff9397d74a94235facb2d005b8af3377fc38a1b4be655b10e2daf89b468398e1a91ff021ddfd3d23855bad28b440b237a9c647dd70bc270167ae39ef9d9ac2ddfac13de12d919be6d43970d6ecaab2345e09f918a6f12a93e3887ec087f310de610aa1ed3430677b2de0eea41fd9bde0dde551025476d5c5563df74ea40090d72c00c8170f5138d62c6176637b3ac298e0896bc32512f5fb2d560fd54189273f23155b2092f68ec2e67a08b80c397736c6d7c179842cff7f00d0780e7c078b17021b1f7a570d0123fee296559607960b353cbfe72c502602614fe020206cb6a99269cde4b60f58225ee04e289eaa435279a49759402dddc767ffdede72a38b71c077f6e5154ee1a5fab12171ef4dcc7aac8ced38260e217d48aeabf06ae719d2c8b0a4fcfa4d982f4da6142e3f643000ac8a5d5ae553c36022ed7b89988d65d389e80676142111798b13ed1a8a7f9e097cbb5c36e6b2d3b35dcb0e2007ecb870c2137ea3c7f27e7a6aec807bd1056fc26b901bc54da7a593ebdde261340fe18bf3fbf6601dd032d40cc57122de5318c45ea3470290c710fa77162eeb6c29b4b136fb6a88cdc2fc3deec720a8bd4324cdf4b73c90b62a3883ed1ff0a105860e012fdb00a5853bf4227b105855a0131d9a334d7b88dacbc9f92675a39bb1793dcd22d41c9037345fb71e3548adf118025cbdcaf7cbfdcdeb80d9cdf4424f37475b1b51cce27780f857730d22f38768d44b9905416cad1c0056a03366bd2f8363e72fd56c4d02bee5c256daa9f68d1e63aa500bfd2583706ffdcc6bb95b5649d630bba92f2b3651eda32177b009389dae7334c6f3ad67299fab21bae4f1a7a2aab9a1c6d7f38fc230944005dd6819b621404af8ad74b080a4948b7fc272f65035e688f6ce04c498393415feeaa2c1801d11b126bf1e8315eb7f65457b731da70515fa52790580b547cbd3f27048b29ad5adc2b6a7610a473113aa4625776928fcd5a4dfa7134b4c6c3fb8dd2c72d412d272592ed1bb4909e1fc149ae88ad9eb038b0bb9077de52b41a31f7b3f42f3651f31c96f11ce307071973461139c9536c619fe30c6026dc9e78a28b31d3e39fc370304f8f077024b3eb3d8085641c256bda6f7f8b01dce9ec8898a77a526d5a6faff4c55a6b592560474d8e26406f8c8077dfd41b00b58c42295521dfba5d54ebdbe76a868946f9a2c10516161613eb618aa427502f80c6bcec6138c1f32806a849000ef09bd4967781762ecb3fee037a82f0103777a86c374891ab923a0c6de2d1c0400247eff31abb781e19ee270a47d471ca810cb68db4fe2a35d16bda49c67f6f5bc684bd997ef9f76142a341cecb025f497750c430d71a1e31232689025a8b445d689089811ea6405704ff71b2ef39a1ed570d0bf380132d32a1ea961f51e421f592279f1506912c838dbcf126ce3d1582e32c70fe932df599eab3b2bfedad100ca874de1300fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000000000000001312d005381a6ac452c017023e22a18f5ed62a25fd93aa69a22399468e13222eaa3d46b00061b1000033a7ec4227c55fb74e3f439ac42fc25dbc44c3ad1ac517799fe573c3ba00f5c55639abcbf5faf18cf8065550c6502770190c12dc467b10870e4bb490dfa0042db551a5f228affb013c8d4663623b0584d07d39e553591fa6edecf12e89d5a18c1f633bfac3aa31e1ea1ff324ddcb39e22449ab3c340f2c059c30223d47a82ad7e64fbc6bfc64dbccc3b991e555b3aabbd279489b3f8b6fe0e01792d523678b6a0f60ffeae359b593f7e1e4a5c768e9fcdabc423cb19e5e15be2ffdb9353ac19602e5dc94465d372abc0495c645b1823b1e84dbb3023f9d6cb8470d98d5845f501d804bac7ab6735e6ad1efa3ba05069ad8aa59ce7b53c46361639220a40ee40ac523d1e520bec94918fb25571a3d20083af06431ffe50fe9f2949714b125151bbc44dd5d8e1ca415eecfcd1530ef94d5a1c39f2f8dfb1b4b6a3cdb0804c2bea088e522cfb10378b637662afd1bffd17498c7dc8a7fb8d4ab97960d61e7ed5c3cfc5df77efc6cbf3c74b2438fb3e81ec2a779183fad1487ca527221941c1f59a3029efe088d1f37e7b03ab10a3336faee43c71d4a7f2b952271ac0b9709e4c87050c082ea961cc5c141d9455918f60aff12d2be1c51cad5e00ccfd4552baf41220a46ea93cdef5a77af9ca9a248cec71ac16f726ee4a5961533bed5837f24e87e1cd871b32ae6812b945594471a1533fa1c9345975a9c0ee94a39466fc7b7b9ef3822a3f0bedffbd5c8b01b428ff266afd28167b9d261d23dd398ecd4e32704ae7ea71a002c8514261b8e7fbcc6a1d2d66e140a468f5e4967edd85b741654279c0a5957524a8824b29469ba9f1cd72686a3e57fe08f894c7144bd513948e13538fba50234926364ce4c2360c88b6064e5680df71aedf695066e4cb5ccb2d5f0024b40b737d9b8b1f3e0f84c0fb693b4d1341c1780bc65802de7a81c4080fe3edadabc3627a255edd405098041389112f6a38202241204dd6f2d949c7f05d0f4d71d1b45e0d63f5ab6e57f4e8e85480b6b249d90dde207f332298f1a0fb9be0577b48bcaf5c2c8c413601037252005d6bf809363f192d6d6e9f976dae6ff1a836e985dc6c9b36a23de343e41c1f6415bbb3d9e321457cb7cebc687940db24e28e95b207bd2f6b47c815e6eabc11f4d75f81ab3d66fd20e1900607166d5b9f3f7ea697b1409a48f2d14aa1bcb6e8e73cc30e895edbabbc3ae9294a10f14c8d20cb4adbbe4dbb65cc41896f9582f3bfaf09c05c996e86892c24dac3ffe6ee7abfc5a60d81e249e534e065e61300f9088d6706f694c6f182516afc1b798f264c2831f434b25980b2ca445b356077478eabd275431ec974dabf203f274f7ca40a1eee8beaffefef469b9ab2377904375bdcf0cd8ce7117a835784e4f70cc048f04c62a4cb9fcb058597022b7523ff66abc1f93c140289fb758e7f2bee700e3f115ee503e76be983c5a8dd3e7f5c5e6029ee14af2b940851287ba5104994206c888a5e5f4dbca06127dea382cd0e368da52b924c967f5dd10b7021628292e34105d191a8aeb1e246be83685f553cb858c87f69f66f704c7ce0ffd1adbb610b2fdd562f26d86ffb333573e8b9a5a34d0998153f7c8d8426f9fb215c97e75530eab3ae0189d5930ae58a86b98a6cf59612fd09cb58ad4d1bd415cc041f8a1ecb5b8eab711700b3f2f7b63b57545efcc3bc2abf6224e6ce725926679daf1de033471f5a38d7e1ce9ad0349cd486102c1384956a469d83cc740e3ee1fddcbb9315e4b3a8489bc29168a1a823c1dd8bd0086d6eab123e0165c7d13d121158badb991c2401bde86d30233d275946acab7b0645f91e13676a32ee047b41ab51e9581d5ec9e61079c2292dbaf6ebb827b5d9955329b1251db24b9e3d78a02ef71b3c0595372ff50bc7c89cca6c93f78f07c3b285dca3d18ef671c11d00fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000010000000000e4e1c0963c4f67ddbf9158cf994c9b800bca63e3850f605bc85fb20901bc321fea49f100061b100002d3be755d19eb3f22830ed19c131bdded376e979d5d5caf48590ce4dd49ca28156027dc02b5ea32642693dff80fabd9bedfd1d30fb63cb956af2faae4482ddcb7a566f3efb99ceb035bdf6486629c5394e9ef47d4f522cc5ffb26d34a3d8bed1b9a1e15fdce639b4ea4b04dadb725000400d1c7cbb731a896e1bb11374ccfd7eeb67ada369e7796072ea24cdd27d343574409eca54527f9362f64ef9f9048a17a575eb292a5f56953f51e1a982af8b7855bbdaac0c9c2c05cb51a8e761d91af07a5083da19b9c1b270679649270f09b79971200344783f71c9c69744679fe44472148163ee09933f2a98f39a695a1e005282143c78cee3b59348d74c679a7cdf423106d24d9fc8468388591d0dba05a2e17cfdd1a0e59025feed98597794ebde5114ea049f3791591815d7dbe45844dcc47d1ed84df345558a55dc63b4927eaac22700f0900db5ee173c1e33d12f04891bb6f2e8efdbecdd8e0027f09e57570458ae13dc7af76bb7bacfff1ba21c6a079132df3b5c174ff324aa6c8798677d8eb3695975141cbf666fedf7689f624b0c64ebf0c8cb240b1a6426af1be3d5fa490b59845a405b2f8857093200deca6363ddfb0de045d2e78d66972640175a71efb88d7a099c20c4170b6bede458422b7232c069ac55ec6cda7eba9967de4ded2d3aa34732bd3fca4881fcfc160dbfdc87cd3ee205eb947286ff869387ff28ddaf96ab9d5a5f4133fe54b40b5f474a1f3d8f0ff7c38a19aeeacc31f7a6fa881d9bc86c150e768c61c6edc0a9e44fdab89245a8396263841335d4b9bfc9a0742f588dcfa2caad92d628157bb8b00afcb7c58c98ef0021742134e54d76b45d562f7b491f52db40327c47291e9b95c621368cc0ab4b8f4900ba33a2c6e7a6bc550bedc38003392be68ca6d00a8f2e67d2257c93c17728420e748f5994597d3a3fb5d5cfe069c774f2dfc3165d2fb5fdac1ecee75b59dcaf2d90cad5fba836d4b77c6b7610074b5dd32c18261d2e6ba08c53f9dd83e38f52b861e721d66144843563435c5381f12726de5e71d83db4c146f9b3b9b61690eaeeff6ec8d81cf33632c19618d6570d38e0221b86bf6dc75b18b97605f70428639d7d7e27610f180728284d139a4ad6f4d9620001ecd91e36e03c13dd97da08ec7fb0fba109d38f5c1a379e5e7ae605f6a5b5621c21f7a8512e85667a09f9e1a002cd96922c52efc56df06aebcaec109ba7efe52ba802a1f70d5118e0f791912e4eba469511f0c4da0aa0fb389aaaabf1a4a88cb501de945ef4bc435569af2ff6c9a4f71d0cba8fedf66cae778aa9d736d6b569ea018db2d809aabf49c8b0b1c36f8328b309107ad48185f58343262055308bac59d799b353e4ccc751e70cf732a01b5ae6067754ca3c3e7fa41f87072c7cf1e6c245c268a467cfba367c977a3729d0ab3c6684ce8cdad117ca2b8845a7edaf6d448ccc99fe021b37624f26184262edb6490e54c1ebeb5a02d9855c55bfb75127dc68f7b30821ddb4063dd1bb122919618f6bda7f84b6e1ac23e4b2945c96c982dc21630b38cf9b26ad12ef6ba5183954edcf5be015c5e5e6a89be58985585d06d7b652fc41069457148c9163d0685bb3d3107ba2593a1e50f75f3cc6852d0c92fe05598fa4114269c093c8673363a96b94afe3814d8760ad35e42c1dda1ad7745cc8e7f283deed2ce70a47c1b946beadb30b9ead8871b4a28722c59a95b191507ece52e2303066202d140d465347a3886a7581a65cc7d5d19631fc101bab643775437e32614882e9c8cf73c9290399ab2cafb04d8e4e7209012309364432879c1d3c55b34857bd73ad00ffc509a841c3f5be33474c3746ac56cd6e80fdf4f8d394ec43028647506a75b60f1987c44f58ebc2884e2da28f978950ec4bb467b122d03731ee94eca2b1649f5694afd1388fe2ae60670fe09d5b3400024298f74ea20dfd9553593a0552e2f49865f68fd90ff42a62a22eae9968954a13b010000002bd67d0e000000000022002023e496feeaf508bb034b35a7e0e128c117b14803364e4fa9eab8a82f6a91cd41475221024009df2051daaebc0a2512544bbb645f7594d9c1de0328dea8de4021a7a8c74d2103e668793b9c4635c532f3e2e2819c9166e181ea47e25aa7aea0934bc424cb3d0c52aefd018b0200000001298f74ea20dfd9553593a0552e2f49865f68fd90ff42a62a22eae9968954a13b0100000000cda2a880084a01000000000000220020891687ea17efd8e693c47eb2d4ae6540d57304086a397a4b9b92d67c241645634a01000000000000220020e52eeb1627f5175241a09c2ddc9d76f328d80640bd1f5295e1772c76df36eb17983a0000000000002200204651e3ec6c4fed0b1f605b5741a6a8625f493f781e2c819ec8c3c6e7348af71f983a0000000000002200205f4ed602c54e242b68a69a4f2c3e5705a822cfc99ebe09c773d3ebe9e664dc07983a000000000000220020b2f26c100af28a13b2244425b39ef455cfc0f65631dae208ef8b78cc442e93f6204e000000000000220020cb5de3c3c0e6f31526a01918323d64ca6ab611d9748354850c7f3a9bfd35668388840200000000002200205ba45f7fb84eccb104b280ec6ea4c261794c0ccef55d48cd24306c2dbd6685116ed50a0000000000220020d5c5a151e77693d1e9dfa22cf95627caf2e07d6ab6302dd43ad6642261e572d7e42c132000020400fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000000000000000e4e1c0fce4d408665c5cd9eac99ce7babb852909bdbbfab979d981d95fa274af872c8200061b1000029a8bab32cfd069273baf8a1bfc003d975249c1797cf1d1f41ff75d17dc778623721f87c8fe614e47f3b432a325d8879389bd672c368bdf045b5f33527ea7ad5aae4ec7736bad1eefd197b234bc0268689ce2399ae3e8b19330491228efca2f62ce2d75d6f14117171ffb89cc19aefee4747dae2d943b061ec2b80f726a3de455c3f9c88f3291911dfd8f792c8cba419f6062b0031680cb6d73d5b18338311be0a26bae61ee4a73a89287deb7a2da9db0e2247bbdb3b064d1277566cb5838cf1888a8cb21950624b94891fdf592aa9bcad7ce1bd3680dab343bcd023543a34c634ff4ef85ed6177e8ab68296313530bcfc0d82a090a7651649fb08ab02df9b5b03b8787d3b667d0172f938f50352b3e9fea859c623be7a7a881ae34b6eefac2fc9d6fa0b85e707fb5c66816d32c6abb49a3ad15f8e0827b84f4b19e1f5a44178e442a415d0cf8a012e9e76296df8cfea71807aaad372991257ad52fa350ce272c27e25afcb6bc914f6926a5fbb8051e2f7e94735e00d8e781d51be4907f190fdce6570e4597169119a48eaa5660ba8207c40328a8ced33f8f381cdca7cd98ebe9fc38adbc734520e9075153e42c8802fa9f25d11fd5cde6fc194dc57fba5d391e4e85aeaab58a5a8e18396601c806d070b419cfa8e622776f4a81604cd7eadc9569b14d66767986d6836c10b6ddd04df141938431e423bb96f33bc5cf6d9ec496903f88b030216a01d07f7ea9a5bef992e6253b8a85b16419c4591694c45156bc67d8db0159ba0850d74f137061b9c917c5d3a45927be102a3934070292059c3d049954546b729bc9da4102c4e48157a9095f2567a06a4da5b15751ae492118f49312e9fba0dbc4170597a5da0e17da41320d99b385731c89bb1264b351f1355da52c39c59449a429bfe3ab7623b883bd3ea8fc01992f8874fce447ceff85120579919835660dc38d81388997ce3c7a99caebb32148e5a234ec21a994bcbd9c53f83bf8a78146069ef724e63201e25e9a357a3411eba4e03d09fe498ed23161028304036ec418fdbaf991afd5bc947e7957168574d5587c7e3b024f8f14b08ef7f07fcb287383a7def2e357a5d1cee69e2c691cd8105c54edaf746987e21ed529cc86f48ade1ff95c9d652549e2d1294c31342de7f815bf4fc0b6318ddf41d68f53c171bd0759b672466be594f316f3354e046be1dc0782e08c9f59eeb41a654a7122291c93cdf53f0dbbce659f3050edef293229818f54e36dad64ae5b297337254f456834d2338d1bb8e73f6bd2b1108e34e622514e9cf86227683a8a24bb1d3854dea043ff5ee770eef0ef300e34b994d6939fe6581d0b5c2f47d52482451e78024d092630796ad29411f379ec2a22efd7cc70ee9397d89f09b2b42d0371ddff55a85df1da314050c26e44c1617e7651ecfdd3ff5a79a3285aed565691119fae1802a75f156f21c9c7026ad75d60aa021fffa4fd5e60b86146fff244e8f1db52ab5c97d157d528d5a29ac3ba4dbddbf976a83caf7c2a9fbd3fe3869dc4a1ac9b641ab58ebd0e4b6ad97fddaf36e730d37713010679e594a13b2d56a028509bf85967e04f254258ba7be8c789db448cf65802755527b33efd2cbe310f972df46e7c425e99442faef2e7cd9134d0e05dfa3b83f18228cfbb0daa43dcd08ae572b497f403298871f679b235c16862d144a81336fb5e99d1037064f901c3235f03823b9ca44685669c3dd969dabcee9a50f36161c4772b17f7dd32ebe0e095433b46aa41a12014c602fd014176c4399521e4ef16f0cd58976ddc504eec915fa13fcfe366d203afbd5e1a63eb3c9f05ac9bb31542fd8a174a27d1237226d554df928ddde9a53849f0361b5d667e5bc4a063ac12bb7c29e4022be347c919626a23465368af050e9061af56f784962ae9bcb3d902b01400fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000010000000000e4e1c085748b11066ba78c7a4783329c29fafda6a601e9a52fdf6da215c053494c09c900061b1000030aa45d94b0c31aa64ba3bb2578325dd1a14262334ecf1685f9bbefa73e7e521961719f11427cde91873bad602f7f47bd3b5f1e5c853db889695f7136f2f98e19aa3aeda6c06b2858a3a2cf634768015c113e8ec16fc75a4e95a95fd9425ea134729c355a37fd7878467225bd1a6585f23edbab470ad8846aa820e36c3e28aff2ba86ef4d814373bee10cc51a5d5aac8b576e6dd34472d1d7397341bf6972fce46e1cc49e656a5f667f4c30fd0ffc0444366be23fab79135f7bf47b449f0aa77bc7669af5307f1c890fd047282f74c5f15aaadea00d2288ed30d1ccbeb93c946aacc0ff186414603ac0b9cf943e533c10bbc2c1ca8376c8ad1dd2af606e9ada581c7aef350b8b5332227b32b4b5e285443770c2ebbf4690ce908e76e6bc701c1744300aa148275ca197816c36cbfe483b4f67870d9ffdd63a97c2a49fee181ff9397d74a94235facb2d005b8af3377fc38a1b4be655b10e2daf89b468398e1a91ff021ddfd3d23855bad28b440b237a9c647dd70bc270167ae39ef9d9ac2ddfac13de12d919be6d43970d6ecaab2345e09f918a6f12a93e3887ec087f310de610aa1ed3430677b2de0eea41fd9bde0dde551025476d5c5563df74ea40090d72c00c8170f5138d62c6176637b3ac298e0896bc32512f5fb2d560fd54189273f23155b2092f68ec2e67a08b80c397736c6d7c179842cff7f00d0780e7c078b17021b1f7a570d0123fee296559607960b353cbfe72c502602614fe020206cb6a99269cde4b60f58225ee04e289eaa435279a49759402dddc767ffdede72a38b71c077f6e5154ee1a5fab12171ef4dcc7aac8ced38260e217d48aeabf06ae719d2c8b0a4fcfa4d982f4da6142e3f643000ac8a5d5ae553c36022ed7b89988d65d389e80676142111798b13ed1a8a7f9e097cbb5c36e6b2d3b35dcb0e2007ecb870c2137ea3c7f27e7a6aec807bd1056fc26b901bc54da7a593ebdde261340fe18bf3fbf6601dd032d40cc57122de5318c45ea3470290c710fa77162eeb6c29b4b136fb6a88cdc2fc3deec720a8bd4324cdf4b73c90b62a3883ed1ff0a105860e012fdb00a5853bf4227b105855a0131d9a334d7b88dacbc9f92675a39bb1793dcd22d41c9037345fb71e3548adf118025cbdcaf7cbfdcdeb80d9cdf4424f37475b1b51cce27780f857730d22f38768d44b9905416cad1c0056a03366bd2f8363e72fd56c4d02bee5c256daa9f68d1e63aa500bfd2583706ffdcc6bb95b5649d630bba92f2b3651eda32177b009389dae7334c6f3ad67299fab21bae4f1a7a2aab9a1c6d7f38fc230944005dd6819b621404af8ad74b080a4948b7fc272f65035e688f6ce04c498393415feeaa2c1801d11b126bf1e8315eb7f65457b731da70515fa52790580b547cbd3f27048b29ad5adc2b6a7610a473113aa4625776928fcd5a4dfa7134b4c6c3fb8dd2c72d412d272592ed1bb4909e1fc149ae88ad9eb038b0bb9077de52b41a31f7b3f42f3651f31c96f11ce307071973461139c9536c619fe30c6026dc9e78a28b31d3e39fc370304f8f077024b3eb3d8085641c256bda6f7f8b01dce9ec8898a77a526d5a6faff4c55a6b592560474d8e26406f8c8077dfd41b00b58c42295521dfba5d54ebdbe76a868946f9a2c10516161613eb618aa427502f80c6bcec6138c1f32806a849000ef09bd4967781762ecb3fee037a82f0103777a86c374891ab923a0c6de2d1c0400247eff31abb781e19ee270a47d471ca810cb68db4fe2a35d16bda49c67f6f5bc684bd997ef9f76142a341cecb025f497750c430d71a1e31232689025a8b445d689089811ea6405704ff71b2ef39a1ed570d0bf380132d32a1ea961f51e421f592279f1506912c838dbcf126ce3d1582e32c70fe932df599eab3b2bfedad100ca874de1301fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000000000000001312d005381a6ac452c017023e22a18f5ed62a25fd93aa69a22399468e13222eaa3d46b00061b1000033a7ec4227c55fb74e3f439ac42fc25dbc44c3ad1ac517799fe573c3ba00f5c55639abcbf5faf18cf8065550c6502770190c12dc467b10870e4bb490dfa0042db551a5f228affb013c8d4663623b0584d07d39e553591fa6edecf12e89d5a18c1f633bfac3aa31e1ea1ff324ddcb39e22449ab3c340f2c059c30223d47a82ad7e64fbc6bfc64dbccc3b991e555b3aabbd279489b3f8b6fe0e01792d523678b6a0f60ffeae359b593f7e1e4a5c768e9fcdabc423cb19e5e15be2ffdb9353ac19602e5dc94465d372abc0495c645b1823b1e84dbb3023f9d6cb8470d98d5845f501d804bac7ab6735e6ad1efa3ba05069ad8aa59ce7b53c46361639220a40ee40ac523d1e520bec94918fb25571a3d20083af06431ffe50fe9f2949714b125151bbc44dd5d8e1ca415eecfcd1530ef94d5a1c39f2f8dfb1b4b6a3cdb0804c2bea088e522cfb10378b637662afd1bffd17498c7dc8a7fb8d4ab97960d61e7ed5c3cfc5df77efc6cbf3c74b2438fb3e81ec2a779183fad1487ca527221941c1f59a3029efe088d1f37e7b03ab10a3336faee43c71d4a7f2b952271ac0b9709e4c87050c082ea961cc5c141d9455918f60aff12d2be1c51cad5e00ccfd4552baf41220a46ea93cdef5a77af9ca9a248cec71ac16f726ee4a5961533bed5837f24e87e1cd871b32ae6812b945594471a1533fa1c9345975a9c0ee94a39466fc7b7b9ef3822a3f0bedffbd5c8b01b428ff266afd28167b9d261d23dd398ecd4e32704ae7ea71a002c8514261b8e7fbcc6a1d2d66e140a468f5e4967edd85b741654279c0a5957524a8824b29469ba9f1cd72686a3e57fe08f894c7144bd513948e13538fba50234926364ce4c2360c88b6064e5680df71aedf695066e4cb5ccb2d5f0024b40b737d9b8b1f3e0f84c0fb693b4d1341c1780bc65802de7a81c4080fe3edadabc3627a255edd405098041389112f6a38202241204dd6f2d949c7f05d0f4d71d1b45e0d63f5ab6e57f4e8e85480b6b249d90dde207f332298f1a0fb9be0577b48bcaf5c2c8c413601037252005d6bf809363f192d6d6e9f976dae6ff1a836e985dc6c9b36a23de343e41c1f6415bbb3d9e321457cb7cebc687940db24e28e95b207bd2f6b47c815e6eabc11f4d75f81ab3d66fd20e1900607166d5b9f3f7ea697b1409a48f2d14aa1bcb6e8e73cc30e895edbabbc3ae9294a10f14c8d20cb4adbbe4dbb65cc41896f9582f3bfaf09c05c996e86892c24dac3ffe6ee7abfc5a60d81e249e534e065e61300f9088d6706f694c6f182516afc1b798f264c2831f434b25980b2ca445b356077478eabd275431ec974dabf203f274f7ca40a1eee8beaffefef469b9ab2377904375bdcf0cd8ce7117a835784e4f70cc048f04c62a4cb9fcb058597022b7523ff66abc1f93c140289fb758e7f2bee700e3f115ee503e76be983c5a8dd3e7f5c5e6029ee14af2b940851287ba5104994206c888a5e5f4dbca06127dea382cd0e368da52b924c967f5dd10b7021628292e34105d191a8aeb1e246be83685f553cb858c87f69f66f704c7ce0ffd1adbb610b2fdd562f26d86ffb333573e8b9a5a34d0998153f7c8d8426f9fb215c97e75530eab3ae0189d5930ae58a86b98a6cf59612fd09cb58ad4d1bd415cc041f8a1ecb5b8eab711700b3f2f7b63b57545efcc3bc2abf6224e6ce725926679daf1de033471f5a38d7e1ce9ad0349cd486102c1384956a469d83cc740e3ee1fddcbb9315e4b3a8489bc29168a1a823c1dd8bd0086d6eab123e0165c7d13d121158badb991c2401bde86d30233d275946acab7b0645f91e13676a32ee047b41ab51e9581d5ec9e61079c2292dbaf6ebb827b5d9955329b1251db24b9e3d78a02ef71b3c0595372ff50bc7c89cca6c93f78f07c3b285dca3d18ef671c11d01fd05ac0080c08fadc020f8a18f3ff671801ee3e7e22118d70c923104e221bf0fb74b91ee4100000000000000010000000000e4e1c0963c4f67ddbf9158cf994c9b800bca63e3850f605bc85fb20901bc321fea49f100061b100002d3be755d19eb3f22830ed19c131bdded376e979d5d5caf48590ce4dd49ca28156027dc02b5ea32642693dff80fabd9bedfd1d30fb63cb956af2faae4482ddcb7a566f3efb99ceb035bdf6486629c5394e9ef47d4f522cc5ffb26d34a3d8bed1b9a1e15fdce639b4ea4b04dadb725000400d1c7cbb731a896e1bb11374ccfd7eeb67ada369e7796072ea24cdd27d343574409eca54527f9362f64ef9f9048a17a575eb292a5f56953f51e1a982af8b7855bbdaac0c9c2c05cb51a8e761d91af07a5083da19b9c1b270679649270f09b79971200344783f71c9c69744679fe44472148163ee09933f2a98f39a695a1e005282143c78cee3b59348d74c679a7cdf423106d24d9fc8468388591d0dba05a2e17cfdd1a0e59025feed98597794ebde5114ea049f3791591815d7dbe45844dcc47d1ed84df345558a55dc63b4927eaac22700f0900db5ee173c1e33d12f04891bb6f2e8efdbecdd8e0027f09e57570458ae13dc7af76bb7bacfff1ba21c6a079132df3b5c174ff324aa6c8798677d8eb3695975141cbf666fedf7689f624b0c64ebf0c8cb240b1a6426af1be3d5fa490b59845a405b2f8857093200deca6363ddfb0de045d2e78d66972640175a71efb88d7a099c20c4170b6bede458422b7232c069ac55ec6cda7eba9967de4ded2d3aa34732bd3fca4881fcfc160dbfdc87cd3ee205eb947286ff869387ff28ddaf96ab9d5a5f4133fe54b40b5f474a1f3d8f0ff7c38a19aeeacc31f7a6fa881d9bc86c150e768c61c6edc0a9e44fdab89245a8396263841335d4b9bfc9a0742f588dcfa2caad92d628157bb8b00afcb7c58c98ef0021742134e54d76b45d562f7b491f52db40327c47291e9b95c621368cc0ab4b8f4900ba33a2c6e7a6bc550bedc38003392be68ca6d00a8f2e67d2257c93c17728420e748f5994597d3a3fb5d5cfe069c774f2dfc3165d2fb5fdac1ecee75b59dcaf2d90cad5fba836d4b77c6b7610074b5dd32c18261d2e6ba08c53f9dd83e38f52b861e721d66144843563435c5381f12726de5e71d83db4c146f9b3b9b61690eaeeff6ec8d81cf33632c19618d6570d38e0221b86bf6dc75b18b97605f70428639d7d7e27610f180728284d139a4ad6f4d9620001ecd91e36e03c13dd97da08ec7fb0fba109d38f5c1a379e5e7ae605f6a5b5621c21f7a8512e85667a09f9e1a002cd96922c52efc56df06aebcaec109ba7efe52ba802a1f70d5118e0f791912e4eba469511f0c4da0aa0fb389aaaabf1a4a88cb501de945ef4bc435569af2ff6c9a4f71d0cba8fedf66cae778aa9d736d6b569ea018db2d809aabf49c8b0b1c36f8328b309107ad48185f58343262055308bac59d799b353e4ccc751e70cf732a01b5ae6067754ca3c3e7fa41f87072c7cf1e6c245c268a467cfba367c977a3729d0ab3c6684ce8cdad117ca2b8845a7edaf6d448ccc99fe021b37624f26184262edb6490e54c1ebeb5a02d9855c55bfb75127dc68f7b30821ddb4063dd1bb122919618f6bda7f84b6e1ac23e4b2945c96c982dc21630b38cf9b26ad12ef6ba5183954edcf5be015c5e5e6a89be58985585d06d7b652fc41069457148c9163d0685bb3d3107ba2593a1e50f75f3cc6852d0c92fe05598fa4114269c093c8673363a96b94afe3814d8760ad35e42c1dda1ad7745cc8e7f283deed2ce70a47c1b946beadb30b9ead8871b4a28722c59a95b191507ece52e2303066202d140d465347a3886a7581a65cc7d5d19631fc101bab643775437e32614882e9c8cf73c9290399ab2cafb04d8e4e7209012309364432879c1d3c55b34857bd73ad00ffc509a841c3f5be33474c3746ac56cd6e80fdf4f8d394ec43028647506a75b60f1987c44f58ebc2884e2da28f978950ec4bb467b122d03731ee94eca2b1649f5694afd1388fe09d5b340fe2ae606707353c5311d659852b837506caa8f0ba5746b23a04c7a9663004f583245ce17bc033ff4187890461554b5d54904615de7aacadb4fab6a0fc4f1069b5c5018d7657800" + ) + val state = Serialization.deserialize(bin).value + assertIs(state) + assertIs(state.spliceStatus) + assertEquals((state.spliceStatus as SpliceStatus.WaitingForSigs).session.fundingTx.tx.sharedOutput.htlcAmount, 65000000.msat) + } + @Test fun `forward compatibility test`() { val bin = Hex.decode( @@ -85,22 +108,21 @@ class StateSerializationTestsCommon : LightningTestSuite() { fun commitSigSize(maxIncoming: Int, maxOutgoing: Int): Int { val (alice1, bob1) = addHtlcs(alice, bob, MilliSatoshi(6000_000), maxOutgoing) val (bob2, alice2) = addHtlcs(bob1, alice1, MilliSatoshi(6000_000), maxIncoming) - val (_, actions) = alice2.process(ChannelCommand.Commitment.Sign) - val commitSig0 = actions.findOutgoingMessage() - - val (bob3, actions1) = bob2.process(ChannelCommand.MessageReceived(commitSig0)) - val commandSign0 = actions1.findCommand() + val (alice3, bob3) = crossSign(alice2, bob2) - val (_, actions2) = bob3.process(commandSign0) - val commitSig1 = actions2.findOutgoingMessage() + assertIs>(alice3) + assertIs>(bob3) + val (_, commitSig0, _, commitSig1) = SpliceTestsCommon.spliceInAndOutWithoutSigs(alice3, bob3, listOf(50_000.sat), 50_000.sat) + assertFalse(commitSig1.channelData.isEmpty()) val bina = LightningMessage.encode(commitSig0) val binb = LightningMessage.encode(commitSig1) return max(bina.size, binb.size) } - // with 6 incoming payments and 6 outgoing payments, we can still add our encrypted backup to commig_sig messages - assertTrue(commitSigSize(6, 6) < 65000) + // with 5 incoming payments and 5 outgoing payments, we can still add our encrypted backup to commig_sig messages + // and stay below the 65k limit for a future channel_reestablish message of unknown size + assertTrue(commitSigSize(5, 5) < 60_000) } @Test @@ -116,7 +138,6 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertIs(splice) assertNull(splice.session.liquidityLease) assertTrue(splice.session.localCommit.isLeft) - assertContentEquals(bin, Serialization.serialize(state).dropLast(2).toByteArray()) // we add a discriminator byte and the liquidity lease count (0x0100) assertTrue(state.liquidityLeases.isEmpty()) val state1 = state.copy( liquidityLeases = listOf( @@ -135,7 +156,6 @@ class StateSerializationTestsCommon : LightningTestSuite() { assertIs(splice) assertNull(splice.session.liquidityLease) assertTrue(splice.session.localCommit.isRight) - assertContentEquals(bin, Serialization.serialize(state).dropLast(2).toByteArray()) // we add a discriminator byte and the liquidity lease count (0x0100) assertTrue(state.liquidityLeases.isEmpty()) val state1 = state.copy( liquidityLeases = listOf( diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index d5324e6db..c82be7b9c 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -567,7 +567,7 @@ class TransactionsTestsCommon : LightningTestSuite() { // htlc3 and htlc4 are completely identical, their relative order can't be enforced. assertEquals(5, htlcTxs.size) htlcTxs.forEach { tx -> assertTrue(tx is Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx) } - val htlcIds = htlcTxs.sortedBy { it.input.outPoint.index }.map { it.htlcId } + val htlcIds = htlcTxs.map { it.htlcId } assertTrue(htlcIds == listOf(1L, 3L, 4L, 5L, 2L) || htlcIds == listOf(1L, 4L, 3L, 5L, 2L)) assertTrue(htlcOut4.publicKeyScript.toHex() < htlcOut5.publicKeyScript.toHex())