diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 8028cd6e1..ea78511ee 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -3,10 +3,9 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Script.tail import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.bitcoin.crypto.musig2.SecretNonce -import fr.acinq.bitcoin.utils.flatMap import fr.acinq.bitcoin.utils.getOrDefault -import fr.acinq.bitcoin.utils.getOrElse import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.electrum.WalletState @@ -90,10 +89,8 @@ data class InteractiveTxParams( /** Amount of the new funding output, which is the sum of the shared input, if any, and both sides' contributions. */ val fundingAmount: Satoshi = (sharedInput?.info?.txOut?.amount ?: 0.sat) + localContribution + remoteContribution - // BOLT 2: MUST set `feerate` greater than or equal to 25/24 times the `feerate` of the previously constructed transaction, rounded down. val minNextFeerate: FeeratePerKw = targetFeerate * 25 / 24 - // BOLT 2: the initiator's serial IDs MUST use even values and the non-initiator odd values. val serialIdParity = if (isInitiator) 0 else 1 @@ -115,30 +112,35 @@ sealed class InteractiveTxInput { sealed class Local : InteractiveTxInput(), Outgoing { abstract val previousTx: Transaction abstract val previousTxOutput: Long - override val txOut: TxOut - get() = previousTx.txOut[previousTxOutput.toInt()] + override val outPoint: OutPoint get() = OutPoint(previousTx, previousTxOutput) + override val txOut: TxOut get() = previousTx.txOut[previousTxOutput.toInt()] } /** A local-only input that funds the interactive transaction. */ - data class LocalOnly(override val serialId: Long, override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt) : Local() { - override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) - } + data class LocalOnly(override val serialId: Long, override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt) : Local() - /** A local input that funds the interactive transaction, coming from a 2-of-2 swap-in transaction. */ - data class LocalLegacySwapIn(override val serialId: Long, override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Local() { - override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) - override val txOut: TxOut = previousTx.txOut[previousTxOutput.toInt()] - } + /** A local input that funds the interactive transaction, coming from a 2-of-2 legacy (pay2wsh) swap-in transaction. */ + data class LocalLegacySwapIn( + override val serialId: Long, + override val previousTx: Transaction, + override val previousTxOutput: Long, + override val sequence: UInt, + val userKey: PublicKey, + val serverKey: PublicKey, + val refundDelay: Int + ) : Local() + /** A local input that funds the interactive transaction, coming from a 2-of-2 swap-in transaction. */ data class LocalSwapIn( override val serialId: Long, override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt, - val swapInParams: TxAddInputTlv.SwapInParams - ) : Local() { - override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) - } + val userKey: PublicKey, + val serverKey: PublicKey, + val userRefundKey: PublicKey, + val refundDelay: Int + ) : Local() /** * A remote input that funds the interactive transaction. @@ -149,23 +151,35 @@ sealed class InteractiveTxInput { /** A remote-only input that funds the interactive transaction. */ data class RemoteOnly(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt) : Remote() - /** A remote input from a swap-in: our peer needs our signature to build a witness for that input. */ - data class RemoteLegacySwapIn(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Remote() + /** A remote input from a legacy (pay2wsh) swap-in: our peer needs our signature to build a witness for that input. */ + data class RemoteLegacySwapIn( + override val serialId: Long, + override val outPoint: OutPoint, + override val txOut: TxOut, + override val sequence: UInt, + val userKey: PublicKey, + val serverKey: PublicKey, + val refundDelay: Int + ) : Remote() + /** A remote input from a swap-in: our peer needs our signature to build a witness for that input. */ data class RemoteSwapIn( override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt, - val swapInParams: TxAddInputTlv.SwapInParams + val userKey: PublicKey, + val serverKey: PublicKey, + val userRefundKey: PublicKey, + val refundDelay: Int ) : Remote() /** The shared input can be added by us or by our peer, depending on who initiated the protocol. */ data class Shared( override val serialId: Long, override val outPoint: OutPoint, - val publicKeyScript: ByteVector, override - val sequence: UInt, + val publicKeyScript: ByteVector, + override val sequence: UInt, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi ) : InteractiveTxInput(), Incoming, Outgoing { @@ -293,14 +307,14 @@ data class FundingContributions(val inputs: List, v i.previousTx.stripInputWitnesses(), i.outputIndex.toLong(), 0xfffffffdU, - swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) - + swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay + ) else -> InteractiveTxInput.LocalSwapIn( 0, i.previousTx.stripInputWitnesses(), i.outputIndex.toLong(), 0xfffffffdU, - TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay), + swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay ) } } @@ -396,12 +410,8 @@ data class SharedTransaction( fun localOnlyInputs(): List = localInputs.filterIsInstance() - fun localSwapInputs(): List = localInputs.filterIsInstance() - fun remoteOnlyInputs(): List = remoteInputs.filterIsInstance() - fun remoteSwapInputs(): List = remoteInputs.filterIsInstance() - fun buildUnsignedTx(): Transaction { val sharedTxIn = sharedInput?.let { i -> listOf(Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong()))) } ?: listOf() val localTxIn = localInputs.map { i -> Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong())) } @@ -417,14 +427,12 @@ data class SharedTransaction( fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { val unsignedTx = buildUnsignedTx() val sharedSig = fundingParams.sharedInput?.sign(keyManager.channelKeys(localParams.fundingKeyPath), unsignedTx) - val sharedOutput = fundingParams.sharedInput?.let { i -> mapOf(i.info.outPoint to i.info.txOut) } ?: mapOf() - val localOutputs = localInputs.associate { i -> i.outPoint to i.txOut } - val remoteOutputs = remoteInputs.associate { i -> i.outPoint to i.txOut } - val previousOutputsMap = sharedOutput + localOutputs + remoteOutputs - val previousOutputs = unsignedTx.txIn.map { previousOutputsMap[it.outPoint]!! }.toList() + // NB: the order in this list must match the order of the transaction's inputs. + val previousOutputs = unsignedTx.txIn.map { spentOutputs[it.outPoint]!! } - // nonces that we've received for all musig2 swap-in - val receivedNonces: Map = when (session.txCompleteReceived) { + // Public nonces for all the musig2 swap-in inputs (local and remote). + // We have verified that one nonce was provided for each input when receiving `tx_complete`. + val remoteNonces: Map = when (session.txCompleteReceived) { null -> mapOf() else -> (localInputs.filterIsInstance() + remoteInputs.filterIsInstance()) .sortedBy { it.serialId } @@ -432,30 +440,29 @@ data class SharedTransaction( .associate { it.first.serialId to it.second } } - // If we are swapping funds in, we provide our partial signatures to the corresponding inputs. - val swapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + val legacySwapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs .filterIsInstance() .find { txIn.outPoint == it.outPoint } ?.let { input -> keyManager.swapInOnChainWallet.signSwapInputUserLegacy(unsignedTx, i, input.previousTx.txOut) } }.filterNotNull() - val swapUserPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs .filterIsInstance() - .find { txIn.outPoint == it.outPoint && session.secretNonces.containsKey(it.serialId) && receivedNonces.containsKey(it.serialId) } + .find { txIn.outPoint == it.outPoint } ?.let { input -> + // We generate our secret nonce when sending the corresponding input, we know it exists in the map. val userNonce = session.secretNonces[input.serialId]!! - val serverNonce = receivedNonces[input.serialId]!! - keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, previousOutputs, userNonce, serverNonce) - .map { psig -> TxSignaturesTlv.PartialSignature(psig, userNonce.second, serverNonce) } + val serverNonce = remoteNonces[input.serialId]!! + keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, previousOutputs, userNonce.first, userNonce.second, serverNonce) + .map { TxSignaturesTlv.PartialSignature(it, userNonce.second, serverNonce) } .getOrDefault(null) } }.filterNotNull() // If the remote is swapping funds in, they'll need our partial signatures to finalize their witness. - val swapServerSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + val legacySwapServerSigs = unsignedTx.txIn.mapIndexed { i, txIn -> remoteInputs .filterIsInstance() .find { txIn.outPoint == it.outPoint } @@ -465,23 +472,23 @@ data class SharedTransaction( swapInProtocol.signSwapInputServer(unsignedTx, i, input.txOut, serverKey) } }.filterNotNull() - val swapServerPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> remoteInputs .filterIsInstance() - .find { txIn.outPoint == it.outPoint && session.secretNonces.containsKey(it.serialId) && receivedNonces.containsKey(it.serialId) } + .find { txIn.outPoint == it.outPoint } ?.let { input -> val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) - val userNonce = session.secretNonces[input.serialId]!! - val serverNonce = receivedNonces[input.serialId]!! - val swapInProtocol = SwapInProtocol(input.swapInParams.userKey, serverKey.publicKey(), input.swapInParams.userRefundKey, input.swapInParams.refundDelay) - swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, serverNonce, serverKey, userNonce) - .map { psig -> TxSignaturesTlv.PartialSignature(psig, userNonce.second, serverNonce) } + val swapInProtocol = SwapInProtocol(input.userKey, serverKey.publicKey(), input.userRefundKey, input.refundDelay) + // We generate our secret nonce when receiving the corresponding input, we know it exists in the map. + val serverNonce = session.secretNonces[input.serialId]!! + val userNonce = remoteNonces[input.serialId]!! + swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, serverKey, serverNonce.first, userNonce, serverNonce.second) + .map { TxSignaturesTlv.PartialSignature(it, userNonce, serverNonce.second) } .getOrDefault(null) } }.filterNotNull() - return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, swapUserSigs, swapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) + return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, legacySwapUserSigs, legacySwapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) } } @@ -526,39 +533,39 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, data class FullySignedSharedTransaction(override val tx: SharedTransaction, override val localSigs: TxSignatures, val remoteSigs: TxSignatures, val sharedSigs: ScriptWitness?) : SignedSharedTransaction() { override val signedTx = run { + val unsignedTx = tx.buildUnsignedTx() + // NB: the order in this list must match the order of the transaction's inputs. + val previousOutputs = unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! } val sharedTxIn = tx.sharedInput?.let { i -> listOf(Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), sharedSigs ?: ScriptWitness.empty))) } ?: listOf() val localOnlyTxIn = tx.localOnlyInputs().sortedBy { i -> i.serialId }.zip(localSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), w)) } - val localSwapTxIn = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> + val localLegacySwapTxIn = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocolLegacy(i.userKey, i.serverKey, i.refundDelay) val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), witness)) } - val localSwapTxInMusig2 = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> + val localSwapTxIn = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val swapInProtocol = SwapInProtocol(i.swapInParams) - val unsignedTx = tx.buildUnsignedTx() - val witness = swapInProtocol.witness(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, userSig, serverSig) - require(witness.isRight) { "cannot compute aggregated signature" } - Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness.right!!)) + val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.userRefundKey, i.refundDelay) + // The remote partial signature may be invalid: when receiving their tx_signatures, we verify that the resulting transaction is valid. + val witness = swapInProtocol.witness(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, previousOutputs, userSig.localNonce, userSig.remoteNonce, userSig.sig, serverSig.sig).getOrDefault(ScriptWitness.empty) + Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) } - val remoteOnlyTxIn = tx.remoteOnlyInputs().sortedBy { i -> i.serialId }.zip(remoteSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), w)) } - val remoteSwapTxIn = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> + val remoteLegacySwapTxIn = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocolLegacy(i.userKey, i.serverKey, i.refundDelay) val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) } - val remoteSwapTxInMusig2 = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> + val remoteSwapTxIn = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val swapInProtocol = SwapInProtocol(i.swapInParams) - val unsignedTx = tx.buildUnsignedTx() - val witness = swapInProtocol.witness(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, userSig, serverSig) - require(witness.isRight) { "cannot compute aggregated signature" } - Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness.right!!)) + val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.userRefundKey, i.refundDelay) + // The remote partial signature may be invalid: when receiving their tx_signatures, we verify that the resulting transaction is valid. + val witness = swapInProtocol.witness(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, previousOutputs, userSig.localNonce, userSig.remoteNonce, userSig.sig, serverSig.sig).getOrDefault(ScriptWitness.empty) + Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) } - val inputs = (sharedTxIn + localOnlyTxIn + localSwapTxIn + localSwapTxInMusig2 + remoteOnlyTxIn + remoteSwapTxIn + remoteSwapTxInMusig2).sortedBy { (serialId, _) -> serialId }.map { (_, i) -> i } + val inputs = (sharedTxIn + localOnlyTxIn + localLegacySwapTxIn + localSwapTxIn + remoteOnlyTxIn + remoteLegacySwapTxIn + remoteSwapTxIn).sortedBy { (serialId, _) -> serialId }.map { (_, i) -> i } val sharedTxOut = listOf(Pair(tx.sharedOutput.serialId, TxOut(tx.sharedOutput.amount, tx.sharedOutput.pubkeyScript))) val localTxOut = tx.localOutputs.map { o -> Pair(o.serialId, TxOut(o.amount, o.pubkeyScript)) } val remoteTxOut = tx.remoteOutputs.map { o -> Pair(o.serialId, TxOut(o.amount, o.pubkeyScript)) } @@ -594,11 +601,12 @@ sealed class InteractiveTxSessionAction { data class InvalidTxWeight(val channelId: ByteVector32, val txId: TxId) : RemoteFailure() { override fun toString(): String = "transaction weight is too big for standardness rules (txId=$txId)" } data class InvalidTxFeerate(val channelId: ByteVector32, val txId: TxId, val targetFeerate: FeeratePerKw, val actualFeerate: FeeratePerKw) : RemoteFailure() { override fun toString(): String = "transaction feerate too low (txId=$txId, targetFeerate=$targetFeerate, actualFeerate=$actualFeerate" } data class InvalidTxDoesNotDoubleSpendPreviousTx(val channelId: ByteVector32, val txId: TxId, val previousTxId: TxId) : RemoteFailure() { override fun toString(): String = "transaction replacement with txId=$txId doesn't double-spend previous attempt (txId=$previousTxId)" } - data class MissingNonce(val channelId: ByteVector32, val serialId: Long): RemoteFailure() { override fun toString(): String = "missing musig2 nonce for input serial_id=$serialId)" } + data class MissingNonce(val channelId: ByteVector32, val expected: Int, val received: Int): RemoteFailure() { override fun toString(): String = "some musig2 nonces are missing: we expected $expected nonces but received only $received" } // @formatter:on } data class InteractiveTxSession( + val remoteNodeId: PublicKey, val channelKeys: KeyManager.ChannelKeys, val swapInKeys: KeyManager.SwapInOnChainKeys, val fundingParams: InteractiveTxParams, @@ -632,6 +640,7 @@ data class InteractiveTxSession( // +-------+ +-------+ constructor( + remoteNodeId: PublicKey, channelKeys: KeyManager.ChannelKeys, swapInKeys: KeyManager.SwapInOnChainKeys, fundingParams: InteractiveTxParams, @@ -641,6 +650,7 @@ data class InteractiveTxSession( fundingContributions: FundingContributions, previousTxs: List = listOf() ) : this( + remoteNodeId, channelKeys, swapInKeys, fundingParams, @@ -655,11 +665,14 @@ data class InteractiveTxSession( fun send(): Pair { return when (val msg = toSend.firstOrNull()) { null -> { - val localMusig2SwapIns = localInputs.filterIsInstance() - val remoteMusig2SwapIns = remoteInputs.filterIsInstance() - val serialIds = (localMusig2SwapIns.map { it.serialId } + remoteMusig2SwapIns.map { it.serialId }).sorted() - val nonces = serialIds.map { secretNonces[it]?.second }.filterNotNull() - val txComplete = TxComplete(fundingParams.channelId, nonces) + val localSwapIns = localInputs.filterIsInstance() + val remoteSwapIns = remoteInputs.filterIsInstance() + val publicNonces = (localSwapIns + remoteSwapIns) + .map { it.serialId } + .sorted() + // We generate secret nonces whenever we send and receive tx_add_input, so we know they exist in the map. + .map { serialId -> secretNonces[serialId]!!.second } + val txComplete = TxComplete(fundingParams.channelId, publicNonces) val next = copy(txCompleteSent = txComplete) if (next.isComplete) { Pair(next, next.validateTx(txComplete)) @@ -667,7 +680,6 @@ data class InteractiveTxSession( Pair(next, InteractiveTxSessionAction.SendMessage(txComplete)) } } - is Either.Left -> { val txAddInput = when (msg.value) { is InteractiveTxInput.LocalOnly -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence) @@ -675,28 +687,26 @@ data class InteractiveTxSession( val swapInParams = TxAddInputTlv.SwapInParamsLegacy(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) } - is InteractiveTxInput.LocalSwapIn -> { val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay) TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) } - is InteractiveTxInput.Shared -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.outPoint, msg.value.sequence) } - val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = null) - val next1 = when (msg.value) { - is InteractiveTxInput.LocalSwapIn -> { - // generate a secret nonce for this input if we don't already have one - val secretNonce = next.secretNonces[msg.value.serialId] ?: run { - SecretNonce.generate(randomBytes32(), swapInKeys.userPrivateKey, swapInKeys.userPublicKey, null, null, null) + val nextSecretNonces = when (msg.value) { + // Generate a secret nonce for this input if we don't already have one. + is InteractiveTxInput.LocalSwapIn -> when (secretNonces[msg.value.serialId]) { + null -> { + val secretNonce = Musig2.generateNonce(randomBytes32(), swapInKeys.userPrivateKey, listOf(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey)) + secretNonces + (msg.value.serialId to secretNonce) } - next.copy(secretNonces = next.secretNonces + (msg.value.serialId to secretNonce)) + else -> secretNonces } - else -> next + else -> secretNonces } - Pair(next1, InteractiveTxSessionAction.SendMessage(txAddInput)) + val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = null, secretNonces = nextSecretNonces) + Pair(next, InteractiveTxSessionAction.SendMessage(txAddInput)) } - is Either.Right -> { val next = copy(toSend = toSend.tail(), localOutputs = localOutputs + msg.value, txCompleteSent = null) val txAddOutput = when (msg.value) { @@ -739,11 +749,25 @@ data class InteractiveTxSession( val outpoint = OutPoint(message.previousTx, message.previousTxOutput) val txOut = message.previousTx.txOut[message.previousTxOutput.toInt()] when { - message.swapInParams != null -> { - InteractiveTxInput.RemoteSwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParams) - } - - message.swapInParamsLegacy != null -> InteractiveTxInput.RemoteLegacySwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParamsLegacy.userKey, message.swapInParamsLegacy.serverKey, message.swapInParamsLegacy.refundDelay) + message.swapInParams != null -> InteractiveTxInput.RemoteSwapIn( + message.serialId, + outpoint, + txOut, + message.sequence, + message.swapInParams.userKey, + message.swapInParams.serverKey, + message.swapInParams.userRefundKey, + message.swapInParams.refundDelay + ) + message.swapInParamsLegacy != null -> InteractiveTxInput.RemoteLegacySwapIn( + message.serialId, + outpoint, + txOut, + message.sequence, + message.swapInParamsLegacy.userKey, + message.swapInParamsLegacy.serverKey, + message.swapInParamsLegacy.refundDelay + ) else -> InteractiveTxInput.RemoteOnly(message.serialId, outpoint, txOut, message.sequence) } } @@ -754,16 +778,19 @@ data class InteractiveTxSession( if (message.sequence > 0xfffffffdU) { return Either.Left(InteractiveTxSessionAction.NonReplaceableInput(message.channelId, message.serialId, input.outPoint.txid, input.outPoint.index, message.sequence.toLong())) } - val session1 = this.copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = null) - val session2 = when (input) { - is InteractiveTxInput.RemoteSwapIn -> { - val secretNonce = secretNonces[input.serialId] ?: SecretNonce.generate(randomBytes32(), null, input.swapInParams.serverKey, null, null, null) - session1.copy(secretNonces = secretNonces + (input.serialId to secretNonce)) + val secretNonces1 = when (input) { + // Generate a secret nonce for this input if we don't already have one. + is InteractiveTxInput.RemoteSwapIn -> when (secretNonces[input.serialId]) { + null -> { + val secretNonce = Musig2.generateNonce(randomBytes32(), swapInKeys.localServerPrivateKey(remoteNodeId), listOf(input.userKey, input.serverKey)) + secretNonces + (input.serialId to secretNonce) + } + else -> secretNonces } - - else -> session1 + else -> secretNonces } - return Either.Right(session2) + val session1 = this.copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = null, secretNonces = secretNonces1) + return Either.Right(session1) } private fun receiveOutput(message: TxAddOutput): Either { @@ -795,14 +822,12 @@ data class InteractiveTxSession( { next -> next.send() } ) } - is TxAddOutput -> { receiveOutput(message).fold( { f -> Pair(this, f) }, { output -> copy(remoteOutputs = remoteOutputs + output, outputsReceivedCount = outputsReceivedCount + 1, txCompleteReceived = null).send() } ) } - is TxRemoveInput -> { val remoteInputs1 = remoteInputs.filterNot { i -> (i as InteractiveTxInput).serialId == message.serialId } if (remoteInputs.size != remoteInputs1.size) { @@ -812,7 +837,6 @@ data class InteractiveTxSession( Pair(this, InteractiveTxSessionAction.UnknownSerialId(message.channelId, message.serialId)) } } - is TxRemoveOutput -> { val remoteOutputs1 = remoteOutputs.filterNot { o -> (o as InteractiveTxOutput).serialId == message.serialId } if (remoteOutputs.size != remoteOutputs1.size) { @@ -822,7 +846,6 @@ data class InteractiveTxSession( Pair(this, InteractiveTxSessionAction.UnknownSerialId(message.channelId, message.serialId)) } } - is TxComplete -> { val next = copy(txCompleteReceived = message) if (next.isComplete) { @@ -873,17 +896,13 @@ data class InteractiveTxSession( } sharedInputs.first() } - val receivedNonces = (localInputs.filterIsInstance() + remoteInputs.filterIsInstance()) - .sortedBy { it.serialId } - .zip(txCompleteReceived.publicNonces) - .associate { it.first.serialId to it.second } - localOnlyInputs.filterIsInstance().forEach { - receivedNonces[it.serialId] ?: return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) - } - remoteOnlyInputs.filterIsInstance().forEach { - receivedNonces[it.serialId] ?: return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) + // Our peer must send us one nonce for each swap input (local and remote), ordered by serial_id. + val swapInputsCount = localInputs.count { it is InteractiveTxInput.LocalSwapIn } + remoteInputs.count { it is InteractiveTxInput.RemoteSwapIn } + if (txCompleteReceived.publicNonces.size != swapInputsCount) { + return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, swapInputsCount, txCompleteReceived.publicNonces.size) } + val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputs, remoteOnlyInputs, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) val tx = sharedTx.buildUnsignedTx() if (sharedTx.localAmountIn < sharedTx.localAmountOut || sharedTx.remoteAmountIn < sharedTx.remoteAmountOut) { @@ -1001,7 +1020,6 @@ data class InteractiveTxSigningSession( } } } - is Either.Right -> Pair(this, InteractiveTxSigningSessionAction.WaitForTxSigs) } } 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 f5251d5bd..b99ccd05e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -475,6 +475,7 @@ data class Normal( targetFeerate = cmd.message.feerate ) val session = InteractiveTxSession( + staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, @@ -557,6 +558,7 @@ data class Normal( is Either.Right -> { // The splice initiator always sends the first interactive-tx message. val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession( + staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index e8a6bcb18..99ca939d9 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -2,7 +2,6 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.DeterministicWallet.hardened -import fr.acinq.bitcoin.crypto.musig2.AggregatedNonce import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.io.ByteArrayInput @@ -10,8 +9,8 @@ import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.DefaultSwapInParams import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.transactions.SwapInProtocolLegacy import fr.acinq.lightning.transactions.SwapInProtocol +import fr.acinq.lightning.transactions.SwapInProtocolLegacy import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toByteVector @@ -124,8 +123,6 @@ interface KeyManager { ) { private val userExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserKeyPath(chain)) private val userRefundExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserRefundKeyPath(chain)) - private val swapExtendedPublicKey = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain))) - private val xpub = DeterministicWallet.encode(swapExtendedPublicKey, DeterministicWallet.tpub) val userPrivateKey: PrivateKey = userExtendedPrivateKey.privateKey val userPublicKey: PublicKey = userPrivateKey.publicKey() @@ -136,32 +133,19 @@ interface KeyManager { private val localServerExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain)) fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey - // legacy p2wsh-based swap-in protocol, with a fixed on-chain address - val legacySwapInProtocol = SwapInProtocolLegacy(userPublicKey, remoteServerPublicKey, refundDelay) - val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) - val descriptor = SwapInProtocol.descriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, userRefundExtendedPrivateKey) + val descriptor = swapInProtocol.descriptor(chain, userRefundExtendedPrivateKey) - /** - * The output script descriptor matching our legacy swap-in addresses. - * That descriptor can be imported in bitcoind to recover funds after the refund delay. - */ - val legacyDescriptor = run { - // Since child public keys cannot be derived from a master xpub when hardened derivation is used, - // we need to provide the fingerprint of the master xpub and the hardened derivation path. - // This lets wallets that have access to the master xpriv derive the corresponding private and public keys. - val masterFingerprint = ByteVector(Crypto.hash160(DeterministicWallet.publicKey(master).publickeybytes).take(4).toByteArray()) - val encodedChildKey = DeterministicWallet.encode(DeterministicWallet.publicKey(userExtendedPrivateKey), testnet = chain != NodeParams.Chain.Mainnet) - val userKey = "[${masterFingerprint.toHex()}/${encodedSwapInUserKeyPath(chain)}]$encodedChildKey" - "wsh(and_v(v:pk($userKey),or_d(pk(${remoteServerPublicKey.toHex()}),older($refundDelay))))" - } + // legacy p2wsh-based swap-in protocol, with a fixed on-chain address + val legacySwapInProtocol = SwapInProtocolLegacy(userPublicKey, remoteServerPublicKey, refundDelay) + val legacyDescriptor = legacySwapInProtocol.descriptor(chain, master, userExtendedPrivateKey) fun signSwapInputUserLegacy(fundingTx: Transaction, index: Int, parentTxOuts: List): ByteVector64 { - return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()] , userPrivateKey) + return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()], userPrivateKey) } - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: Pair, serverNonce: IndividualNonce): Either { - return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, userNonce, serverNonce) + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce): Either { + return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, privateNonce, userNonce, serverNonce) } /** @@ -172,12 +156,11 @@ interface KeyManager { * @return a signed transaction that spends our swap-in transaction. It cannot be published until `swapInTx` has enough confirmations */ fun createRecoveryTransaction(swapInTx: Transaction, address: String, feeRate: FeeratePerKw): Transaction? { - val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(legacySwapInProtocol.pubkeyScript)) || it.publicKeyScript.contentEquals(Script.write(swapInProtocol.pubkeyScript))} + val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(legacySwapInProtocol.pubkeyScript)) || it.publicKeyScript.contentEquals(Script.write(swapInProtocol.pubkeyScript)) } return if (utxos.isEmpty()) { null } else { - val pubKeyScript = Bitcoin.addressToPublicKeyScript(chain.chainHash, address).right - pubKeyScript?.let { script -> + Bitcoin.addressToPublicKeyScript(chain.chainHash, address).right?.let { script -> val ourOutput = TxOut(utxos.map { it.amount }.sum(), script) val unsignedTx = Transaction( version = 2, @@ -187,7 +170,7 @@ interface KeyManager { ) fun sign(tx: Transaction, index: Int, utxo: TxOut): Transaction { - return if (legacySwapInProtocol.isMine(utxo)) { + return if (Script.isPay2wsh(utxo.publicKeyScript.toByteArray())) { val sig = legacySwapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey) tx.updateWitness(index, legacySwapInProtocol.witnessRefund(sig)) } else { @@ -197,15 +180,11 @@ interface KeyManager { } val fees = run { - val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> - sign(tx, index, utxo) - } + val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> sign(tx, index, utxo) } Transactions.weight2fee(feeRate, recoveryTx.weight()) } val unsignedTx1 = unsignedTx.copy(txOut = listOf(ourOutput.copy(amount = ourOutput.amount - fees))) - val recoveryTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> - sign(tx, index, utxo) - } + val recoveryTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> sign(tx, index, utxo) } // this tx is signed but cannot be published until swapInTx has `refundDelay` confirmations recoveryTx } @@ -220,10 +199,10 @@ interface KeyManager { fun swapInUserKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(0) - fun swapInUserRefundKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(0) / 0L - fun swapInLocalServerKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(1) + fun swapInUserRefundKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(2) / 0L + fun encodedSwapInUserKeyPath(chain: NodeParams.Chain) = when (chain) { NodeParams.Chain.Regtest, NodeParams.Chain.Testnet -> "51h/0h/0h" NodeParams.Chain.Mainnet -> "52h/0h/0h" 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 8fdd12c26..f2a2fb250 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -249,7 +249,10 @@ object Deserialization { previousTx = readTransaction(), previousTxOutput = readNumber(), sequence = readNumber().toUInt(), - swapInParams = TxAddInputTlv.SwapInParams.read(this) + userKey = readPublicKey(), + serverKey = readPublicKey(), + userRefundKey = readPublicKey(), + refundDelay = readNumber().toInt(), ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Local::class}") } @@ -269,13 +272,16 @@ object Deserialization { userKey = readPublicKey(), serverKey = readPublicKey(), refundDelay = readNumber().toInt() - ) + ) 0x03 -> InteractiveTxInput.RemoteSwapIn( serialId = readNumber(), outPoint = readOutPoint(), txOut = TxOut.read(readDelimitedByteArray()), sequence = readNumber().toUInt(), - swapInParams = TxAddInputTlv.SwapInParams.read(this) + userKey = readPublicKey(), + serverKey = readPublicKey(), + userRefundKey = readPublicKey(), + refundDelay = readNumber().toInt() ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::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 0e6a4e9c9..27004fadf 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -1,9 +1,9 @@ package fr.acinq.lightning.serialization.v4 import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Output -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.lightning.FeatureSupport import fr.acinq.lightning.Features import fr.acinq.lightning.channel.* @@ -288,7 +288,10 @@ object Serialization { writeBtcObject(previousTx) writeNumber(previousTxOutput) writeNumber(sequence.toLong()) - swapInParams.write(this@writeLocalInteractiveTxInput) + writePublicKey(userKey) + writePublicKey(serverKey) + writePublicKey(userRefundKey) + writeNumber(refundDelay) } } @@ -316,7 +319,10 @@ object Serialization { writeBtcObject(outPoint) writeBtcObject(txOut) writeNumber(sequence.toLong()) - swapInParams.write(this@writeRemoteInteractiveTxInput) + writePublicKey(userKey) + writePublicKey(serverKey) + writePublicKey(userRefundKey) + writeNumber(refundDelay) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index 87e98b9a1..64559036a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -1,12 +1,12 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* -import fr.acinq.bitcoin.crypto.musig2.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce +import fr.acinq.bitcoin.crypto.musig2.Musig2 +import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.NodeParams import fr.acinq.lightning.crypto.KeyManager -import fr.acinq.lightning.wire.TxAddInputTlv -import fr.acinq.lightning.wire.TxSignaturesTlv /** * new swap-in protocol based on musig2 and taproot: (user key + server key) OR (user refund key + delay) @@ -15,104 +15,85 @@ import fr.acinq.lightning.wire.TxSignaturesTlv * we use a different user key for the refund case: this allows us to generate generic descriptor for all swap-in addresses * (see the descriptor() method below) */ -class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val userRefundKey: PublicKey, val refundDelay: Int) { - constructor(swapInParams: TxAddInputTlv.SwapInParams) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.userRefundKey, swapInParams.refundDelay) - - // the refund script is generated from this policy: and_v(v:pk(user),older(refundDelay)) - // it does not depend upon the user's or server's key, just the user's refund key and the refund delay +data class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val userRefundKey: PublicKey, val refundDelay: Int) { + // The key path uses musig2 with the user and server keys. + private val internalPublicKey = Musig2.aggregateKeys(listOf(userPublicKey, serverPublicKey)) + // The script path contains a refund script, generated from this policy: and_v(v:pk(user),older(refundDelay)). + // It does not depend upon the user's or server's key, just the user's refund key and the refund delay. private val refundScript = listOf(OP_PUSHDATA(userRefundKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) private val scriptTree = ScriptTree.Leaf(0, refundScript) - - // the internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key - private val internalPubKey = KeyAggCache.create(listOf(userPublicKey, serverPublicKey)).first - - // it is tweaked with the script's merkle root to get the pubkey that will be exposed in a regular p2tr script - val pubkeyScript: List = Script.pay2tr(internalPubKey, scriptTree) + val pubkeyScript: List = Script.pay2tr(internalPublicKey, scriptTree) fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).right!! - fun witness(fundingTx: Transaction, index: Int, parentTxOuts: List, userPartialSig: TxSignaturesTlv.PartialSignature, serverPartialSig: TxSignaturesTlv.PartialSignature): Either { + fun witness(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: IndividualNonce, serverNonce: IndividualNonce, userPartialSig: ByteVector32, serverPartialSig: ByteVector32): Either { val publicKeys = listOf(userPublicKey, serverPublicKey) - val publicNonces = listOf(userPartialSig.localNonce, userPartialSig.remoteNonce) - val sigs = listOf(userPartialSig.sig, serverPartialSig.sig) + val publicNonces = listOf(userNonce, serverNonce) + val sigs = listOf(userPartialSig, serverPartialSig) return Musig2.aggregateTaprootSignatures(sigs, fundingTx, index, parentTxOuts, publicKeys, publicNonces, scriptTree).map { aggregateSig -> Script.witnessKeyPathPay2tr(aggregateSig) } } - fun witnessRefund(userSig: ByteVector64): ScriptWitness = Script.witnessScriptPathPay2tr(internalPubKey, scriptTree, ScriptWitness(listOf(userSig)), scriptTree) + fun witnessRefund(userSig: ByteVector64): ScriptWitness = Script.witnessScriptPathPay2tr(internalPublicKey, scriptTree, ScriptWitness(listOf(userSig)), scriptTree) - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey, userNonce: Pair, serverNonce: IndividualNonce): Either { - require(userPrivateKey.publicKey() == userPublicKey) - return Musig2.signTaprootInput(userPrivateKey, fundingTx, index, parentTxOuts, listOf(userPublicKey, serverPublicKey), userNonce.first, listOf(userNonce.second, serverNonce), scriptTree) + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce): Either { + require(userPrivateKey.publicKey() == userPublicKey) { "user private key does not match expected public key: are you using the refund key instead of the user key?" } + val publicKeys = listOf(userPublicKey, serverPublicKey) + val publicNonces = listOf(userNonce, serverNonce) + return Musig2.signTaprootInput(userPrivateKey, fundingTx, index, parentTxOuts, publicKeys, privateNonce, publicNonces, scriptTree) } fun signSwapInputRefund(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey): ByteVector64 { - require(userPrivateKey.publicKey() == userRefundKey) - val merkleRoot = scriptTree.hash() - val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, merkleRoot) - return Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak) + require(userPrivateKey.publicKey() == userRefundKey) { "refund private key does not match expected public key: are you using the user key instead of the refund key?" } + return Transaction.signInputTaprootScriptPath(userPrivateKey, fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, scriptTree.hash()) } - fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: IndividualNonce, serverPrivateKey: PrivateKey, serverNonce: Pair): Either { - return Musig2.signTaprootInput(serverPrivateKey, fundingTx, index, parentTxOuts, listOf(userPublicKey, serverPublicKey), serverNonce.first, listOf(userNonce, serverNonce.second), scriptTree) + fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List, serverPrivateKey: PrivateKey, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce): Either { + val publicKeys = listOf(userPublicKey, serverPublicKey) + val publicNonces = listOf(userNonce, serverNonce) + return Musig2.signTaprootInput(serverPrivateKey, fundingTx, index, parentTxOuts, publicKeys, privateNonce, publicNonces, scriptTree) } - companion object { - /** - * - * @param chain chain we're on - * @param userPublicKey user public key (used for the mutual path) - * @param serverPublicKey server public key (used for the mutual path) - * @param refundDelay refund delay (in blocks) - * @param masterRefundKey master private key for the refund keys. we assume that there is a single level of derivation to compute the refund keys - * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to recover user funds once the funding delay has passed - */ - fun descriptor(chain: NodeParams.Chain, userPublicKey: PublicKey, serverPublicKey: PublicKey, refundDelay: Int, masterRefundKey: DeterministicWallet.ExtendedPrivateKey): String { - // the internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key - val internalPubKey = KeyAggCache.Companion.create(listOf(userPublicKey, serverPublicKey)).first - val prefix = when (chain) { - NodeParams.Chain.Mainnet -> DeterministicWallet.xprv - else -> DeterministicWallet.tprv - } - val xpriv = DeterministicWallet.encode(masterRefundKey, prefix) - val desc = "tr(${internalPubKey.value},and_v(v:pk($xpriv/*),older($refundDelay)))" - val checksum = Descriptor.checksum(desc) - return "$desc#$checksum" + /** + * @param chain chain we're on. + * @param masterRefundKey master private key for the refund keys: we assume that there is a single level of derivation to compute the refund keys. + * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to recover user funds once the funding delay has passed. + */ + fun descriptor(chain: NodeParams.Chain, masterRefundKey: DeterministicWallet.ExtendedPrivateKey): String { + val prefix = when (chain) { + NodeParams.Chain.Mainnet -> DeterministicWallet.xprv + else -> DeterministicWallet.tprv } + val xpriv = DeterministicWallet.encode(masterRefundKey, prefix) + val desc = "tr(${internalPublicKey.value},and_v(v:pk($xpriv/*),older($refundDelay)))" + val checksum = Descriptor.checksum(desc) + return "$desc#$checksum" + } - /** - * - * @param chain chain we're on - * @param userPublicKey user public key (used for the mutual path) - * @param serverPublicKey server public key (used for the mutual path) - * @param refundDelay refund delay (in blocks) - * @param masterRefundKey master public key for the refund keys. we assume that there is a single level of derivation to compute the refund keys - * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to create a watch-only wallet for your swap-in transactions - */ - fun descriptor(chain: NodeParams.Chain, userPublicKey: PublicKey, serverPublicKey: PublicKey, refundDelay: Int, masterRefundKey: DeterministicWallet.ExtendedPublicKey): String { - // the internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key - val internalPubKey = KeyAggCache.Companion.create(listOf(userPublicKey, serverPublicKey)).first - val prefix = when (chain) { - NodeParams.Chain.Mainnet -> DeterministicWallet.xpub - else -> DeterministicWallet.tpub - } - val xpub = DeterministicWallet.encode(masterRefundKey, prefix) - val path = masterRefundKey.path.toString().replace('\'', 'h').removePrefix("m") - val desc = "tr(${internalPubKey.value},and_v(v:pk($xpub$path/*),older($refundDelay)))" - val checksum = Descriptor.checksum(desc) - return "$desc#$checksum" + /** + * @param chain chain we're on. + * @param masterRefundKey master public key for the refund keys: we assume that there is a single level of derivation to compute the refund keys. + * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to create a watch-only wallet for your swap-in transactions. + */ + fun descriptor(chain: NodeParams.Chain, masterRefundKey: DeterministicWallet.ExtendedPublicKey): String { + // the internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key + val prefix = when (chain) { + NodeParams.Chain.Mainnet -> DeterministicWallet.xpub + else -> DeterministicWallet.tpub } + val xpub = DeterministicWallet.encode(masterRefundKey, prefix) + val path = masterRefundKey.path.toString().replace('\'', 'h').removePrefix("m") + val desc = "tr(${internalPublicKey.value},and_v(v:pk($xpub$path/*),older($refundDelay)))" + val checksum = Descriptor.checksum(desc) + return "$desc#$checksum" } } /** * legacy swap-in protocol, that uses p2wsh and a single "user + server OR user + delay" script */ -class SwapInProtocolLegacy(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { - - constructor(swapInParams: TxAddInputTlv.SwapInParamsLegacy) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.refundDelay) - +data class SwapInProtocolLegacy(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { // This script was generated with https://bitcoin.sipa.be/miniscript/ using the following miniscript policy: // and(pk(),or(99@pk(),older())) // @formatter:off @@ -126,8 +107,6 @@ class SwapInProtocolLegacy(val userPublicKey: PublicKey, val serverPublicKey: Pu val pubkeyScript: List = Script.pay2wsh(redeemScript) - fun isMine(txOut: TxOut): Boolean = txOut.publicKeyScript.contentEquals(Script.write(pubkeyScript)) - fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).right!! fun witness(userSig: ByteVector64, serverSig: ByteVector64): ScriptWitness { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index ece5cdf7b..939b3b9e6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -481,18 +481,27 @@ data class TxSignatures( val witnesses: List, val tlvs: TlvStream = TlvStream.empty() ) : InteractiveTxMessage(), HasChannelId, HasEncryptedChannelData { - constructor(channelId: ByteVector32, tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, swapInUserSigs: List, swapInServerSigs: List, swapInUserPartialSigs: List, swapInServerPartialSigs: List) : this( + constructor( + channelId: ByteVector32, + tx: Transaction, + witnesses: List, + previousFundingSig: ByteVector64?, + swapInUserSigs: List, + swapInServerSigs: List, + swapInUserPartialSigs: List, + swapInServerPartialSigs: List + ) : this( channelId, tx.txid, witnesses, TlvStream( - listOfNotNull( + setOfNotNull( previousFundingSig?.let { TxSignaturesTlv.PreviousFundingTxSig(it) }, if (swapInUserSigs.isNotEmpty()) TxSignaturesTlv.SwapInUserSigs(swapInUserSigs) else null, if (swapInServerSigs.isNotEmpty()) TxSignaturesTlv.SwapInServerSigs(swapInServerSigs) else null, if (swapInUserPartialSigs.isNotEmpty()) TxSignaturesTlv.SwapInUserPartialSigs(swapInUserPartialSigs) else null, if (swapInServerPartialSigs.isNotEmpty()) TxSignaturesTlv.SwapInServerPartialSigs(swapInServerPartialSigs) else null, - ).toSet() + ) ), ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index 54550a9b9..0461246ce 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, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) assertEquals(0xfffffffdU, inputA1.sequence) @@ -74,24 +74,24 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertTrue(sharedTxB.sharedTx.localFees < sharedTxA.sharedTx.localFees) // Bob sends signatures first as he contributed less than Alice. - val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 3) // Alice detects invalid signatures from Bob. val sigsInvalidTxId = signedTxB.localSigs.copy(txId = TxId(randomBytes32())) - assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidTxId)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidTxId)) val sigsMissingUserSigs = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(listOf()), TxSignaturesTlv.SwapInServerSigs(signedTxB.localSigs.swapInServerSigs))) - assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserSigs)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserSigs)) val sigsMissingServerSigs = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(signedTxB.localSigs.swapInUserSigs), TxSignaturesTlv.SwapInServerSigs(listOf()))) - assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerSigs)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerSigs)) val sigsInvalidUserSig = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(listOf(randomBytes64())), TxSignaturesTlv.SwapInServerSigs(signedTxB.localSigs.swapInServerSigs))) - assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserSig)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserSig)) val sigsInvalidServerSig = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(signedTxB.localSigs.swapInUserSigs), TxSignaturesTlv.SwapInServerSigs(signedTxB.localSigs.swapInServerSigs.reversed()))) - assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerSig)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerSig)) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 3) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) @@ -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, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // 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) @@ -146,13 +146,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertTrue(sharedTxB.sharedTx.localFees < sharedTxA.sharedTx.localFees) // Alice sends signatures first as she contributed less than Bob. - val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId) + val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) // The resulting transaction is valid and has the right feerate. - val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) @@ -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, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -206,13 +206,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertTrue(sharedTxA.sharedTx.remoteFees < sharedTxA.sharedTx.localFees) // Alice contributes more than Bob to the funding output, but Bob's inputs are bigger than Alice's, so Alice must sign first. - val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId) + val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) // The resulting transaction is valid and has the right feerate. - val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) Transaction.correctlySpends(signedTxB.signedTx, (sharedTxA.sharedTx.localInputs + sharedTxB.sharedTx.localInputs).map { it.previousTx }, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val feerate = Transactions.fee2rate(signedTxB.tx.fees, signedTxB.signedTx.weight()) @@ -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, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -263,13 +263,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 2_800_000.msat) // Bob sends signatures first as he did not contribute at all. - val signedTxB = sharedTxB.sharedTx.sign(bob4, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob4, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 0) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 2) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 2) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 0) @@ -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, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -338,14 +338,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_116_000.msat) // Bob sends signatures first as he contributed less than Alice. - val signedTxB = sharedTxB.sharedTx.sign(bob4, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob4, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) @@ -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, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -412,7 +412,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_000_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) assertTrue(signedTxB.localSigs.witnesses.isEmpty()) assertTrue(signedTxB.localSigs.swapInUserSigs.isEmpty()) @@ -420,7 +420,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertTrue(signedTxA.localSigs.witnesses.isEmpty()) assertTrue(signedTxA.localSigs.swapInUserSigs.isEmpty()) @@ -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, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_add_output --- Bob @@ -492,13 +492,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_000_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) assertTrue(signedTxB.localSigs.swapInUserSigs.isEmpty()) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertTrue(signedTxA.localSigs.swapInUserSigs.isEmpty()) assertNotNull(signedTxA.localSigs.previousFundingTxSig) @@ -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, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA1) = sendMessage(alice0) // Alice <-- tx_add_input --- Bob @@ -569,14 +569,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_240_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) @@ -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, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, 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, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, 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, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, 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, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, 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, emptySet(), f.fundingContributionsA) + val alice0 = InteractiveTxSession(f.nodeIdB, 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, emptySet(), f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.nodeIdB, 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, emptySet(), f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.nodeIdB, 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,10 +785,9 @@ 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, emptySet(), f.fundingContributionsA).send() + var (alice, _) = InteractiveTxSession(f.nodeIdB, 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 // Alice --- tx_message --> Bob val (alice1, _) = alice.receive(TxAddOutput(f.channelId, 2 * i.toLong() + 1, 2500.sat, validScript)) alice = alice1 @@ -804,7 +803,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, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdB, 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 +816,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, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdA, 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 +834,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, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdA, 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 +847,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, emptySet(), f.fundingContributionsA) - val bob0 = InteractiveTxSession(f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, 0.msat, 0.msat, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, 0.msat, 0.msat, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -869,16 +868,16 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNull(sharedTxB.txComplete) // Alice didn't send her user key, so Bob thinks there aren't any swap inputs - val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.nodeIdA) assertTrue(signedTxB.localSigs.swapInServerSigs.isEmpty()) // Alice is unable to sign her input since Bob didn't provide his signature. - assertNull(sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs)) + assertNull(sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.nodeIdB).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs)) } @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, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdA, 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 +898,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(f.fundingParamsA.sharedInput) assertNotNull(f.fundingParamsB.sharedInput) - 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) + val alice0 = InteractiveTxSession(f.nodeIdB, f.channelKeysA, f.keyManagerA.swapInOnChainWallet, f.fundingParamsA, balanceA, balanceB, emptySet(), f.fundingContributionsA) + val bob0 = InteractiveTxSession(f.nodeIdA, f.channelKeysB, f.keyManagerB.swapInOnChainWallet, f.fundingParamsB, balanceB, balanceA, emptySet(), f.fundingContributionsB) // Alice --- tx_add_input --> Bob val (alice1, inputA) = sendMessage(alice0) // Alice <-- tx_complete --- Bob @@ -939,7 +938,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, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdA, 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 +947,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, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdA, 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 +963,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, emptySet(), f.fundingContributionsB) + val bob0 = InteractiveTxSession(f.nodeIdA, 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)) @@ -995,7 +994,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, emptySet(), f.fundingContributionsB, listOf(firstAttempt, secondAttempt)) + val bob0 = InteractiveTxSession(f.nodeIdA, 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 @@ -1081,7 +1080,10 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val localParamsB: LocalParams, val fundingParamsB: InteractiveTxParams, val fundingContributionsB: FundingContributions - ) + ) { + val nodeIdA = localParamsA.nodeId + val nodeIdB = localParamsB.nodeId + } private fun createFixture( fundingAmountA: Satoshi, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index f022d7b41..eccfc4e4d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -54,7 +54,6 @@ import fr.acinq.lightning.transactions.Transactions.swapInputWeight import fr.acinq.lightning.transactions.Transactions.swapInputWeightLegacy import fr.acinq.lightning.transactions.Transactions.weight2fee import fr.acinq.lightning.utils.* -import fr.acinq.lightning.wire.TxSignaturesTlv import fr.acinq.lightning.wire.UpdateAddHtlc import kotlin.random.Random import kotlin.test.* @@ -482,75 +481,6 @@ class TransactionsTestsCommon : LightningTestSuite() { } } - @Test - fun `spend 2-of-2 swap-in taproot without musig2 version`() { - val userPrivateKey = PrivateKey(ByteArray(32) { 1 }) - val serverPrivateKey = PrivateKey(ByteArray(32) { 2 }) - - // mutual agreement script is generated from this policy: and_v(v:pk(A),pk(B)) - val mutualScript = listOf(OP_PUSHDATA(userPrivateKey.xOnlyPublicKey()), OP_CHECKSIGVERIFY, OP_PUSHDATA(serverPrivateKey.xOnlyPublicKey()), OP_CHECKSIG) - - // the refund script is generated from this policy: and_v(v:pk(user),older(refundDelay)) - val refundDelay = 144 - val refundScript = listOf(OP_PUSHDATA(userPrivateKey.xOnlyPublicKey()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) - - // we have a simple script tree with 2 leaves - val scriptTree = ScriptTree.Branch( - ScriptTree.Leaf(0, mutualScript), - ScriptTree.Leaf(1, refundScript) - ) - val merkleRoot = scriptTree.hash() - - // we choose a pubkey that does not have a corresponding private key: our swap-in tx can only be spent through the script path, not the key path - val internalPubkey = XonlyPublicKey(PublicKey.fromHex("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")) - val (tweakedKey, parity) = internalPubkey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot)) - - val swapInTx = Transaction( - version = 2, - txIn = listOf(), - txOut = listOf(TxOut(Satoshi(10000), listOf(OP_1, OP_PUSHDATA(tweakedKey)))), - lockTime = 0 - ) - - // The transaction can be spent if the user and the server produce a signature. - run { - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = TxIn.SEQUENCE_FINAL)), - txOut = listOf(TxOut(Satoshi(10000), pay2wpkh(PrivateKey(randomBytes32()).publicKey()))), - lockTime = 0 - ) - // we want to spend the left leave of the tree, so we provide the hash of the right leave (to be able to recompute the merkle root of the tree) - val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + - internalPubkey.value.toByteArray() + - scriptTree.right.hash().toByteArray() - - val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, scriptTree.left.hash()) - val userSig = Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak) - val serverSig = Crypto.signSchnorr(txHash, serverPrivateKey, Crypto.SchnorrTweak.NoTweak) - - val signedTx = tx.updateWitness(0, ScriptWitness.empty.push(serverSig).push(userSig).push(mutualScript).push(controlBlock)) - Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - - // Or it can be spent with only the user's signature, after a delay. - run { - val tx = Transaction( - version = 2, - txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = refundDelay.toLong())), - txOut = listOf(TxOut(Satoshi(10000), pay2wpkh(PrivateKey(randomBytes32()).publicKey()))), - lockTime = 0 - ) - val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + - internalPubkey.value.toByteArray() + - scriptTree.left.hash().toByteArray() - val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, scriptTree.right.hash()) - val userSig = Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak) - val signedTx = tx.updateWitness(0, ScriptWitness.empty.push(userSig).push(refundScript).push(controlBlock)) - Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - } - @Test fun `spend 2-of-2 swap-in taproot-musig2 version`() { val userPrivateKey = PrivateKey(ByteArray(32) { 1 }) @@ -584,13 +514,11 @@ class TransactionsTestsCommon : LightningTestSuite() { val serverNonce = Musig2.generateNonce(randomBytes32(), serverPrivateKey, listOf(serverPrivateKey.publicKey(), userPrivateKey.publicKey())) // Once they have each other's public nonce, they can produce partial signatures. - val userSig = swapInProtocol.signSwapInputUser(tx, 0, swapInTx.txOut, userPrivateKey, userNonce, serverNonce.second).right!! - val serverSig = swapInProtocol.signSwapInputServer(tx, 0, swapInTx.txOut, userNonce.second,serverPrivateKey, serverNonce).right!! + val userSig = swapInProtocol.signSwapInputUser(tx, 0, swapInTx.txOut, userPrivateKey, userNonce.first, userNonce.second, serverNonce.second).right!! + val serverSig = swapInProtocol.signSwapInputServer(tx, 0, swapInTx.txOut, serverPrivateKey, serverNonce.first, userNonce.second, serverNonce.second).right!! // Once they have each other's partial signature, they can aggregate them into a valid signature. - val userPartialSig = TxSignaturesTlv.PartialSignature(userSig, userNonce.second, serverNonce.second) - val serverPartialSig = TxSignaturesTlv.PartialSignature(serverSig, serverNonce.second, userNonce.second) - val witness = swapInProtocol.witness(tx, 0, swapInTx.txOut, userPartialSig, serverPartialSig).right + val witness = swapInProtocol.witness(tx, 0, swapInTx.txOut, userNonce.second, serverNonce.second, userSig, serverSig).right assertNotNull(witness) val signedTx = tx.updateWitness(0, witness) Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -627,10 +555,9 @@ class TransactionsTestsCommon : LightningTestSuite() { @Test fun `swap-in input weight -- musig2 version`() { val pubkey = randomKey().publicKey() - // DER-encoded ECDSA signatures usually take up to 72 bytes. val sig = ByteVector64.fromValidHex("90b658d172a51f1b3f1a2becd30942397f5df97da8cd2c026854607e955ad815ccfd87d366e348acc32aaf15ff45263aebbb7ecc913a0e5999133f447aee828c") val tx = Transaction(2, listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 2), 0)), listOf(TxOut(50_000.sat, pay2wpkh(pubkey))), 0) - val witness = ScriptWitness(listOf(sig)) + val witness = Script.witnessKeyPathPay2tr(sig) val swapInput = TxIn(OutPoint(TxId(ByteVector32.Zeroes), 3), ByteVector.empty, 0, witness) val txWithAdditionalInput = tx.copy(txIn = tx.txIn + listOf(swapInput)) val inputWeight = txWithAdditionalInput.weight() - tx.weight() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 46952f8aa..89bad0c05 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -382,20 +382,23 @@ class LightningCodecsTestsCommon : LightningTestSuite() { fun `encode - decode interactive-tx messages`() { val channelId1 = ByteVector32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") val channelId2 = ByteVector32("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") -// val swapInUserKey = PublicKey.fromHex("03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b") -// val swapInServerKey = PublicKey.fromHex("03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f") -// val swapInRefundDelay = 144 - val swapInSignatures = listOf( + val swapInUserKey = PublicKey.fromHex("03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b") + val swapInServerKey = PublicKey.fromHex("03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f") + val swapInUserRefundKey = PublicKey.fromHex("033a47288cdae4b25818d0d82802bc114c6f7184f2e071602fa4e3d69881ae2cce") + val swapInRefundDelay = 144 + val legacySwapInSignatures = listOf( ByteVector64("c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3"), ByteVector64("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), ) - val pubKey1 = PrivateKey.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").publicKey() - val pubKey2 = PrivateKey.fromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").publicKey() - val nonce1 = IndividualNonce(pubKey1.value + pubKey1.value) - val nonce2 = IndividualNonce(pubKey2.value + pubKey2.value) + val nonces = listOf( + IndividualNonce("03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7"), + IndividualNonce("031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04"), + IndividualNonce("02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a"), + IndividualNonce("036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + ) val swapInPartialSignatures = listOf( - TxSignaturesTlv.PartialSignature(ByteVector32("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), nonce1, nonce2), - TxSignaturesTlv.PartialSignature(ByteVector32("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), nonce1, nonce2) + TxSignaturesTlv.PartialSignature(ByteVector32("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), nonces[0], nonces[1]), + TxSignaturesTlv.PartialSignature(ByteVector32("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), nonces[2], nonces[3]) ) val signature = ByteVector64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") // This is a random mainnet transaction. @@ -406,28 +409,31 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val tx2 = Transaction.read( "0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000" ) - val testCases = listOf( // @formatter:off TxAddInput(channelId1, 561, tx1, 1, 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005"), TxAddInput(channelId2, 0, tx2, 2, 0u) to ByteVector("0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000"), TxAddInput(channelId1, 561, tx1, 0, 0xfffffffdu) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 fffffffd"), TxAddInput(channelId1, 561, OutPoint(tx1, 1), 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106"), + TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParamsLegacy(swapInUserKey, swapInServerKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534603462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f00000090"), + TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInUserRefundKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04556703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f033a47288cdae4b25818d0d82802bc114c6f7184f2e071602fa4e3d69881ae2cce00000090"), TxAddOutput(channelId1, 1105, 2047.sat, ByteVector("00149357014afd0ccd265658c9ae81efa995e771f472")) to ByteVector("0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472"), TxRemoveInput(channelId2, 561) to ByteVector("0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231"), TxRemoveOutput(channelId1, 1) to ByteVector("0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001"), TxComplete(channelId1) to ByteVector("0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), TxSignatures(channelId1, tx2, listOf(ScriptWitness(listOf(ByteVector("68656c6c6f2074686572652c2074686973206973206120626974636f6e212121"), ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), ScriptWitness(listOf(ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), TxSignatures(channelId2, tx1, listOf(), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), - TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), swapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures.take(1), swapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), swapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1 f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd 0148cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b50268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b50268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1 f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd 0148cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b50268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b50268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5"), - TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures.take(1), swapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), legacySwapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd0148 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), + TxSignatures(channelId2, tx1, listOf(), signature, legacySwapInSignatures.take(1), legacySwapInSignatures.drop(1), swapInPartialSignatures.take(1), swapInPartialSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5 fd025f a4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc03097c9a5c786c4638d9f9f3460e8bebdfd4b5df4028942f89356a530316491d3003c522e17501cdbe722ac83b2187495c6c35d9cedae48bbb59433727e4f5c610d7031eef07e08298e3fb0332f97cd7139c18a364d88b2b4fa46c78fed0a5b86e4bcb03602f97bbde47fe4618e58d3b8ffaabd5f959477df870aed6d0075d1b5d464e04 fd0261 a4 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd02d73ec0b15bae2f8a6331bdc5620f8eb2d50e5511470a5a9912172cc3651048f7024a4148b89e0500f55197f38823aec5d0ddf600437a3ab257469aca957e94137a036b678ad3a55192180adbedf8fc9178df1cecf19281386710e7c21da44349c8b602219efb684532a7cb40dbee62c87e3e6dca4658c9d80f6a7608d4c1e8c9d581a3"), TxInitRbf(channelId1, 8388607, FeeratePerKw(4000.sat)) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(1_500_000.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(0.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000"),