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 4adb771c6..174fbf8b8 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt @@ -173,17 +173,9 @@ class ElectrumMiniWallet( } fun computeScriptHash(bitcoinAddress: String): ByteVector32? { - return when (val result = Bitcoin.addressToPublicKeyScript(chainHash, bitcoinAddress)) { - is AddressToPublicKeyScriptResult.Failure -> { - logger.error { "cannot subscribe to $bitcoinAddress ($result)" } - null - } - - is AddressToPublicKeyScriptResult.Success -> { - val pubkeyScript = ByteVector(Script.write(result.script)) - return ElectrumClient.computeScriptHash(pubkeyScript) - } - } + return Bitcoin.addressToPublicKeyScript(chainHash, bitcoinAddress) + .map { ElectrumClient.computeScriptHash(Script.write(it).byteVector()) } + .right } job = launch { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index b380513f2..23f78d566 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -441,10 +441,9 @@ data class SharedTransaction( ?.let { input -> val userNonce = session.secretNonces[input.serialId]!! val serverNonce = receivedNonces[input.serialId]!! - IndividualNonce.aggregate(listOf(userNonce.second, serverNonce)) - .flatMap { commonNonce -> keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, previousOutputs, userNonce.first, commonNonce) - .map { psig -> TxSignatures.Companion.PartialSignature(psig, commonNonce) } - }.getOrDefault(null) + keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, previousOutputs, userNonce, serverNonce) + .map { psig -> TxSignaturesTlv.PartialSignature(psig, userNonce.second, serverNonce) } + .getOrDefault(null) } }.filterNotNull() @@ -469,10 +468,9 @@ data class SharedTransaction( val userNonce = session.secretNonces[input.serialId]!! val serverNonce = receivedNonces[input.serialId]!! val swapInProtocol = SwapInProtocol(input.swapInParams.userKey, serverKey.publicKey(), input.swapInParams.userRefundKey, input.swapInParams.refundDelay) - IndividualNonce.aggregate(listOf(userNonce.second, serverNonce)) - .flatMap { commonNonce -> swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, commonNonce, serverKey, userNonce.first) - .map { psig -> TxSignatures.Companion.PartialSignature(psig, commonNonce) } - }.getOrDefault(null) + swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, serverNonce, serverKey, userNonce) + .map { psig -> TxSignaturesTlv.PartialSignature(psig, userNonce.second, serverNonce) } + .getOrDefault(null) } }.filterNotNull() @@ -532,10 +530,8 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over val localSwapTxInMusig2 = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocol(i.swapInParams) - val commonNonce = userSig.aggregatedPublicNonce val unsignedTx = tx.buildUnsignedTx() - val witness = swapInProtocol.session(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) - .flatMap { s -> s.add(listOf(userSig.sig, serverSig.sig)).map { commonSig -> swapInProtocol.witness(commonSig) } } + val witness = swapInProtocol.witness(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, userSig, serverSig) require(witness.isRight) { "cannot compute aggregated signature" } Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness.right!!)) } @@ -550,10 +546,8 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over val remoteSwapTxInMusig2 = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocol(i.swapInParams) - val commonNonce = userSig.aggregatedPublicNonce val unsignedTx = tx.buildUnsignedTx() - val witness = swapInProtocol.session(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) - .flatMap { s -> s.add(listOf(userSig.sig, serverSig.sig)).map { commonSig -> swapInProtocol.witness(commonSig) } } + val witness = swapInProtocol.witness(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, userSig, serverSig) require(witness.isRight) { "cannot compute aggregated signature" } Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness.right!!)) } @@ -684,8 +678,7 @@ data class InteractiveTxSession( is InteractiveTxInput.LocalSwapIn -> { // generate a secret nonce for this input if we don't already have one val secretNonce = next.secretNonces[msg.value.serialId] ?: run { - val s = SecretNonce.generate(randomBytes32(), swapInKeys.userPrivateKey, swapInKeys.userPublicKey, null, null, null) - s.getOrElse { error("cannot generate secret nonce") } + SecretNonce.generate(randomBytes32(), swapInKeys.userPrivateKey, swapInKeys.userPublicKey, null, null, null) } next.copy(secretNonces = next.secretNonces + (msg.value.serialId to secretNonce)) } @@ -755,7 +748,6 @@ data class InteractiveTxSession( val session2 = when (input) { is InteractiveTxInput.RemoteSwapIn -> { val secretNonce = secretNonces[input.serialId] ?: SecretNonce.generate(randomBytes32(), null, input.swapInParams.serverKey, null, null, null) - .getOrElse { error("cannot generate secret nonce") } session1.copy(secretNonces = secretNonces + (input.serialId to secretNonce)) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt index 7b31498ba..ebfe7f934 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt @@ -112,7 +112,7 @@ sealed class ChannelState { // this code is only executed for the first transition to Closing, so there can only be one transaction here val closingTx = newState.mutualClosePublished.first() val finalAmount = closingTx.toLocalOutput?.amount ?: 0.sat - val address = closingTx.toLocalOutput?.publicKeyScript?.let { Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, it.toByteArray()).result } ?: "unknown" + val address = closingTx.toLocalOutput?.publicKeyScript?.let { Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, it.toByteArray()).right } ?: "unknown" listOf( ChannelAction.Storage.StoreOutgoingPayment.ViaClose( amount = finalAmount, @@ -141,7 +141,7 @@ sealed class ChannelState { val address = Bitcoin.addressFromPublicKeyScript( chainHash = staticParams.nodeParams.chainHash, pubkeyScript = oldState.commitments.params.localParams.defaultFinalScriptPubKey.toByteArray() // force close always send to the default script - ).result ?: "unknown" + ).right ?: "unknown" listOf( ChannelAction.Storage.StoreOutgoingPayment.ViaClose( amount = channelBalance.truncateToSatoshi(), diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 17d62121a..9e150ca16 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -762,7 +762,7 @@ data class Normal( ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceOut( amount = txOut.amount, miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(), - address = Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, txOut.publicKeyScript.toByteArray()).result ?: "unknown", + address = Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, txOut.publicKeyScript.toByteArray()).right ?: "unknown", txId = action.fundingTx.txId ) }) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 1e34459ea..e8a6bcb18 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.DeterministicWallet.hardened import fr.acinq.bitcoin.crypto.musig2.AggregatedNonce +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.crypto.musig2.SecretNonce import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.utils.Either @@ -159,8 +160,8 @@ interface KeyManager { return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()] , userPrivateKey) } - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: SecretNonce, commonNonce: AggregatedNonce): Either { - return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, userNonce, commonNonce) + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: Pair, serverNonce: IndividualNonce): Either { + return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, userNonce, serverNonce) } /** @@ -175,7 +176,7 @@ interface KeyManager { return if (utxos.isEmpty()) { null } else { - val pubKeyScript = Bitcoin.addressToPublicKeyScript(chain.chainHash, address).result + val pubKeyScript = Bitcoin.addressToPublicKeyScript(chain.chainHash, address).right pubKeyScript?.let { script -> val ourOutput = TxOut(utxos.map { it.amount }.sum(), script) val unsignedTx = Transaction( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index 5dcf8a61d..87e98b9a1 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -1,14 +1,12 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* -import fr.acinq.bitcoin.crypto.musig2.AggregatedNonce -import fr.acinq.bitcoin.crypto.musig2.KeyAggCache -import fr.acinq.bitcoin.crypto.musig2.SecretNonce -import fr.acinq.bitcoin.crypto.musig2.Session +import fr.acinq.bitcoin.crypto.musig2.* import fr.acinq.bitcoin.utils.Either -import fr.acinq.bitcoin.utils.flatMap import fr.acinq.lightning.NodeParams +import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.wire.TxAddInputTlv +import fr.acinq.lightning.wire.TxSignaturesTlv /** * new swap-in protocol based on musig2 and taproot: (user key + server key) OR (user refund key + delay) @@ -20,62 +18,44 @@ import fr.acinq.lightning.wire.TxAddInputTlv class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val userRefundKey: PublicKey, val refundDelay: Int) { constructor(swapInParams: TxAddInputTlv.SwapInParams) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.userRefundKey, swapInParams.refundDelay) - // the redeem script is just the refund script. it is generated from this policy: and_v(v:pk(user),older(refundDelay)) + // the refund script is generated from this policy: and_v(v:pk(user),older(refundDelay)) // it does not depend upon the user's or server's key, just the user's refund key and the refund delay - val redeemScript = listOf(OP_PUSHDATA(userRefundKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) - private val scriptTree = ScriptTree.Leaf(0, redeemScript) - private val merkleRoot = scriptTree.hash() + private val refundScript = listOf(OP_PUSHDATA(userRefundKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) + private val scriptTree = ScriptTree.Leaf(0, refundScript) // the internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key - private val internalPubKeyAndCache = run { - val c = KeyAggCache.add(listOf(userPublicKey, serverPublicKey), null) - if (c.isLeft) error("key aggregation failed") else c.right!! - } - private val internalPubKey = internalPubKeyAndCache.first - private val cache = internalPubKeyAndCache.second - - - // it is tweaked with the script's merkle root to get the pubkey that will be exposed - private val commonPubKeyAndParity = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot)) - val commonPubKey = commonPubKeyAndParity.first - private val parity = commonPubKeyAndParity.second - val pubkeyScript: List = Script.pay2tr(commonPubKey) + private val internalPubKey = KeyAggCache.create(listOf(userPublicKey, serverPublicKey)).first - private val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + internalPubKey.value.toByteArray() + // it is tweaked with the script's merkle root to get the pubkey that will be exposed in a regular p2tr script + val pubkeyScript: List = Script.pay2tr(internalPubKey, scriptTree) - fun isMine(txOut: TxOut): Boolean = txOut.publicKeyScript.contentEquals(Script.write(pubkeyScript)) - - fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! + fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).right!! - fun witness(commonSig: ByteVector64): ScriptWitness = ScriptWitness(listOf(commonSig)) + fun witness(fundingTx: Transaction, index: Int, parentTxOuts: List, userPartialSig: TxSignaturesTlv.PartialSignature, serverPartialSig: TxSignaturesTlv.PartialSignature): Either { + val publicKeys = listOf(userPublicKey, serverPublicKey) + val publicNonces = listOf(userPartialSig.localNonce, userPartialSig.remoteNonce) + val sigs = listOf(userPartialSig.sig, serverPartialSig.sig) + return Musig2.aggregateTaprootSignatures(sigs, fundingTx, index, parentTxOuts, publicKeys, publicNonces, scriptTree).map { aggregateSig -> + Script.witnessKeyPathPay2tr(aggregateSig) + } + } - fun witnessRefund(userSig: ByteVector64): ScriptWitness = ScriptWitness.empty.push(userSig).push(redeemScript).push(controlBlock) + fun witnessRefund(userSig: ByteVector64): ScriptWitness = Script.witnessScriptPathPay2tr(internalPubKey, scriptTree, ScriptWitness(listOf(userSig)), scriptTree) - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey, userNonce: SecretNonce, commonNonce: AggregatedNonce): Either { + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey, userNonce: Pair, serverNonce: IndividualNonce): Either { require(userPrivateKey.publicKey() == userPublicKey) - val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - - return cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true) - .flatMap { (c, _) -> Session.build(commonNonce, txHash, c).map { s -> Pair(s, c) } } - .flatMap { (s, c) -> s.sign(userNonce, userPrivateKey, c) } + return Musig2.signTaprootInput(userPrivateKey, fundingTx, index, parentTxOuts, listOf(userPublicKey, serverPublicKey), userNonce.first, listOf(userNonce.second, serverNonce), scriptTree) } fun signSwapInputRefund(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey): ByteVector64 { + require(userPrivateKey.publicKey() == userRefundKey) + val merkleRoot = scriptTree.hash() val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, merkleRoot) return Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak) } - fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List, commonNonce: AggregatedNonce, serverPrivateKey: PrivateKey, serverNonce: SecretNonce): Either { - val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - return cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true) - .flatMap { (c, _) -> Session.build(commonNonce, txHash, c).map { s -> Pair(s, c) } } - .flatMap { (s, c) -> s.sign(serverNonce, serverPrivateKey, c) } - } - - fun session(fundingTx: Transaction, index: Int, parentTxOuts: List, commonNonce: AggregatedNonce): Either { - val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - return cache.tweak(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true) - .flatMap { (c, _) -> Session.build(commonNonce, txHash, c) } + fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: IndividualNonce, serverPrivateKey: PrivateKey, serverNonce: Pair): Either { + return Musig2.signTaprootInput(serverPrivateKey, fundingTx, index, parentTxOuts, listOf(userPublicKey, serverPublicKey), serverNonce.first, listOf(userNonce, serverNonce.second), scriptTree) } companion object { @@ -88,18 +68,17 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe * @param masterRefundKey master private key for the refund keys. we assume that there is a single level of derivation to compute the refund keys * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to recover user funds once the funding delay has passed */ - fun descriptor(chain: NodeParams.Chain, userPublicKey: PublicKey, serverPublicKey: PublicKey, refundDelay: Int, masterRefundKey: DeterministicWallet.ExtendedPrivateKey): Either { + fun descriptor(chain: NodeParams.Chain, userPublicKey: PublicKey, serverPublicKey: PublicKey, refundDelay: Int, masterRefundKey: DeterministicWallet.ExtendedPrivateKey): String { // the internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key - return KeyAggCache.Companion.add(listOf(userPublicKey, serverPublicKey)).map { (internalPubKey, _) -> - val prefix = when (chain) { - NodeParams.Chain.Mainnet -> DeterministicWallet.xprv - else -> DeterministicWallet.tprv - } - val xpriv = DeterministicWallet.encode(masterRefundKey, prefix) - val desc = "tr(${internalPubKey.value},and_v(v:pk($xpriv/*),older($refundDelay)))" - val checksum = Descriptor.checksum(desc) - "$desc#$checksum" + val internalPubKey = KeyAggCache.Companion.create(listOf(userPublicKey, serverPublicKey)).first + val prefix = when (chain) { + NodeParams.Chain.Mainnet -> DeterministicWallet.xprv + else -> DeterministicWallet.tprv } + val xpriv = DeterministicWallet.encode(masterRefundKey, prefix) + val desc = "tr(${internalPubKey.value},and_v(v:pk($xpriv/*),older($refundDelay)))" + val checksum = Descriptor.checksum(desc) + return "$desc#$checksum" } /** @@ -111,19 +90,18 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe * @param masterRefundKey master public key for the refund keys. we assume that there is a single level of derivation to compute the refund keys * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to create a watch-only wallet for your swap-in transactions */ - fun descriptor(chain: NodeParams.Chain, userPublicKey: PublicKey, serverPublicKey: PublicKey, refundDelay: Int, masterRefundKey: DeterministicWallet.ExtendedPublicKey): Any { + fun descriptor(chain: NodeParams.Chain, userPublicKey: PublicKey, serverPublicKey: PublicKey, refundDelay: Int, masterRefundKey: DeterministicWallet.ExtendedPublicKey): String { // the internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key - return KeyAggCache.Companion.add(listOf(userPublicKey, serverPublicKey)).map { (internalPubKey, _) -> - val prefix = when (chain) { - NodeParams.Chain.Mainnet -> DeterministicWallet.xpub - else -> DeterministicWallet.tpub - } - val xpub = DeterministicWallet.encode(masterRefundKey, prefix) - val path = masterRefundKey.path.toString().replace('\'', 'h').removePrefix("m") - val desc = "tr(${internalPubKey.value},and_v(v:pk($xpub$path/*),older($refundDelay)))" - val checksum = Descriptor.checksum(desc) - return "$desc#$checksum" + val internalPubKey = KeyAggCache.Companion.create(listOf(userPublicKey, serverPublicKey)).first + val prefix = when (chain) { + NodeParams.Chain.Mainnet -> DeterministicWallet.xpub + else -> DeterministicWallet.tpub } + val xpub = DeterministicWallet.encode(masterRefundKey, prefix) + val path = masterRefundKey.path.toString().replace('\'', 'h').removePrefix("m") + val desc = "tr(${internalPubKey.value},and_v(v:pk($xpub$path/*),older($refundDelay)))" + val checksum = Descriptor.checksum(desc) + return "$desc#$checksum" } } } @@ -150,7 +128,7 @@ class SwapInProtocolLegacy(val userPublicKey: PublicKey, val serverPublicKey: Pu fun isMine(txOut: TxOut): Boolean = txOut.publicKeyScript.contentEquals(Script.write(pubkeyScript)) - fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! + fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).right!! fun witness(userSig: ByteVector64, serverSig: ByteVector64): ScriptWitness { return ScriptWitness(listOf(Scripts.der(serverSig, SigHash.SIGHASH_ALL), Scripts.der(userSig, SigHash.SIGHASH_ALL), Script.write(redeemScript).byteVector())) @@ -161,11 +139,30 @@ class SwapInProtocolLegacy(val userPublicKey: PublicKey, val serverPublicKey: Pu } fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOut: TxOut, userKey: PrivateKey): ByteVector64 { - require(userKey.publicKey() == userPublicKey) + require(userKey.publicKey() == userPublicKey) { "user private key does not match expected public key: are you using the refund key instead of the user key?" } return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, userKey) } fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, serverKey: PrivateKey): ByteVector64 { return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, serverKey) } + + /** + * The output script descriptor matching our legacy swap-in addresses. + * That descriptor can be imported in bitcoind to recover funds after the refund delay. + * + * @param chain chain we're on. + * @param masterRefundKey master private key for the swap-in wallet. + * @param userPrivateKey user refund private key, derived from the master private key. + * @return a p2wsh descriptor that can be imported in bitcoin core (from version 24 on) to recover user funds once the funding delay has passed. + */ + fun descriptor(chain: NodeParams.Chain, masterRefundKey: DeterministicWallet.ExtendedPrivateKey, userPrivateKey: DeterministicWallet.ExtendedPrivateKey): String { + // Since child public keys cannot be derived from a master xpub when hardened derivation is used, + // we need to provide the fingerprint of the master xpub and the hardened derivation path. + // This lets wallets that have access to the master xpriv derive the corresponding private and public keys. + val masterFingerprint = ByteVector(Crypto.hash160(DeterministicWallet.publicKey(masterRefundKey).publickeybytes).take(4).toByteArray()) + val encodedChildKey = DeterministicWallet.encode(DeterministicWallet.publicKey(userPrivateKey), testnet = chain != NodeParams.Chain.Mainnet) + val userKey = "[${masterFingerprint.toHex()}/${KeyManager.SwapInOnChainKeys.encodedSwapInUserKeyPath(chain)}]$encodedChildKey" + return "wsh(and_v(v:pk($userKey),or_d(pk(${serverPublicKey.toHex()}),older($refundDelay))))" + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index b9697b26a..b3c2e6f68 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt @@ -1,10 +1,9 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output -import fr.acinq.bitcoin.crypto.musig2.AggregatedNonce -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.utils.toByteVector64 @@ -72,8 +71,8 @@ sealed class TxRemoveInputTlv : Tlv sealed class TxRemoveOutputTlv : Tlv sealed class TxCompleteTlv : Tlv { - /** nonces for all Musig2 swap-in inputs, ordered by serial id */ - data class Nonces(val nonces: List): TxCompleteTlv() { + /** Public nonces for all Musig2 swap-in inputs (local and remote), ordered by serial id. */ + data class Nonces(val nonces: List) : TxCompleteTlv() { override val tag: Long get() = Nonces.tag override fun write(out: Output) { @@ -133,42 +132,51 @@ sealed class TxSignaturesTlv : Tlv { } } - data class SwapInUserPartialSigs(val psigs: List) : TxSignaturesTlv() { + /** A partial musig2 signature, with the corresponding local and remote public nonces. */ + data class PartialSignature(val sig: ByteVector32, val localNonce: IndividualNonce, val remoteNonce: IndividualNonce) + + /** Partial musig2 signatures from the swap user for inputs that belong to them. */ + data class SwapInUserPartialSigs(val psigs: List) : TxSignaturesTlv() { override val tag: Long get() = SwapInUserPartialSigs.tag override fun write(out: Output) = psigs.forEach { psig -> LightningCodecs.writeBytes(psig.sig, out) - LightningCodecs.writeBytes(psig.aggregatedPublicNonce.toByteArray(), out) + LightningCodecs.writeBytes(psig.localNonce.toByteArray(), out) + LightningCodecs.writeBytes(psig.remoteNonce.toByteArray(), out) } companion object : TlvValueReader { const val tag: Long = 607 override fun read(input: Input): SwapInUserPartialSigs { - val count = input.availableBytes / (32 + 66) + val count = input.availableBytes / (32 + 66 + 66) val psigs = (0 until count).map { val sig = LightningCodecs.bytes(input, 32).byteVector32() - val nonce = AggregatedNonce(LightningCodecs.bytes(input, 66)) - TxSignatures.Companion.PartialSignature(sig, nonce) + val localNonce = IndividualNonce(LightningCodecs.bytes(input, 66)) + val remoteNonce = IndividualNonce(LightningCodecs.bytes(input, 66)) + PartialSignature(sig, localNonce, remoteNonce) } return SwapInUserPartialSigs(psigs) } } } - data class SwapInServerPartialSigs(val psigs: List) : TxSignaturesTlv() { + /** Partial musig2 signatures from the swap server for inputs that belong to the user. */ + data class SwapInServerPartialSigs(val psigs: List) : TxSignaturesTlv() { override val tag: Long get() = SwapInServerPartialSigs.tag override fun write(out: Output) = psigs.forEach { psig -> LightningCodecs.writeBytes(psig.sig, out) - LightningCodecs.writeBytes(psig.aggregatedPublicNonce.toByteArray(), out) + LightningCodecs.writeBytes(psig.localNonce.toByteArray(), out) + LightningCodecs.writeBytes(psig.remoteNonce.toByteArray(), out) } companion object : TlvValueReader { const val tag: Long = 609 override fun read(input: Input): SwapInServerPartialSigs { - val count = input.availableBytes / (32 + 66) + val count = input.availableBytes / (32 + 66 + 66) val psigs = (0 until count).map { val sig = LightningCodecs.bytes(input, 32).byteVector32() - val nonce = AggregatedNonce(LightningCodecs.bytes(input, 66)) - TxSignatures.Companion.PartialSignature(sig, nonce) + val localNonce = IndividualNonce(LightningCodecs.bytes(input, 66)) + val remoteNonce = IndividualNonce(LightningCodecs.bytes(input, 66)) + PartialSignature(sig, localNonce, remoteNonce) } return SwapInServerPartialSigs(psigs) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index ceeb33976..a09dd4f2c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -1,7 +1,6 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* -import fr.acinq.bitcoin.crypto.musig2.AggregatedNonce import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput @@ -481,7 +480,7 @@ data class TxSignatures( val witnesses: List, val tlvs: TlvStream = TlvStream.empty() ) : InteractiveTxMessage(), HasChannelId, HasEncryptedChannelData { - constructor(channelId: ByteVector32, tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, swapInUserSigs: List, swapInServerSigs: List, swapInUserPartialSigs: List, swapInServerPartialSigs: List) : this( + constructor(channelId: ByteVector32, tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, swapInUserSigs: List, swapInServerSigs: List, swapInUserPartialSigs: List, swapInServerPartialSigs: List) : this( channelId, tx.txid, witnesses, @@ -501,8 +500,8 @@ data class TxSignatures( val previousFundingTxSig: ByteVector64? = tlvs.get()?.sig val swapInUserSigs: List = tlvs.get()?.sigs ?: listOf() val swapInServerSigs: List = tlvs.get()?.sigs ?: listOf() - val swapInUserPartialSigs: List = tlvs.get()?.psigs ?: listOf() - val swapInServerPartialSigs: List = tlvs.get()?.psigs ?: listOf() + val swapInUserPartialSigs: List = tlvs.get()?.psigs ?: listOf() + val swapInServerPartialSigs: List = tlvs.get()?.psigs ?: listOf() override val channelData: EncryptedChannelData get() = tlvs.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): TxSignatures = copy(tlvs = tlvs.addOrUpdate(TxSignaturesTlv.ChannelData(ecd))) @@ -523,8 +522,6 @@ data class TxSignatures( companion object : LightningMessageReader { const val type: Long = 71 - data class PartialSignature(val sig: ByteVector32, val aggregatedPublicNonce: AggregatedNonce) - @Suppress("UNCHECKED_CAST") val readers = mapOf( TxSignaturesTlv.PreviousFundingTxSig.tag to TxSignaturesTlv.PreviousFundingTxSig.Companion as TlvValueReader, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumClientTest.kt b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumClientTest.kt index f3d640a9c..6050d2f16 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumClientTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumClientTest.kt @@ -72,7 +72,7 @@ class ElectrumClientTest : LightningTestSuite() { @Test fun `status should be empty for unused addresses`() = runTest { client -> val address = "bc1qzp2r7k62chyyq7g8ppw2dxp2lcrt629ym0swqy" - val scriptHash = ElectrumClient.computeScriptHash(Script.write(Bitcoin.addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, address).result!!).byteVector()) + val scriptHash = ElectrumClient.computeScriptHash(Script.write(Bitcoin.addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, address).right!!).byteVector()) val status = client.startScriptHashSubscription(scriptHash) assertNull(status.status) } 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 a6a86a3d5..378410906 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWalletTest.kt @@ -140,7 +140,7 @@ class ElectrumMiniWalletTest : LightningTestSuite() { ), actual = walletState.utxos.map { val txOut = it.previousTx.txOut[it.outputIndex] - val address = Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, txOut.publicKeyScript.toByteArray()).result!! + val address = Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, txOut.publicKeyScript.toByteArray()).right!! Triple(address, it.previousTx.txid to it.outputIndex, txOut.amount) }.toSet() ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt index b62c61501..1ebc2f2e1 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/HelpersTestsCommon.kt @@ -21,7 +21,7 @@ class HelpersTestsCommon : LightningTestSuite() { fun `compute address from pubkey script`() { val pub = PrivateKey(Hex.decode("0101010101010101010101010101010101010101010101010101010101010101")).publicKey() - fun address(script: List, chainHash: BlockHash) = Bitcoin.addressFromPublicKeyScript(chainHash, Script.write(script)).result + fun address(script: List, chainHash: BlockHash) = Bitcoin.addressFromPublicKeyScript(chainHash, Script.write(script)).right listOf(Block.LivenetGenesisBlock.hash, Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash).forEach { assertEquals(address(Script.pay2pkh(pub), it), computeP2PkhAddress(pub, it)) @@ -44,7 +44,7 @@ class HelpersTestsCommon : LightningTestSuite() { Triple("a91481b9ac6a59b53927da7277b5ad5460d781b365d987", Block.LivenetGenesisBlock.hash, "3DWwX7NYjnav66qygrm4mBCpiByjammaWy"), ).forEach { assertEquals( - Bitcoin.addressFromPublicKeyScript(it.second, Hex.decode(it.first)).result, + Bitcoin.addressFromPublicKeyScript(it.second, Hex.decode(it.first)).right, it.third ) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index 3eed40118..aca2cd373 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -119,10 +119,6 @@ data class LNChannel( val serialized = Serialization.serialize(state) val deserialized = Serialization.deserialize(serialized).value - - if (deserialized != state) { - error("serialization error") - } assertEquals(removeRbfAttempt(state), deserialized, "serialization error") } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index fec591f9d..764939f10 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -18,7 +18,6 @@ import fr.acinq.lightning.wire.CommitSig import fr.acinq.lightning.wire.EncryptedChannelData import fr.acinq.lightning.wire.LightningMessage import fr.acinq.lightning.wire.LiquidityAds -import fr.acinq.lightning.wire.TxSignatures import fr.acinq.secp256k1.Hex import kotlin.math.max import kotlin.test.* diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index b4a0468ba..189780984 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -7,10 +7,7 @@ import fr.acinq.bitcoin.Script.pay2wpkh import fr.acinq.bitcoin.Script.pay2wsh import fr.acinq.bitcoin.Script.write import fr.acinq.bitcoin.crypto.Pack -import fr.acinq.bitcoin.crypto.musig2.IndividualNonce -import fr.acinq.bitcoin.crypto.musig2.KeyAggCache -import fr.acinq.bitcoin.crypto.musig2.SecretNonce -import fr.acinq.bitcoin.utils.flatMap +import fr.acinq.bitcoin.crypto.musig2.Musig2 import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Lightning.randomBytes32 @@ -53,10 +50,11 @@ import fr.acinq.lightning.transactions.Transactions.makeHtlcPenaltyTx import fr.acinq.lightning.transactions.Transactions.makeHtlcTxs import fr.acinq.lightning.transactions.Transactions.makeMainPenaltyTx import fr.acinq.lightning.transactions.Transactions.sign -import fr.acinq.lightning.transactions.Transactions.swapInputWeightLegacy import fr.acinq.lightning.transactions.Transactions.swapInputWeight +import fr.acinq.lightning.transactions.Transactions.swapInputWeightLegacy import fr.acinq.lightning.transactions.Transactions.weight2fee import fr.acinq.lightning.utils.* +import fr.acinq.lightning.wire.TxSignaturesTlv import fr.acinq.lightning.wire.UpdateAddHtlc import kotlin.random.Random import kotlin.test.* @@ -580,32 +578,21 @@ class TransactionsTestsCommon : LightningTestSuite() { txOut = listOf(TxOut(Satoshi(10000), pay2wpkh(PrivateKey(randomBytes32()).publicKey()))), lockTime = 0 ) - // this is the beginning of an interactive musig2 signing session. if user and server are disconnected before they have exchanged partial - // signatures they will have to start again with fresh nonces - - val commonSig = KeyAggCache.add(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())) - .flatMap { (_, cache) -> - SecretNonce.generate(randomBytes32(), userPrivateKey, userPrivateKey.publicKey(), null, cache, null) - .flatMap { userNonce -> - SecretNonce.generate(randomBytes32(), serverPrivateKey, serverPrivateKey.publicKey(), null, cache, null) - .flatMap { serverNonce -> - IndividualNonce.aggregate(listOf(userNonce.second, serverNonce.second)) - .flatMap { commonNonce -> - swapInProtocol.signSwapInputUser(tx, 0, swapInTx.txOut, userPrivateKey, userNonce.first, commonNonce) - .flatMap { userSig -> - swapInProtocol.signSwapInputServer(tx, 0, swapInTx.txOut, commonNonce, serverPrivateKey, serverNonce.first) - .flatMap { serverSig -> - swapInProtocol.session(tx, 0, swapInTx.txOut, commonNonce) - .flatMap { session -> session.add(listOf(userSig, serverSig)) } - } - } - } - } - } - } - assertTrue(commonSig.isRight) - - val signedTx = tx.updateWitness(0, swapInProtocol.witness(commonSig.right!!)) + // The first step of a musig2 signing session is to exchange nonces. + // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. + val userNonce = Musig2.generateNonce(randomBytes32(), userPrivateKey, listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())) + val serverNonce = Musig2.generateNonce(randomBytes32(), serverPrivateKey, listOf(serverPrivateKey.publicKey(), userPrivateKey.publicKey())) + + // Once they have each other's public nonce, they can produce partial signatures. + val userSig = swapInProtocol.signSwapInputUser(tx, 0, swapInTx.txOut, userPrivateKey, userNonce, serverNonce.second).right!! + val serverSig = swapInProtocol.signSwapInputServer(tx, 0, swapInTx.txOut, userNonce.second,serverPrivateKey, serverNonce).right!! + + // Once they have each other's partial signature, they can aggregate them into a valid signature. + val userPartialSig = TxSignaturesTlv.PartialSignature(userSig, userNonce.second, serverNonce.second) + val serverPartialSig = TxSignaturesTlv.PartialSignature(serverSig, serverNonce.second, userNonce.second) + val witness = swapInProtocol.witness(tx, 0, swapInTx.txOut, userPartialSig, serverPartialSig).right + assertNotNull(witness) + val signedTx = tx.updateWitness(0, witness) Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -643,8 +630,7 @@ class TransactionsTestsCommon : LightningTestSuite() { // DER-encoded ECDSA signatures usually take up to 72 bytes. val sig = ByteVector64.fromValidHex("90b658d172a51f1b3f1a2becd30942397f5df97da8cd2c026854607e955ad815ccfd87d366e348acc32aaf15ff45263aebbb7ecc913a0e5999133f447aee828c") val tx = Transaction(2, listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 2), 0)), listOf(TxOut(50_000.sat, pay2wpkh(pubkey))), 0) - val swapInProtocol = SwapInProtocol(pubkey, pubkey, pubkey, 144) - val witness = swapInProtocol.witness(sig) + val witness = ScriptWitness(listOf(sig)) val swapInput = TxIn(OutPoint(TxId(ByteVector32.Zeroes), 3), ByteVector.empty, 0, witness) val txWithAdditionalInput = tx.copy(txIn = tx.txIn + listOf(swapInput)) val inputWeight = txWithAdditionalInput.weight() - tx.weight() diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index df056d068..46952f8aa 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -1,9 +1,9 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput -import fr.acinq.bitcoin.crypto.musig2.AggregatedNonce import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes import fr.acinq.lightning.Lightning.randomBytes32 @@ -391,9 +391,11 @@ class LightningCodecsTestsCommon : LightningTestSuite() { ) val pubKey1 = PrivateKey.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").publicKey() val pubKey2 = PrivateKey.fromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").publicKey() + val nonce1 = IndividualNonce(pubKey1.value + pubKey1.value) + val nonce2 = IndividualNonce(pubKey2.value + pubKey2.value) val swapInPartialSignatures = listOf( - TxSignatures.Companion.PartialSignature(ByteVector32("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), AggregatedNonce(pubKey1.value + pubKey2.value)), - TxSignatures.Companion.PartialSignature(ByteVector32("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), AggregatedNonce(pubKey1.value + pubKey2.value)) + TxSignaturesTlv.PartialSignature(ByteVector32("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), nonce1, nonce2), + TxSignaturesTlv.PartialSignature(ByteVector32("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), nonce1, nonce2) ) val signature = ByteVector64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") // This is a random mainnet transaction. @@ -411,7 +413,6 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TxAddInput(channelId2, 0, tx2, 2, 0u) to ByteVector("0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000"), TxAddInput(channelId1, 561, tx1, 0, 0xfffffffdu) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 fffffffd"), TxAddInput(channelId1, 561, OutPoint(tx1, 1), 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106"), - //TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534603462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f00000090"), TxAddOutput(channelId1, 1105, 2047.sat, ByteVector("00149357014afd0ccd265658c9ae81efa995e771f472")) to ByteVector("0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472"), TxRemoveInput(channelId2, 561) to ByteVector("0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231"), TxRemoveOutput(channelId1, 1) to ByteVector("0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001"), @@ -424,8 +425,8 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), TxSignatures(channelId2, tx1, listOf(), signature, listOf(), swapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 000 0fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f c4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 000 0fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 c4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1 f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f fd 0148cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b50268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b50268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb1 f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 fd 0148cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b50268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b50268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5"), TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures.take(1), swapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), TxInitRbf(channelId1, 8388607, FeeratePerKw(4000.sat)) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(1_500_000.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360"),