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