From 506003f4ec4b9b3217b0f325febabd0af1b0f09b Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 19 Feb 2024 11:14:03 +0100 Subject: [PATCH] Rotate taproot swap-in addresses (#584) Addresses are generated one by one, as soon as we detect that the last generated address has been used. There is no lookahead. Scanning 'synchronous' (not really synchronous, but we rely the least possible on the mailbox). This makes the `window` disappear because we are scanning addresses one by one. Some cleanup by extracting wallets outside of `Peer`. --------- Co-authored-by: sstone --- build.gradle.kts | 1 + .../blockchain/electrum/ElectrumMiniWallet.kt | 141 ++++++++--- .../blockchain/electrum/FinalWallet.kt | 32 +++ .../blockchain/electrum/SwapInWallet.kt | 44 ++++ .../acinq/lightning/channel/InteractiveTx.kt | 23 +- .../fr/acinq/lightning/crypto/KeyManager.kt | 34 ++- .../kotlin/fr/acinq/lightning/io/Peer.kt | 17 +- .../serialization/v4/Deserialization.kt | 1 + .../serialization/v4/Serialization.kt | 1 + .../lightning/transactions/SwapInProtocol.kt | 1 + .../electrum/ElectrumMiniWalletTest.kt | 233 ++++++++++++++++-- .../electrum/SwapInManagerTestsCommon.kt | 54 ++-- .../electrum/SwapInWalletTestsCommon.kt | 28 +++ .../channel/InteractiveTxTestsCommon.kt | 16 +- .../fr/acinq/lightning/channel/TestsHelper.kt | 5 +- .../channel/states/QuiescenceTestsCommon.kt | 6 +- .../channel/states/SpliceTestsCommon.kt | 2 +- .../WaitForFundingConfirmedTestsCommon.kt | 2 +- .../crypto/LocalKeyManagerTestsCommon.kt | 4 +- 19 files changed, 518 insertions(+), 127 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/FinalWallet.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWallet.kt create mode 100644 src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWalletTestsCommon.kt diff --git a/build.gradle.kts b/build.gradle.kts index e97a8ea50..6c969711f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -306,6 +306,7 @@ if (currentOs.isLinux) { filter.excludeTestsMatching("*IntegrationTest") filter.excludeTestsMatching("*ElectrumClientTest") filter.excludeTestsMatching("*ElectrumMiniWalletTest") + filter.excludeTestsMatching("*SwapInWalletTestsCommon") } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt index bb5efe148..57766eb16 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt @@ -1,8 +1,11 @@ package fr.acinq.lightning.blockchain.electrum +import co.touchlab.kermit.Logger import fr.acinq.bitcoin.* import fr.acinq.lightning.SwapInParams -import fr.acinq.lightning.logging.* +import fr.acinq.lightning.logging.debug +import fr.acinq.lightning.logging.info +import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.sum import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -13,13 +16,16 @@ import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch -data class WalletState(val addresses: Map>) { - val utxos: List = addresses.flatMap { it.value } +data class WalletState(val addresses: Map) { + val utxos: List = addresses.flatMap { it.value.utxos } val totalBalance = utxos.map { it.amount }.sum() + val lastDerivedAddress: Pair? = addresses + .mapNotNull { entry -> (entry.value.meta as? AddressMeta.Derived)?.let { entry.key to it } } + .maxByOrNull { it.second.index } fun withoutReservedUtxos(reserved: Set): WalletState { return copy(addresses = addresses.mapValues { - it.value.filter { item -> !reserved.contains(item.outPoint) } + it.value.copy(utxos = it.value.utxos.filter { item -> !reserved.contains(item.outPoint) }) }) } @@ -27,11 +33,24 @@ data class WalletState(val addresses: Map>) { swapInParams = swapInParams, currentBlockHeight = currentBlockHeight, all = utxos, ) - data class Utxo(val txId: TxId, val outputIndex: Int, val blockHeight: Long, val previousTx: Transaction) { + data class Utxo(val txId: TxId, val outputIndex: Int, val blockHeight: Long, val previousTx: Transaction, val addressMeta: AddressMeta) { val outPoint = OutPoint(previousTx, outputIndex.toLong()) val amount = previousTx.txOut[outputIndex].amount } + data class AddressState(val meta: AddressMeta, val alreadyUsed: Boolean, val utxos: List) + + sealed class AddressMeta { + data object Single : AddressMeta() + data class Derived(val index: Int) : AddressMeta() + } + + val AddressMeta.indexOrNull: Int? + get() = when (this) { + is AddressMeta.Single -> null + is AddressMeta.Derived -> this.index + } + /** * The utxos of a wallet may be discriminated against their number of confirmations. Typically, this is used in the * context of a funding, which should happen only after a given depth. @@ -82,6 +101,9 @@ private sealed interface WalletCommand { data object ElectrumConnected : WalletCommand data class ElectrumNotification(val msg: ElectrumResponse) : WalletCommand data class AddAddress(val bitcoinAddress: String) : WalletCommand + data class AddAddressGenerator(val generator: AddressGenerator) : WalletCommand + + class AddressGenerator(val generateAddress: (Int) -> String) } } @@ -92,23 +114,19 @@ class ElectrumMiniWallet( val chainHash: BlockHash, private val client: IElectrumClient, private val scope: CoroutineScope, - loggerFactory: LoggerFactory, - private val name: String = "" + private val logger: Logger ) : CoroutineScope by scope { - private val logger = MDCLogger(loggerFactory.newLogger(this::class)) - private fun mdc(): Map { - return mapOf( - "wallet" to name, - "utxos" to walletStateFlow.value.utxos.size, - "balance" to walletStateFlow.value.totalBalance - ) - } - // state flow with the current balance private val _walletStateFlow = MutableStateFlow(WalletState(emptyMap())) val walletStateFlow get() = _walletStateFlow.asStateFlow() + // generator, if used + private var addressGenerator: WalletCommand.Companion.AddressGenerator? = null + + // all current meta associated to each address + private var addressMetas: Map = emptyMap() + // all currently watched script hashes and their corresponding bitcoin address private var scriptHashes: Map = emptyMap() @@ -121,6 +139,12 @@ class ElectrumMiniWallet( } } + fun addAddressGenerator(generator: (Int) -> String) { + launch { + mailbox.send(WalletCommand.Companion.AddAddressGenerator(WalletCommand.Companion.AddressGenerator(generator))) + } + } + /** This function should only be used in tests, to test the wallet notification flow. */ fun setWalletState(walletState: WalletState) { launch { @@ -131,25 +155,31 @@ class ElectrumMiniWallet( private val job: Job init { + suspend fun WalletState.processSubscriptionResponse(msg: ScriptHashSubscriptionResponse): WalletState { val bitcoinAddress = scriptHashes[msg.scriptHash] + val addressMeta = bitcoinAddress?.let { addressMetas[it] } return when { - bitcoinAddress == null -> { - // this should never happen - logger.error { "received subscription response for script hash ${msg.scriptHash} that does not match any address" } + bitcoinAddress == null || addressMeta == null -> { + // this will happen because multiple wallets may be sharing the same Electrum connection (e.g. swap-in and final wallet) + logger.debug { "received subscription response for script hash ${msg.scriptHash} that does not match any address" } this } - msg.status == null -> this.copy(addresses = this.addresses + (bitcoinAddress to listOf())) + msg.status == null -> { + logger.info { "address=$bitcoinAddress index=${addressMeta.indexOrNull ?: "n/a"} utxos=(unused)" } + this.copy(addresses = this.addresses + (bitcoinAddress to WalletState.AddressState(addressMeta, alreadyUsed = false, utxos = listOf()))) + } else -> { val unspents = client.getScriptHashUnspents(msg.scriptHash) - val previouslysKnownTxs = (_walletStateFlow.value.addresses[bitcoinAddress] ?: emptyList()).map { it.txId to it.previousTx }.toMap() + val previouslysKnownTxs = (this.addresses[bitcoinAddress]?.utxos ?: emptyList()).associate { it.txId to it.previousTx } val utxos = unspents .mapNotNull { item -> (previouslysKnownTxs[item.txid] ?: client.getTx(item.txid))?.let { item to it } } // only retrieve txs from electrum if necessary and ignore the utxo if the parent tx cannot be retrieved - .map { (item, previousTx) -> WalletState.Utxo(item.txid, item.outputIndex, item.blockHeight, previousTx) } - val nextWalletState = this.copy(addresses = this.addresses + (bitcoinAddress to utxos)) - logger.info(mdc()) { "${unspents.size} utxo(s) for address=$bitcoinAddress balance=${nextWalletState.totalBalance}" } + .map { (item, previousTx) -> WalletState.Utxo(item.txid, item.outputIndex, item.blockHeight, previousTx, addressMeta) } + val nextAddressState = WalletState.AddressState(addressMeta, alreadyUsed = true, utxos) + val nextWalletState = this.copy(addresses = this.addresses + (bitcoinAddress to nextAddressState)) + logger.info { "address=$bitcoinAddress index=${addressMeta.indexOrNull ?: "n/a"} utxos=${unspents.size} amount=${unspents.sumOf { it.value }.sat}" } unspents.forEach { logger.debug { "utxo=${it.outPoint.txid}:${it.outPoint.index} amount=${it.value} sat" } } - nextWalletState + return nextWalletState } } } @@ -159,11 +189,10 @@ class ElectrumMiniWallet( * Depending on the status of the electrum connection, the subscription may or may not be sent to a server. * It is the responsibility of the caller to resubscribe on reconnection. */ - suspend fun subscribe(scriptHash: ByteVector32, bitcoinAddress: String) { - kotlin.runCatching { client.startScriptHashSubscription(scriptHash) }.map { response -> - logger.info { "subscribed to address=$bitcoinAddress scriptHash=$scriptHash" } - _walletStateFlow.value = _walletStateFlow.value.processSubscriptionResponse(response) - } + suspend fun WalletState.subscribe(scriptHash: ByteVector32, bitcoinAddress: String): WalletState { + val response = client.startScriptHashSubscription(scriptHash) + logger.debug { "subscribed to address=$bitcoinAddress scriptHash=$scriptHash" } + return processSubscriptionResponse(response) } fun computeScriptHash(bitcoinAddress: String): ByteVector32? { @@ -172,6 +201,30 @@ class ElectrumMiniWallet( .right } + suspend fun WalletState.addAddress(bitcoinAddress: String, meta: WalletState.AddressMeta): WalletState { + return computeScriptHash(bitcoinAddress)?.let { scriptHash -> + if (!scriptHashes.containsKey(scriptHash)) { + logger.debug { "adding new address=${bitcoinAddress} index=${meta.indexOrNull ?: "n/a"}" } + scriptHashes = scriptHashes + (scriptHash to bitcoinAddress) + addressMetas = addressMetas + (bitcoinAddress to meta) + subscribe(scriptHash, bitcoinAddress) + } else this + } ?: this + } + + suspend fun WalletState.addAddress(generator: WalletCommand.Companion.AddressGenerator, addressIndex: Int): WalletState { + return this.addAddress(generator.generateAddress(addressIndex), WalletState.AddressMeta.Derived(addressIndex)) + } + + suspend fun WalletState.maybeGenerateNext(generator: WalletCommand.Companion.AddressGenerator): WalletState { + val lastDerivedAddressState = this.lastDerivedAddress?.let { this.addresses[it.first] } + return when { + lastDerivedAddressState == null -> this.addAddress(generator, 0).maybeGenerateNext(generator) // there is no existing derived address: initialization + lastDerivedAddressState.alreadyUsed -> this.addAddress(generator, lastDerivedAddressState.meta.indexOrNull!! + 1).maybeGenerateNext(generator) // most recent derived address is used, need to generate a new one + else -> this // nothing to do + } + } + job = launch { launch { // listen to connection events @@ -185,23 +238,33 @@ class ElectrumMiniWallet( mailbox.consumeAsFlow().collect { when (it) { is WalletCommand.Companion.ElectrumConnected -> { - logger.info(mdc()) { "electrum connected" } - scriptHashes.forEach { (scriptHash, address) -> subscribe(scriptHash, address) } + logger.info { "electrum connected" } + val walletState1 = scriptHashes + .toList() + .fold(_walletStateFlow.value) { walletState, (scriptHash, address) -> + walletState.subscribe(scriptHash, address) + } + val walletState2 = addressGenerator?.let { gen -> walletState1.maybeGenerateNext(gen) } ?: walletState1 + _walletStateFlow.value = walletState2 + } is WalletCommand.Companion.ElectrumNotification -> { if (it.msg is ScriptHashSubscriptionResponse) { - _walletStateFlow.value = _walletStateFlow.value.processSubscriptionResponse(it.msg) + val walletState1 = _walletStateFlow.value.processSubscriptionResponse(it.msg) + val walletState2 = addressGenerator?.let { gen -> walletState1.maybeGenerateNext(gen) } ?: walletState1 + _walletStateFlow.value = walletState2 } } is WalletCommand.Companion.AddAddress -> { - computeScriptHash(it.bitcoinAddress)?.let { scriptHash -> - if (!scriptHashes.containsKey(scriptHash)) { - logger.info(mdc()) { "adding new address=${it.bitcoinAddress}" } - scriptHashes = scriptHashes + (scriptHash to it.bitcoinAddress) - subscribe(scriptHash, it.bitcoinAddress) - } + _walletStateFlow.value = _walletStateFlow.value.addAddress(it.bitcoinAddress, WalletState.AddressMeta.Single) + } + is WalletCommand.Companion.AddAddressGenerator -> { + if (addressGenerator == null) { + logger.info { "adding new address generator" } + addressGenerator = it.generator + _walletStateFlow.value = _walletStateFlow.value.maybeGenerateNext(it.generator) } } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/FinalWallet.kt b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/FinalWallet.kt new file mode 100644 index 000000000..49e6ef9a7 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/FinalWallet.kt @@ -0,0 +1,32 @@ +package fr.acinq.lightning.blockchain.electrum + +import fr.acinq.bitcoin.Bitcoin +import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.logging.LoggerFactory +import fr.acinq.lightning.logging.info +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.launch + +class FinalWallet( + chain: Bitcoin.Chain, + finalWalletKeys: KeyManager.Bip84OnChainKeys, + electrum: IElectrumClient, + scope: CoroutineScope, + loggerFactory: LoggerFactory +) { + private val logger = loggerFactory.newLogger(this::class) + + val wallet = ElectrumMiniWallet(chain.chainHash, electrum, scope, logger) + val finalAddress: String = finalWalletKeys.address(addressIndex = 0L).also { wallet.addAddress(it) } + + init { + scope.launch { + wallet.walletStateFlow + .distinctUntilChangedBy { it.totalBalance } + .collect { wallet -> + logger.info { "${wallet.totalBalance} available on final wallet with ${wallet.utxos.size} utxos" } + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWallet.kt b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWallet.kt new file mode 100644 index 000000000..c77888c31 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWallet.kt @@ -0,0 +1,44 @@ +package fr.acinq.lightning.blockchain.electrum + +import fr.acinq.bitcoin.Bitcoin +import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.logging.LoggerFactory +import fr.acinq.lightning.logging.info +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +class SwapInWallet( + chain: Bitcoin.Chain, + swapInKeys: KeyManager.SwapInOnChainKeys, + electrum: IElectrumClient, + scope: CoroutineScope, + loggerFactory: LoggerFactory +) { + private val logger = loggerFactory.newLogger(this::class) + + val wallet = ElectrumMiniWallet(chain.chainHash, electrum, scope, logger) + + val legacySwapInAddress: String = swapInKeys.legacySwapInProtocol.address(chain) + .also { wallet.addAddress(it) } + val swapInAddressFlow = MutableStateFlow?>(null) + .also { wallet.addAddressGenerator(generator = { index -> swapInKeys.getSwapInProtocol(index).address(chain) }) } + + init { + scope.launch { + // address rotation + wallet.walletStateFlow + .map { it.lastDerivedAddress } + .filterNotNull() + .distinctUntilChanged() + .collect { (address, derived) -> + logger.info { "setting current swap-in address=$address index=${derived.index}" } + swapInAddressFlow.emit(address to derived.index) + } + } + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 7b3daeabf..5074949ee 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -92,8 +92,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 @@ -139,6 +141,7 @@ sealed class InteractiveTxInput { override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt, + val addressIndex: Int, val userKey: PublicKey, val serverKey: PublicKey, val userRefundKey: PublicKey, @@ -304,21 +307,24 @@ data class FundingContributions(val inputs: List, v } val sharedInput = sharedUtxo?.let { (i, balances) -> listOf(InteractiveTxInput.Shared(0, i.info.outPoint, i.info.txOut.publicKeyScript, 0xfffffffdU, balances.toLocal, balances.toRemote, balances.toHtlcs)) } ?: listOf() val localInputs = walletInputs.map { i -> - when { - Script.isPay2wsh(i.previousTx.txOut[i.outputIndex].publicKeyScript.toByteArray()) -> - InteractiveTxInput.LocalLegacySwapIn( + when (val meta = i.addressMeta) { + is WalletState.AddressMeta.Derived -> { + val swapInProtocol = swapInKeys.getSwapInProtocol(meta.index) + InteractiveTxInput.LocalSwapIn( 0, i.previousTx.stripInputWitnesses(), i.outputIndex.toLong(), 0xfffffffdU, - swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay + addressIndex = meta.index, + swapInProtocol.userPublicKey, swapInProtocol.serverPublicKey, swapInProtocol.userRefundKey, swapInProtocol.refundDelay ) - else -> InteractiveTxInput.LocalSwapIn( + } + else -> InteractiveTxInput.LocalLegacySwapIn( 0, i.previousTx.stripInputWitnesses(), i.outputIndex.toLong(), 0xfffffffdU, - swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay + swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay ) } } @@ -459,7 +465,7 @@ data class SharedTransaction( // We generate our secret nonce when sending the corresponding input, we know it exists in the map. val userNonce = session.secretNonces[input.serialId]!! val serverNonce = remoteNonces[input.serialId]!! - keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, previousOutputs, userNonce.first, userNonce.second, serverNonce) + keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, previousOutputs, userNonce.first, userNonce.second, serverNonce, input.addressIndex) .map { TxSignaturesTlv.PartialSignature(it, userNonce.second, serverNonce) } .getOrDefault(null) } @@ -694,7 +700,8 @@ data class InteractiveTxSession( TxAddInput(fundingParams.channelId, inputOutgoing.serialId, inputOutgoing.previousTx, inputOutgoing.previousTxOutput, inputOutgoing.sequence, TlvStream(swapInParams)) } is InteractiveTxInput.LocalSwapIn -> { - val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay) + val swapInProtocol = swapInKeys.getSwapInProtocol(inputOutgoing.addressIndex) + val swapInParams = TxAddInputTlv.SwapInParams(swapInProtocol.userPublicKey, swapInProtocol.serverPublicKey, swapInProtocol.userRefundKey, swapInProtocol.refundDelay) TxAddInput(fundingParams.channelId, inputOutgoing.serialId, inputOutgoing.previousTx, inputOutgoing.previousTxOutput, inputOutgoing.sequence, TlvStream(swapInParams)) } is InteractiveTxInput.Shared -> TxAddInput(fundingParams.channelId, inputOutgoing.serialId, inputOutgoing.outPoint, inputOutgoing.sequence) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 850e2ede3..a9ef63b0b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -126,13 +126,16 @@ 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 - val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) + // legacy p2wsh-based swap-in protocol, with a fixed on-chain address + val legacySwapInProtocol = SwapInProtocolLegacy(userPublicKey, remoteServerPublicKey, refundDelay) + val legacyDescriptor = SwapInProtocolLegacy.descriptor(chain, DeterministicWallet.publicKey(master), DeterministicWallet.publicKey(userExtendedPrivateKey), remoteServerPublicKey, refundDelay) + + fun signSwapInputUserLegacy(fundingTx: Transaction, index: Int, parentTxOuts: List): ByteVector64 { + return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()], userPrivateKey) + } // this is a private descriptor that can be used as-is to recover swap-in funds once the refund delay has passed // it is compatible with address rotation as long as refund keys are derived directly from userRefundExtendedPrivateKey @@ -143,15 +146,18 @@ interface KeyManager { // README: it cannot be used to derive private keys, but it can be used to derive swap-in addresses val publicDescriptor = SwapInProtocol.publicDescriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, DeterministicWallet.publicKey(userRefundExtendedPrivateKey)) - // legacy p2wsh-based swap-in protocol, with a fixed on-chain address - val legacySwapInProtocol = SwapInProtocolLegacy(userPublicKey, remoteServerPublicKey, refundDelay) - val legacyDescriptor = SwapInProtocolLegacy.descriptor(chain, DeterministicWallet.publicKey(master), DeterministicWallet.publicKey(userExtendedPrivateKey), remoteServerPublicKey, refundDelay) - - fun signSwapInputUserLegacy(fundingTx: Transaction, index: Int, parentTxOuts: List): ByteVector64 { - return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()], userPrivateKey) + /** + * @param addressIndex address index + * @return the swap-in protocol that matches the input public key script + */ + fun getSwapInProtocol(addressIndex: Int): SwapInProtocol { + val userRefundPrivateKey: PrivateKey = DeterministicWallet.derivePrivateKey(userRefundExtendedPrivateKey, addressIndex.toLong()).privateKey + val userRefundPublicKey: PublicKey = userRefundPrivateKey.publicKey() + return SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) } - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce): Either { + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce, addressIndex: Int): Either { + val swapInProtocol = getSwapInProtocol(addressIndex) return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, privateNonce, userNonce, serverNonce) } @@ -163,7 +169,8 @@ 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 swapInProtocols = (0 until 100).map { getSwapInProtocol(it) } + 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 { @@ -181,6 +188,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 ea21ba563..a71eefda2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -190,12 +190,8 @@ class Peer( } } - val finalWallet = ElectrumMiniWallet(nodeParams.chainHash, watcher.client, scope, nodeParams.loggerFactory, name = "final") - 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 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 finalWallet = FinalWallet(nodeParams.chain, nodeParams.keyManager.finalOnChainWallet, watcher.client, scope, nodeParams.loggerFactory) + val swapInWallet = SwapInWallet(nodeParams.chain, nodeParams.keyManager.swapInOnChainWallet, watcher.client, scope, nodeParams.loggerFactory) private var swapInJob: Job? = null @@ -221,13 +217,6 @@ class Peer( input.send(WrappedChannelCommand(it.channelId, ChannelCommand.WatchReceived(it))) } } - launch { - finalWallet.walletStateFlow - .distinctUntilChangedBy { it.totalBalance } - .collect { wallet -> - logger.info { "${wallet.totalBalance} available on final wallet with ${wallet.utxos.size} utxos" } - } - } launch { // we don't restore closed channels val bootChannels = db.channels.listLocalChannels().filterNot { it is Closed || it is LegacyWaitForFundingConfirmed } @@ -455,7 +444,7 @@ class Peer( logger.info { "waiting for peer to be ready" } waitForPeerReady() swapInJob = launch { - swapInWallet.walletStateFlow + swapInWallet.wallet.walletStateFlow .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/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 5d84a6cd2..d7d5ff33a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -261,6 +261,7 @@ object Deserialization { previousTx = readTransaction(), previousTxOutput = readNumber(), sequence = readNumber().toUInt(), + addressIndex = readNumber().toInt(), userKey = readPublicKey(), serverKey = readPublicKey(), userRefundKey = readPublicKey(), 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 229bd854f..48aab0206 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -289,6 +289,7 @@ object Serialization { writeBtcObject(previousTx) writeNumber(previousTxOutput) writeNumber(sequence.toLong()) + writeNumber(addressIndex) writePublicKey(userKey) writePublicKey(serverKey) writePublicKey(userRefundKey) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index 2381494f7..9090e256a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -22,6 +22,7 @@ data class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: Pub private val refundScript = listOf(OP_PUSHDATA(userRefundKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) private val scriptTree = ScriptTree.Leaf(0, refundScript) val pubkeyScript: List = Script.pay2tr(internalPublicKey, scriptTree) + val serializedPubkeyScript = Script.write(pubkeyScript).byteVector() fun address(chain: Bitcoin.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).right!! diff --git a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt index 3b15c2781..3273f5220 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt @@ -8,7 +8,9 @@ import fr.acinq.lightning.SwapInParams import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.tests.utils.runSuspendTest import fr.acinq.lightning.utils.sat +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.first +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals @@ -16,16 +18,39 @@ import kotlin.time.Duration.Companion.seconds class ElectrumMiniWalletTest : LightningTestSuite() { + private val logger = loggerFactory.newLogger(this::class) + @Test fun `single address with no utxos`() = runSuspendTest(timeout = 15.seconds) { val client = connectToMainnetServer() - val wallet = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, loggerFactory) - wallet.addAddress("bc1qyjmhaptq78vh5j7tnzu7ujayd8sftjahphxppz") + val wallet = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, logger) + val address = "bc1qyjmhaptq78vh5j7tnzu7ujayd8sftjahphxppz" + wallet.addAddress(address) - val walletState = wallet.walletStateFlow.first { it.addresses.isNotEmpty() } + val walletState = wallet.walletStateFlow.drop(1).first() // first emitted wallet is empty - assertEquals(0, walletState.utxos.size) - assertEquals(0.sat, walletState.totalBalance) + assertEquals( + expected = WalletState(mapOf(address to WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = false, utxos = emptyList()))), + actual = walletState + ) + + wallet.stop() + client.stop() + } + + @Test + fun `single address with no utxos -- already used`() = runSuspendTest(timeout = 15.seconds) { + val client = connectToMainnetServer() + val wallet = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, logger) + val address = "15NixkHzN4qW5h2kkmUwpUTf5jCEJNCw9o" + wallet.addAddress(address) + + val walletState = wallet.walletStateFlow.drop(1).first() // first emitted wallet is empty + + assertEquals( + expected = WalletState(mapOf(address to WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos = emptyList()))), + actual = walletState + ) wallet.stop() client.stop() @@ -34,10 +59,10 @@ class ElectrumMiniWalletTest : LightningTestSuite() { @Test fun `single address with existing utxos`() = runSuspendTest(timeout = 15.seconds) { val client = connectToMainnetServer() - val wallet = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, loggerFactory) + val wallet = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, logger) wallet.addAddress("14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2") - val walletState = wallet.walletStateFlow.first { it.addresses.isNotEmpty() } + val walletState = wallet.walletStateFlow.drop(1).first() // first emitted wallet is empty // This address has 3 transactions confirmed at block 100 002 and 3 transactions confirmed at block 100 003. assertEquals(6, walletState.utxos.size) @@ -100,7 +125,7 @@ class ElectrumMiniWalletTest : LightningTestSuite() { @Test fun `multiple addresses`() = runSuspendTest(timeout = 15.seconds) { val client = connectToMainnetServer() - val wallet = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, loggerFactory) + val wallet = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, logger) wallet.addAddress("16MmJT8VqW465GEyckWae547jKVfMB14P8") wallet.addAddress("14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2") wallet.addAddress("1NHFyu1uJ1UoDjtPjqZ4Et3wNCyMGCJ1qV") @@ -110,7 +135,6 @@ class ElectrumMiniWalletTest : LightningTestSuite() { // this has been checked on the blockchain assertEquals(4 + 6 + 1, walletState.utxos.size) assertEquals(72_000_000.sat + 30_000_000.sat + 2_000_000.sat, walletState.totalBalance) - assertEquals(11, walletState.utxos.size) // make sure txid is correct has electrum api is confusing assertContains( walletState.utxos, @@ -118,7 +142,84 @@ class ElectrumMiniWalletTest : LightningTestSuite() { previousTx = Transaction.read("0100000001758713310361270b5ec4cae9b0196cb84fdb2f174d29f9367ad341963fa83e56010000008b483045022100d7b8759aeffe9d829a5df062420eb25017d7341244e49cfede16136a0c0b8dd2022031b42048e66b1f82f7fa99a22954e2709269838ef587c20118e493ced0d63e21014104b9251638d1475b9c62e1cf03129c835bcd5ab843aa0016412e8b39e3f8f7188d3b59023bce2002a2e409ea070c7070392b65d9ae8c8631ae2672a8fbb4f62bbdffffffff02404b4c00000000001976a9143675767783fdf1922f57ab4bb783f3a88dfa609488ac404b4c00000000001976a9142b6ba7c9d796b75eef7942fc9288edd37c32f5c388ac00000000"), outputIndex = 1, blockHeight = 100_003, - txId = TxId("971af80218684017722429be08548d1f30a2f1f220abc064380cbca5cabf7623") + txId = TxId("971af80218684017722429be08548d1f30a2f1f220abc064380cbca5cabf7623"), + addressMeta = WalletState.AddressMeta.Single + ) + ) + + assertEquals( + expected = setOf( + Triple("16MmJT8VqW465GEyckWae547jKVfMB14P8", TxId("c1e943938e0bf2e9e6feefe22af0466514a58e9f7ed0f7ada6fd8e6dbeca0742") to 1, 39_000_000.sat), + Triple("16MmJT8VqW465GEyckWae547jKVfMB14P8", TxId("2cf392ecf573a638f01f72c276c3b097d05eb58f39e165eacc91b8a8df09fbd8") to 0, 12_000_000.sat), + Triple("16MmJT8VqW465GEyckWae547jKVfMB14P8", TxId("149a098d6261b7f9359a572d797c4a41b62378836a14093912618b15644ba402") to 1, 11_000_000.sat), + Triple("16MmJT8VqW465GEyckWae547jKVfMB14P8", TxId("2dd9cb7bcebb74b02efc85570a462f22a54a613235bee11d0a2c791342a26007") to 1, 10_000_000.sat), + Triple("14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2", TxId("71b3dbaca67e9f9189dad3617138c19725ab541ef0b49c05a94913e9f28e3f4e") to 0, 5_000_000.sat), + Triple("14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2", TxId("21d2eb195736af2a40d42107e6abd59c97eb6cffd4a5a7a7709e86590ae61987") to 0, 5_000_000.sat), + Triple("14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2", TxId("74d681e0e03bafa802c8aa084379aa98d9fcd632ddc2ed9782b586ec87451f20") to 1, 5_000_000.sat), + Triple("14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2", TxId("563ea83f9641d37a36f9294d172fdb4fb86c19b0e9cac45e0b27610331138775") to 0, 5_000_000.sat), + Triple("14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2", TxId("971af80218684017722429be08548d1f30a2f1f220abc064380cbca5cabf7623") to 1, 5_000_000.sat), + Triple("14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2", TxId("b1ec9c44009147f3cee26caba45abec2610c74df9751fad14074119b5314da21") to 0, 5_000_000.sat), + Triple("1NHFyu1uJ1UoDjtPjqZ4Et3wNCyMGCJ1qV", TxId("602839d82ac6c9aafd1a20fff5b23e11a99271e7cc238d2e48b352219b2b87ab") to 1, 2_000_000.sat), + ), + actual = walletState.utxos.map { + val txOut = it.previousTx.txOut[it.outputIndex] + val address = Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, txOut.publicKeyScript.toByteArray()).right!! + Triple(address, it.previousTx.txid to it.outputIndex, txOut.amount) + }.toSet() + ) + + wallet.stop() + client.stop() + } + + @Test + fun `multiple addresses with generator`() = runSuspendTest(timeout = 15.seconds) { + val client = connectToMainnetServer() + val wallet = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, logger) + wallet.addAddressGenerator { + when (it) { + 0 -> "16MmJT8VqW465GEyckWae547jKVfMB14P8" // has utxos + 1 -> "15NixkHzN4qW5h2kkmUwpUTf5jCEJNCw9o" // no utxo, but already used + 2 -> "16hBdohKfkzPnDAA1ne3RPX8jjNhWW3eox" // no utxo, but already used + 3 -> "14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2" // has utxos + 4 -> "1NHFyu1uJ1UoDjtPjqZ4Et3wNCyMGCJ1qV" // has utxos + 5 -> "12yyz3bCKjUoQQPaYbrmzYVM4h7bYa9QYj" // no utxo, but already used + 6 -> "1NtgeYfAGMwt1vqdJrNTRuPu4Hnqpd4sKX" // fresh unused address + else -> error("should not go that far") + } + } + + val walletState = wallet.walletStateFlow.drop(1).first() // first emitted wallet is empty + + // this has been checked on the blockchain + assertEquals(4 + 6 + 1, walletState.utxos.size) + assertEquals(72_000_000.sat + 30_000_000.sat + 2_000_000.sat, walletState.totalBalance) + // make sure txid is correct as electrum api is confusing + walletState.utxos.forEach { assertEquals(it.txId, it.previousTx.txid) } + assertContains( + walletState.utxos, + WalletState.Utxo( // utxo for 14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2 + txId = TxId("971af80218684017722429be08548d1f30a2f1f220abc064380cbca5cabf7623"), + previousTx = Transaction.read("0100000001758713310361270b5ec4cae9b0196cb84fdb2f174d29f9367ad341963fa83e56010000008b483045022100d7b8759aeffe9d829a5df062420eb25017d7341244e49cfede16136a0c0b8dd2022031b42048e66b1f82f7fa99a22954e2709269838ef587c20118e493ced0d63e21014104b9251638d1475b9c62e1cf03129c835bcd5ab843aa0016412e8b39e3f8f7188d3b59023bce2002a2e409ea070c7070392b65d9ae8c8631ae2672a8fbb4f62bbdffffffff02404b4c00000000001976a9143675767783fdf1922f57ab4bb783f3a88dfa609488ac404b4c00000000001976a9142b6ba7c9d796b75eef7942fc9288edd37c32f5c388ac00000000"), + outputIndex = 1, + blockHeight = 100_003, + addressMeta = WalletState.AddressMeta.Derived(3) + ) + ) + assertContains( + walletState.addresses.toList(), + "12yyz3bCKjUoQQPaYbrmzYVM4h7bYa9QYj" to WalletState.AddressState( + meta = WalletState.AddressMeta.Derived(5), + alreadyUsed = true, + utxos = emptyList() + ) + ) + assertContains( + walletState.addresses.toList(), + "1NtgeYfAGMwt1vqdJrNTRuPu4Hnqpd4sKX" to WalletState.AddressState( + meta = WalletState.AddressMeta.Derived(6), + alreadyUsed = false, + utxos = emptyList() ) ) @@ -147,16 +248,120 @@ class ElectrumMiniWalletTest : LightningTestSuite() { client.stop() } + @Ignore + fun `perf test generator`() = runSuspendTest(timeout = 45.seconds) { + val client = connectToMainnetServer() + val wallet = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, logger) + val addresses = listOf( + "114uCKBNBxp4g1fZrcka2M69oV4BVePkad", + "12CY87ECoGY8LJ6VvSUUYjqpHyYPHJnqj1", + "12VzzUpnDwWpWxChB6yR4rLLXqLQ5m7sU5", + "1322UMyRef9rZ87vkciVsgnuBTwBmCPFRe", + "137hvJS73Sztn9DUz8RchURHpkWXSmik8G", + "13BVsQidbmZhaMRgXe8V7sAV5KBwats8rC", + "13Z3AAqKaTQDFxWC4v9Fu3Qvpvh96YSAVM", + "13dCDhE9tfyVjDNHtM7Fs9jb5JCfrPVjn3", + "14348op8fK4W6W4nX1J9yZUGokJrRYECik", + "145UHBQAmGRpdsXVKhdHtFndnJ8XN8fLjq", + "14SanaYZzhyD5FHXxnXauayxfQBdcXLd5U", + "15NixkHzN4qW5h2kkmUwpUTf5jCEJNCw9o", + "15V8o9ejz5voGj3WK5Y1mDm3FZCDuquss2", + "162Tz9M9GN6F6v8i5eSdULffP36QZh8ZUN", + "16cXz1ecwDk65cawVEU6VmJ4ow64jRaL9n", + "16gBiciGuMRbqvbPvv2cA6GmE22yo6LuRj", + "16nFkG1Y38ufZxZdA9F5gRppAQbe6ymMux", + "172r9DykshKtD5AsXptYuB4f8j8xr1whJx", + "17Eo7qeohz9igYsg6haip7HHfd9irQaGLp", + "17YiV3fymmrsHTY5ZgWGvuHiBWgTDxweo1", + "17eTgKJkvkGGQpuXynCtPFw4aq1vZZhbiC", + "17rVcPa5eSBKMogUDtSATsvD31XN7TK727", + "182dAVj6n99LiVjL65GxBHDaoqL4D4eupx", + "187vwp8PtBiuMwcTeJcZKyM8LubBjU3ytu", + "18EarThzGxJPmjik8dcnfCiczdQ5VFpLAv", + "18HzHv4kPxJ1nSG1b8d67dB9TRy9DAKBVu", + "18TG9ubxK3MU6DrCFgKiv3XTAE6SH3aDXA", + "18jnQ3E4P3yA7mBMizmnN1h7TStojwufQ1", + "18qBZXpyzvf1RSUPAd6AYHZvKR83PUEwzx", + "18vEXh45QgF4KUZhbTAu9baq188ETMfNBj", + "19B2FMsUevkGq8W4ZapS6LPvcotDNhrkaA", + "19QspZMDQg6r5n1JM5VP7DaTwq3HD3ZSq4", + "1A5YxdfPY4JJWqwBcCeZY2FhiQ2afTbQBa", + "1AYx4g8BSeVkCmLSGD1VeVeAseCzLkkoHj", + "1Ah3eUSz3a7hWQQHbTKC4GYicVZdwwaj3y", + "1C2WyWKSEGUCzw9HrJecJtxBiaEAaaH45b", + "1C6vvX6mEbSUWCaEdKFE3Z5HWiyWzveEz4", + "1CFre5K8Htx4Emao3D3mia813vP32EtCGB", + "1CMvZqzZEfQVSwWhuYT1LL4iLze2bFFBA", + "1CUjJSYzfeLsRRTC1mogZ9vmqwqQGJarxr", + "1CbPns3oQZ7fzEVHmj32nM2CA1tJNqzNxf", + "1CiYN9JPAoHt7oumuEPqpRDWeDDTFdqsrW", + "1CjHxcwoLSGQCJZk4U9XWuvfpYF7F4ppDG", + "1D49cHVy6m6iA5Ba7KT3sAjKGKEPxNJ6Ab", + "1D7qwTKonm3ocoUkb9wcCxX3nwrhfLZSG2", + "1DfS5wjxWo46U8J9okABrVAXs2UyuCCSZW", + "1ECENFEGVQfNkchkGD66TDWrhmCfw4VmMQ", + "1EjqA74tRDpu5YUEwk2yann6oQvcqeqUrt", + "1F39Up1U9Wz9a9YpQRQY7Vo4SXVkY17hoq", + "1FHQkdk3A4JXBK1GTGGHK8h1n6brFHfY3E", + "1FPXH5QCZHBbjN7C2P8Mi2pG3j96GPuYQE", + "1FrXcD6SczqhUXFcihQaR6gGFm9PFWSrTu", + "1Fwy2Kz7hHA5euKEUCmafjAomy8bVYXap7", + "1G9tzuNYXys8wJeJQzUfYQh13iHaWUmPVa", + "1GDxkebMi5hTcpF619r3VW5KRzLa9N2qPM", + "1GYgimY73Zqt3RzByhqbWGwJo5WJ3NpLRs", + "1GtUbGFBNav9GQoFE8RakHmApRW4y2a6v6", + "1HKwVwbnQNphz8JsEp9kJoZ9LZxQFETzQQ", + "1HLBi58BQvBvLvmkFCiJdxdUUapGiFMWEL", + "1HMyzjG9Xk5A98oDgDcDuZcDypRPvgzFhk", + "1HSS3RVVDLzACWU4VYXKXWcLhaytU7rZmH", + "1HbS9EWzEMQUP5aEtEHbYaA75QT8e2syJo", + "1HjfdwDEngtPgoGyTChwqDAb94NFUNAFU1", + "1HomrXLp1PSjJ4ztKT41whsLCGunPytV5o", + "1Hrxj68qi3ma19FrjZxAtF569skNu157AJ", + "1JPiiKYLeFsoqQs57Fsk1AB2WaaapL4Deu", + "1JyJkw5bSXfFxLjVr1znKhbohSQu9L5Zej", + "1K2N6ra6FR2mQsCpXTC8aC7Ltte2FvGRav", + "1K6JwqW3D4uF54X3XKVMK83TcA6USj4EAa", + "1KBGvNUkhZeVFkMXm63cTrMcqwXMomBhSF", + "1KGpPb3UtfVAsCCWVHH7xDNkJ9o3dkcuLG", + "1KaEafBf6rnNhEVfA9MMz97HyBfEb1n92", + "1KfzGnxQ2DVAtuDvaeQfEAUsGaJeEdjGdx", + "1L6azLTYi4jBmFMVDGMdD6wVC6c2A5VzY6", + "1LTrAV1sF75F7FT4TyLszwqpBoGPoaQVkj", + "1LcgGunavVU4ssbDfpyy65oqrn1sb4mgfW", + "1LjE3iQ3G1DzdpJKvbfDUAzHx1DR2ZF3YX", + "1LqTafjDWgEmdi4fNX1oK9NPmR3dCZ2WrF", + "1MFzTqdiSrRsxP3wWFvVuBMvGHsuy1tW82", + "1MGwaMGvCi7VZ5RBqBPfALFv1JXa1PayAr", + "1MvpAZgGnWStTXDzgseZJgCjXZZ4k9Ss5W", + "1NGCVyaJHLrKhPK5pNHvaoVqbsrVUiobcp", + "1NpCHRwmc7CcKVP1o6kQiRRBf7o8Gtnq8k", + "1PGucoekd9R5ALVKkPCHzBgww55Lk7K7B2", + "1PH2tuYqJE5AYRU2YyXJVkKKSsi5u347H7", + "1QFkC7PTtpNp5k6pdkgjpKBwGQP96akFTE", + "1X67cVbXYDwETmymUUneGGmstV248vwwe", + "1eBc1GnsXK7sxPqGpDq4HJyjacgG2R37t", + "1russ5GJDodxVJY59Au7MDYwA8mRV5EC9", + "1tFEamDdQV93gmLLQx4s2yFwedLqcNaPa", + "1tN3AJhBjC4TmE2vw1wmFaD8dfG9Cn2eF", + "1NtgeYfAGMwt1vqdJrNTRuPu4Hnqpd4sKX" + ) + wallet.addAddressGenerator { + if (it < addresses.size) addresses[it] else error("should not go that far") + } + wallet.walletStateFlow.drop(1).first() // first emitted wallet is empty + } + @Test fun `parallel wallets`() = runSuspendTest(timeout = 15.seconds) { val client = connectToMainnetServer() - val wallet1 = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, loggerFactory, name = "addr-16MmJT") - val wallet2 = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, loggerFactory, name = "addr-14xb2H") + val wallet1 = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, logger) + val wallet2 = ElectrumMiniWallet(Block.LivenetGenesisBlock.hash, client, this, logger) wallet1.addAddress("16MmJT8VqW465GEyckWae547jKVfMB14P8") wallet2.addAddress("14xb2HATmkBzrHf4CR2hZczEtjYpTh92d2") - val walletState1 = wallet1.walletStateFlow.first { it.utxos.size == 4 } - val walletState2 = wallet2.walletStateFlow.first { it.utxos.size == 6 } + val walletState1 = wallet1.walletStateFlow.drop(1).first() // first emitted wallet is empty + val walletState2 = wallet2.walletStateFlow.drop(1).first() // first emitted wallet is empty assertEquals(7_200_0000.sat, walletState1.totalBalance) assertEquals(3_000_0000.sat, walletState2.totalBalance) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt index 7ac28bcd1..0741a3c1f 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt @@ -34,12 +34,13 @@ class SwapInManagerTestsCommon : LightningTestSuite() { Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 2), 0)), listOf(TxOut(50_000.sat, dummyScript), TxOut(75_000.sat, dummyScript)), 0), Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(25_000.sat, dummyScript)), 0) ) - val unspent = listOf( - WalletState.Utxo(parentTxs[0].txid, 0, 100, parentTxs[0]), // deeply confirmed - WalletState.Utxo(parentTxs[0].txid, 1, 100, parentTxs[0]), // deeply confirmed - WalletState.Utxo(parentTxs[1].txid, 0, 149, parentTxs[1]), // recently confirmed + val utxos = listOf( + WalletState.Utxo(parentTxs[0].txid, 0, 100, parentTxs[0], WalletState.AddressMeta.Single), // deeply confirmed + WalletState.Utxo(parentTxs[0].txid, 1, 100, parentTxs[0], WalletState.AddressMeta.Single), // deeply confirmed + WalletState.Utxo(parentTxs[1].txid, 0, 149, parentTxs[1], WalletState.AddressMeta.Single), // recently confirmed ) - WalletState(mapOf(dummyAddress to unspent)) + val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) + WalletState(mapOf(dummyAddress to addressState)) } val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 3, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) mgr.process(cmd).also { result -> @@ -57,10 +58,11 @@ class SwapInManagerTestsCommon : LightningTestSuite() { Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(25_000.sat, dummyScript)), 0) ) val utxos = listOf( - WalletState.Utxo(parentTxs[0].txid, 0, 100, parentTxs[0]), // recently confirmed - WalletState.Utxo(parentTxs[1].txid, 0, 0, parentTxs[1]), // unconfirmed + WalletState.Utxo(parentTxs[0].txid, 0, 100, parentTxs[0], WalletState.AddressMeta.Single), // recently confirmed + WalletState.Utxo(parentTxs[1].txid, 0, 0, parentTxs[1], WalletState.AddressMeta.Single), // unconfirmed ) - WalletState(mapOf(dummyAddress to utxos)) + val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) + WalletState(mapOf(dummyAddress to addressState)) } val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 101, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 3, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) mgr.process(cmd).also { assertNull(it) } @@ -75,10 +77,11 @@ class SwapInManagerTestsCommon : LightningTestSuite() { Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(25_000.sat, dummyScript)), 0) ) val utxos = listOf( - WalletState.Utxo(parentTxs[0].txid, 0, 100, parentTxs[0]), // exceeds refund delay - WalletState.Utxo(parentTxs[1].txid, 0, 120, parentTxs[1]), // exceeds max confirmation before refund + WalletState.Utxo(parentTxs[0].txid, 0, 100, parentTxs[0], WalletState.AddressMeta.Single), // exceeds refund delay + WalletState.Utxo(parentTxs[1].txid, 0, 120, parentTxs[1], WalletState.AddressMeta.Single), // exceeds max confirmation before refund ) - WalletState(mapOf(dummyAddress to utxos)) + val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) + WalletState(mapOf(dummyAddress to addressState)) } val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 130, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 3, maxConfirmations = 10, refundDelay = 15), trustedTxs = emptySet()) mgr.process(cmd).also { assertNull(it) } @@ -94,11 +97,12 @@ class SwapInManagerTestsCommon : LightningTestSuite() { ) val wallet = run { val utxos = listOf( - WalletState.Utxo(parentTxs[0].txid, 0, 100, parentTxs[0]), // deeply confirmed - WalletState.Utxo(parentTxs[1].txid, 0, 150, parentTxs[1]), // recently confirmed - WalletState.Utxo(parentTxs[2].txid, 0, 0, parentTxs[2]), // unconfirmed + WalletState.Utxo(parentTxs[0].txid, 0, 100, parentTxs[0], WalletState.AddressMeta.Single), // deeply confirmed + WalletState.Utxo(parentTxs[1].txid, 0, 150, parentTxs[1], WalletState.AddressMeta.Single), // recently confirmed + WalletState.Utxo(parentTxs[2].txid, 0, 0, parentTxs[2], WalletState.AddressMeta.Single), // unconfirmed ) - WalletState(mapOf(dummyAddress to utxos)) + val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) + WalletState(mapOf(dummyAddress to addressState)) } val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = parentTxs.map { it.txid }.toSet()) mgr.process(cmd).also { result -> @@ -112,8 +116,9 @@ class SwapInManagerTestsCommon : LightningTestSuite() { val mgr = SwapInManager(listOf(), logger) val wallet = run { val parentTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 1), 0)), listOf(TxOut(75_000.sat, dummyScript)), 0) - val utxos = WalletState.Utxo(parentTx.txid, 0, 100, parentTx) - WalletState(mapOf(dummyAddress to listOf(utxos))) + val utxos = listOf(WalletState.Utxo(parentTx.txid, 0, 100, parentTx, WalletState.AddressMeta.Single)) + val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) + WalletState(mapOf(dummyAddress to addressState)) } val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) mgr.process(cmd).also { assertNotNull(it) } @@ -133,8 +138,9 @@ class SwapInManagerTestsCommon : LightningTestSuite() { fun `swap funds -- ignore inputs from pending channel`() { val (waitForFundingSigned, _) = WaitForFundingSignedTestsCommon.init() val wallet = run { - val utxos = waitForFundingSigned.state.signingSession.fundingTx.tx.localInputs.map { i -> WalletState.Utxo(i.outPoint.txid, i.outPoint.index.toInt(), 100, i.previousTx) } - WalletState(mapOf(dummyAddress to utxos)) + val utxos = waitForFundingSigned.state.signingSession.fundingTx.tx.localInputs.map { i -> WalletState.Utxo(i.outPoint.txid, i.outPoint.index.toInt(), 100, i.previousTx, WalletState.AddressMeta.Single) } + val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) + WalletState(mapOf(dummyAddress to addressState)) } val mgr = SwapInManager(listOf(waitForFundingSigned.state), logger) val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) @@ -153,8 +159,9 @@ class SwapInManagerTestsCommon : LightningTestSuite() { val inputs = alice1.commitments.active.map { it.localFundingStatus }.filterIsInstance().flatMap { it.sharedTx.tx.localInputs } assertEquals(3, inputs.size) // 1 initial funding input and 2 splice inputs val wallet = run { - val utxos = inputs.map { i -> WalletState.Utxo(i.outPoint.txid, i.outPoint.index.toInt(), 100, i.previousTx) } - WalletState(mapOf(dummyAddress to utxos)) + val utxos = inputs.map { i -> WalletState.Utxo(i.outPoint.txid, i.outPoint.index.toInt(), 100, i.previousTx, WalletState.AddressMeta.Single) } + val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) + WalletState(mapOf(dummyAddress to addressState)) } val mgr = SwapInManager(listOf(alice1.state), logger) val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) @@ -183,8 +190,9 @@ class SwapInManagerTestsCommon : LightningTestSuite() { assertEquals(1, alice3.commitments.all.size) assertIs(alice3.commitments.latest.localFundingStatus) val wallet = run { - val utxos = inputs.map { i -> WalletState.Utxo(i.outPoint.txid, i.outPoint.index.toInt(), 100, i.previousTx) } - WalletState(mapOf(dummyAddress to utxos)) + val utxos = inputs.map { i -> WalletState.Utxo(i.outPoint.txid, i.outPoint.index.toInt(), 100, i.previousTx, WalletState.AddressMeta.Single) } + val addressState = WalletState.AddressState(WalletState.AddressMeta.Single, alreadyUsed = true, utxos) + WalletState(mapOf(dummyAddress to addressState)) } val mgr = SwapInManager(listOf(alice3.state), logger) val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWalletTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWalletTestsCommon.kt new file mode 100644 index 000000000..0b9268d1b --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWalletTestsCommon.kt @@ -0,0 +1,28 @@ +package fr.acinq.lightning.blockchain.electrum + +import fr.acinq.bitcoin.Bitcoin +import fr.acinq.bitcoin.MnemonicCode +import fr.acinq.lightning.crypto.LocalKeyManager +import fr.acinq.lightning.tests.TestConstants +import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.tests.utils.runSuspendTest +import fr.acinq.lightning.utils.toByteVector +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds + +class SwapInWalletTestsCommon : LightningTestSuite() { + + @Test + fun `swap-in wallet test`() = runSuspendTest(timeout = 15.seconds) { + val mnemonics = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".split(" ") + val keyManager = LocalKeyManager(MnemonicCode.toSeed(mnemonics, "").toByteVector(), Bitcoin.Chain.Testnet, TestConstants.aliceSwapInServerXpub) + val client = connectToTestnetServer() + val wallet = SwapInWallet(Bitcoin.Chain.Testnet, keyManager.swapInOnChainWallet, client, this, loggerFactory) + + // addresses 0 to 3 have funds on them, the current address is the 4th + assertEquals(4, wallet.swapInAddressFlow.filterNotNull().first().second) + } +} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index afda04b1c..955293a92 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -704,7 +704,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val fundingParams = InteractiveTxParams(randomBytes32(), true, 150_000.sat, 50_000.sat, pubKey, 0, 660.sat, FeeratePerKw(2500.sat)) run { val previousTx = Transaction(2, listOf(), listOf(TxOut(293.sat, Script.pay2wpkh(pubKey))), 0) - val result = FundingContributions.create(channelKeys, swapInKeys, fundingParams, listOf(WalletState.Utxo(previousTx.txid, 0, 0, previousTx))).left + val result = FundingContributions.create(channelKeys, swapInKeys, fundingParams, listOf(WalletState.Utxo(previousTx.txid, 0, 0, previousTx, WalletState.AddressMeta.Single))).left assertNotNull(result) assertIs(result) } @@ -712,19 +712,19 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val txIn = (1..1000).map { TxIn(OutPoint(TxId(randomBytes32()), 3), ByteVector.empty, 0, Script.witnessPay2wpkh(pubKey, Transactions.PlaceHolderSig)) } val txOut = (1..1000).map { i -> TxOut(1000.sat * i, Script.pay2wpkh(pubKey)) } val previousTx = Transaction(2, txIn, txOut, 0) - val result = FundingContributions.create(channelKeys, swapInKeys, fundingParams, listOf(WalletState.Utxo(previousTx.txid, 53, 0, previousTx))).left + val result = FundingContributions.create(channelKeys, swapInKeys, fundingParams, listOf(WalletState.Utxo(previousTx.txid, 53, 0, previousTx, WalletState.AddressMeta.Single))).left assertNotNull(result) assertIs(result) } run { val previousTx = Transaction(2, listOf(), listOf(TxOut(80_000.sat, Script.pay2wpkh(pubKey)), TxOut(60_000.sat, Script.pay2wpkh(pubKey))), 0) - val result = FundingContributions.create(channelKeys, swapInKeys, fundingParams, listOf(WalletState.Utxo(previousTx.txid, 0, 0, previousTx), WalletState.Utxo(previousTx.txid, 1, 0, previousTx))).left + val result = FundingContributions.create(channelKeys, swapInKeys, fundingParams, listOf(WalletState.Utxo(previousTx.txid, 0, 0, previousTx, WalletState.AddressMeta.Single), WalletState.Utxo(previousTx.txid, 1, 0, previousTx, WalletState.AddressMeta.Single))).left assertNotNull(result) assertIs(result) } run { val previousTx = Transaction(2, listOf(), listOf(TxOut(80_000.sat, Script.pay2wpkh(pubKey)), TxOut(70_001.sat, Script.pay2wpkh(pubKey))), 0) - val result = FundingContributions.create(channelKeys, swapInKeys, fundingParams, listOf(WalletState.Utxo(previousTx.txid, 0, 0, previousTx), WalletState.Utxo(previousTx.txid, 1, 0, previousTx))).left + val result = FundingContributions.create(channelKeys, swapInKeys, fundingParams, listOf(WalletState.Utxo(previousTx.txid, 0, 0, previousTx, WalletState.AddressMeta.Single), WalletState.Utxo(previousTx.txid, 1, 0, previousTx, WalletState.AddressMeta.Single))).left assertNotNull(result) assertIs(result) } @@ -1308,16 +1308,16 @@ class InteractiveTxTestsCommon : LightningTestSuite() { } private fun createWallet(onChainKeys: KeyManager.SwapInOnChainKeys, amounts: List, legacyAmounts: List = listOf()): List { - return amounts.map { amount -> + return amounts.withIndex().map { amount -> val txIn = listOf(TxIn(OutPoint(TxId(randomBytes32()), 2), 0)) - val txOut = listOf(TxOut(amount, onChainKeys.swapInProtocol.pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) + val txOut = listOf(TxOut(amount.value, onChainKeys.getSwapInProtocol(amount.index).pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) val parentTx = Transaction(2, txIn, txOut, 0) - WalletState.Utxo(parentTx.txid, 0, 0, parentTx) + WalletState.Utxo(parentTx.txid, 0, 0, parentTx, WalletState.AddressMeta.Derived(amount.index)) } + legacyAmounts.map { amount -> val txIn = listOf(TxIn(OutPoint(TxId(randomBytes32()), 2), 0)) val txOut = listOf(TxOut(amount, onChainKeys.legacySwapInProtocol.pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) val parentTx = Transaction(2, txIn, txOut, 0) - WalletState.Utxo(parentTx.txid, 0, 0, parentTx) + WalletState.Utxo(parentTx.txid, 0, 0, parentTx, WalletState.AddressMeta.Single) } } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index 1a616cb1e..b2fc90955 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -410,9 +410,10 @@ object TestsHelper { } fun createWallet(keyManager: KeyManager, amount: Satoshi): Pair> { - val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, swapInProtocol.pubkeyScript) } + val swapInAddressIndex = 0 + val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, getSwapInProtocol(swapInAddressIndex).pubkeyScript) } val parentTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 3), 0)), listOf(TxOut(amount, script)), 0) - return privateKey to listOf(WalletState.Utxo(parentTx.txid, 0, 42, parentTx)) + return privateKey to listOf(WalletState.Utxo(parentTx.txid, 0, 42, parentTx, WalletState.AddressMeta.Derived(swapInAddressIndex))) } fun addHtlc(amount: MilliSatoshi, payer: LNChannel, payee: LNChannel): Triple, LNChannel>, ByteVector32, UpdateAddHtlc> { diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt index 317ff8650..430b971de 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/QuiescenceTestsCommon.kt @@ -504,12 +504,12 @@ class QuiescenceTestsCommon : LightningTestSuite() { companion object { private fun createWalletWithFunds(keyManager: KeyManager, utxos: List): List { - val script = keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript - return utxos.map { amount -> + return utxos.mapIndexed { index, amount -> + val script = keyManager.swapInOnChainWallet.getSwapInProtocol(index).pubkeyScript val txIn = listOf(TxIn(OutPoint(TxId(Lightning.randomBytes32()), 2), 0)) val txOut = listOf(TxOut(amount, script), TxOut(150.sat, Script.pay2wpkh(Lightning.randomKey().publicKey()))) val parentTx = Transaction(2, txIn, txOut, 0) - WalletState.Utxo(parentTx.txid, 0, 42, parentTx) + WalletState.Utxo(parentTx.txid, 0, 42, parentTx, WalletState.AddressMeta.Derived(index)) } } 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 479bf035b..f8d7eb039 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -1512,7 +1512,7 @@ class SpliceTestsCommon : LightningTestSuite() { val txIn = listOf(TxIn(OutPoint(TxId(Lightning.randomBytes32()), 2), 0)) val txOut = listOf(TxOut(amount, script), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) val parentTx = Transaction(2, txIn, txOut, 0) - WalletState.Utxo(parentTx.txid, 0, 42, parentTx) + WalletState.Utxo(parentTx.txid, 0, 42, parentTx, WalletState.AddressMeta.Single) } } 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 02fb630d8..ea7881698 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt @@ -445,7 +445,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { // Alice adds a new input that increases her contribution and covers the additional fees. val script = alice.staticParams.nodeParams.keyManager.swapInOnChainWallet.legacySwapInProtocol.pubkeyScript val parentTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 1), 0)), listOf(TxOut(30_000.sat, script)), 0) - val wallet1 = wallet + listOf(WalletState.Utxo(parentTx.txid, 0, 42, parentTx)) + val wallet1 = wallet + listOf(WalletState.Utxo(parentTx.txid, 0, 42, parentTx, WalletState.AddressMeta.Single)) 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 968fc2a07..d236e5b83 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -200,8 +200,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.getSwapInProtocol(0).pubkeyScript), + TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.getSwapInProtocol(1).pubkeyScript), TxOut(Satoshi(150000), Script.pay2wpkh(randomKey().publicKey())) ), lockTime = 0