From e5dad76bfa1383423b8ff2c5f288e139269c6993 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 8 Jan 2024 14:11:16 +0100 Subject: [PATCH] Address review comments - add a pubkey script to the SharedInput() class (we don't need the full TxOut which we can recreate) - remove aggregate nonce check ins FullySignedTx: code already handles transactions that are not properly signed - generate musig2 nonces when we send TxAddInput --- RECOVERY.md | 8 ++-- .../acinq/lightning/channel/InteractiveTx.kt | 41 +++++++++++-------- .../serialization/v4/Deserialization.kt | 4 +- .../serialization/v4/Serialization.kt | 2 +- .../lightning/transactions/Transactions.kt | 2 +- 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/RECOVERY.md b/RECOVERY.md index e414446fe..18c0d5886 100644 --- a/RECOVERY.md +++ b/RECOVERY.md @@ -28,7 +28,7 @@ swap-in transactions to your wallet are indistinguishable from other p2tr transa The swap transaction's output can be spent using either: -1. A aggregated musig2 signature built from a partial signature from the user's wallet and a partial signature from the remote node +1. An aggregated musig2 signature built from a partial signature from the user's wallet and a partial signature from the remote node 2. A signature from the user's wallet after a refund delay Funds can be recovered using the second option and [Bitcoin Core](https://github.com/bitcoin/bitcoin). @@ -38,7 +38,7 @@ This process will become simpler once popular on-chain wallets (such as [electru ### Get your wallet descriptor -lighting-kmp provides both a public descriptor and private descriptor for your swap-in wallet. +lightning-kmp provides both a public descriptor and private descriptor for your swap-in wallet. The public descriptor can be used to create a watch-only wallet for your swap-in funds. The private descriptor can be used to recover your swap-in funds, after the refund delay has passed. :warning: Do not share this private descriptor with anyone ! @@ -62,13 +62,13 @@ tr(,and_v(v:pk(/),older(, v } } } - val sharedInput = sharedUtxo?.let { (i, balances) -> listOf(InteractiveTxInput.Shared(0, i.info.outPoint, i.info.txOut, 0xfffffffdU, balances.toLocal, balances.toRemote)) } ?: listOf() + val sharedInput = sharedUtxo?.let { (i, balances) -> listOf(InteractiveTxInput.Shared(0, i.info.outPoint, i.info.txOut.publicKeyScript, 0xfffffffdU, balances.toLocal, balances.toRemote)) } ?: listOf() val localInputs = walletInputs.map { i -> when { Script.isPay2wsh(i.previousTx.txOut[i.outputIndex].publicKeyScript.toByteArray()) -> @@ -525,7 +533,6 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over val localSwapTxInMusig2 = 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) - require(userSig.aggregatedPublicNonce == serverSig.aggregatedPublicNonce) { "aggregated public nonces mismatch for local input ${i.serialId}" } val commonNonce = userSig.aggregatedPublicNonce val unsignedTx = tx.buildUnsignedTx() val ctx = swapInProtocol.session(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) @@ -544,7 +551,6 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over val remoteSwapTxInMusig2 = 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) - require(userSig.aggregatedPublicNonce == serverSig.aggregatedPublicNonce) { "aggregated public nonces mismatch for remote input ${i.serialId}" } val commonNonce = userSig.aggregatedPublicNonce val unsignedTx = tx.buildUnsignedTx() val ctx = swapInProtocol.session(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) @@ -646,19 +652,12 @@ data class InteractiveTxSession( fun send(): Pair { return when (val msg = toSend.firstOrNull()) { null -> { - // generate a new secret nonce for each musig2 new swapin every time we send TxComplete val localMusig2SwapIns = localInputs.filterIsInstance() - val secretNonces1 = localMusig2SwapIns.fold(secretNonces) { nonces, i -> - nonces + (i.serialId to (nonces[i.serialId] ?: SecretNonce.generate(randomBytes32(), swapInKeys.userPrivateKey, swapInKeys.userPublicKey, null, null, null))) - } val remoteMusig2SwapIns = remoteInputs.filterIsInstance() - val secretNonces2 = remoteMusig2SwapIns.fold(secretNonces1) { nonces, i -> - nonces + (i.serialId to (nonces[i.serialId] ?: SecretNonce.generate(randomBytes32(),null, i.swapInParams.serverKey, null, null, null))) - } val serialIds = (localMusig2SwapIns.map { it.serialId } + remoteMusig2SwapIns.map { it.serialId }).sorted() - val nonces = serialIds.map { secretNonces2[it]?.second }.filterNotNull() + val nonces = serialIds.map { secretNonces[it]?.second }.filterNotNull() val txComplete = TxComplete(fundingParams.channelId, nonces) - val next = copy(secretNonces = secretNonces2, txCompleteSent = txComplete) + val next = copy(txCompleteSent = txComplete) if (next.isComplete) { Pair(next, next.validateTx(txComplete)) } else { @@ -667,7 +666,6 @@ data class InteractiveTxSession( } is Either.Left -> { - 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.LocalLegacySwapIn -> { @@ -682,7 +680,16 @@ data class InteractiveTxSession( is InteractiveTxInput.Shared -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.outPoint, msg.value.sequence) } - Pair(next, InteractiveTxSessionAction.SendMessage(txAddInput)) + 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] ?: SecretNonce.generate(randomBytes32(), swapInKeys.userPrivateKey, swapInKeys.userPublicKey, null, null, null) + next.copy(secretNonces = next.secretNonces + (msg.value.serialId to secretNonce)) + } + else -> next + } + Pair(next1, InteractiveTxSessionAction.SendMessage(txAddInput)) } is Either.Right -> { @@ -709,7 +716,7 @@ data class InteractiveTxSession( val expectedSharedOutpoint = fundingParams.sharedInput?.info?.outPoint ?: return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) val receivedSharedOutpoint = message.sharedInput ?: return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) if (expectedSharedOutpoint != receivedSharedOutpoint) return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) - InteractiveTxInput.Shared(message.serialId, receivedSharedOutpoint, fundingParams.sharedInput.info.txOut, message.sequence, previousFunding.toLocal, previousFunding.toRemote) + InteractiveTxInput.Shared(message.serialId, receivedSharedOutpoint, fundingParams.sharedInput.info.txOut.publicKeyScript, message.sequence, previousFunding.toLocal, previousFunding.toRemote) } else -> { 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 27bbb9ece..3da39f371 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -212,7 +212,7 @@ object Deserialization { 0x01 -> InteractiveTxInput.Shared( serialId = readNumber(), outPoint = readOutPoint(), - txOut = TxOut(Satoshi(0), ByteVector.empty), + publicKeyScript = ByteVector.empty, sequence = readNumber().toUInt(), localAmount = readNumber().msat, remoteAmount = readNumber().msat, @@ -220,7 +220,7 @@ object Deserialization { 0x02 -> InteractiveTxInput.Shared( serialId = readNumber(), outPoint = readOutPoint(), - txOut = readTxOut(), + publicKeyScript = readDelimitedByteArray().byteVector(), sequence = readNumber().toUInt(), localAmount = readNumber().msat, remoteAmount = readNumber().msat, 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 479603832..72f7fb560 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -258,7 +258,7 @@ object Serialization { write(0x02) writeNumber(serialId) writeBtcObject(outPoint) - writeBtcObject(txOut) + writeDelimited(publicKeyScript.toByteArray()) writeNumber(sequence.toLong()) writeNumber(localAmount.toLong()) writeNumber(remoteAmount.toLong()) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index 0ba287779..0caa2444a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -159,7 +159,7 @@ object Transactions { * - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] using the revocation secret (published by local) * - [[HtlcPenaltyTx]] spends competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] for the same outputs (published by local) */ - // legacy swap-in. witness is 2 signatures (73 bytes) + redeem script (77 bytes)) + // legacy swap-in. witness is 2 signatures (73 bytes) + redeem script (77 bytes) const val swapInputWeightLegacy = 392 // musig2 swap-in. witness is a single Schnorr signature (64 bytes) const val swapInputWeight = 233