From 3b6879b2ef36a2a3c23d0d96dba22444dd7a4f4b Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 16 Jan 2024 16:03:58 +0100 Subject: [PATCH] Add support for setting nSequence and nLocktime We add `nSequence` and `nLocktime` to `closing_complete`, to allow the initiator to decide what values to use to provide better anonymity and protection against fee sniping. --- .../main/scala/fr/acinq/eclair/channel/Helpers.scala | 12 +++++++----- .../scala/fr/acinq/eclair/channel/fsm/Channel.scala | 12 ++++++------ .../fr/acinq/eclair/transactions/Transactions.scala | 4 ++-- .../wire/protocol/LightningMessageCodecs.scala | 2 ++ .../eclair/wire/protocol/LightningMessageTypes.scala | 2 +- .../acinq/eclair/transactions/TransactionsSpec.scala | 8 ++++---- .../wire/protocol/LightningMessageCodecsSpec.scala | 10 +++++----- 7 files changed, 27 insertions(+), 23 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 399a9384f9..53a4ce78a0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -690,11 +690,13 @@ object Helpers { } /** We are the closer: we sign closing transactions for which we pay the fees. */ - def makeSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = { + def makeSimpleClosingTx(currentBlockHeight: BlockHeight, keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = { require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit = true, allowOpReturn = true), "invalid localScriptPubkey") require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit = true, allowOpReturn = true), "invalid remoteScriptPubkey") + // We want to signal replaceability and use a widely used value for nSequence to avoid fingerprinting. + val sequence = 0xFFFFFFFDL val closingFee = { - val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), localScriptPubkey, remoteScriptPubkey) + val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), sequence, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) dummyClosingTxs.preferred_opt match { case Some(dummyTx) => val dummySignedTx = Transactions.addSigs(dummyTx, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig) @@ -702,14 +704,14 @@ object Helpers { case None => return Left(CannotGenerateClosingTx(commitment.channelId)) } } - val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, localScriptPubkey, remoteScriptPubkey) + val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, sequence, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) // The actual fee we're paying will be bigger than the one we previously computed if we omit our output. val actualFee = closingTxs.preferred_opt match { case Some(closingTx) if closingTx.fee > 0.sat => closingTx.fee case _ => return Left(CannotGenerateClosingTx(commitment.channelId)) } val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val closingComplete = ClosingComplete(commitment.channelId, actualFee, TlvStream(Set( + val closingComplete = ClosingComplete(commitment.channelId, actualFee, sequence, currentBlockHeight.toLong, TlvStream(Set( closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))), closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserNoClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))), closingTxs.remoteOnly_opt.map(tx => ClosingTlv.NoCloserClosee(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))), @@ -724,7 +726,7 @@ object Helpers { */ def signSimpleClosingTx(keyManager: ChannelKeyManager, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = { val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees) - val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, localScriptPubkey, remoteScriptPubkey) + val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, closingComplete.sequence, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) // If our output isn't dust, they must provide a signature for a transaction that includes it. // Note that we're the closee, so we look for signatures including the closee output. (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 10a4be66e2..f05fc1ec4e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -734,7 +734,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // there are no pending signed changes, let's directly negotiate a closing transaction if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) { val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates) - MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, closingFeerate) match { + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, closingFeerate) match { case Left(f) => log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) goto(NEGOTIATING_SIMPLE) using DATA_NEGOTIATING_SIMPLE(d.commitments, localShutdown, remoteShutdown, Nil, Nil) storing() sending sendList @@ -1330,7 +1330,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) { val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates) - MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, closingFeerate) match { + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, closingFeerate) match { case Left(f) => log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) goto(NEGOTIATING_SIMPLE) using DATA_NEGOTIATING_SIMPLE(d.commitments, localShutdown, remoteShutdown, Nil, Nil) storing() sending revocation @@ -1381,7 +1381,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String) if (Features.canUseFeature(d.commitments.params.localParams.initFeatures, d.commitments.params.remoteParams.initFeatures, Features.SimpleClose)) { val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates) - MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, closingFeerate) match { + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, closingFeerate) match { case Left(f) => log.warning("cannot create local closing txs, waiting for remote closing_complete: {}", f.getMessage) goto(NEGOTIATING_SIMPLE) using DATA_NEGOTIATING_SIMPLE(d.commitments, localShutdown, remoteShutdown, Nil, Nil) storing() @@ -1560,7 +1560,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with if (remoteShutdown.scriptPubKey != d.remoteShutdown.scriptPubKey) { // Our peer changed their closing script: we sign a new version of our closing transaction using the new script. val feerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates) - MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, feerate) match { + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, feerate) match { case Left(_) => stay() using d.copy(remoteShutdown = remoteShutdown) storing() case Right((closingTxs, closingComplete)) => stay() using d.copy(remoteShutdown = remoteShutdown, proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) storing() sending closingComplete } @@ -1621,7 +1621,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with if (localShutdown_opt.nonEmpty || c.feerates.nonEmpty) { val localScript = localShutdown_opt.map(_.scriptPubKey).getOrElse(d.localShutdown.scriptPubKey) val feerate = c.feerates.map(_.preferred).getOrElse(nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates)) - MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, localScript, d.remoteShutdown.scriptPubKey, feerate) match { + MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, localScript, d.remoteShutdown.scriptPubKey, feerate) match { case Left(f) => handleCommandError(f, c) case Right((closingTxs, closingComplete)) => log.info("new closing transaction created with script={} fees={}", localScript, closingComplete.fees) @@ -2241,7 +2241,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with // We retransmit our shutdown: we may have updated our script and they may not have received it. // We also sign a new round of closing transactions since network fees may have changed while we were offline. val closingFeerate = nodeParams.onChainFeeConf.getClosingFeerate(nodeParams.currentFeerates) - Closing.MutualClose.makeSimpleClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, closingFeerate) match { + Closing.MutualClose.makeSimpleClosingTx(nodeParams.currentBlockHeight, keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, closingFeerate) match { case Left(_) => goto(NEGOTIATING_SIMPLE) using d sending d.localShutdown case Right((closingTxs, closingComplete)) => goto(NEGOTIATING_SIMPLE) using d.copy(proposedClosingTxs = d.proposedClosingTxs :+ closingTxs) sending Seq(d.localShutdown, closingComplete) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index f774bf4af1..4f005afd17 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -840,10 +840,10 @@ object Transactions { val all: Seq[ClosingTx] = Seq(localAndRemote_opt, localOnly_opt, remoteOnly_opt).flatten } - def makeSimpleClosingTxs(input: InputInfo, spec: CommitmentSpec, fee: SimpleClosingTxFee, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector): ClosingTxs = { + def makeSimpleClosingTxs(input: InputInfo, spec: CommitmentSpec, fee: SimpleClosingTxFee, sequence: Long, lockTime: Long, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector): ClosingTxs = { require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs") - val txNoOutput = Transaction(2, Seq(TxIn(input.outPoint, ByteVector.empty, sequence = 0xFFFFFFFDL)), Nil, 0) + val txNoOutput = Transaction(2, Seq(TxIn(input.outPoint, ByteVector.empty, sequence)), Nil, lockTime) val (toLocalAmount, toRemoteAmount) = fee match { case SimpleClosingTxFee.PaidByUs(fee) => (spec.toLocal.truncateToSatoshi - fee, spec.toRemote.truncateToSatoshi) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index f5e73d3c41..1cefca4ceb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -230,6 +230,8 @@ object LightningMessageCodecs { val closingCompleteCodec: Codec[ClosingComplete] = ( ("channelId" | bytes32) :: ("fees" | satoshi) :: + ("sequence" | uint32) :: + ("lockTime" | uint32) :: ("tlvStream" | ClosingTlv.closingTlvCodec)).as[ClosingComplete] val closingSigCodec: Codec[ClosingSig] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 5c8f7589c3..c52ea905f3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -333,7 +333,7 @@ case class ClosingSigned(channelId: ByteVector32, val feeRange_opt = tlvStream.get[ClosingSignedTlv.FeeRange] } -case class ClosingComplete(channelId: ByteVector32, fees: Satoshi, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { +case class ClosingComplete(channelId: ByteVector32, fees: Satoshi, sequence: Long, lockTime: Long, tlvStream: TlvStream[ClosingTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { val closerNoCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserNoClosee].map(_.sig) val noCloserCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.NoCloserClosee].map(_.sig) val closerAndCloseeSig_opt: Option[ByteVector64] = tlvStream.get[ClosingTlv.CloserAndClosee].map(_.sig) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 3256ad9950..c86a283617 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, ripemd160, sha256} import fr.acinq.bitcoin.scalacompat.Script.{pay2wpkh, pay2wsh, write} -import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} @@ -831,7 +831,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // Different amounts, both outputs untrimmed, local is closer (option_simple_close): val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 250_000_000 msat) - val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(5_000 sat), localPubKeyScript, remotePubKeyScript) + val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(5_000 sat), 0xFFFFFFFDL, 0, localPubKeyScript, remotePubKeyScript) assert(closingTxs.localAndRemote_opt.nonEmpty) assert(closingTxs.localOnly_opt.nonEmpty) assert(closingTxs.remoteOnly_opt.isEmpty) @@ -868,7 +868,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // Their output is trimmed (option_simple_close): val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 1_000_000 msat) - val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByThem(800 sat), localPubKeyScript, remotePubKeyScript) + val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByThem(800 sat), 0xFFFFFFFDL, 0, localPubKeyScript, remotePubKeyScript) assert(closingTxs.all.size == 1) assert(closingTxs.localOnly_opt.nonEmpty) val toLocal = closingTxs.localOnly_opt.flatMap(_.toLocalOutput).get @@ -886,7 +886,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // Our output is trimmed (option_simple_close): val spec = CommitmentSpec(Set.empty, feeratePerKw, 1_000_000 msat, 150_000_000 msat) - val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(800 sat), localPubKeyScript, remotePubKeyScript) + val closingTxs = makeSimpleClosingTxs(commitInput, spec, SimpleClosingTxFee.PaidByUs(800 sat), 0xFFFFFFFDL, 0, localPubKeyScript, remotePubKeyScript) assert(closingTxs.all.size == 1) assert(closingTxs.remoteOnly_opt.nonEmpty) assert(closingTxs.remoteOnly_opt.flatMap(_.toLocalOutput).isEmpty) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 684d8bca1d..d6f31720ce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -386,11 +386,11 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val sig2 = ByteVector64(hex"02020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") val sig3 = ByteVector64(hex"03030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") val testCases = Seq( - hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451" -> ClosingComplete(channelId, 1105 sat), - hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingComplete(channelId, 1105 sat, TlvStream(ClosingTlv.NoCloserClosee(sig1))), - hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingComplete(channelId, 1105 sat, TlvStream(ClosingTlv.CloserAndClosee(sig1))), - hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202" -> ClosingComplete(channelId, 1105 sat, TlvStream(ClosingTlv.CloserNoClosee(sig1), ClosingTlv.CloserAndClosee(sig2))), - hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303" -> ClosingComplete(channelId, 1105 sat, TlvStream(ClosingTlv.CloserNoClosee(sig1), ClosingTlv.NoCloserClosee(sig2), ClosingTlv.CloserAndClosee(sig3))), + hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 00000000 00000000" -> ClosingComplete(channelId, 1105 sat, 0, 0), + hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 fffffffd 000c96a8 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingComplete(channelId, 1105 sat, 0xFFFFFFFDL, 825_000, TlvStream(ClosingTlv.NoCloserClosee(sig1))), + hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 fffffffe 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingComplete(channelId, 1105 sat, 0xFFFFFFFEL, 0, TlvStream(ClosingTlv.CloserAndClosee(sig1))), + hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 ffffffff 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202" -> ClosingComplete(channelId, 1105 sat, 0xFFFFFFFFL, 0, TlvStream(ClosingTlv.CloserNoClosee(sig1), ClosingTlv.CloserAndClosee(sig2))), + hex"0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 00000000 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303" -> ClosingComplete(channelId, 1105 sat, 0, 0, TlvStream(ClosingTlv.CloserNoClosee(sig1), ClosingTlv.NoCloserClosee(sig2), ClosingTlv.CloserAndClosee(sig3))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86" -> ClosingSig(channelId), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingSig(channelId, TlvStream(ClosingTlv.NoCloserClosee(sig1))), hex"0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingSig(channelId, TlvStream(ClosingTlv.CloserAndClosee(sig1))),