diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index d45eafa51..0fb624219 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -2,14 +2,14 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Script.tail +import fr.acinq.bitcoin.musig2.PublicNonce +import fr.acinq.bitcoin.musig2.SecretNonce +import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.crypto.KeyManager -import fr.acinq.lightning.transactions.CommitmentSpec -import fr.acinq.lightning.transactions.Scripts -import fr.acinq.lightning.transactions.SwapInProtocol -import fr.acinq.lightning.transactions.Transactions +import fr.acinq.lightning.transactions.* import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* import kotlinx.coroutines.CompletableDeferred @@ -80,8 +80,10 @@ 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 @@ -112,9 +114,48 @@ sealed class InteractiveTxInput { } /** 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 userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Local() { + data class LocalSwapIn( + override val serialId: Long, + override val previousTx: Transaction, + override val previousTxOutput: Long, + override val sequence: UInt, + val swapInParams: TxAddInputTlv.SwapInParams, + val userNonce: SecretNonce? = null, + val serverNonce: PublicNonce? = null, + val commonNonce: PublicNonce? = null + ) : Local() { override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) override val txOut: TxOut = previousTx.txOut[previousTxOutput.toInt()] + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as LocalSwapIn + + if (serialId != other.serialId) return false + if (previousTx != other.previousTx) return false + if (previousTxOutput != other.previousTxOutput) return false + if (sequence != other.sequence) return false + if (swapInParams != other.swapInParams) return false + if (commonNonce != other.commonNonce) return false + if (outPoint != other.outPoint) return false + if (txOut != other.txOut) return false + + return true + } + + override fun hashCode(): Int { + var result = serialId.hashCode() + result = 31 * result + previousTx.hashCode() + result = 31 * result + previousTxOutput.hashCode() + result = 31 * result + sequence.hashCode() + result = 31 * result + swapInParams.hashCode() + result = 31 * result + (commonNonce?.hashCode() ?: 0) + result = 31 * result + outPoint.hashCode() + result = 31 * result + txOut.hashCode() + return result + } + } /** @@ -129,10 +170,41 @@ sealed class InteractiveTxInput { 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 RemoteSwapIn(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() + data class RemoteSwapIn( + override val serialId: Long, + override val outPoint: OutPoint, + override val txOut: TxOut, + override val sequence: UInt, + val swapInParams: TxAddInputTlv.SwapInParams, + val userNonce: SecretNonce? = null, + val serverNonce: PublicNonce? = null, + val commonNonce: PublicNonce? = null + ) : Remote() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as RemoteSwapIn + + if (serialId != other.serialId) return false + if (outPoint != other.outPoint) return false + if (txOut != other.txOut) return false + if (sequence != other.sequence) return false + if (swapInParams != other.swapInParams) return false + if (commonNonce != other.commonNonce) return false + + return true + } - data class RemoteSwapInV2(override val serialId: Long, override val outPoint: OutPoint, val txOuts: List, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Remote() { - override val txOut: TxOut get() = txOuts[outPoint.index.toInt()] + override fun hashCode(): Int { + var result = serialId.hashCode() + result = 31 * result + outPoint.hashCode() + result = 31 * result + txOut.hashCode() + result = 31 * result + sequence.hashCode() + result = 31 * result + swapInParams.hashCode() + result = 31 * result + (commonNonce?.hashCode() ?: 0) + return result + } } /** The shared input can be added by us or by our peer, depending on who initiated the protocol. */ @@ -251,7 +323,16 @@ data class FundingContributions(val inputs: List, v } } val sharedInput = sharedUtxo?.let { (i, balances) -> listOf(InteractiveTxInput.Shared(0, i.info.outPoint, 0xfffffffdU, balances.toLocal, balances.toRemote)) } ?: listOf() - val localInputs = walletInputs.map { i -> InteractiveTxInput.LocalSwapIn(0, i.previousTx.stripInputWitnesses(), i.outputIndex.toLong(), 0xfffffffdU, swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) } + val localInputs = walletInputs.map { i -> + val version = if (Script.isPay2wsh(i.previousTx.txOut[i.outputIndex].publicKeyScript.toByteArray())) 1 else 2 + InteractiveTxInput.LocalSwapIn( + 0, + i.previousTx.stripInputWitnesses(), + i.outputIndex.toLong(), + 0xfffffffdU, + TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay, version) + ) + } return if (params.isInitiator) { Either.Right(sortFundingContributions(params, sharedInput + localInputs, sharedOutput + nonChangeOutputs + changeOutput)) } else { @@ -349,24 +430,61 @@ data class SharedTransaction( fun sign(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() + // If we are swapping funds in, we provide our partial signatures to the corresponding inputs. val swapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs + .filterIsInstance() + .filter { it.swapInParams.version == 1 } .find { txIn.outPoint == it.outPoint } - ?.let { input -> keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, input.txOut) } + ?.let { input -> keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, input.previousTx.txOut) } }.filterNotNull() + + val swapUserPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + localInputs + .filterIsInstance() + .filter { it.swapInParams.version == 2 } + .find { txIn.outPoint == it.outPoint } + ?.let { input -> + require(input.userNonce != null) + require(input.serverNonce != null) + keyManager.swapInOnChainWallet.signSwapInputUserMusig2(unsignedTx, i, previousOutputs, input.userNonce, input.serverNonce) + } + }.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 -> remoteInputs .filterIsInstance() + .filter { it.swapInParams.version == 1 } .find { txIn.outPoint == it.outPoint } ?.let { input -> val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) - val swapInProtocol = SwapInProtocol(input.userKey, serverKey.publicKey(), input.refundDelay) + val swapInProtocol = SwapInProtocol(input.swapInParams.userKey, serverKey.publicKey(), input.swapInParams.refundDelay) swapInProtocol.signSwapInputServer(unsignedTx, i, input.txOut, serverKey) } }.filterNotNull() - return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, swapUserSigs, swapServerSigs)) + + val swapServerPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + remoteInputs + .filterIsInstance() + .filter { it.swapInParams.version == 2 } + .find { txIn.outPoint == it.outPoint } + ?.let { input -> + val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) + require(input.userNonce != null) + require(input.serverNonce != null) + val swapInProtocol = SwapInProtocolMusig2(input.swapInParams.userKey, serverKey.publicKey(), input.swapInParams.refundDelay) + swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, input.serverNonce, serverKey, input.userNonce) + } + }.filterNotNull() + + return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, swapUserSigs, swapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) } } @@ -383,10 +501,13 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, override val signedTx = null fun addRemoteSigs(channelKeys: KeyManager.ChannelKeys, fundingParams: InteractiveTxParams, remoteSigs: TxSignatures): FullySignedSharedTransaction? { - if (localSigs.swapInUserSigs.size != tx.localInputs.size) return null + if (localSigs.swapInUserSigs.size != tx.localInputs.filterIsInstance().filter { it.swapInParams.version == 1 }.size) return null + if (localSigs.swapInUserPartialSigs.size != tx.localInputs.filterIsInstance().filter { it.swapInParams.version == 2 }.size) return null + if (remoteSigs.swapInUserSigs.size != tx.remoteSwapInputs().filter { it.swapInParams.version ==1 }.size) return null + if (remoteSigs.swapInUserPartialSigs.size != tx.remoteSwapInputs().filter { it.swapInParams.version ==2 }.size) return null + if (remoteSigs.swapInServerSigs.size != tx.localInputs.filter { it is InteractiveTxInput.LocalSwapIn && it.swapInParams.version == 1}.size) return null + if (remoteSigs.swapInServerPartialSigs.size != tx.localInputs.filter { it is InteractiveTxInput.LocalSwapIn && it.swapInParams.version == 2}.size) return null if (remoteSigs.witnesses.size != tx.remoteOnlyInputs().size) return null - if (remoteSigs.swapInUserSigs.size != tx.remoteSwapInputs().size) return null - if (remoteSigs.swapInServerSigs.size != tx.localInputs.size) return null if (remoteSigs.txId != localSigs.txId) return null val sharedSigs = fundingParams.sharedInput?.let { when (it) { @@ -398,11 +519,12 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, ) } } - val fullySignedTx = FullySignedSharedTransaction(tx, localSigs, remoteSigs, sharedSigs) + val sharedOutput = fundingParams.sharedInput?.let { i -> mapOf(i.info.outPoint to i.info.txOut) } ?: mapOf() val localOutputs = tx.localInputs.associate { i -> i.outPoint to i.txOut } val remoteOutputs = tx.remoteInputs.associate { i -> i.outPoint to i.txOut } val previousOutputs = sharedOutput + localOutputs + remoteOutputs + val fullySignedTx = FullySignedSharedTransaction(tx, localSigs, remoteSigs, sharedSigs, previousOutputs) return when (runTrying { Transaction.correctlySpends(fullySignedTx.signedTx, previousOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) { is Try.Success -> fullySignedTx is Try.Failure -> null @@ -410,24 +532,45 @@ 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() { +data class FullySignedSharedTransaction(override val tx: SharedTransaction, override val localSigs: TxSignatures, val remoteSigs: TxSignatures, val sharedSigs: ScriptWitness?, val spentOutputs: Map = mapOf()) : SignedSharedTransaction() { override val signedTx = run { 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.localSwapInputs().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> + val localSwapTxIn = tx.localSwapInputs().filter { it.swapInParams.version == 1 }.sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.refundDelay) + val swapInProtocol = SwapInProtocol(i.swapInParams) val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), witness)) } + val localSwapTxInMusig2 = tx.localSwapInputs().filter { it.swapInParams.version == 2 }.sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> + val (userSig, serverSig) = sigs + val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) + val commonNonce = i.commonNonce!! + val unsignedTx = tx.buildUnsignedTx() + val ctx = swapInProtocol.signingCtx(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { spentOutputs[it.outPoint]!! }, commonNonce) + val commonSig = ctx.partialSigAgg(listOf(userSig, serverSig)) + val witness = swapInProtocol.witness(commonSig) + Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), 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.remoteSwapInputs().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> + val remoteSwapTxIn = tx.remoteSwapInputs().filter { it.swapInParams.version == 1 }.sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.refundDelay) + val swapInProtocol = SwapInProtocol(i.swapInParams.userKey, i.swapInParams.serverKey, i.swapInParams.refundDelay) val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) } - val inputs = (sharedTxIn + localOnlyTxIn + localSwapTxIn + remoteOnlyTxIn + remoteSwapTxIn).sortedBy { (serialId, _) -> serialId }.map { (_, i) -> i } + val remoteSwapTxInMusig2 = tx.remoteSwapInputs().filter { it.swapInParams.version == 2 }.sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> + val (userSig, serverSig) = sigs + val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) + val commonNonce = i.commonNonce!! + val unsignedTx = tx.buildUnsignedTx() + val ctx = swapInProtocol.signingCtx(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { spentOutputs[it.outPoint]!! }, commonNonce) + val commonSig = ctx.partialSigAgg(listOf(userSig, serverSig)) + val witness = swapInProtocol.witness(commonSig) + 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 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)) } @@ -463,6 +606,7 @@ sealed class InteractiveTxSessionAction { data class InvalidTxWeight(val channelId: ByteVector32, val txId: ByteVector32) : RemoteFailure() { override fun toString(): String = "transaction weight is too big for standardness rules (txId=$txId)" } data class InvalidTxFeerate(val channelId: ByteVector32, val txId: ByteVector32, 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: ByteVector32, val previousTxId: ByteVector32) : 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)" } // @formatter:on } @@ -477,8 +621,8 @@ data class InteractiveTxSession( val remoteInputs: List = listOf(), val localOutputs: List = listOf(), val remoteOutputs: List = listOf(), - val txCompleteSent: Boolean = false, - val txCompleteReceived: Boolean = false, + val txCompleteSent: TxComplete? = null, + val txCompleteReceived: TxComplete? = null, val inputsReceivedCount: Int = 0, val outputsReceivedCount: Int = 0, ) { @@ -514,31 +658,45 @@ data class InteractiveTxSession( previousTxs ) - val isComplete: Boolean = txCompleteSent && txCompleteReceived + val isComplete: Boolean = txCompleteSent != null && txCompleteReceived != null fun send(): Pair { return when (val msg = toSend.firstOrNull()) { null -> { - val txComplete = TxComplete(fundingParams.channelId) - val next = copy(txCompleteSent = true) + // generate a new secret nonce for each musig2 new swapin every time we send TxComplete + val currentNonces = txCompleteSent?.secretNonces ?: mapOf() + fun userNonce(serialId: Long) = currentNonces.getOrElse(serialId) { SecretNonce.generate(swapInKeys.userPrivateKey, swapInKeys.userPublicKey, null, null, null, randomBytes32()) } + fun serverNonce(serialId: Long, serverKey: PublicKey) = currentNonces.getOrElse(serialId) { SecretNonce.generate(null, serverKey, null, null, null, randomBytes32()) } + val localMusig2SwapIns = localInputs.filterIsInstance().filter { swapInKeys.swapInProtocolMusig2.isMine(it.txOut) } + val localNonces = localMusig2SwapIns.map { it.serialId to userNonce(it.serialId) }.toMap() + val remoteMusig2SwapIns = remoteInputs.filterIsInstance().filter { it.swapInParams.version == 2 } + val remoteNonces = remoteMusig2SwapIns.map { it.serialId to serverNonce(it.serialId, it.swapInParams.serverKey) }.toMap() + val txComplete = TxComplete(fundingParams.channelId, secretNonces = localNonces + remoteNonces) + val next = copy(txCompleteSent = txComplete) if (next.isComplete) { Pair(next, next.validateTx(txComplete)) } else { Pair(next, InteractiveTxSessionAction.SendMessage(txComplete)) } } + is Either.Left -> { - val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = false) - val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) + val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = null) val txAddInput = when (msg.value) { is InteractiveTxInput.LocalOnly -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence) - is InteractiveTxInput.LocalSwapIn -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) + is InteractiveTxInput.LocalSwapIn -> { + val version = if (swapInKeys.swapInProtocolMusig2.isMine(msg.value.txOut)) 2 else 1 + val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay, version) + 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) } Pair(next, InteractiveTxSessionAction.SendMessage(txAddInput)) } + is Either.Right -> { - val next = copy(toSend = toSend.tail(), localOutputs = localOutputs + msg.value, txCompleteSent = false) + val next = copy(toSend = toSend.tail(), localOutputs = localOutputs + msg.value, txCompleteSent = null) val txAddOutput = when (msg.value) { is InteractiveTxOutput.Local -> TxAddOutput(fundingParams.channelId, msg.value.serialId, msg.value.amount, msg.value.pubkeyScript) is InteractiveTxOutput.Shared -> TxAddOutput(fundingParams.channelId, msg.value.serialId, msg.value.amount, msg.value.pubkeyScript) @@ -563,6 +721,7 @@ data class InteractiveTxSession( if (expectedSharedOutpoint != receivedSharedOutpoint) return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) InteractiveTxInput.Shared(message.serialId, receivedSharedOutpoint, message.sequence, previousFunding.toLocal, previousFunding.toRemote) } + else -> { if (message.previousTx.txOut.size <= message.previousTxOutput) { return Either.Left(InteractiveTxSessionAction.InputOutOfBounds(message.channelId, message.serialId, message.previousTx.txid, message.previousTxOutput)) @@ -579,7 +738,7 @@ data class InteractiveTxSession( val txOut = message.previousTx.txOut[message.previousTxOutput.toInt()] when (message.swapInParams) { null -> InteractiveTxInput.RemoteOnly(message.serialId, outpoint, txOut, message.sequence) - else -> InteractiveTxInput.RemoteSwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParams.userKey, message.swapInParams.serverKey, message.swapInParams.refundDelay) + else -> InteractiveTxInput.RemoteSwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParams) } } } @@ -618,35 +777,39 @@ data class InteractiveTxSession( is TxAddInput -> { receiveInput(message).fold( { f -> Pair(this, f) }, - { input -> copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = false).send() } + { input -> copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = null).send() } ) } + is TxAddOutput -> { receiveOutput(message).fold( { f -> Pair(this, f) }, - { output -> copy(remoteOutputs = remoteOutputs + output, outputsReceivedCount = outputsReceivedCount + 1, txCompleteReceived = false).send() } + { 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) { - val next = copy(remoteInputs = remoteInputs1, txCompleteReceived = false) + val next = copy(remoteInputs = remoteInputs1, txCompleteReceived = null) next.send() } else { 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) { - val next = copy(remoteOutputs = remoteOutputs1, txCompleteReceived = false) + val next = copy(remoteOutputs = remoteOutputs1, txCompleteReceived = null) next.send() } else { Pair(this, InteractiveTxSessionAction.UnknownSerialId(message.channelId, message.serialId)) } } + is TxComplete -> { - val next = copy(txCompleteReceived = true) + val next = copy(txCompleteReceived = message) if (next.isComplete) { Pair(next, next.validateTx(null)) } else { @@ -657,6 +820,10 @@ data class InteractiveTxSession( } private fun validateTx(txComplete: TxComplete?): InteractiveTxSessionAction { + // tx_complete MUST have been sent and received for us to reach this state, require is used here to tell the compiler that txCompleteSent and txCompleteReceived are not null + require(txCompleteSent != null) + require(txCompleteReceived != null) + if (localInputs.size + remoteInputs.size > 252 || localOutputs.size + remoteOutputs.size > 252) { return InteractiveTxSessionAction.InvalidTxInputOutputCount(fundingParams.channelId, localInputs.size + remoteInputs.size, localOutputs.size + remoteOutputs.size) } @@ -691,8 +858,34 @@ data class InteractiveTxSession( } sharedInputs.first() } + val localOnlyInputsWithNonces = localOnlyInputs.map { + when { + it is InteractiveTxInput.LocalSwapIn && swapInKeys.swapInProtocolMusig2.isMine(it.txOut) -> { + val userNonce = txCompleteSent.secretNonces[it.serialId] + val serverNonce = txCompleteReceived.serverNonces[it.serialId] + if (userNonce == null || serverNonce == null) return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) + val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) + it.copy(userNonce = userNonce, serverNonce = serverNonce, commonNonce = commonNonce) + } + + else -> it + } + } + val remoteOnlyInputsWithNonces = remoteOnlyInputs.map { + when { + it is InteractiveTxInput.RemoteSwapIn && it.swapInParams.version == 2 -> { + val userNonce = txCompleteSent.secretNonces[it.serialId] + val serverNonce = txCompleteReceived.serverNonces[it.serialId] + if (userNonce == null || serverNonce == null) return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) + val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) + it.copy(userNonce = userNonce, serverNonce = serverNonce, commonNonce = commonNonce) + } + + else -> it + } + } - val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputs, remoteOnlyInputs, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) + val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputsWithNonces, remoteOnlyInputsWithNonces, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) val tx = sharedTx.buildUnsignedTx() if (sharedTx.localAmountIn < sharedTx.localAmountOut || sharedTx.remoteAmountIn < sharedTx.remoteAmountOut) { return InteractiveTxSessionAction.InvalidTxChangeAmount(fundingParams.channelId, tx.txid) @@ -794,6 +987,7 @@ data class InteractiveTxSigningSession( logger.info { "signedLocalCommitTx=$signedLocalCommitTx" } Pair(this, InteractiveTxSigningSessionAction.AbortFundingAttempt(InvalidCommitmentSignature(fundingParams.channelId, signedLocalCommitTx.tx.txid))) } + is Try.Success -> { val signedLocalCommit = LocalCommit(localCommit.value.index, localCommit.value.spec, PublishableTxs(signedLocalCommitTx, listOf())) if (shouldSignFirst(channelParams, fundingTx.tx)) { @@ -807,6 +1001,7 @@ data class InteractiveTxSigningSession( } } } + is Either.Right -> Pair(this, InteractiveTxSigningSessionAction.WaitForTxSigs) } } @@ -908,7 +1103,14 @@ sealed class RbfStatus { sealed class SpliceStatus { object None : SpliceStatus() data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : SpliceStatus() - data class InProgress(val replyTo: CompletableDeferred?, val spliceSession: InteractiveTxSession, val localPushAmount: MilliSatoshi, val remotePushAmount: MilliSatoshi, val origins: List) : SpliceStatus() + data class InProgress( + val replyTo: CompletableDeferred?, + val spliceSession: InteractiveTxSession, + val localPushAmount: MilliSatoshi, + val remotePushAmount: MilliSatoshi, + val origins: List + ) : SpliceStatus() + data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List) : SpliceStatus() object Aborted : SpliceStatus() } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 1ac72e6f6..7c598cb67 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -3,10 +3,13 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.DeterministicWallet.hardened import fr.acinq.bitcoin.io.ByteArrayInput +import fr.acinq.bitcoin.musig2.PublicNonce +import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.lightning.DefaultSwapInParams import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.transactions.SwapInProtocol +import fr.acinq.lightning.transactions.SwapInProtocolMusig2 import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toByteVector @@ -128,9 +131,7 @@ interface KeyManager { fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, refundDelay) - val redeemScript: List = swapInProtocol.redeemScript - val pubkeyScript: List = swapInProtocol.pubkeyScript - val address: String = swapInProtocol.address(chain) + val swapInProtocolMusig2 = SwapInProtocolMusig2(userPublicKey, remoteServerPublicKey, refundDelay) /** * The output script descriptor matching our swap-in addresses. @@ -146,13 +147,13 @@ interface KeyManager { "wsh(and_v(v:pk($userKey),or_d(pk(${remoteServerPublicKey.toHex()}),older($refundDelay))))" } - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOut: TxOut): ByteVector64 { - return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOut, userPrivateKey) + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List): ByteVector64 { + return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()] , userPrivateKey) } - fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, remoteNodeId: PublicKey): ByteVector64 { - return swapInProtocol.signSwapInputServer(fundingTx, index, parentTxOut, localServerPrivateKey(remoteNodeId)) - } + fun signSwapInputUserMusig2(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: SecretNonce, serverNonce: PublicNonce): ByteVector32 { + return swapInProtocolMusig2.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, userNonce, serverNonce) + } /** * Create a recovery transaction that spends a swap-in transaction after the refund delay has passed @@ -162,7 +163,7 @@ 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(pubkeyScript)) } + val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(swapInProtocol.pubkeyScript)) || it.publicKeyScript.contentEquals(Script.write(swapInProtocolMusig2.pubkeyScript))} return if (utxos.isEmpty()) { null } else { @@ -175,17 +176,26 @@ interface KeyManager { txOut = listOf(ourOutput), lockTime = 0 ) - val fees = run { - val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> + + fun sign(tx: Transaction, index: Int, utxo: TxOut): Transaction { + return if (swapInProtocol.isMine(utxo)) { val sig = swapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey) tx.updateWitness(index, swapInProtocol.witnessRefund(sig)) + } else { + val sig = swapInProtocolMusig2.signSwapInputRefund(tx, index, utxos, userPrivateKey) + tx.updateWitness(index, swapInProtocolMusig2.witnessRefund(sig)) + } + } + + val fees = run { + 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 -> - val sig = swapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey) - tx.updateWitness(index, swapInProtocol.witnessRefund(sig)) + sign(tx, index, utxo) } // this tx is signed but cannot be published until swapInTx has `refundDelay` confirmations recoveryTx diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 05eec50b1..4b7f24220 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -187,7 +187,8 @@ class Peer( val finalAddress: String = nodeParams.keyManager.finalOnChainWallet.address(addressIndex = 0L).also { finalWallet.addAddress(it) } val swapInWallet = ElectrumMiniWallet(nodeParams.chainHash, watcher.client, scope, nodeParams.loggerFactory, name = "swap-in") - val swapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.address.also { swapInWallet.addAddress(it) } + val swapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.swapInProtocol.address(nodeParams.chain).also { swapInWallet.addAddress(it) } + val swapInAddressMusig2: String = nodeParams.keyManager.swapInOnChainWallet.swapInProtocolMusig2.address(nodeParams.chain).also { swapInWallet.addAddress(it) } private var swapInJob: Job? = null 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 56766eb2f..4e047f628 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.serialization.v4 import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.Input +import fr.acinq.bitcoin.musig2.PublicNonce import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Features import fr.acinq.lightning.ShortChannelId @@ -222,9 +223,8 @@ object Deserialization { previousTx = readTransaction(), previousTxOutput = readNumber(), sequence = readNumber().toUInt(), - userKey = readPublicKey(), - serverKey = readPublicKey(), - refundDelay = readNumber().toInt(), + swapInParams = TxAddInputTlv.SwapInParams.read(this), + commonNonce = readNullable { readPublicNonce() } ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Local::class}") } @@ -241,18 +241,8 @@ object Deserialization { outPoint = readOutPoint(), txOut = TxOut.read(readDelimitedByteArray()), sequence = readNumber().toUInt(), - userKey = readPublicKey(), - serverKey = readPublicKey(), - refundDelay = readNumber().toInt() - ) - 0x03 -> InteractiveTxInput.RemoteSwapInV2( - serialId = readNumber(), - outPoint = readOutPoint(), - txOuts = readCollection { TxOut.read(readDelimitedByteArray()) }.toList(), - sequence = readNumber().toUInt(), - userKey = readPublicKey(), - serverKey = readPublicKey(), - refundDelay = readNumber().toInt() + swapInParams = TxAddInputTlv.SwapInParams.read(this), + commonNonce = readNullable { readPublicNonce() } ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::class}") } @@ -312,6 +302,7 @@ object Deserialization { localSigs = readLightningMessage() as TxSignatures, remoteSigs = readLightningMessage() as TxSignatures, sharedSigs = readNullable { readScriptWitness() }, + spentOutputs = readCollection { readOutPoint() to readTxOut() }.toMap() ) else -> error("unknown discriminator $discriminator for class ${SignedSharedTransaction::class}") } @@ -544,6 +535,8 @@ object Deserialization { private fun Input.readOutPoint(): OutPoint = OutPoint.read(readDelimitedByteArray()) + private fun Input.readTxOut(): TxOut = TxOut.read(readDelimitedByteArray()) + private fun Input.readTransaction(): Transaction = Transaction.read(readDelimitedByteArray()) private fun Input.readTransactionWithInputInfo(): Transactions.TransactionWithInputInfo = when (val discriminator = read()) { @@ -583,6 +576,8 @@ object Deserialization { private fun Input.readPublicKey() = PublicKey(ByteArray(33).also { read(it, 0, it.size) }) + private fun Input.readPublicNonce() = PublicNonce.fromBin(ByteArray(66).also { read(it, 0, it.size) }) + private fun Input.readDelimitedByteArray(): ByteArray { val size = readNumber().toInt() return ByteArray(size).also { read(it, 0, size) } 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 30011ff30..290f2345d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.serialization.v4 import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Output +import fr.acinq.bitcoin.musig2.PublicNonce import fr.acinq.lightning.FeatureSupport import fr.acinq.lightning.Features import fr.acinq.lightning.channel.* @@ -12,6 +13,7 @@ import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.* import fr.acinq.lightning.utils.Either import fr.acinq.lightning.wire.LightningCodecs import fr.acinq.lightning.wire.LightningMessage +import fr.acinq.lightning.wire.TxAddInputTlv /** * Serialization for [ChannelStateWithCommitments]. @@ -273,9 +275,8 @@ object Serialization { writeBtcObject(previousTx) writeNumber(previousTxOutput) writeNumber(sequence.toLong()) - writePublicKey(userKey) - writePublicKey(serverKey) - writeNumber(refundDelay) + swapInParams.write(this@writeLocalInteractiveTxInput) + writeNullable(commonNonce){ writePublicNonce(it)} } } @@ -293,19 +294,8 @@ object Serialization { writeBtcObject(outPoint) writeBtcObject(txOut) writeNumber(sequence.toLong()) - writePublicKey(userKey) - writePublicKey(serverKey) - writeNumber(refundDelay) - } - is InteractiveTxInput.RemoteSwapInV2 -> i.run { - write(0x03) - writeNumber(serialId) - writeBtcObject(outPoint) - writeCollection(i.txOuts) { o -> writeBtcObject(o) } - writeNumber(sequence.toLong()) - writePublicKey(userKey) - writePublicKey(serverKey) - writeNumber(refundDelay) + swapInParams.write(this@writeRemoteInteractiveTxInput) + writeNullable(commonNonce){ writePublicNonce(it)} } } @@ -363,6 +353,10 @@ object Serialization { writeLightningMessage(localSigs) writeLightningMessage(remoteSigs) writeNullable(sharedSigs) { writeScriptWitness(it) } + writeCollection(spentOutputs.toList()){ x -> + writeBtcObject(x.first) + writeBtcObject(x.second) + } } } @@ -649,6 +643,8 @@ object Serialization { private fun Output.writePublicKey(o: PublicKey) = write(o.value.toByteArray()) + private fun Output.writePublicNonce(o: PublicNonce) = write(o.toByteArray()) + private fun Output.writeDelimited(o: ByteArray) { writeNumber(o.size) write(o) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index 297923770..11cd27772 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -7,8 +7,13 @@ import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.bitcoin.musig2.SessionCtx import fr.acinq.lightning.Lightning import fr.acinq.lightning.NodeParams +import fr.acinq.lightning.wire.TxAddInputTlv +import org.kodein.log.newLogger class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { + + constructor(swapInParams: TxAddInputTlv.SwapInParams) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.refundDelay) + // This script was generated with https://bitcoin.sipa.be/miniscript/ using the following miniscript policy: // and(pk(),or(99@pk(),older())) // @formatter:off @@ -22,6 +27,8 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe 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).result!! fun witness(userSig: ByteVector64, serverSig: ByteVector64): ScriptWitness { @@ -43,6 +50,8 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe } class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { + constructor(swapInParams: TxAddInputTlv.SwapInParams) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.refundDelay) + // the redeem script is just the refund script. it is generated from this policy: and_v(v:pk(user),older(refundDelay)) val redeemScript = listOf(OP_PUSHDATA(userPublicKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) private val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, Script.write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) @@ -55,6 +64,8 @@ class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: Pu private val executionData = Script.ExecutionData(annex = null, tapleafHash = merkleRoot) private val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + internalPubKey.value.toByteArray() + fun isMine(txOut: TxOut): Boolean = txOut.publicKeyScript.contentEquals(Script.write(pubkeyScript)) + fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! fun witness(commonSig: ByteVector64): ScriptWitness = ScriptWitness(listOf(commonSig)) @@ -64,9 +75,9 @@ class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: Pu fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey, userNonce: SecretNonce, serverNonce: PublicNonce): ByteVector32 { require(userPrivateKey.publicKey() == userPublicKey) val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - + val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) val ctx = SessionCtx( - PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)), + commonNonce, listOf(userPrivateKey.publicKey(), serverPublicKey), listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), txHash @@ -81,9 +92,9 @@ class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: Pu fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: PublicNonce, serverPrivateKey: PrivateKey, serverNonce: SecretNonce): ByteVector32 { val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - + val commonNonce = PublicNonce.aggregate(listOf(userNonce, serverNonce.publicNonce())) val ctx = SessionCtx( - PublicNonce.aggregate(listOf(userNonce, serverNonce.publicNonce())), + commonNonce, listOf(userPublicKey, serverPrivateKey.publicKey()), listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), txHash diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index da9906de0..68b6f43e2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.bitcoin.musig2.PublicNonce import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.utils.toByteVector32 @@ -24,12 +25,13 @@ sealed class TxAddInputTlv : Tlv { } /** When adding a swap-in input to an interactive-tx, the user needs to provide the corresponding script parameters. */ - data class SwapInParams(val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : TxAddInputTlv() { + data class SwapInParams(val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int, val version: Int) : TxAddInputTlv() { override val tag: Long get() = SwapInParams.tag override fun write(out: Output) { LightningCodecs.writeBytes(userKey.value, out) LightningCodecs.writeBytes(serverKey.value, out) LightningCodecs.writeU32(refundDelay, out) + LightningCodecs.writeU32(version, out) } companion object : TlvValueReader { @@ -37,7 +39,8 @@ sealed class TxAddInputTlv : Tlv { override fun read(input: Input): SwapInParams = SwapInParams( PublicKey(LightningCodecs.bytes(input, 33)), PublicKey(LightningCodecs.bytes(input, 33)), - LightningCodecs.u32(input) + LightningCodecs.u32(input), + if (input.availableBytes >= 4) LightningCodecs.u32(input) else 1 ) } } @@ -49,7 +52,32 @@ sealed class TxRemoveInputTlv : Tlv sealed class TxRemoveOutputTlv : Tlv -sealed class TxCompleteTlv : Tlv +sealed class TxCompleteTlv : Tlv { + data class Nonces(val nonces: Map): TxCompleteTlv() { + override val tag: Long get() = Nonces.tag + + override fun write(out: Output) { + LightningCodecs.writeU16(nonces.size, out) + nonces.forEach { (serialId, nonce) -> + LightningCodecs.writeBigSize(serialId, out) + LightningCodecs.writeBytes(nonce.toByteArray(), out) + } + } + + companion object : TlvValueReader { + const val tag: Long = 101 + override fun read(input: Input): Nonces { + val noncesCount = LightningCodecs.u16(input) + val nonces = (1..noncesCount).map { + val serialId = LightningCodecs.bigSize(input) + val nonce = PublicNonce.fromBin(LightningCodecs.bytes(input, 66)) + serialId to nonce + } + return Nonces(nonces.toMap()) + } + } + } +} sealed class TxSignaturesTlv : Tlv { /** When doing a splice, each peer must provide their signature for the previous 2-of-2 funding output. */ @@ -93,6 +121,34 @@ sealed class TxSignaturesTlv : Tlv { } } + data class SwapInUserPartialSigs(val sigs: List) : TxSignaturesTlv() { + override val tag: Long get() = SwapInUserPartialSigs.tag + override fun write(out: Output) = sigs.forEach { sig -> LightningCodecs.writeBytes(sig, out) } + + companion object : TlvValueReader { + const val tag: Long = 607 + override fun read(input: Input): SwapInUserPartialSigs { + val count = input.availableBytes / 32 + val sigs = (0 until count).map { LightningCodecs.bytes(input, 32).byteVector32() } + return SwapInUserPartialSigs(sigs) + } + } + } + + data class SwapInServerPartialSigs(val sigs: List) : TxSignaturesTlv() { + override val tag: Long get() = SwapInServerPartialSigs.tag + override fun write(out: Output) = sigs.forEach { sig -> LightningCodecs.writeBytes(sig, out) } + + companion object : TlvValueReader { + const val tag: Long = 609 + override fun read(input: Input): SwapInServerPartialSigs { + val count = input.availableBytes / 32 + val sigs = (0 until count).map { LightningCodecs.bytes(input, 32).byteVector32() } + return SwapInServerPartialSigs(sigs) + } + } + } + data class ChannelData(val ecb: EncryptedChannelData) : TxSignaturesTlv() { override val tag: Long get() = ChannelData.tag override fun write(out: Output) = LightningCodecs.writeBytes(ecb.data, out) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 5b69146bb..0ac21e34b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -5,6 +5,8 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.bitcoin.musig2.PublicNonce +import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelType @@ -444,16 +446,26 @@ data class TxRemoveOutput( data class TxComplete( override val channelId: ByteVector32, - val tlvs: TlvStream = TlvStream.empty() + val secretNonces: Map = mapOf(), + val tlvs: TlvStream = if (secretNonces.isEmpty()) TlvStream.empty() else TlvStream(TxCompleteTlv.Nonces(secretNonces.mapValues { it.value.publicNonce() })) ) : InteractiveTxConstructionMessage(), HasChannelId { override val type: Long get() = TxComplete.type - override fun write(out: Output) = LightningCodecs.writeBytes(channelId.toByteArray(), out) + val serverNonces: Map = tlvs.get()?.nonces?.toMap() ?: mapOf() + + override fun write(out: Output) { + LightningCodecs.writeBytes(channelId.toByteArray(), out) + // secret nonces are NOT serialized !! + TlvStreamSerializer(false, readers).write(tlvs, out) + } companion object : LightningMessageReader { const val type: Long = 70 - override fun read(input: Input): TxComplete = TxComplete(LightningCodecs.bytes(input, 32).byteVector32()) + @Suppress("UNCHECKED_CAST") + val readers = mapOf(TxCompleteTlv.Nonces.tag to TxCompleteTlv.Nonces.Companion as TlvValueReader) + + override fun read(input: Input): TxComplete = TxComplete(LightningCodecs.bytes(input, 32).byteVector32(), mapOf(), TlvStreamSerializer(false, readers).read(input)) } } @@ -463,7 +475,7 @@ 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) : this( + constructor(channelId: ByteVector32, tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, swapInUserSigs: List, swapInServerSigs: List, swapInUserPartialSigs: List, swapInServerPartialSigs: List) : this( channelId, tx.hash, witnesses, @@ -472,6 +484,8 @@ data class TxSignatures( 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() ), ) @@ -482,6 +496,8 @@ data class TxSignatures( val previousFundingTxSig: ByteVector64? = tlvs.get()?.sig val swapInUserSigs: List = tlvs.get()?.sigs ?: listOf() val swapInServerSigs: List = tlvs.get()?.sigs ?: listOf() + val swapInUserPartialSigs: List = tlvs.get()?.sigs ?: listOf() + val swapInServerPartialSigs: List = tlvs.get()?.sigs ?: listOf() override val channelData: EncryptedChannelData get() = tlvs.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): TxSignatures = copy(tlvs = tlvs.addOrUpdate(TxSignaturesTlv.ChannelData(ecd))) @@ -506,6 +522,8 @@ data class TxSignatures( TxSignaturesTlv.PreviousFundingTxSig.tag to TxSignaturesTlv.PreviousFundingTxSig.Companion as TlvValueReader, TxSignaturesTlv.SwapInUserSigs.tag to TxSignaturesTlv.SwapInUserSigs.Companion as TlvValueReader, TxSignaturesTlv.SwapInServerSigs.tag to TxSignaturesTlv.SwapInServerSigs.Companion as TlvValueReader, + TxSignaturesTlv.SwapInUserPartialSigs.tag to TxSignaturesTlv.SwapInUserPartialSigs.Companion as TlvValueReader, + TxSignaturesTlv.SwapInServerPartialSigs.tag to TxSignaturesTlv.SwapInServerPartialSigs.Companion as TlvValueReader, TxSignaturesTlv.ChannelData.tag to TxSignaturesTlv.ChannelData.Companion as TlvValueReader, ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index 345f379d5..93249fd4b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -983,7 +983,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val sharedOutput = InteractiveTxOutput.Shared(0, f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), 100_000_000.msat, 0.msat) val previousTx1 = Transaction(2, listOf(), listOf(TxOut(150_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) - val previousTx2 = Transaction(2, listOf(), listOf(TxOut(160_000.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(175_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) + val previousTx2 = Transaction(2, listOf(), listOf(TxOut(160_000.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(200_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() val firstAttempt = FullySignedSharedTransaction( SharedTransaction(null, sharedOutput, listOf(), listOf(InteractiveTxInput.RemoteOnly(2, OutPoint(previousTx1, 0), TxOut(125_000.sat, validScript), 0u)), listOf(), listOf(), 0), @@ -1049,14 +1049,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87") ) ) - val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, listOf(), listOf()) + val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, listOf(), listOf(), listOf(), listOf()) val nonInitiatorWitness = ScriptWitness( listOf( ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484") ) ) - val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, listOf(), listOf()) + val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, listOf(), listOf(), listOf(), listOf()) val initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs, null) assertEquals(initiatorSignedTx.feerate, FeeratePerKw(262.sat)) val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs, null) @@ -1216,7 +1216,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { private fun createWallet(onChainKeys: KeyManager.SwapInOnChainKeys, amounts: List): List { return amounts.map { amount -> val txIn = listOf(TxIn(OutPoint(randomBytes32(), 2), 0)) - val txOut = listOf(TxOut(amount, onChainKeys.pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) + val txOut = listOf(TxOut(amount, onChainKeys.swapInProtocol.pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) val parentTx = Transaction(2, txIn, txOut, 0) WalletState.Utxo(parentTx, 0, 0) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index 2b012dc39..c3c465b7d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -18,6 +18,7 @@ import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* +import io.ktor.util.reflect.* import kotlinx.serialization.encodeToString import org.kodein.log.LoggerFactory import org.kodein.log.newLogger @@ -117,9 +118,18 @@ data class LNChannel( else -> state } + fun removeSecretNonces(input: InteractiveTxInput.Local): InteractiveTxInput.Local = when(input) { + is InteractiveTxInput.LocalSwapIn -> input.copy(userNonce = null, serverNonce = null) + else -> input + } + fun removeSecretNonces(input: SharedTransaction): SharedTransaction = input.copy(localInputs = input.localInputs.map { removeSecretNonces(it) }) + val serialized = Serialization.serialize(state) val deserialized = Serialization.deserialize(serialized).value + if (deserialized != state) { + error("serialization error") + } assertEquals(removeRbfAttempt(state), deserialized, "serialization error") } @@ -406,7 +416,7 @@ object TestsHelper { } fun createWallet(keyManager: KeyManager, amount: Satoshi): Pair> { - val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, pubkeyScript) } + val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, swapInProtocolMusig2.pubkeyScript) } val parentTx = Transaction(2, listOf(TxIn(OutPoint(randomBytes32(), 3), 0)), listOf(TxOut(amount, script)), 0) return privateKey to listOf(WalletState.Utxo(parentTx, 0, 42)) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index 3b6110b4e..deb84283b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -1158,7 +1158,7 @@ class SpliceTestsCommon : LightningTestSuite() { } private fun createWalletWithFunds(keyManager: KeyManager, amounts: List): List { - val script = keyManager.swapInOnChainWallet.pubkeyScript + val script = keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript return amounts.map { amount -> val txIn = listOf(TxIn(OutPoint(Lightning.randomBytes32(), 2), 0)) val txOut = listOf(TxOut(amount, script), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt index ca278662e..0fac7b37c 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt @@ -443,7 +443,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val previousFundingTx = alice.state.latestFundingTx.sharedTx assertIs(previousFundingTx) // Alice adds a new input that increases her contribution and covers the additional fees. - val script = alice.staticParams.nodeParams.keyManager.swapInOnChainWallet.pubkeyScript + val script = alice.staticParams.nodeParams.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript val parentTx = Transaction(2, listOf(TxIn(OutPoint(randomBytes32(), 1), 0)), listOf(TxOut(30_000.sat, script)), 0) val wallet1 = wallet + listOf(WalletState.Utxo(parentTx, 0, 42)) return ChannelCommand.Funding.BumpFundingFee(previousFundingTx.feerate * 1.1, previousFundingParams.localContribution + 20_000.sat, wallet1, previousFundingTx.tx.lockTime + 1) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt index 9bd0811a5..b3108165d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -195,12 +196,16 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val swapInTx = Transaction(version = 2, txIn = listOf(), txOut = listOf( - TxOut(Satoshi(100000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWallet.address).result!!), - TxOut(Satoshi(150000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWallet.address).result!!) + TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript), + TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript), + TxOut(Satoshi(150000), Script.pay2wpkh(randomKey().publicKey())), + TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocolMusig2.pubkeyScript), + TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocolMusig2.pubkeyScript), + TxOut(Satoshi(150000), Script.pay2wpkh(randomKey().publicKey())) ), lockTime = 0) val recoveryTx = TestConstants.Alice.keyManager.swapInOnChainWallet.createRecoveryTransaction(swapInTx, TestConstants.Alice.keyManager.finalOnChainWallet.address(0), FeeratePerKw(FeeratePerByte(Satoshi(5))))!! - assertEquals(swapInTx.txOut.size, recoveryTx.txIn.size) + assertEquals(4, recoveryTx.txIn.size) Transaction.correctlySpends(recoveryTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index 0f7117262..95b271179 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -13,6 +13,7 @@ import fr.acinq.lightning.utils.value import fr.acinq.lightning.wire.CommitSig import fr.acinq.lightning.wire.EncryptedChannelData import fr.acinq.lightning.wire.LightningMessage +import fr.acinq.lightning.wire.TxSignatures import fr.acinq.secp256k1.Hex import kotlin.math.max import kotlin.test.* diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index 824e4ba11..46fd7c0e0 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -449,7 +449,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val swapInTx = Transaction( version = 2, txIn = listOf(TxIn(OutPoint(randomBytes32(), 2), 0)), - txOut = listOf(TxOut(100_000.sat, userWallet.pubkeyScript)), + txOut = listOf(TxOut(100_000.sat, userWallet.swapInProtocol.pubkeyScript)), lockTime = 0 ) // The transaction can be spent if the user and the server produce a signature. @@ -460,7 +460,7 @@ class TransactionsTestsCommon : LightningTestSuite() { txOut = listOf(TxOut(90_000.sat, pay2wpkh(randomKey().publicKey()))), lockTime = 0 ) - val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first()) + val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut) val serverKey = TestConstants.Bob.keyManager.swapInOnChainWallet.localServerPrivateKey(TestConstants.Alice.nodeParams.nodeId) val serverSig = userWallet.swapInProtocol.signSwapInputServer(fundingTx, 0, swapInTx.txOut.first(), serverKey) val witness = userWallet.swapInProtocol.witness(userSig, serverSig) @@ -475,7 +475,7 @@ class TransactionsTestsCommon : LightningTestSuite() { txOut = listOf(TxOut(90_000.sat, pay2wpkh(randomKey().publicKey()))), lockTime = 0 ) - val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first()) + val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut) val witness = userWallet.swapInProtocol.witnessRefund(userSig) val signedTx = fundingTx.updateWitness(0, witness) Transaction.correctlySpends(signedTx, listOf(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 7753c6df7..7b8cdd269 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput +import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes import fr.acinq.lightning.Lightning.randomBytes32 @@ -374,6 +375,12 @@ class LightningCodecsTestsCommon : LightningTestSuite() { ByteVector64("c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3"), ByteVector64("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), ) + val swapInPartialSignatures = listOf( + ByteVector32("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), + ByteVector32("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd") +// ByteVector32("ae4e6a974787be4f3796e1754f63e3fed50fa8bff867fccb4fa69b8dacf16cea"), +// ByteVector32("6d1abccfb85f5990309524d534676b2cb55f28f1099d951833d97a7e3bf31f3f"), + ) val signature = ByteVector64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") // This is a random mainnet transaction. val tx1 = Transaction.read( @@ -383,26 +390,29 @@ 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.SwapInParams(swapInUserKey, swapInServerKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534603462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f00000090"), + TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInRefundDelay, 1))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534a03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f0000009000000001"), 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()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), - TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), swapInSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures.take(1), swapInSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), swapInSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures.take(1), swapInSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + 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(), 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 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f 40 ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 40 ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), + 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"), 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"), @@ -422,6 +432,17 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val encoded = LightningMessage.encode(message) assertEquals(encoded.byteVector(), bin) } + + val txComplete = TxComplete( + randomBytes32(), + mapOf( + 3L to SecretNonce.generate(null, randomKey().publicKey(), null, null, null, randomBytes32()), + 5L to SecretNonce.generate(null, randomKey().publicKey(), null, null, null, randomBytes32()), + 2L to SecretNonce.generate(null, randomKey().publicKey(), null, null, null, randomBytes32()), + ) + ) + val decoded = LightningMessage.decode(LightningMessage.encode(txComplete)) + assertEquals(txComplete.copy(secretNonces = mapOf()), decoded) } @Test