From e576219554ad5cdb92e3a861da267acfcb5c0776 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 10 Jun 2024 17:34:42 +0200 Subject: [PATCH] Partial support for PSBT v2 Here we only support taproot fields that would allow us to sign BIP86 transactions. --- build.gradle.kts | 2 +- .../acinq/bitcoin/LexicographicalOrdering.kt | 6 + .../kotlin/fr/acinq/bitcoin/psbt/Psbt.kt | 294 +++++++++++++++--- .../fr/acinq/bitcoin/psbt/PsbtTestsCommon.kt | 101 +++++- 4 files changed, 357 insertions(+), 46 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 0c7dd93c..37a75ab4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ plugins { val currentOs = org.gradle.internal.os.OperatingSystem.current() group = "fr.acinq.bitcoin" -version = "0.19.0" +version = "0.20.0-SNAPSHOT" repositories { google() diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/LexicographicalOrdering.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/LexicographicalOrdering.kt index 7467ca21..31b44d5a 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/LexicographicalOrdering.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/LexicographicalOrdering.kt @@ -67,9 +67,15 @@ public object LexicographicalOrdering { @JvmStatic public fun isLessThan(a: PublicKey, b: PublicKey): Boolean = isLessThan(a.value, b.value) + @JvmStatic + public fun isLessThan(a: XonlyPublicKey, b: XonlyPublicKey): Boolean = isLessThan(a.value, b.value) + @JvmStatic public fun compare(a: PublicKey, b: PublicKey): Int = if (a == b) 0 else if (isLessThan(a, b)) -1 else 1 + @JvmStatic + public fun compare(a: XonlyPublicKey, b: XonlyPublicKey): Int = if (a == b) 0 else if (isLessThan(a, b)) -1 else 1 + /** * @param tx input transaction * @return the input tx with inputs and outputs sorted in lexicographical order diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/psbt/Psbt.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/psbt/Psbt.kt index 19dd55a8..d7824696 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/psbt/Psbt.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/psbt/Psbt.kt @@ -17,6 +17,7 @@ package fr.acinq.bitcoin.psbt import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.Transaction.Companion.hashForSigningSchnorr import fr.acinq.bitcoin.crypto.Pack import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput @@ -48,8 +49,8 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< */ public constructor(tx: Transaction) : this( Global(Version, tx.copy(txIn = tx.txIn.map { it.copy(signatureScript = ByteVector.empty, witness = ScriptWitness.empty) }), listOf(), listOf()), - tx.txIn.map { Input.PartiallySignedInputWithoutUtxo(null, mapOf(), setOf(), setOf(), setOf(), setOf(), listOf()) }, - tx.txOut.map { Output.UnspecifiedOutput(mapOf(), listOf()) } + tx.txIn.map { Input.PartiallySignedInputWithoutUtxo(null, mapOf(), setOf(), setOf(), setOf(), setOf(), null, mapOf(), null, listOf()) }, + tx.txOut.map { Output.UnspecifiedOutput(mapOf(), null, mapOf(), listOf()) } ) /** @@ -70,7 +71,10 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< redeemScript: List? = null, witnessScript: List? = null, sighashType: Int? = null, - derivationPaths: Map = mapOf() + derivationPaths: Map = mapOf(), + taprootKeySignature: ByteVector? = null, + taprootInternalKey: XonlyPublicKey? = null, + taprootDerivationPaths: Map = mapOf() ): Either { val inputIndex = global.tx.txIn.indexOfFirst { it.outPoint == outPoint } if (inputIndex < 0) return Either.Left(UpdateFailure.InvalidInput("psbt transaction does not spend the provided outpoint")) @@ -87,15 +91,22 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< input.sha256, input.hash160, input.hash256, + taprootKeySignature ?: input.taprootKeySignature, + input.taprootDerivationPaths + taprootDerivationPaths, + taprootInternalKey ?: input.taprootInternalKey, input.unknown ) + is Input.WitnessInput.PartiallySignedWitnessInput -> input.copy( txOut = txOut, redeemScript = redeemScript ?: input.redeemScript, witnessScript = witnessScript ?: input.witnessScript, sighashType = sighashType ?: input.sighashType, - derivationPaths = input.derivationPaths + derivationPaths + derivationPaths = input.derivationPaths + derivationPaths, + taprootInternalKey = taprootInternalKey ?: input.taprootInternalKey, + taprootDerivationPaths = input.taprootDerivationPaths + taprootDerivationPaths ) + is Input.NonWitnessInput.PartiallySignedNonWitnessInput -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update segwit input: it has already been updated with non-segwit data")) is Input.FinalizedInputWithoutUtxo -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update segwit input: it has already been finalized")) is Input.WitnessInput.FinalizedWitnessInput -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update segwit input: it has already been finalized")) @@ -114,6 +125,8 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< * @param witnessScript witness script if known and applicable (when using p2wsh). * @param sighashType sighash type if one should be specified. * @param derivationPaths derivation paths for keys used by this utxo. + * @param taprootInternalKey internal key used by this utxo. + * @param taprootDerivationPaths taproot derivation paths for keys used by this utxo. * @return psbt with the matching input updated. */ public fun updateWitnessInputTx( @@ -122,7 +135,10 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< redeemScript: List? = null, witnessScript: List? = null, sighashType: Int? = null, - derivationPaths: Map = mapOf() + derivationPaths: Map = mapOf(), + taprootKeySignature: ByteVector? = null, + taprootInternalKey: XonlyPublicKey? = null, + taprootDerivationPaths: Map = mapOf() ): Either { if (outputIndex >= inputTx.txOut.size) return Either.Left(UpdateFailure.InvalidInput("output index must exist in the input tx")) val outpoint = OutPoint(inputTx, outputIndex.toLong()) @@ -141,16 +157,23 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< input.sha256, input.hash160, input.hash256, + taprootKeySignature ?: input.taprootKeySignature, + input.taprootDerivationPaths + taprootDerivationPaths, + taprootInternalKey ?: input.taprootInternalKey, input.unknown ) + is Input.WitnessInput.PartiallySignedWitnessInput -> input.copy( txOut = inputTx.txOut[outputIndex], nonWitnessUtxo = inputTx, redeemScript = redeemScript ?: input.redeemScript, witnessScript = witnessScript ?: input.witnessScript, sighashType = sighashType ?: input.sighashType, - derivationPaths = input.derivationPaths + derivationPaths + derivationPaths = input.derivationPaths + derivationPaths, + taprootInternalKey = taprootInternalKey ?: input.taprootInternalKey, + taprootDerivationPaths = input.taprootDerivationPaths + taprootDerivationPaths ) + is Input.NonWitnessInput.PartiallySignedNonWitnessInput -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update segwit input: it has already been updated with non-segwit data")) is Input.FinalizedInputWithoutUtxo -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update segwit input: it has already been finalized")) is Input.WitnessInput.FinalizedWitnessInput -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update segwit input: it has already been finalized")) @@ -194,6 +217,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< input.hash256, input.unknown ) + is Input.NonWitnessInput.PartiallySignedNonWitnessInput -> input.copy( inputTx = inputTx, outputIndex = outputIndex, @@ -201,12 +225,14 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< sighashType = sighashType ?: input.sighashType, derivationPaths = input.derivationPaths + derivationPaths ) + is Input.WitnessInput.PartiallySignedWitnessInput -> input.copy( nonWitnessUtxo = inputTx, redeemScript = redeemScript ?: input.redeemScript, sighashType = sighashType ?: input.sighashType, derivationPaths = input.derivationPaths + derivationPaths ) + is Input.FinalizedInputWithoutUtxo -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update non-segwit input: it has already been finalized")) is Input.WitnessInput.FinalizedWitnessInput -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update non-segwit input: it has already been finalized")) is Input.NonWitnessInput.FinalizedNonWitnessInput -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update non-segwit input: it has already been finalized")) @@ -246,7 +272,9 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< outputIndex: Int, witnessScript: List? = null, redeemScript: List? = null, - derivationPaths: Map = mapOf() + derivationPaths: Map = mapOf(), + taprootInternalKey: XonlyPublicKey? = null, + taprootDerivationPaths: Map = mapOf() ): Either { if (outputIndex >= global.tx.txOut.size) return Either.Left(UpdateFailure.InvalidInput("output index must exist in the global tx")) val updatedOutput = when (val output = outputs[outputIndex]) { @@ -254,9 +282,12 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< is Output.WitnessOutput -> output.copy( witnessScript = witnessScript ?: output.witnessScript, redeemScript = redeemScript ?: output.redeemScript, - derivationPaths = output.derivationPaths + derivationPaths + derivationPaths = output.derivationPaths + derivationPaths, + taprootInternalKey = taprootInternalKey ?: output.taprootInternalKey, + taprootDerivationPaths = output.taprootDerivationPaths + taprootDerivationPaths ) - is Output.UnspecifiedOutput -> Output.WitnessOutput(witnessScript, redeemScript, output.derivationPaths + derivationPaths, output.unknown) + + is Output.UnspecifiedOutput -> Output.WitnessOutput(witnessScript, redeemScript, output.derivationPaths + derivationPaths, output.taprootInternalKey, output.taprootDerivationPaths, output.unknown) } return Either.Right(this.copy(outputs = outputs.updated(outputIndex, updatedOutput))) } @@ -280,6 +311,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< redeemScript = redeemScript ?: output.redeemScript, derivationPaths = output.derivationPaths + derivationPaths ) + is Output.WitnessOutput -> return Either.Left(UpdateFailure.CannotUpdateOutput(outputIndex, "cannot update non-segwit output: it has already been updated with segwit data")) is Output.UnspecifiedOutput -> Output.NonWitnessOutput(redeemScript, output.derivationPaths + derivationPaths, output.unknown) } @@ -331,6 +363,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< signWitness(priv, inputIndex, input, global) } } + is Input.NonWitnessInput.PartiallySignedNonWitnessInput -> { if (input.inputTx.txid != txIn.outPoint.txid) { Either.Left(UpdateFailure.InvalidNonWitnessUtxo("non-witness utxo does not match unsigned tx input")) @@ -340,6 +373,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< signNonWitness(priv, inputIndex, input, global) } } + is Input.FinalizedInputWithoutUtxo -> Either.Left(UpdateFailure.CannotSignInput(inputIndex, "cannot sign: input has already been finalized")) is Input.WitnessInput.FinalizedWitnessInput -> Either.Left(UpdateFailure.CannotSignInput(inputIndex, "cannot sign: input has already been finalized")) is Input.NonWitnessInput.FinalizedNonWitnessInput -> Either.Left(UpdateFailure.CannotSignInput(inputIndex, "cannot sign: input has already been finalized")) @@ -355,6 +389,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< }.getOrElse { return Either.Left(UpdateFailure.InvalidNonWitnessUtxo("failed to parse redeem script")) } + else -> { // If a redeem script is provided in the partially signed input, the utxo must be a p2sh for that script. val p2sh = Script.write(Script.pay2sh(input.redeemScript)) @@ -370,38 +405,87 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< } private fun signWitness(priv: PrivateKey, inputIndex: Int, input: Input.WitnessInput.PartiallySignedWitnessInput, global: Global): Either> { - val redeemScript = when (input.redeemScript) { - null -> runCatching { - Script.parse(input.txOut.publicKeyScript) - }.getOrElse { - return Either.Left(UpdateFailure.InvalidWitnessUtxo("failed to parse redeem script")) + val spentOutputs = this.inputs.mapNotNull { it.witnessUtxo ?: it.nonWitnessUtxo?.txOut?.get(this.global.tx.txIn[inputIndex].outPoint.index.toInt()) } + val pubkeyScript = Script.parse(input.txOut.publicKeyScript) + + return when { + Script.isPay2wpkh(pubkeyScript) -> when (input.witnessScript) { + null -> { + Either.Left(UpdateFailure.InvalidWitnessUtxo("missing witness script")) + } + + else -> { + val sig = ByteVector(Transaction.signInput(global.tx, inputIndex, Script.write(input.witnessScript), input.sighashType ?: SigHash.SIGHASH_ALL, input.amount, SigVersion.SIGVERSION_WITNESS_V0, priv)) + Either.Right(Pair(input.copy(partialSigs = input.partialSigs + (priv.publicKey() to sig)), sig)) + } } - else -> { - // If a redeem script is provided in the partially signed input, the utxo must be a p2sh for that script (we're using p2sh-embedded segwit). - val p2sh = Script.write(Script.pay2sh(input.redeemScript)) - if (!input.txOut.publicKeyScript.contentEquals(p2sh)) { - return Either.Left(UpdateFailure.InvalidWitnessUtxo("redeem script does not match witness utxo scriptPubKey")) - } else { - input.redeemScript + + Script.isPay2wsh(pubkeyScript) -> when { + input.witnessScript == null -> { + Either.Left(UpdateFailure.InvalidWitnessUtxo("missing witness script")) + } + + pubkeyScript != Script.pay2wsh(input.witnessScript) -> Either.Left(UpdateFailure.InvalidWitnessUtxo("witness script does not match redeemScript or scriptPubKey")) + else -> { + val sig = ByteVector(Transaction.signInput(global.tx, inputIndex, input.witnessScript, input.sighashType ?: SigHash.SIGHASH_ALL, input.amount, SigVersion.SIGVERSION_WITNESS_V0, priv)) + Either.Right(Pair(input.copy(partialSigs = input.partialSigs + (priv.publicKey() to sig)), sig)) } } - } - return when (input.witnessScript) { - null -> { - val sig = ByteVector(Transaction.signInput(global.tx, inputIndex, redeemScript, input.sighashType ?: SigHash.SIGHASH_ALL, input.amount, SigVersion.SIGVERSION_WITNESS_V0, priv)) - Either.Right(Pair(input.copy(partialSigs = input.partialSigs + (priv.publicKey() to sig)), sig)) + + Script.isPay2tr(pubkeyScript) -> when { + input.taprootInternalKey == null -> { + Either.Left(UpdateFailure.InvalidWitnessUtxo("missing taproot internal key")) + } + + else -> { + val sig = Transaction.signInputTaprootKeyPath(priv, global.tx, inputIndex, spentOutputs, input.sighashType ?: SigHash.SIGHASH_DEFAULT, null) + val sigAndSighashType = input.sighashType?.let { sig.concat(it.toByte()) } ?: sig + Either.Right(Pair(input.copy(taprootKeySignature = sigAndSighashType), sigAndSighashType)) + } } + + Script.isPay2sh(pubkeyScript) -> when { + input.redeemScript == null -> { + Either.Left(UpdateFailure.InvalidWitnessUtxo("missing redeem script")) + } + + pubkeyScript != Script.pay2sh(input.redeemScript) -> { + Either.Left(UpdateFailure.InvalidWitnessUtxo("redeem script does not match witness utxo scriptPubKey")) + } + + else -> when { + input.witnessScript == null -> { + val sig = ByteVector(Transaction.signInput(global.tx, inputIndex, input.redeemScript, input.sighashType ?: SigHash.SIGHASH_ALL, input.amount, SigVersion.SIGVERSION_WITNESS_V0, priv)) + Either.Right(Pair(input.copy(partialSigs = input.partialSigs + (priv.publicKey() to sig)), sig)) + } + + !Script.isPay2wpkh(input.redeemScript) && input.redeemScript != Script.pay2wsh(input.witnessScript) -> { + Either.Left(UpdateFailure.InvalidWitnessUtxo("witness script does not match redeemScript or scriptPubKey")) + } + + else -> { + val sig = ByteVector(Transaction.signInput(global.tx, inputIndex, input.witnessScript, input.sighashType ?: SigHash.SIGHASH_ALL, input.amount, SigVersion.SIGVERSION_WITNESS_V0, priv)) + Either.Right(Pair(input.copy(partialSigs = input.partialSigs + (priv.publicKey() to sig)), sig)) + } + } + } + else -> { - if (!Script.isPay2wpkh(redeemScript) && redeemScript != Script.pay2wsh(input.witnessScript)) { - Either.Left(UpdateFailure.InvalidWitnessUtxo("witness script does not match redeemScript or scriptPubKey")) - } else { - val sig = ByteVector(Transaction.signInput(global.tx, inputIndex, input.witnessScript, input.sighashType ?: SigHash.SIGHASH_ALL, input.amount, SigVersion.SIGVERSION_WITNESS_V0, priv)) - Either.Right(Pair(input.copy(partialSigs = input.partialSigs + (priv.publicKey() to sig)), sig)) + val script = input.witnessScript ?: input.redeemScript ?: kotlin.runCatching { Script.parse(input.txOut.publicKeyScript) }.getOrNull() + when (script) { + null -> { + Either.Left(UpdateFailure.InvalidWitnessUtxo("failed to parse redeem script")) + } + + else -> { + val sig = ByteVector(Transaction.signInput(global.tx, inputIndex, script, input.sighashType ?: SigHash.SIGHASH_ALL, input.amount, SigVersion.SIGVERSION_WITNESS_V0, priv)) + Either.Right(Pair(input.copy(partialSigs = input.partialSigs + (priv.publicKey() to sig)), sig)) + } } } } } - + /** * Implements the PSBT finalizer role: finalizes a given segwit input. * This will clear all fields from the input except the utxo, scriptSig, scriptWitness and unknown entries. @@ -434,6 +518,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< val finalizedInput = Input.WitnessInput.FinalizedWitnessInput(input.txOut, input.nonWitnessUtxo, scriptWitness, scriptSig, input.ripemd160, input.sha256, input.hash160, input.hash256, input.unknown) Either.Right(this.copy(inputs = this.inputs.updated(inputIndex, finalizedInput))) } + else -> Either.Left(UpdateFailure.CannotFinalizeInput(inputIndex, ("cannot finalize: input has already been finalized"))) } } @@ -469,6 +554,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< val finalizedInput = Input.NonWitnessInput.FinalizedNonWitnessInput(input.inputTx, input.outputIndex, scriptSig, input.ripemd160, input.sha256, input.hash160, input.hash256, input.unknown) Either.Right(this.copy(inputs = this.inputs.updated(inputIndex, finalizedInput))) } + else -> Either.Left(UpdateFailure.CannotFinalizeInput(inputIndex, ("cannot finalize: input has already been finalized"))) } } @@ -490,6 +576,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< if (input.inputTx.txOut.size <= txIn.outPoint.index) return Either.Left(UpdateFailure.CannotExtractTx("non-witness utxo index out of bounds")) input.inputTx.txOut[txIn.outPoint.index.toInt()] } + is Input.WitnessInput.FinalizedWitnessInput -> input.txOut else -> return Either.Left(UpdateFailure.CannotExtractTx("some utxos are missing")) } @@ -586,6 +673,9 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< inputs.flatMap { it.sha256 }.toSet(), inputs.flatMap { it.hash160 }.toSet(), inputs.flatMap { it.hash256 }.toSet(), + inputs.firstNotNullOfOrNull { it.taprootKeySignature }, + inputs.flatMap { it.taprootDerivationPaths.toList() }.toMap(), + inputs.firstNotNullOfOrNull { it.taprootInternalKey }, combineUnknown(inputs.map { it.unknown }) ) @@ -593,6 +683,8 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< outputs.firstNotNullOfOrNull { it.redeemScript }, outputs.firstNotNullOfOrNull { it.witnessScript }, outputs.flatMap { it.derivationPaths.toList() }.toMap(), + outputs.firstNotNullOfOrNull { it.taprootInternalKey }, + outputs.flatMap { it.taprootDerivationPaths.toList() }.toMap(), combineUnknown(outputs.map { it.unknown }) ) @@ -683,6 +775,16 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< input.sha256.forEach { writeDataEntry(DataEntry(ByteVector("0b") + Crypto.sha256(it), it), out) } input.hash160.forEach { writeDataEntry(DataEntry(ByteVector("0c") + Crypto.hash160(it), it), out) } input.hash256.forEach { writeDataEntry(DataEntry(ByteVector("0d") + Crypto.hash256(it), it), out) } + input.taprootKeySignature?.let { writeDataEntry(DataEntry(ByteVector("13"), it), out) } + sortXonlyPublicKeys(input.taprootDerivationPaths).forEach { (publicKey, path) -> + val key = ByteVector("16") + publicKey.value + val bao = ByteArrayOutput() + BtcSerializer.writeVarint(path.leaves.size, bao) + path.leaves.forEach { BtcSerializer.writeBytes(it, bao) } + BtcSerializer.writeBytes(ByteVector(Pack.writeInt32BE(path.masterKeyFingerprint.toInt())).concat(path.keyPath.path.map { ByteVector(Pack.writeInt32LE(it.toInt())) }), bao) + writeDataEntry(DataEntry(key, bao.toByteArray().byteVector()), out) + } + input.taprootInternalKey?.let { writeDataEntry(DataEntry(ByteVector("17"), it.value), out) } input.unknown.forEach { writeDataEntry(it, out) } out.write(0x00) // separator } @@ -696,6 +798,15 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< val value = ByteVector(Pack.writeInt32BE(path.masterKeyFingerprint.toInt())).concat(path.keyPath.path.map { ByteVector(Pack.writeInt32LE(it.toInt())) }) writeDataEntry(DataEntry(key, value), out) } + output.taprootInternalKey?.let { writeDataEntry(DataEntry(ByteVector("05"), it.value), out) } + sortXonlyPublicKeys(output.taprootDerivationPaths).forEach { (publicKey, path) -> + val key = ByteVector("07") + publicKey.value + val bao = ByteArrayOutput() + BtcSerializer.writeVarint(path.leaves.size, bao) + path.leaves.forEach { BtcSerializer.writeBytes(it, bao) } + BtcSerializer.writeBytes(ByteVector(Pack.writeInt32BE(path.masterKeyFingerprint.toInt())).concat(path.keyPath.path.map { ByteVector(Pack.writeInt32LE(it.toInt())) }), bao) + writeDataEntry(DataEntry(key, bao.toByteArray().byteVector()), out) + } output.unknown.forEach { writeDataEntry(it, out) } out.write(0x00) // separator } @@ -706,6 +817,11 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< return publicKeys.toList().sortedWith { a, b -> LexicographicalOrdering.compare(a.first, b.first) } } + /** We use lexicographic ordering on the public keys. */ + private fun sortXonlyPublicKeys(publicKeys: Map): List> { + return publicKeys.toList().sortedWith { a, b -> LexicographicalOrdering.compare(a.first, b.first) } + } + private fun writeDataEntry(entry: DataEntry, output: fr.acinq.bitcoin.io.Output) { BtcSerializer.writeVarint(entry.key.size(), output) output.write(entry.key.bytes) @@ -798,7 +914,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< /********** Inputs **********/ val inputs = global.tx.txIn.map { txIn -> - val keyTypes = setOf(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0a, 0x0b, 0x0c, 0x0d) + val keyTypes = setOf(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x13, 0x16, 0x17, 0x0a, 0x0b, 0x0c, 0x0d) val entries = readDataMap(input).getOrElse { return when (it) { is ReadEntryFailure.DuplicateKeys -> Either.Left(ParseFailure.DuplicateKeys) @@ -890,6 +1006,35 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< } } } + val taprootKeySignature = known.find { it.key[0] == 0x13.toByte() }?.let { + when { + it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("taproot keypath signature key must contain exactly 1 byte")) + it.value.size() != 64 && it.value.size() != 65 -> return Either.Left(ParseFailure.InvalidTxInput("taproot keypath signature must contain 64 or 65 bytes")) + else -> it.value + } + } + val taprootDerivationPaths = known.filter { it.key[0] == 0x16.toByte() }.map { + when { + it.key.size() != 33 -> return Either.Left(ParseFailure.InvalidTxInput("taproot derivation path key must contain exactly 32 bytes")) + else -> { + val xonlyPublicKey = XonlyPublicKey(it.key.drop(1).toByteArray().byteVector32()) + val valueInput = ByteArrayInput(it.value.toByteArray()) + val numLeaves = BtcSerializer.varint(valueInput).toInt() + val leaves = (0 until numLeaves).map { BtcSerializer.bytes(valueInput, 32).byteVector32() } + val masterKeyFingerprint = Pack.int32BE(valueInput).toLong() + val childCount = (valueInput.availableBytes / 4) + val paths = KeyPath((0 until childCount).map { _ -> BtcSerializer.uint32(valueInput).toLong() }) + xonlyPublicKey to TaprootBip32DerivationPath(leaves, masterKeyFingerprint, paths) + } + } + }.toMap() + val taprootInternalKey = known.find { it.key[0] == 0x17.toByte() }?.let { + when { + it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("taproot internal key entry must have an empty key")) + it.value.size() != 32 -> return Either.Left(ParseFailure.InvalidTxInput("taproot internal key entry must have a 32 bytes value")) + else -> XonlyPublicKey(it.value.toByteArray().byteVector32()) + } + } val ripemd160Preimages = known.filter { it.key[0] == 0x0a.toByte() }.map { when { it.key.size() != 21 -> return Either.Left(ParseFailure.InvalidTxInput("ripemd160 hash must contain exactly 20 bytes")) @@ -933,13 +1078,16 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< sha256Preimages, hash160Preimages, hash256Preimages, + taprootKeySignature, + taprootDerivationPaths, + taprootInternalKey, unknown ) } /********** Outputs **********/ val outputs = global.tx.txOut.map { - val keyTypes = setOf(0x00, 0x01, 0x02) + val keyTypes = setOf(0x00, 0x01, 0x02, 0x05, 0x07) val entries = readDataMap(input).getOrElse { return when (it) { is ReadEntryFailure.DuplicateKeys -> Either.Left(ParseFailure.DuplicateKeys) @@ -972,7 +1120,29 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< } } }.toMap() - createOutput(redeemScript, witnessScript, derivationPaths, unknown) + val taprootInternalKey = known.find { it.key[0] == 0x05.toByte() }?.let { + when { + it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("taproot internal key entry must have an empty key")) + it.value.size() != 32 -> return Either.Left(ParseFailure.InvalidTxInput("taproot internal key entry must have a 32 bytes value")) + else -> XonlyPublicKey(it.value.toByteArray().byteVector32()) + } + } + val taprootDerivationPaths = known.filter { it.key[0] == 0x07.toByte() }.map { + when { + it.key.size() != 33 -> return Either.Left(ParseFailure.InvalidTxInput("taproot derivation path key must contain exactly 32 bytes")) + else -> { + val xonlyPublicKey = XonlyPublicKey(it.key.drop(1).toByteArray().byteVector32()) + val valueInput = ByteArrayInput(it.value.toByteArray()) + val numLeaves = BtcSerializer.varint(valueInput).toInt() + val leaves = (0 until numLeaves).map { BtcSerializer.bytes(valueInput, 32).byteVector32() } + val masterKeyFingerprint = Pack.int32BE(valueInput).toLong() + val childCount = (valueInput.availableBytes / 4) + val paths = KeyPath((0 until childCount).map { _ -> BtcSerializer.uint32(valueInput).toLong() }) + xonlyPublicKey to TaprootBip32DerivationPath(leaves, masterKeyFingerprint, paths) + } + } + }.toMap() + createOutput(redeemScript, witnessScript, derivationPaths, taprootInternalKey, taprootDerivationPaths, unknown) } return if (input.availableBytes != 0) { @@ -997,6 +1167,9 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< sha256: Set, hash160: Set, hash256: Set, + taprootKeySignature: ByteVector?, + taprootDerivationPaths: Map, + taprootInternalKey: XonlyPublicKey?, unknown: List ): Input { val emptied = redeemScript == null && witnessScript == null && partialSigs.isEmpty() && derivationPaths.isEmpty() && sighashType == null @@ -1006,9 +1179,9 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< witnessUtxo != null && scriptWitness != null && emptied -> Input.WitnessInput.FinalizedWitnessInput(witnessUtxo, nonWitnessUtxo, scriptWitness, scriptSig, ripemd160, sha256, hash160, hash256, unknown) nonWitnessUtxo != null && scriptSig != null && emptied -> Input.NonWitnessInput.FinalizedNonWitnessInput(nonWitnessUtxo, txIn.outPoint.index.toInt(), scriptSig, ripemd160, sha256, hash160, hash256, unknown) (scriptSig != null || scriptWitness != null) && emptied -> Input.FinalizedInputWithoutUtxo(scriptWitness, scriptSig, ripemd160, sha256, hash160, hash256, unknown) - witnessUtxo != null -> Input.WitnessInput.PartiallySignedWitnessInput(witnessUtxo, nonWitnessUtxo, sighashType, partialSigs, derivationPaths, redeemScript, witnessScript, ripemd160, sha256, hash160, hash256, unknown) + witnessUtxo != null -> Input.WitnessInput.PartiallySignedWitnessInput(witnessUtxo, nonWitnessUtxo, sighashType, partialSigs, derivationPaths, redeemScript, witnessScript, ripemd160, sha256, hash160, hash256, taprootKeySignature, taprootDerivationPaths, taprootInternalKey, unknown) nonWitnessUtxo != null -> Input.NonWitnessInput.PartiallySignedNonWitnessInput(nonWitnessUtxo, txIn.outPoint.index.toInt(), sighashType, partialSigs, derivationPaths, redeemScript, ripemd160, sha256, hash160, hash256, unknown) - else -> Input.PartiallySignedInputWithoutUtxo(sighashType, derivationPaths, ripemd160, sha256, hash160, hash256, unknown) + else -> Input.PartiallySignedInputWithoutUtxo(sighashType, derivationPaths, ripemd160, sha256, hash160, hash256, taprootKeySignature, taprootDerivationPaths, taprootInternalKey, unknown) // @formatter:on } } @@ -1017,11 +1190,13 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< redeemScript: List?, witnessScript: List?, derivationPaths: Map, + taprootInternalKey: XonlyPublicKey?, + taprootDerivationPaths: Map, unknown: List ): Output = when { - witnessScript != null -> Output.WitnessOutput(witnessScript, redeemScript, derivationPaths, unknown) + witnessScript != null -> Output.WitnessOutput(witnessScript, redeemScript, derivationPaths, taprootInternalKey, taprootDerivationPaths, unknown) redeemScript != null -> Output.NonWitnessOutput(redeemScript, derivationPaths, unknown) - else -> Output.UnspecifiedOutput(derivationPaths, unknown) + else -> Output.UnspecifiedOutput(derivationPaths, taprootInternalKey, taprootDerivationPaths, unknown) } private sealed class ReadEntryFailure { @@ -1041,6 +1216,7 @@ public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List< Either.Right(entries) } } + is ReadEntryFailure.InvalidData -> Either.Left(ReadEntryFailure.InvalidData) else -> Either.Left(result.value) } @@ -1076,6 +1252,13 @@ public data class ExtendedPublicKeyWithMaster(@JvmField val prefix: Long, @JvmFi */ public data class KeyPathWithMaster(@JvmField val masterKeyFingerprint: Long, @JvmField val keyPath: KeyPath) + +/** + * @param masterKeyFingerprint fingerprint of the master key. + * @param keyPath bip 32 derivation path. + */ +public data class TaprootBip32DerivationPath(@JvmField val leaves: List, @JvmField val masterKeyFingerprint: Long, @JvmField val keyPath: KeyPath) + public data class DataEntry(@JvmField val key: ByteVector, @JvmField val value: ByteVector) /** @@ -1122,6 +1305,12 @@ public sealed class Input { public abstract val hash160: Set /** Hash256 preimages (e.g. for miniscript hash challenges). */ public abstract val hash256: Set + /** taproot keypath signature */ + public abstract val taprootKeySignature: ByteVector? + /** Derivation paths used for taproot signatures, key is the internal key */ + public abstract val taprootDerivationPaths: Map + /** Internal key used for taproot signatures */ + public abstract val taprootInternalKey: XonlyPublicKey? /** (optional) Unknown global entries. */ public abstract val unknown: List // @formatter:on @@ -1137,7 +1326,10 @@ public sealed class Input { override val sha256: Set, override val hash160: Set, override val hash256: Set, - override val unknown: List + override val taprootKeySignature: ByteVector?, + override val taprootDerivationPaths: Map, + override val taprootInternalKey: XonlyPublicKey?, + override val unknown: List, ) : Input() { override val nonWitnessUtxo: Transaction? = null override val witnessUtxo: TxOut? = null @@ -1167,6 +1359,9 @@ public sealed class Input { override val sighashType: Int? = null override val partialSigs: Map = mapOf() override val derivationPaths: Map = mapOf() + override val taprootKeySignature: ByteVector? = null + override val taprootDerivationPaths: Map = mapOf() + override val taprootInternalKey: XonlyPublicKey? = null override val redeemScript: List? = null override val witnessScript: List? = null } @@ -1190,6 +1385,9 @@ public sealed class Input { override val sha256: Set, override val hash160: Set, override val hash256: Set, + override val taprootKeySignature: ByteVector?, + override val taprootDerivationPaths: Map, + override val taprootInternalKey: XonlyPublicKey?, override val unknown: List ) : WitnessInput() { override val scriptSig: List? = null @@ -1211,6 +1409,9 @@ public sealed class Input { override val sighashType: Int? = null override val partialSigs: Map = mapOf() override val derivationPaths: Map = mapOf() + override val taprootKeySignature: ByteVector? = null + override val taprootDerivationPaths: Map = mapOf() + override val taprootInternalKey: XonlyPublicKey? = null override val redeemScript: List? = null override val witnessScript: List? = null } @@ -1226,6 +1427,9 @@ public sealed class Input { // The following fields should only be present for inputs which spend segwit outputs (including P2SH embedded ones). override val witnessUtxo: TxOut? = null override val witnessScript: List? = null + override val taprootKeySignature: ByteVector? = null + override val taprootDerivationPaths: Map = mapOf() + override val taprootInternalKey: XonlyPublicKey? = null /** A partially signed non-segwit input. More signatures may need to be added before it can be finalized. */ public data class PartiallySignedNonWitnessInput( @@ -1275,6 +1479,10 @@ public sealed class Output { public abstract val witnessScript: List? /** Derivation paths used to produce the public keys associated to this output. */ public abstract val derivationPaths: Map + /** Internal key used to produce the public key associated to this output. */ + public abstract val taprootInternalKey: XonlyPublicKey? + /** Taproot Derivation paths used to produce the public keys associated to this output. */ + public abstract val taprootDerivationPaths: Map /** (optional) Unknown global entries. */ public abstract val unknown: List // @formatter:on @@ -1286,6 +1494,8 @@ public sealed class Output { override val unknown: List ) : Output() { override val witnessScript: List? = null + override val taprootInternalKey: XonlyPublicKey? = null + override val taprootDerivationPaths: Map = mapOf() } /** A segwit output. */ @@ -1293,12 +1503,16 @@ public sealed class Output { override val witnessScript: List?, override val redeemScript: List?, override val derivationPaths: Map, + override val taprootInternalKey: XonlyPublicKey?, + override val taprootDerivationPaths: Map, override val unknown: List ) : Output() /** An output for which usage of segwit is currently unknown. */ public data class UnspecifiedOutput( override val derivationPaths: Map, + override val taprootInternalKey: XonlyPublicKey?, + override val taprootDerivationPaths: Map, override val unknown: List ) : Output() { override val redeemScript: List? = null diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/psbt/PsbtTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/psbt/PsbtTestsCommon.kt index eec88935..67781ad7 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/psbt/PsbtTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/psbt/PsbtTestsCommon.kt @@ -24,6 +24,8 @@ import fr.acinq.bitcoin.SigHash.SIGHASH_SINGLE import fr.acinq.bitcoin.utils.Either import fr.acinq.bitcoin.utils.flatMap import fr.acinq.secp256k1.Hex +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.test.* class PsbtTestsCommon { @@ -711,7 +713,7 @@ class PsbtTestsCommon { ), listOf(OP_2DROP), listOf(OP_1), - setOf(), setOf(), setOf(), setOf(), listOf() + setOf(), setOf(), setOf(), setOf(), null, mapOf(), null, listOf() ), Input.WitnessInput.PartiallySignedWitnessInput( inputTx2.txOut[0], @@ -721,7 +723,7 @@ class PsbtTestsCommon { mapOf(), listOf(OP_RETURN), listOf(OP_8), - setOf(), setOf(), setOf(), setOf(), listOf() + setOf(), setOf(), setOf(), setOf(), null, mapOf(), null, listOf() ) ), listOf( @@ -733,7 +735,7 @@ class PsbtTestsCommon { ), listOf() ), - Output.WitnessOutput(listOf(OP_4), listOf(OP_ADD), mapOf(), listOf()) + Output.WitnessOutput(listOf(OP_4), listOf(OP_ADD), mapOf(), null, mapOf(), listOf()) ) ) assertEquals(combined.right, expected) @@ -886,9 +888,9 @@ class PsbtTestsCommon { 0 ) val psbt = Psbt(globalTx) - assertEquals(psbt.getInput(2), Input.PartiallySignedInputWithoutUtxo(null, mapOf(), setOf(), setOf(), setOf(), setOf(), listOf())) + assertEquals(psbt.getInput(2), Input.PartiallySignedInputWithoutUtxo(null, mapOf(), setOf(), setOf(), setOf(), setOf(), null, mapOf(), null, listOf())) assertNull(psbt.getInput(6)) - assertEquals(psbt.getInput(OutPoint(inputTx, 1)), Input.PartiallySignedInputWithoutUtxo(null, mapOf(), setOf(), setOf(), setOf(), setOf(), listOf())) + assertEquals(psbt.getInput(OutPoint(inputTx, 1)), Input.PartiallySignedInputWithoutUtxo(null, mapOf(), setOf(), setOf(), setOf(), setOf(), null, mapOf(), null, listOf())) assertNull(psbt.getInput(OutPoint(TxHash(ByteVector32.Zeroes), 0))) // We can't sign the psbt before adding the utxo details (updater role). @@ -1218,6 +1220,95 @@ class PsbtTestsCommon { assertEquals(updated.outputs[1].derivationPaths, mapOf(priv2.publicKey() to KeyPathWithMaster(0, KeyPath("m/7'")))) } + @OptIn(ExperimentalEncodingApi::class) + private fun readPsbt(base64: String): Psbt = Psbt.read(Base64.decode(StringBuilder(base64))).right!! + + @Test + fun `update and sign BIP84 transactions`() { + val (_, xprv) = DeterministicWallet.ExtendedPrivateKey.decode("tprv8ZgxMBicQKsPcxF57E46D8PbHjCT52N8k3MMv866bLMSb8JE5WyQfmTwZysBpCM2GxPjnHaj2rknNASmQXzACfXv6WSGCYTgHrVqjFFFLkZ") + val unsigned = + "cHNidP8BAJoCAAAAAk9/vNA/BH6KortUyvRY2vwTyXJIj7gk0H9IqLUrgzfBAAAAAAD9////gToNNEw2bL6QvOs2jkyKddHoJirxi4rCXrMTE1v+ReYAAAAAAP3///8CADtMAAAAAAAWABRAQtq+BJH+5C8o6/TnlJLJqQeBgUB4fQEAAAAAFgAUIc3AXNCi5djPdVHJa/mgkpEI04wAAAAAAAEAcQIAAAABdSEVHlJ9Ew9DAOcKbHiwjChVe+5PcPNThXdhk3mdD2YBAAAAAP3///8CAC0xAQAAAAAWABReCUIlAdwsMi1K4Fo1h48jv+au+GAXPCgBAAAAFgAU9DPe48u3pTRG0fdURX+4iPVjATiGAAAAAQEfAC0xAQAAAAAWABReCUIlAdwsMi1K4Fo1h48jv+au+CIGAoTn1edGP2J/n+wBG1X+7wLkW2QvytthcpL18w6nfZUeGBptno9UAACAAQAAgAAAAIAAAAAABAAAAAABAHECAAAAAWVylKM/oSuybxSZTFuCDpaQQtQbn6ryUoHlnCixBHVOAQAAAAD9////AoCWmAAAAAAAFgAUJIPtpCTZrH7xfUzaGOCPu833FutgFzwoAQAAABYAFLQYho7ZH1c4eTEi5Weist4VYeGrmQAAAAEBH4CWmAAAAAAAFgAUJIPtpCTZrH7xfUzaGOCPu833FusiBgOQNU/Ja9jwChLrkTcpjgDflY/HJ3PiBLLFbelWHiR/ghgabZ6PVAAAgAEAAIAAAACAAAAAAAMAAAAAIgIDUWh+lhIj3q3i7d5FQ0bo6rKFCCmun7KvXithtO/vjAwYGm2ej1QAAIABAACAAAAAgAEAAAAEAAAAACICAqZl0TkJI9ELk6ENXE4AB/Ks4D3Q/fZUFDrE9R6lwBTeGBptno9UAACAAQAAgAAAAIABAAAAAwAAAAA=" + val psbt = readPsbt(unsigned) + // all inputs our ours + psbt.inputs.forEach { input -> + val spentAddress = Bitcoin.addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, input.witnessUtxo!!.publicKeyScript.toByteArray()).right!! + input.derivationPaths.forEach { + val address = Bitcoin.computeBIP84Address(it.key, Block.RegtestGenesisBlock.hash) + assertEquals(spentAddress, address) + val privateKey = DeterministicWallet.derivePrivateKey(xprv, it.value.keyPath) + assertEquals(privateKey.publicKey, it.key) + } + } + // output #0 is ours + run { + val output = psbt.outputs.get(0) + assertEquals(1, output.derivationPaths.size) + val pub = output.derivationPaths.keys.first() + val path = output.derivationPaths.values.first().keyPath + val privateKey = DeterministicWallet.derivePrivateKey(xprv, path) + assertEquals(pub, privateKey.publicKey) + } + val privateKey0 = DeterministicWallet.derivePrivateKey(xprv, "m/84'/1'/0'/0/4").privateKey + val privateKey1 = DeterministicWallet.derivePrivateKey(xprv, "m/84'/1'/0'/0/3").privateKey + val signedTx = psbt.updateWitnessInput(psbt.global.tx.txIn.get(0).outPoint, psbt.inputs[0].witnessUtxo!!, witnessScript = Script.pay2pkh(privateKey0.publicKey())) + .flatMap { it.updateWitnessInput(psbt.global.tx.txIn.get(1).outPoint, psbt.inputs[1].witnessUtxo!!, witnessScript = Script.pay2pkh(privateKey1.publicKey())) } + .flatMap { it.sign(privateKey0, 0) } + .flatMap { it.psbt.sign(privateKey1, 1) } + .flatMap { + val sig0 = it.psbt.inputs[0].partialSigs[privateKey0.publicKey()]!! + it.psbt.finalizeWitnessInput(0, Script.witnessPay2wpkh(privateKey0.publicKey(), sig0)) + } + .flatMap { + val sig1 = it.inputs[1].partialSigs[privateKey1.publicKey()]!! + it.finalizeWitnessInput(1, Script.witnessPay2wpkh(privateKey1.publicKey(), sig1)) + } + .flatMap { it.extract() }.right!! + + val spent = psbt.global.tx.txIn.mapIndexed { i, input -> input.outPoint to psbt.inputs[i].witnessUtxo!! }.toMap() + Transaction.correctlySpends(signedTx, spent, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + @Test + fun `update and sign BIP86 transactions`() { + val (_, xprv) = DeterministicWallet.ExtendedPrivateKey.decode("tprv8ZgxMBicQKsPcxF57E46D8PbHjCT52N8k3MMv866bLMSb8JE5WyQfmTwZysBpCM2GxPjnHaj2rknNASmQXzACfXv6WSGCYTgHrVqjFFFLkZ") + val unsigned = + "cHNidP8BALICAAAAAnUhFR5SfRMPQwDnCmx4sIwoVXvuT3DzU4V3YZN5nQ9mAAAAAAD9////ZXKUoz+hK7JvFJlMW4IOlpBC1BufqvJSgeWcKLEEdU4AAAAAAP3///8CQHh9AQAAAAAiUSAch/yYu5X/xhIE0dJBpxXmcoO7apfuWe9h5r8TUgarmrA6TAAAAAAAIlEgumvX915DsXN95wy9i1NwNIHRD5FvsZS0MhYJSc6scEMAAAAAAAEBK4CWmAAAAAAAIlEgA5k6sLVM+T6e9de+9yNooBVxOTk7z66oI/d6zSK2KD4hFsLRgzgW8EenrwQnN/PEbArhJKNuS9ReeGO20zZ+Wg42GQAabZ6PVgAAgAEAAIAAAACAAAAAAAAAAAABFyDC0YM4FvBHp68EJzfzxGwK4SSjbkvUXnhjttM2floONgABASsALTEBAAAAACJRIJ2xWJk+z3wCH+bPz6YWMzwpL64NCTbnGfGEj43RURvwIRZ5NDy7Cbeva2F/gQNs7M+iMbpZS2Hzi+KLLTwnE7ozJhkAGm2ej1YAAIABAACAAAAAgAAAAAABAAAAARcgeTQ8uwm3r2thf4EDbOzPojG6WUth84viiy08JxO6MyYAAQUgeLISjnykqyiWLXYOkmgNeyCwi4+YZ6hfz9qPzarjrsIhB3iyEo58pKsoli12DpJoDXsgsIuPmGeoX8/aj82q467CGQAabZ6PVgAAgAEAAIAAAACAAQAAAAAAAAAAAQUg+5RjG+KiYu+L/qzi6MhAxq+zbB0bLXmgMBK4bF2xxcwhB/uUYxviomLvi/6s4ujIQMavs2wdGy15oDASuGxdscXMGQAabZ6PVgAAgAEAAIAAAACAAQAAAAEAAAAA" + val psbt = readPsbt(unsigned) + // all inputs our ours + psbt.inputs.forEach { input -> + val spentAddress = Bitcoin.addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, input.witnessUtxo!!.publicKeyScript.toByteArray()).right!! + input.taprootDerivationPaths.forEach { + val address = Bitcoin.computeBIP86Address(it.key, Block.RegtestGenesisBlock.hash) + assertEquals(spentAddress, address) + val privateKey = DeterministicWallet.derivePrivateKey(xprv, it.value.keyPath) + assertEquals(privateKey.publicKey.xOnly(), input.taprootInternalKey) + } + } + // output #0 is ours + run { + val output = psbt.outputs.get(0) + val path = output.taprootDerivationPaths[output.taprootInternalKey!!]!! + val privateKey = DeterministicWallet.derivePrivateKey(xprv, path.keyPath) + assertEquals(output.taprootInternalKey!!, privateKey.publicKey.xOnly()) + } + val privateKey0 = DeterministicWallet.derivePrivateKey(xprv, "m/86'/1'/0'/0/0").privateKey + val privateKey1 = DeterministicWallet.derivePrivateKey(xprv, "m/86'/1'/0'/0/1").privateKey + val signedTx = psbt.sign(privateKey0, 0) + .flatMap { it.psbt.sign(privateKey1, 1) } + .flatMap { + val sig0 = it.psbt.inputs[0].taprootKeySignature!! + it.psbt.finalizeWitnessInput(0, ScriptWitness(listOf(sig0))) + } + .flatMap { + val sig1 = it.inputs[1].taprootKeySignature!! + it.finalizeWitnessInput(1, ScriptWitness(listOf(sig1))) + } + .flatMap { it.extract() }.right!! + + val spent = psbt.global.tx.txIn.mapIndexed { i, input -> input.outPoint to psbt.inputs[i].witnessUtxo!! }.toMap() + Transaction.correctlySpends(signedTx, spent, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + private fun readValidPsbt(hex: String): Psbt { val result = Psbt.read(ByteVector(hex)) assertTrue(result.isRight)