diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 0b9047383..d23d73698 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -285,13 +285,16 @@ data class FundingContributions(val inputs: List, v 0xfffffffdU, swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) - else -> InteractiveTxInput.LocalSwapIn( - 0, - i.previousTx.stripInputWitnesses(), - i.outputIndex.toLong(), - 0xfffffffdU, - TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay), - ) + else -> { + val swapInProtocol = swapInKeys.getSwapInProtocol(i.previousTx.txOut[i.outputIndex].publicKeyScript)!! + InteractiveTxInput.LocalSwapIn( + 0, + i.previousTx.stripInputWitnesses(), + i.outputIndex.toLong(), + 0xfffffffdU, + TxAddInputTlv.SwapInParams(swapInProtocol.userPublicKey, swapInProtocol.serverPublicKey, swapInProtocol.userRefundKey, swapInProtocol.refundDelay), + ) + } } } return if (params.isInitiator) { @@ -674,7 +677,8 @@ data class InteractiveTxSession( } is InteractiveTxInput.LocalSwapIn -> { - val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay) + val swapInProtocol = swapInKeys.getSwapInProtocol(msg.value.previousTx.txOut[msg.value.previousTxOutput.toInt()].publicKeyScript)!! + val swapInParams = TxAddInputTlv.SwapInParams(swapInProtocol.userPublicKey, swapInProtocol.serverPublicKey, swapInProtocol.userRefundKey, swapInProtocol.refundDelay) TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 4288eac4e..e4222b3d6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -128,17 +128,24 @@ interface KeyManager { val userPrivateKey: PrivateKey = userExtendedPrivateKey.privateKey val userPublicKey: PublicKey = userPrivateKey.publicKey() - val userRefundPrivateKey: PrivateKey = DeterministicWallet.derivePrivateKey(userRefundExtendedPrivateKey, 0).privateKey - val userRefundPublicKey: PublicKey = userRefundPrivateKey.publicKey() - private val localServerExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain)) fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey // legacy p2wsh-based swap-in protocol, with a fixed on-chain address val legacySwapInProtocol = SwapInProtocolLegacy(userPublicKey, remoteServerPublicKey, refundDelay) - val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) - val descriptor = SwapInProtocol.descriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, userRefundExtendedPrivateKey) + val swapInProtocols = (0 until 100).map { + val userRefundPrivateKey: PrivateKey = DeterministicWallet.derivePrivateKey(userRefundExtendedPrivateKey, it.toLong()).privateKey + val userRefundPublicKey: PublicKey = userRefundPrivateKey.publicKey() + val protocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) + protocol + } + + /** + * @param pubkeyScript public key script + * @return the swap-in protocol that matches the input public key script + */ + fun getSwapInProtocol(pubkeyScript: ByteVector): SwapInProtocol? = swapInProtocols.find { it.serializedPubkeyScript == pubkeyScript } /** * The output script descriptor matching our legacy swap-in addresses. @@ -159,6 +166,9 @@ interface KeyManager { } fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: SecretNonce, commonNonce: AggregatedNonce): ByteVector32 { + val spentOutput = parentTxOuts[index] + val swapInProtocol = swapInProtocols.find { it.serializedPubkeyScript == spentOutput.publicKeyScript } + require(swapInProtocol != null) { "cannot match swap-in input ${fundingTx.txid}:$index" } return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, userNonce, commonNonce) } @@ -170,7 +180,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(legacySwapInProtocol.pubkeyScript)) || it.publicKeyScript.contentEquals(Script.write(swapInProtocol.pubkeyScript))} + val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(legacySwapInProtocol.pubkeyScript)) || swapInProtocols.find { p -> p.serializedPubkeyScript == it.publicKeyScript } != null } return if (utxos.isEmpty()) { null } else { @@ -189,6 +199,9 @@ interface KeyManager { val sig = legacySwapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey) tx.updateWitness(index, legacySwapInProtocol.witnessRefund(sig)) } else { + val i = swapInProtocols.indexOfFirst { it.serializedPubkeyScript == utxo.publicKeyScript } + val userRefundPrivateKey: PrivateKey = DeterministicWallet.derivePrivateKey(userRefundExtendedPrivateKey, i.toLong()).privateKey + val swapInProtocol = swapInProtocols[i] val sig = swapInProtocol.signSwapInputRefund(tx, index, utxos, userRefundPrivateKey) tx.updateWitness(index, swapInProtocol.witnessRefund(sig)) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 7e098965b..ae78e4b89 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -194,7 +194,8 @@ class Peer( val swapInWallet = ElectrumMiniWallet(nodeParams.chainHash, watcher.client, scope, nodeParams.loggerFactory, name = "swap-in") val legacySwapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.legacySwapInProtocol.address(nodeParams.chain).also { swapInWallet.addAddress(it) } - val swapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.swapInProtocol.address(nodeParams.chain).also { swapInWallet.addAddress(it) } + val swapInAddressFlow = MutableStateFlow(null) + val swapInAddresses: List = nodeParams.keyManager.swapInOnChainWallet.swapInProtocols.map { it.address(nodeParams.chain) }.onEach { swapInWallet.addAddress(it) } private var swapInJob: Job? = null @@ -456,6 +457,10 @@ class Peer( swapInJob = launch { swapInWallet.walletStateFlow .filter { it.consistent } + .onEach { + // take the first unused address, or a random address if there are none + swapInAddressFlow.value = it.addresses.filter { it.value.isEmpty() }.map { it.key }.firstOrNull() ?: it.addresses.keys.random() + } .combine(currentTipFlow.filterNotNull()) { walletState, currentTip -> Pair(walletState, currentTip.first) } .combine(swapInFeeratesFlow.filterNotNull()) { (walletState, currentTip), feerate -> Triple(walletState, currentTip, feerate) } .combine(nodeParams.liquidityPolicy) { (walletState, currentTip, feerate), policy -> TrySwapInFlow(currentTip, walletState, feerate, policy) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index 48373b89a..e5d9cdd3a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -34,6 +34,7 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe val commonPubKey = commonPubKeyAndParity.first private val parity = commonPubKeyAndParity.second val pubkeyScript: List = Script.pay2tr(commonPubKey) + val serializedPubkeyScript = Script.write(pubkeyScript).byteVector() 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() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index b2083a7a4..7dbe60c82 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -409,7 +409,7 @@ object TestsHelper { } fun createWallet(keyManager: KeyManager, amount: Satoshi): Pair> { - val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, swapInProtocol.pubkeyScript) } + val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, swapInProtocols[0].pubkeyScript) } val parentTx = Transaction(2, listOf(TxIn(OutPoint(TxId(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/crypto/LocalKeyManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt index 1dc50342e..686196cc9 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -199,8 +199,8 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.legacySwapInProtocol.pubkeyScript), TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.legacySwapInProtocol.pubkeyScript), TxOut(Satoshi(150000), Script.pay2wpkh(randomKey().publicKey())), - TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript), - TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript), + TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocols[0].pubkeyScript), + TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocols[1].pubkeyScript), TxOut(Satoshi(150000), Script.pay2wpkh(randomKey().publicKey())) ), lockTime = 0)