Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
commonMain.fr.acinq.bitcoin.psbt.Psbt.kt Maven / Gradle / Ivy
Go to download
A simple Kotlin Multiplatform library which implements most of the bitcoin protocol
/*
* Copyright 2021 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.bitcoin.psbt
import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.crypto.Pack
import fr.acinq.bitcoin.io.ByteArrayInput
import fr.acinq.bitcoin.io.ByteArrayOutput
import fr.acinq.bitcoin.io.readNBytes
import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.getOrElse
import kotlin.jvm.JvmField
import kotlin.jvm.JvmStatic
/**
* A partially signed bitcoin transaction: see https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki.
*
* @param global global psbt data containing the transaction to be signed.
* @param inputs signing data for each input of the transaction to be signed (order matches the unsigned tx).
* @param outputs signing data for each output of the transaction to be signed (order matches the unsigned tx).
*/
public data class Psbt(@JvmField val global: Global, @JvmField val inputs: List , @JvmField val outputs: List) {
init {
require(global.tx.txIn.size == inputs.size) { "there must be one partially signed input per input of the unsigned tx" }
require(global.tx.txOut.size == outputs.size) { "there must be one partially signed output per output of the unsigned tx" }
}
/**
* Implements the PSBT creator role; initializes a PSBT for the given unsigned transaction.
*
* @param tx unsigned transaction skeleton.
* @return the psbt with empty inputs and outputs.
*/
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(), null, mapOf(), null, listOf()) },
tx.txOut.map { Output.UnspecifiedOutput(mapOf(), null, mapOf(), listOf()) }
)
/**
* Implements the PSBT updater role; adds information about a given segwit utxo.
* When you have access to the complete input transaction, you should prefer [[updateWitnessInputTx]].
*
* @param outPoint utxo being spent.
* @param txOut transaction output for the provided outPoint.
* @param redeemScript redeem script if known and applicable (when using p2sh-embedded segwit).
* @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.
* @return psbt with the matching input updated.
*/
public fun updateWitnessInput(
outPoint: OutPoint,
txOut: TxOut,
redeemScript: List? = null,
witnessScript: List? = null,
sighashType: Int? = null,
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"))
val updatedInput = when (val input = inputs[inputIndex]) {
is Input.PartiallySignedInputWithoutUtxo -> Input.WitnessInput.PartiallySignedWitnessInput(
txOut,
input.nonWitnessUtxo,
sighashType ?: input.sighashType,
input.partialSigs,
input.derivationPaths + derivationPaths,
redeemScript,
witnessScript,
input.ripemd160,
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,
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"))
is Input.NonWitnessInput.FinalizedNonWitnessInput -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update segwit input: it has already been finalized"))
}
return Either.Right(this.copy(inputs = inputs.updated(inputIndex, updatedInput)))
}
/**
* Implements the PSBT updater role; adds information about a given segwit utxo.
* Note that we always fill the nonWitnessUtxo (see https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#cite_note-7).
*
* @param inputTx transaction containing the utxo.
* @param outputIndex index of the utxo in the inputTx.
* @param redeemScript redeem script if known and applicable (when using p2sh-embedded segwit).
* @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(
inputTx: Transaction,
outputIndex: Int,
redeemScript: List? = null,
witnessScript: List? = null,
sighashType: Int? = null,
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())
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"))
val updatedInput = when (val input = inputs[inputIndex]) {
is Input.PartiallySignedInputWithoutUtxo -> Input.WitnessInput.PartiallySignedWitnessInput(
inputTx.txOut[outputIndex],
inputTx,
sighashType ?: input.sighashType,
input.partialSigs,
input.derivationPaths + derivationPaths,
redeemScript,
witnessScript,
input.ripemd160,
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,
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"))
is Input.NonWitnessInput.FinalizedNonWitnessInput -> return Either.Left(UpdateFailure.CannotUpdateInput(inputIndex, "cannot update segwit input: it has already been finalized"))
}
return Either.Right(this.copy(inputs = inputs.updated(inputIndex, updatedInput)))
}
/**
* Implements the PSBT updater role; adds information about a given non-segwit utxo.
*
* @param inputTx transaction containing the utxo.
* @param outputIndex index of the utxo in the inputTx.
* @param redeemScript redeem script if known and applicable (when using p2sh).
* @param sighashType sighash type if one should be specified.
* @param derivationPaths derivation paths for keys used by this utxo.
* @return psbt with the matching input updated.
*/
public fun updateNonWitnessInput(
inputTx: Transaction,
outputIndex: Int,
redeemScript: List? = null,
sighashType: Int? = null,
derivationPaths: 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())
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"))
val updatedInput = when (val input = inputs[inputIndex]) {
is Input.PartiallySignedInputWithoutUtxo -> Input.NonWitnessInput.PartiallySignedNonWitnessInput(
inputTx,
outputIndex,
sighashType ?: input.sighashType,
input.partialSigs,
input.derivationPaths + derivationPaths,
redeemScript,
input.ripemd160,
input.sha256,
input.hash160,
input.hash256,
input.unknown
)
is Input.NonWitnessInput.PartiallySignedNonWitnessInput -> input.copy(
inputTx = inputTx,
outputIndex = outputIndex,
redeemScript = redeemScript ?: input.redeemScript,
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"))
}
return Either.Right(this.copy(inputs = inputs.updated(inputIndex, updatedInput)))
}
public fun updatePreimageChallenges(outPoint: OutPoint, ripemd160: Set, sha256: Set, hash160: Set, hash256: Set): 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"))
return updatePreimageChallenges(inputIndex, ripemd160, sha256, hash160, hash256)
}
public fun updatePreimageChallenges(inputIndex: Int, ripemd160: Set, sha256: Set, hash160: Set, hash256: Set): Either {
if (inputIndex >= inputs.size) return Either.Left(UpdateFailure.InvalidInput("input index must exist in the input tx"))
val updatedInput = when (val input = inputs[inputIndex]) {
is Input.PartiallySignedInputWithoutUtxo -> input.copy(ripemd160 = ripemd160 + input.ripemd160, sha256 = sha256 + input.sha256, hash160 = hash160 + input.hash160, hash256 = hash256 + input.hash256)
is Input.WitnessInput.PartiallySignedWitnessInput -> input.copy(ripemd160 = ripemd160 + input.ripemd160, sha256 = sha256 + input.sha256, hash160 = hash160 + input.hash160, hash256 = hash256 + input.hash256)
is Input.NonWitnessInput.PartiallySignedNonWitnessInput -> input.copy(ripemd160 = ripemd160 + input.ripemd160, sha256 = sha256 + input.sha256, hash160 = hash160 + input.hash160, hash256 = hash256 + input.hash256)
is Input.WitnessInput.FinalizedWitnessInput -> input.copy(ripemd160 = ripemd160 + input.ripemd160, sha256 = sha256 + input.sha256, hash160 = hash160 + input.hash160, hash256 = hash256 + input.hash256)
is Input.NonWitnessInput.FinalizedNonWitnessInput -> input.copy(ripemd160 = ripemd160 + input.ripemd160, sha256 = sha256 + input.sha256, hash160 = hash160 + input.hash160, hash256 = hash256 + input.hash256)
is Input.FinalizedInputWithoutUtxo -> input.copy(ripemd160 = ripemd160 + input.ripemd160, sha256 = sha256 + input.sha256, hash160 = hash160 + input.hash160, hash256 = hash256 + input.hash256)
}
return Either.Right(this.copy(inputs = inputs.updated(inputIndex, updatedInput)))
}
/**
* Add details for a segwit output.
*
* @param outputIndex index of the output in the psbt.
* @param witnessScript witness script if known and applicable (when using p2wsh).
* @param redeemScript redeem script if known and applicable (when using p2sh-embedded segwit).
* @param derivationPaths derivation paths for keys used by this output.
* @return psbt with the matching output updated.
*/
public fun updateWitnessOutput(
outputIndex: Int,
witnessScript: List? = null,
redeemScript: List? = null,
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]) {
is Output.NonWitnessOutput -> return Either.Left(UpdateFailure.CannotUpdateOutput(outputIndex, "cannot update segwit output: it has already been updated with non-segwit data"))
is Output.WitnessOutput -> output.copy(
witnessScript = witnessScript ?: output.witnessScript,
redeemScript = redeemScript ?: output.redeemScript,
derivationPaths = output.derivationPaths + derivationPaths,
taprootInternalKey = taprootInternalKey ?: output.taprootInternalKey,
taprootDerivationPaths = output.taprootDerivationPaths + taprootDerivationPaths
)
is Output.UnspecifiedOutput -> Output.WitnessOutput(
witnessScript,
redeemScript,
output.derivationPaths + derivationPaths,
taprootInternalKey ?: output.taprootInternalKey,
output.taprootDerivationPaths + taprootDerivationPaths,
output.unknown
)
}
return Either.Right(this.copy(outputs = outputs.updated(outputIndex, updatedOutput)))
}
/**
* Add details for a non-segwit output.
*
* @param outputIndex index of the output in the psbt.
* @param redeemScript redeem script if known and applicable (when using p2sh).
* @param derivationPaths derivation paths for keys used by this output.
* @return psbt with the matching output updated.
*/
public fun updateNonWitnessOutput(
outputIndex: Int,
redeemScript: List? = null,
derivationPaths: 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]) {
is Output.NonWitnessOutput -> output.copy(
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)
}
return Either.Right(this.copy(outputs = outputs.updated(outputIndex, updatedOutput)))
}
/**
* Implements the PSBT signer role: sign a given input.
* The caller needs to carefully verify that it wants to spend that input, and that the unsigned transaction matches
* what it expects.
*
* @param priv private key used to sign the input.
* @param outPoint input that should be signed.
* @return the psbt with a partial signature added (other inputs will not be modified).
*/
public fun sign(priv: PrivateKey, outPoint: OutPoint): 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"))
return sign(priv, inputIndex)
}
/**
* Implements the PSBT signer role: sign a given input.
* The caller needs to carefully verify that it wants to spend that input, and that the unsigned transaction matches
* what it expects.
*
* @param priv private key used to sign the input.
* @param inputIndex index of the input that should be signed.
* @return the psbt with a partial signature added (other inputs will not be modified).
*/
public fun sign(priv: PrivateKey, inputIndex: Int): Either {
if (inputIndex >= inputs.size) return Either.Left(UpdateFailure.InvalidInput("input index must exist in the input tx"))
val input = inputs[inputIndex]
return sign(priv, inputIndex, input, global).map { SignPsbtResult(this.copy(inputs = inputs.updated(inputIndex, it.first)), it.second) }
}
private fun sign(priv: PrivateKey, inputIndex: Int, input: Input, global: Global): Either> {
val txIn = global.tx.txIn[inputIndex]
return when (input) {
is Input.PartiallySignedInputWithoutUtxo -> Either.Left(UpdateFailure.CannotSignInput(inputIndex, "cannot sign: input hasn't been updated with utxo data"))
is Input.WitnessInput.PartiallySignedWitnessInput -> {
if (input.nonWitnessUtxo != null && input.nonWitnessUtxo!!.txid != txIn.outPoint.txid) {
Either.Left(UpdateFailure.InvalidNonWitnessUtxo("non-witness utxo does not match unsigned tx input"))
} else if (input.nonWitnessUtxo != null && input.nonWitnessUtxo!!.txOut.size <= txIn.outPoint.index) {
Either.Left(UpdateFailure.InvalidNonWitnessUtxo("non-witness utxo index out of bounds"))
} else if (!Script.isNativeWitnessScript(input.txOut.publicKeyScript) && !Script.isPayToScript(input.txOut.publicKeyScript.toByteArray())) {
Either.Left(UpdateFailure.InvalidWitnessUtxo("witness utxo must use native segwit or P2SH embedded segwit"))
} else {
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"))
} else if (input.inputTx.txOut.size <= txIn.outPoint.index) {
Either.Left(UpdateFailure.InvalidNonWitnessUtxo("non-witness utxo index out of bounds"))
} else {
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"))
}
}
private fun signNonWitness(priv: PrivateKey, inputIndex: Int, input: Input.NonWitnessInput.PartiallySignedNonWitnessInput, global: Global): Either> {
val txIn = global.tx.txIn[inputIndex]
val redeemScript = when (input.redeemScript) {
null -> runCatching {
Script.parse(input.inputTx.txOut[txIn.outPoint.index.toInt()].publicKeyScript)
}.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))
if (!input.inputTx.txOut[txIn.outPoint.index.toInt()].publicKeyScript.contentEquals(p2sh)) {
return Either.Left(UpdateFailure.InvalidNonWitnessUtxo("redeem script does not match non-witness utxo scriptPubKey"))
} else {
input.redeemScript
}
}
}
val sig = ByteVector(Transaction.signInput(global.tx, inputIndex, redeemScript, input.sighashType ?: SigHash.SIGHASH_ALL, input.amount, SigVersion.SIGVERSION_BASE, priv))
return Either.Right(Pair(input.copy(partialSigs = input.partialSigs + (priv.publicKey() to sig)), sig))
}
private fun signWitness(priv: PrivateKey, inputIndex: Int, input: Input.WitnessInput.PartiallySignedWitnessInput, global: Global): Either> {
val pubkeyScript = runCatching {
Script.parse(input.txOut.publicKeyScript)
}.getOrElse {
return Either.Left(UpdateFailure.InvalidWitnessUtxo("failed to parse pubkeyScript"))
}
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, 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))
}
}
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))
}
}
Script.isPay2tr(pubkeyScript) -> when (input.taprootInternalKey) {
null -> Either.Left(UpdateFailure.InvalidWitnessUtxo("missing taproot internal key"))
else -> {
// When spending taproot inputs, we include *all* of the transaction's inputs in the signed hash.
val spentOutputs = this.inputs.mapIndexedNotNull { idx, txIn -> txIn.witnessUtxo ?: txIn.nonWitnessUtxo?.txOut?.get(this.global.tx.txIn[idx].outPoint.index.toInt()) }
if (spentOutputs.size != this.inputs.size) {
Either.Left(UpdateFailure.InvalidInput("missing txOut for one of our inputs"))
} 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 -> {
val script = input.witnessScript ?: input.redeemScript ?: pubkeyScript
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.
*
* @param outPoint input that should be finalized.
* @param scriptWitness witness script.
* @return a psbt with the given input finalized.
*/
public fun finalizeWitnessInput(outPoint: OutPoint, scriptWitness: ScriptWitness): 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"))
return finalizeWitnessInput(inputIndex, scriptWitness)
}
/**
* 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.
*
* @param inputIndex index of the input that should be finalized.
* @param scriptWitness witness script.
* @return a psbt with the given input finalized.
*/
public fun finalizeWitnessInput(inputIndex: Int, scriptWitness: ScriptWitness): Either {
if (inputIndex >= inputs.size) return Either.Left(UpdateFailure.InvalidInput("input index must exist in the input tx"))
return when (val input = inputs[inputIndex]) {
is Input.PartiallySignedInputWithoutUtxo -> Either.Left(UpdateFailure.CannotFinalizeInput(inputIndex, "cannot finalize: input is missing utxo details"))
is Input.NonWitnessInput.PartiallySignedNonWitnessInput -> Either.Left(UpdateFailure.CannotFinalizeInput(inputIndex, "cannot finalize: input is a non-segwit input"))
is Input.WitnessInput.PartiallySignedWitnessInput -> {
val scriptSig = input.redeemScript?.let { script -> listOf(OP_PUSHDATA(Script.write(script))) } // p2sh-embedded segwit
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")))
}
}
/**
* Implements the PSBT finalizer role: finalizes a given non-segwit input.
* This will clear all fields from the input except the utxo, scriptSig and unknown entries.
*
* @param outPoint input that should be finalized.
* @param scriptSig signature script.
* @return a psbt with the given input finalized.
*/
public fun finalizeNonWitnessInput(outPoint: OutPoint, scriptSig: List): 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"))
return finalizeNonWitnessInput(inputIndex, scriptSig)
}
/**
* Implements the PSBT finalizer role: finalizes a given non-segwit input.
* This will clear all fields from the input except the utxo, scriptSig and unknown entries.
*
* @param inputIndex index of the input that should be finalized.
* @param scriptSig signature script.
* @return a psbt with the given input finalized.
*/
public fun finalizeNonWitnessInput(inputIndex: Int, scriptSig: List): Either {
if (inputIndex >= inputs.size) return Either.Left(UpdateFailure.InvalidInput("input index must exist in the input tx"))
return when (val input = inputs[inputIndex]) {
is Input.PartiallySignedInputWithoutUtxo -> Either.Left(UpdateFailure.CannotFinalizeInput(inputIndex, "cannot finalize: input is missing utxo details"))
is Input.WitnessInput.PartiallySignedWitnessInput -> Either.Left(UpdateFailure.CannotFinalizeInput(inputIndex, "cannot finalize: input is a segwit input"))
is Input.NonWitnessInput.PartiallySignedNonWitnessInput -> {
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")))
}
}
/**
* Implements the PSBT extractor role: extracts a valid transaction from the psbt data.
*
* @return a fully signed, ready-to-broadcast transaction.
*/
public fun extract(): Either {
val (finalTxsIn, utxos) = global.tx.txIn.zip(inputs).map { (txIn, input) ->
val finalTxIn = txIn.copy(
witness = input.scriptWitness ?: ScriptWitness.empty,
signatureScript = input.scriptSig?.let { ByteVector(Script.write(it)) } ?: ByteVector.empty
)
val utxo = when (input) {
is Input.NonWitnessInput.FinalizedNonWitnessInput -> {
if (input.inputTx.txid != txIn.outPoint.txid) return Either.Left(UpdateFailure.CannotExtractTx("non-witness utxo does not match unsigned tx input"))
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"))
}
Pair(finalTxIn, txIn.outPoint to utxo)
}.unzip()
val finalTx = global.tx.copy(txIn = finalTxsIn)
return try {
Transaction.correctlySpends(finalTx, utxos.toMap(), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
Either.Right(finalTx)
} catch (e: Exception) {
Either.Left(UpdateFailure.CannotExtractTx("extracted transaction doesn't pass standard script validation"))
}
}
/**
* Compute the fees paid by the PSBT.
* Note that if some inputs have not been updated yet, the fee cannot be computed.
*/
public fun computeFees(): Satoshi? {
val inputAmounts = inputs.map { input ->
when (input) {
is Input.WitnessInput -> input.amount
is Input.NonWitnessInput -> input.amount
else -> null
}
}
return when {
inputAmounts.any { it == null } -> null
else -> {
val amountOut = global.tx.txOut.sumOf { it.amount.sat }.toSatoshi()
val amountIn = inputAmounts.filterNotNull().sumOf { it.sat }.toSatoshi()
amountIn - amountOut
}
}
}
public fun getInput(outPoint: OutPoint): Input? {
val inputIndex = global.tx.txIn.indexOfFirst { it.outPoint == outPoint }
return if (inputIndex >= 0) inputs[inputIndex] else null
}
public fun getInput(inputIndex: Int): Input? {
return if (0 <= inputIndex && inputIndex < inputs.size) inputs[inputIndex] else null
}
public companion object {
/** Only version 0 is supported for now. */
public const val Version: Long = 0
/**
* Implements the PSBT combiner role: combines multiple psbts for the same unsigned transaction.
*
* @param psbts partially signed bitcoin transactions to combine.
* @return a psbt that contains data from all the input psbts.
*/
@JvmStatic
public fun combine(vararg psbts: Psbt): Either {
return when {
psbts.map { it.global.tx.txid }.toSet().size != 1 -> Either.Left(UpdateFailure.CannotCombine("cannot combine psbts for distinct transactions"))
psbts.map { it.inputs.size }.toSet() != setOf(psbts[0].global.tx.txIn.size) -> Either.Left(UpdateFailure.CannotCombine("some psbts have an invalid number of inputs"))
psbts.map { it.outputs.size }.toSet() != setOf(psbts[0].global.tx.txOut.size) -> Either.Left(UpdateFailure.CannotCombine("some psbts have an invalid number of outputs"))
else -> {
val global = psbts[0].global.copy(
unknown = combineUnknown(psbts.map { it.global.unknown }),
extendedPublicKeys = combineExtendedPublicKeys(psbts.map { it.global.extendedPublicKeys })
)
val combined = Psbt(
global,
global.tx.txIn.indices.map { i -> combineInput(global.tx.txIn[i], psbts.map { it.inputs[i] }) },
global.tx.txOut.indices.map { i -> combineOutput(psbts.map { it.outputs[i] }) }
)
Either.Right(combined)
}
}
}
private fun combineUnknown(unknowns: List>): List = unknowns.flatten().associateBy { it.key }.values.toList()
private fun combineExtendedPublicKeys(keys: List>): List = keys.flatten().associateBy { it.extendedPublicKey }.values.toList()
private fun combineInput(txIn: TxIn, inputs: List ): Input = createInput(
txIn,
inputs.firstNotNullOfOrNull { it.nonWitnessUtxo },
inputs.firstNotNullOfOrNull { it.witnessUtxo },
inputs.firstNotNullOfOrNull { it.sighashType },
inputs.flatMap { it.partialSigs.toList() }.toMap(),
inputs.flatMap { it.derivationPaths.toList() }.toMap(),
inputs.firstNotNullOfOrNull { it.redeemScript },
inputs.firstNotNullOfOrNull { it.witnessScript },
inputs.firstNotNullOfOrNull { it.scriptSig },
inputs.firstNotNullOfOrNull { it.scriptWitness },
inputs.flatMap { it.ripemd160 }.toSet(),
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 })
)
private fun combineOutput(outputs: List): Output = createOutput(
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 })
)
/**
* Joins multiple distinct PSBTs with different inputs and outputs into one PSBT with inputs and outputs from all of
* the PSBTs. No input in any of the PSBTs can be in more than one of the PSBTs.
*
* @param psbts partially signed bitcoin transactions to join.
* @return a psbt that contains data from all the input psbts.
*/
@JvmStatic
public fun join(vararg psbts: Psbt): Either {
return when {
psbts.isEmpty() -> Either.Left(UpdateFailure.CannotJoin("no psbt provided"))
psbts.map { it.global.version }.toSet().size != 1 -> Either.Left(UpdateFailure.CannotJoin("cannot join psbts with different versions"))
psbts.map { it.global.tx.version }.toSet().size != 1 -> Either.Left(UpdateFailure.CannotJoin("cannot join psbts with different tx versions"))
psbts.map { it.global.tx.lockTime }.toSet().size != 1 -> Either.Left(UpdateFailure.CannotJoin("cannot join psbts with different tx lockTime"))
psbts.any { it.global.tx.txIn.size != it.inputs.size || it.global.tx.txOut.size != it.outputs.size } -> Either.Left(UpdateFailure.CannotJoin("some psbts have an invalid number of inputs/outputs"))
psbts.flatMap { it.global.tx.txIn.map { txIn -> txIn.outPoint } }.toSet().size != psbts.sumOf { it.global.tx.txIn.size } -> Either.Left(UpdateFailure.CannotJoin("cannot join psbts that spend the same input"))
else -> {
val global = psbts[0].global.copy(
tx = psbts[0].global.tx.copy(
txIn = psbts.flatMap { it.global.tx.txIn },
txOut = psbts.flatMap { it.global.tx.txOut }
),
extendedPublicKeys = psbts.flatMap { it.global.extendedPublicKeys }.distinct(),
unknown = psbts.flatMap { it.global.unknown }.distinct()
)
Either.Right(psbts[0].copy(
global = global,
inputs = psbts.flatMap { it.inputs },
outputs = psbts.flatMap { it.outputs }
))
}
}
}
@JvmStatic
public fun write(psbt: Psbt): ByteVector {
val output = ByteArrayOutput()
write(psbt, output)
return ByteVector(output.toByteArray())
}
@JvmStatic
public fun write(psbt: Psbt, out: fr.acinq.bitcoin.io.Output) {
/********** Magic header **********/
out.write(0x70)
out.write(0x73)
out.write(0x62)
out.write(0x74)
out.write(0xff)
/********** Global types **********/
writeDataEntry(DataEntry(ByteVector("00"), ByteVector(Transaction.write(psbt.global.tx, Protocol.PROTOCOL_VERSION or Transaction.SERIALIZE_TRANSACTION_NO_WITNESS))), out)
psbt.global.extendedPublicKeys.forEach { xpub ->
val key = ByteArrayOutput()
key.write(0x01) //
Pack.writeInt32BE(xpub.prefix.toInt(), key)
DeterministicWallet.write(xpub.extendedPublicKey, key)
val value = ByteArrayOutput()
Pack.writeInt32BE(xpub.masterKeyFingerprint.toInt(), value)
xpub.extendedPublicKey.path.path.forEach { child -> Pack.writeInt32LE(child.toInt(), value) }
writeDataEntry(DataEntry(ByteVector(key.toByteArray()), ByteVector(value.toByteArray())), out)
}
if (psbt.global.version > 0) {
writeDataEntry(DataEntry(ByteVector("fb"), ByteVector(Pack.writeInt32LE(psbt.global.version.toInt()))), out)
}
psbt.global.unknown.forEach { writeDataEntry(it, out) }
out.write(0x00) // separator
/********** Inputs **********/
psbt.inputs.forEach { input ->
input.nonWitnessUtxo?.let { writeDataEntry(DataEntry(ByteVector("00"), ByteVector(Transaction.write(it))), out) }
input.witnessUtxo?.let { writeDataEntry(DataEntry(ByteVector("01"), ByteVector(TxOut.write(it))), out) }
sortPublicKeys(input.partialSigs).forEach { (publicKey, signature) -> writeDataEntry(DataEntry(ByteVector("02") + publicKey.value, signature), out) }
input.sighashType?.let { writeDataEntry(DataEntry(ByteVector("03"), ByteVector(Pack.writeInt32LE(it))), out) }
input.redeemScript?.let { writeDataEntry(DataEntry(ByteVector("04"), ByteVector(Script.write(it))), out) }
input.witnessScript?.let { writeDataEntry(DataEntry(ByteVector("05"), ByteVector(Script.write(it))), out) }
sortPublicKeys(input.derivationPaths).forEach { (publicKey, path) ->
val key = ByteVector("06") + publicKey.value
val value = ByteVector(Pack.writeInt32BE(path.masterKeyFingerprint.toInt())).concat(path.keyPath.path.map { ByteVector(Pack.writeInt32LE(it.toInt())) })
writeDataEntry(DataEntry(key, value), out)
}
input.scriptSig?.let { writeDataEntry(DataEntry(ByteVector("07"), ByteVector(Script.write(it))), out) }
input.scriptWitness?.let { writeDataEntry(DataEntry(ByteVector("08"), ByteVector(ScriptWitness.write(it))), out) }
input.ripemd160.forEach { writeDataEntry(DataEntry(ByteVector("0a") + Crypto.ripemd160(it), it), out) }
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 value = path.write().byteVector()
writeDataEntry(DataEntry(key, value), out)
}
input.taprootInternalKey?.let { writeDataEntry(DataEntry(ByteVector("17"), it.value), out) }
input.unknown.forEach { writeDataEntry(it, out) }
out.write(0x00) // separator
}
/********** Outputs **********/
psbt.outputs.forEach { output ->
output.redeemScript?.let { writeDataEntry(DataEntry(ByteVector("00"), ByteVector(Script.write(it))), out) }
output.witnessScript?.let { writeDataEntry(DataEntry(ByteVector("01"), ByteVector(Script.write(it))), out) }
sortPublicKeys(output.derivationPaths).forEach { (publicKey, path) ->
val key = ByteVector("02") + publicKey.value
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 value = path.write().byteVector()
writeDataEntry(DataEntry(key, value), out)
}
output.unknown.forEach { writeDataEntry(it, out) }
out.write(0x00) // separator
}
}
/** We use lexicographic ordering on the public keys. */
private fun sortPublicKeys(publicKeys: Map): 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)
BtcSerializer.writeVarint(entry.value.size(), output)
output.write(entry.value.bytes)
}
@JvmStatic
public fun read(input: ByteVector): Either = read(ByteArrayInput(input.toByteArray()))
@JvmStatic
public fun read(input: ByteArray): Either = read(ByteArrayInput(input))
@JvmStatic
public fun read(input: fr.acinq.bitcoin.io.Input): Either {
/********** Magic header **********/
if (input.read() != 0x70 || input.read() != 0x73 || input.read() != 0x62 || input.read() != 0x74) {
return Either.Left(ParseFailure.InvalidMagicBytes)
}
if (input.read() != 0xff) {
return Either.Left(ParseFailure.InvalidSeparator)
}
/********** Global types **********/
val global = run {
val globalMap = readDataMap(input).getOrElse {
return when (it) {
is ReadEntryFailure.DuplicateKeys -> Either.Left(ParseFailure.DuplicateKeys)
else -> Either.Left(ParseFailure.InvalidContent)
}
}
val keyTypes = setOf(0x00.toByte(), 0x01.toByte(), 0xfb.toByte())
val (known, unknown) = globalMap.partition { keyTypes.contains(it.key[0]) }
val version = known.find { it.key[0] == 0xfb.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidPsbtVersion("version key must contain exactly 1 byte"))
it.value.size() != 4 -> return Either.Left(ParseFailure.InvalidPsbtVersion("version must contain exactly 4 bytes"))
else -> {
val v = Pack.int32LE(it.value.bytes).toUInt().toLong()
when {
v > Version -> return Either.Left(ParseFailure.UnsupportedPsbtVersion(v))
else -> v
}
}
}
} ?: 0L
val tx = known.find { it.key[0] == 0x00.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidGlobalTx("global tx key must contain exactly 1 byte"))
else -> {
val tx = try {
Transaction.read(it.value.bytes, Protocol.PROTOCOL_VERSION or Transaction.SERIALIZE_TRANSACTION_NO_WITNESS)
} catch (e: Exception) {
return Either.Left(ParseFailure.InvalidGlobalTx(e.message ?: "failed to parse transaction"))
}
when {
tx.txIn.any { input -> input.hasWitness || !input.signatureScript.isEmpty() } -> return Either.Left(ParseFailure.InvalidGlobalTx("global tx inputs must have empty scriptSigs and witness"))
else -> tx
}
}
}
} ?: return Either.Left(ParseFailure.GlobalTxMissing)
val xpubs = known.filter { it.key[0] == 0x01.toByte() }.map {
when {
it.key.size() != 79 -> return Either.Left(ParseFailure.InvalidExtendedPublicKey(" must contain 78 bytes"))
else -> {
val xpub = ByteArrayInput(it.key.drop(1).toByteArray())
val prefix = Pack.int32BE(xpub).toUInt().toLong()
val depth = xpub.read()
val parent = Pack.int32BE(xpub).toUInt().toLong()
val childNumber = Pack.int32BE(xpub).toUInt().toLong()
val chainCode = ByteVector32(xpub.readNBytes(32)!!)
val publicKey = ByteVector(xpub.readNBytes(33)!!)
when {
it.value.size() != 4 * (depth + 1) -> return Either.Left(ParseFailure.InvalidExtendedPublicKey(" must contain the master key fingerprint and derivation path"))
else -> {
val masterKeyFingerprint = Pack.int32BE(it.value.take(4).toByteArray()).toUInt().toLong()
val derivationPath = KeyPath((0 until depth).map { i -> Pack.int32LE(it.value.slice(4 * (i + 1), 4 * (i + 2)).toByteArray()).toUInt().toLong() })
when {
derivationPath.lastChildNumber != childNumber -> return Either.Left(ParseFailure.InvalidExtendedPublicKey(" last child number mismatch"))
else -> ExtendedPublicKeyWithMaster(prefix, masterKeyFingerprint, DeterministicWallet.ExtendedPublicKey(publicKey, chainCode, depth, derivationPath, parent))
}
}
}
}
}
}
Global(version, tx, xpubs, unknown)
}
/********** Inputs **********/
val inputs = global.tx.txIn.map { txIn ->
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)
else -> Either.Left(ParseFailure.InvalidContent)
}
}
val (known, unknown) = entries.partition { keyTypes.contains(it.key[0]) }
val nonWitnessUtxo = known.find { it.key[0] == 0x00.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("non-witness utxo key must contain exactly 1 byte"))
else -> {
val inputTx = try {
Transaction.read(it.value.bytes)
} catch (e: Exception) {
return Either.Left(ParseFailure.InvalidTxInput(e.message ?: "failed to parse transaction"))
}
when {
inputTx.txid != txIn.outPoint.txid || txIn.outPoint.index >= inputTx.txOut.size -> return Either.Left(ParseFailure.InvalidTxInput("non-witness utxo does not match psbt outpoint"))
else -> inputTx
}
}
}
}
val witnessUtxo = known.find { it.key[0] == 0x01.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("witness utxo key must contain exactly 1 byte"))
else -> {
val txOut = try {
TxOut.read(it.value.bytes)
} catch (e: Exception) {
return Either.Left(ParseFailure.InvalidTxInput(e.message ?: "failed to parse transaction output"))
}
nonWitnessUtxo?.let { tx -> if (tx.txOut[txIn.outPoint.index.toInt()] != txOut) return Either.Left(ParseFailure.InvalidTxInput("witness utxo does not match non-witness utxo output")) }
txOut
}
}
}
val partialSigs = known.filter { it.key[0] == 0x02.toByte() }.map {
when {
it.key.size() != 34 -> return Either.Left(ParseFailure.InvalidTxInput("public key must contain exactly 33 bytes"))
else -> PublicKey(it.key.drop(1)) to it.value
}
}.toMap()
val sighashType = known.find { it.key[0] == 0x03.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("sighash type key must contain exactly 1 byte"))
it.value.size() != 4 -> return Either.Left(ParseFailure.InvalidTxInput("sighash type must contain exactly 4 bytes"))
else -> Pack.int32LE(it.value.bytes)
}
}
val redeemScript = known.find { it.key[0] == 0x04.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("redeem script key must contain exactly 1 byte"))
else -> runCatching { Script.parse(it.value) }.getOrElse { return Either.Left(ParseFailure.InvalidTxInput("failed to parse redeem script")) }
}
}
val witnessScript = known.find { it.key[0] == 0x05.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("witness script key must contain exactly 1 byte"))
else -> runCatching { Script.parse(it.value) }.getOrElse { return Either.Left(ParseFailure.InvalidTxInput("failed to parse witness script")) }
}
}
val derivationPaths = known.filter { it.key[0] == 0x06.toByte() }.map {
when {
it.key.size() != 34 -> return Either.Left(ParseFailure.InvalidTxInput("bip32 derivation public key must contain exactly 33 bytes"))
it.value.size() < 4 || it.value.size() % 4 != 0 -> return Either.Left(ParseFailure.InvalidTxInput("bip32 derivation must contain master key fingerprint and child indexes"))
else -> {
val publicKey = PublicKey(it.key.drop(1))
val masterKeyFingerprint = Pack.int32BE(it.value.take(4).toByteArray()).toUInt().toLong()
val childCount = (it.value.size() / 4) - 1
val derivationPath = KeyPath((0 until childCount).map { i -> Pack.int32LE(it.value.slice(4 * (i + 1), 4 * (i + 2)).toByteArray()).toUInt().toLong() })
publicKey to KeyPathWithMaster(masterKeyFingerprint, derivationPath)
}
}
}.toMap()
val scriptSig = known.find { it.key[0] == 0x07.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("script sig key must contain exactly 1 byte"))
else -> runCatching { Script.parse(it.value) }.getOrElse { return Either.Left(ParseFailure.InvalidTxInput("failed to parse script sig")) }
}
}
val scriptWitness = known.find { it.key[0] == 0x08.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxInput("script witness key must contain exactly 1 byte"))
else -> try {
ScriptWitness.read(it.value.bytes)
} catch (e: Exception) {
return Either.Left(ParseFailure.InvalidTxInput(e.message ?: "failed to parse script witness"))
}
}
}
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 path = TaprootBip32DerivationPath.read(it.value.toByteArray())
xonlyPublicKey to path
}
}
}.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"))
!it.key.drop(1).contentEquals(Crypto.ripemd160(it.value)) -> return Either.Left(ParseFailure.InvalidTxInput("invalid ripemd160 preimage"))
else -> it.value
}
}.toSet()
val sha256Preimages = known.filter { it.key[0] == 0x0b.toByte() }.map {
when {
it.key.size() != 33 -> return Either.Left(ParseFailure.InvalidTxInput("sha256 hash must contain exactly 32 bytes"))
!it.key.drop(1).contentEquals(Crypto.sha256(it.value)) -> return Either.Left(ParseFailure.InvalidTxInput("invalid sha256 preimage"))
else -> it.value
}
}.toSet()
val hash160Preimages = known.filter { it.key[0] == 0x0c.toByte() }.map {
when {
it.key.size() != 21 -> return Either.Left(ParseFailure.InvalidTxInput("hash160 hash must contain exactly 20 bytes"))
!it.key.drop(1).contentEquals(Crypto.hash160(it.value)) -> return Either.Left(ParseFailure.InvalidTxInput("invalid hash160 preimage"))
else -> it.value
}
}.toSet()
val hash256Preimages = known.filter { it.key[0] == 0x0d.toByte() }.map {
when {
it.key.size() != 33 -> return Either.Left(ParseFailure.InvalidTxInput("hash256 hash must contain exactly 32 bytes"))
!it.key.drop(1).contentEquals(Crypto.hash256(it.value)) -> return Either.Left(ParseFailure.InvalidTxInput("invalid hash256 preimage"))
else -> it.value
}
}.toSet()
createInput(
txIn,
nonWitnessUtxo,
witnessUtxo,
sighashType,
partialSigs,
derivationPaths,
redeemScript,
witnessScript,
scriptSig,
scriptWitness,
ripemd160Preimages,
sha256Preimages,
hash160Preimages,
hash256Preimages,
taprootKeySignature,
taprootDerivationPaths,
taprootInternalKey,
unknown
)
}
/********** Outputs **********/
val outputs = global.tx.txOut.map {
val keyTypes = setOf(0x00, 0x01, 0x02, 0x05, 0x07)
val entries = readDataMap(input).getOrElse {
return when (it) {
is ReadEntryFailure.DuplicateKeys -> Either.Left(ParseFailure.DuplicateKeys)
else -> Either.Left(ParseFailure.InvalidContent)
}
}
val (known, unknown) = entries.partition { keyTypes.contains(it.key[0]) }
val redeemScript = known.find { it.key[0] == 0x00.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxOutput("redeem script key must contain exactly 1 byte"))
else -> runCatching { Script.parse(it.value) }.getOrElse { return Either.Left(ParseFailure.InvalidTxOutput("failed to parse redeem script")) }
}
}
val witnessScript = known.find { it.key[0] == 0x01.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxOutput("witness script key must contain exactly 1 byte"))
else -> runCatching { Script.parse(it.value) }.getOrElse { return Either.Left(ParseFailure.InvalidTxOutput("failed to parse witness script")) }
}
}
val derivationPaths = known.filter { it.key[0] == 0x02.toByte() }.map {
when {
it.key.size() != 34 -> return Either.Left(ParseFailure.InvalidTxOutput("bip32 derivation public key must contain exactly 33 bytes"))
it.value.size() < 4 || it.value.size() % 4 != 0 -> return Either.Left(ParseFailure.InvalidTxOutput("bip32 derivation must contain master key fingerprint and child indexes"))
else -> {
val publicKey = PublicKey(it.key.drop(1))
val masterKeyFingerprint = Pack.int32BE(it.value.take(4).toByteArray()).toUInt().toLong()
val childCount = (it.value.size() / 4) - 1
val derivationPath = KeyPath((0 until childCount).map { i -> Pack.int32LE(it.value.slice(4 * (i + 1), 4 * (i + 2)).toByteArray()).toUInt().toLong() })
publicKey to KeyPathWithMaster(masterKeyFingerprint, derivationPath)
}
}
}.toMap()
val taprootInternalKey = known.find { it.key[0] == 0x05.toByte() }?.let {
when {
it.key.size() != 1 -> return Either.Left(ParseFailure.InvalidTxOutput("taproot internal key entry must have an empty key"))
it.value.size() != 32 -> return Either.Left(ParseFailure.InvalidTxOutput("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.InvalidTxOutput("taproot derivation path key must contain exactly 32 bytes"))
else -> {
val xonlyPublicKey = XonlyPublicKey(it.key.drop(1).toByteArray().byteVector32())
val path = TaprootBip32DerivationPath.read(it.value.toByteArray())
xonlyPublicKey to path
}
}
}.toMap()
createOutput(redeemScript, witnessScript, derivationPaths, taprootInternalKey, taprootDerivationPaths, unknown)
}
return if (input.availableBytes != 0) {
Either.Left(ParseFailure.InvalidContent)
} else {
Either.Right(Psbt(global, inputs, outputs))
}
}
private fun createInput(
txIn: TxIn,
nonWitnessUtxo: Transaction?,
witnessUtxo: TxOut?,
sighashType: Int?,
partialSigs: Map,
derivationPaths: Map,
redeemScript: List?,
witnessScript: List?,
scriptSig: List?,
scriptWitness: ScriptWitness?,
ripemd160: Set,
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
return when {
// @formatter:off
// If the input is finalized, it must have been emptied otherwise it's invalid.
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, 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, taprootKeySignature, taprootDerivationPaths, taprootInternalKey, unknown)
// @formatter:on
}
}
private fun createOutput(
redeemScript: List?,
witnessScript: List?,
derivationPaths: Map,
taprootInternalKey: XonlyPublicKey?,
taprootDerivationPaths: Map,
unknown: List
): Output = when {
witnessScript != null -> Output.WitnessOutput(witnessScript, redeemScript, derivationPaths, taprootInternalKey, taprootDerivationPaths, unknown)
redeemScript != null -> Output.NonWitnessOutput(redeemScript, derivationPaths, unknown)
else -> Output.UnspecifiedOutput(derivationPaths, taprootInternalKey, taprootDerivationPaths, unknown)
}
private sealed class ReadEntryFailure {
object DuplicateKeys : ReadEntryFailure()
object InvalidData : ReadEntryFailure()
object EndOfDataMap : ReadEntryFailure()
}
private fun readDataMap(input: fr.acinq.bitcoin.io.Input, entries: List = listOf()): Either> {
return when (val result = readDataEntry(input)) {
is Either.Right -> readDataMap(input, entries + result.value)
is Either.Left -> when (result.value) {
is ReadEntryFailure.EndOfDataMap -> {
if (entries.map { it.key }.toSet().size != entries.size) {
Either.Left(ReadEntryFailure.DuplicateKeys)
} else {
Either.Right(entries)
}
}
is ReadEntryFailure.InvalidData -> Either.Left(ReadEntryFailure.InvalidData)
else -> Either.Left(result.value)
}
}
}
private fun readDataEntry(input: fr.acinq.bitcoin.io.Input): Either {
if (input.availableBytes == 0) return Either.Left(ReadEntryFailure.InvalidData)
val keyLength = BtcSerializer.varint(input).toInt()
if (keyLength == 0) return Either.Left(ReadEntryFailure.EndOfDataMap)
val key = input.readNBytes(keyLength) ?: return Either.Left(ReadEntryFailure.InvalidData)
if (input.availableBytes == 0) return Either.Left(ReadEntryFailure.InvalidData)
val valueLength = BtcSerializer.varint(input).toInt()
val value = input.readNBytes(valueLength) ?: return Either.Left(ReadEntryFailure.InvalidData)
return Either.Right(DataEntry(ByteVector(key), ByteVector(value)))
}
}
}
/**
* @param prefix extended public key version bytes.
* @param masterKeyFingerprint fingerprint of the master key.
* @param extendedPublicKey BIP32 extended public key.
*/
public data class ExtendedPublicKeyWithMaster(@JvmField val prefix: Long, @JvmField val masterKeyFingerprint: Long, @JvmField val extendedPublicKey: DeterministicWallet.ExtendedPublicKey)
/**
* @param masterKeyFingerprint fingerprint of the master key.
* @param keyPath bip 32 derivation path.
*/
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 fun write(): ByteArray {
val out = ByteArrayOutput()
BtcSerializer.writeVarint(leaves.size, out)
leaves.forEach { BtcSerializer.writeBytes(it, out) }
BtcSerializer.writeBytes(ByteVector(Pack.writeInt32BE(masterKeyFingerprint.toInt())).concat(keyPath.path.map { ByteVector(Pack.writeInt32LE(it.toInt())) }), out)
return out.toByteArray()
}
public companion object {
public fun read(bin: ByteArray): TaprootBip32DerivationPath {
val input = ByteArrayInput(bin)
val numLeaves = BtcSerializer.varint(input).toInt()
val leaves = (0 until numLeaves).map { BtcSerializer.bytes(input, 32).byteVector32() }
val masterKeyFingerprint = Pack.int32BE(input).toLong()
val childCount = (input.availableBytes / 4)
val keyPath = KeyPath((0 until childCount).map { _ -> Pack.int32LE(input).toUInt().toLong() })
return TaprootBip32DerivationPath(leaves, masterKeyFingerprint, keyPath)
}
}
}
public data class DataEntry(@JvmField val key: ByteVector, @JvmField val value: ByteVector)
/**
* Global data for the PSBT.
*
* @param version psbt version.
* @param tx partially signed transaction. NB: the transaction must be serialized with the "old" format (without witnesses).
* @param extendedPublicKeys (optional) extended public keys used when signing inputs and producing outputs.
* @param unknown (optional) unknown global entries.
*/
public data class Global(
@JvmField val version: Long,
@JvmField val tx: Transaction,
@JvmField val extendedPublicKeys: List,
@JvmField val unknown: List
)
/** A PSBT input. A valid PSBT must contain one such input per input of the [[Global.tx]]. */
public sealed class Input {
// @formatter:off
/** Non-witness utxo, used when spending non-segwit outputs (may also be included when spending segwit outputs). */
public abstract val nonWitnessUtxo: Transaction?
/** Witness utxo, used when spending segwit outputs. */
public abstract val witnessUtxo: TxOut?
/** Sighash type to be used when producing signatures for this output. */
public abstract val sighashType: Int?
/** Signatures as would be pushed to the stack from a scriptSig or witness. */
public abstract val partialSigs: Map
/** Derivation paths used for the signatures. */
public abstract val derivationPaths: Map
/** Redeem script for this input (when using p2sh). */
public abstract val redeemScript: List?
/** Witness script for this input (when using p2wsh). */
public abstract val witnessScript: List?
/** Fully constructed scriptSig with signatures and any other scripts necessary for the input to pass validation. */
public abstract val scriptSig: List?
/** Fully constructed scriptWitness with signatures and any other scripts necessary for the input to pass validation. */
public abstract val scriptWitness: ScriptWitness?
/** RipeMD160 preimages (e.g. for miniscript hash challenges). */
public abstract val ripemd160: Set
/** Sha256 preimages (e.g. for miniscript hash challenges). */
public abstract val sha256: Set
/** Hash160 preimages (e.g. for miniscript hash challenges). */
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
/**
* A partially signed input without details about the utxo.
* More signatures may need to be added before it can be finalized.
*/
public data class PartiallySignedInputWithoutUtxo(
override val sighashType: Int?,
override val derivationPaths: Map,
override val ripemd160: Set,
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,
) : Input() {
override val nonWitnessUtxo: Transaction? = null
override val witnessUtxo: TxOut? = null
override val redeemScript: List? = null
override val witnessScript: List? = null
override val partialSigs: Map = mapOf()
override val scriptSig: List? = null
override val scriptWitness: ScriptWitness? = null
}
/**
* A fully signed input without details about the utxo.
* Input finalizers should keep the utxo to allow transaction extractors to verify the final network serialized
* transaction, but it's not mandatory, so we may not have it available when parsing psbt inputs.
*/
public data class FinalizedInputWithoutUtxo(
override val scriptWitness: ScriptWitness?,
override val scriptSig: List?,
override val ripemd160: Set,
override val sha256: Set,
override val hash160: Set,
override val hash256: Set,
override val unknown: List
) : Input() {
override val nonWitnessUtxo: Transaction? = null
override val witnessUtxo: TxOut? = null
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
}
/** An input spending a segwit output. */
public sealed class WitnessInput : Input() {
public abstract val txOut: TxOut
public val amount: Satoshi by lazy { txOut.amount }
override val witnessUtxo: TxOut? by lazy { txOut }
/** A partially signed segwit input. More signatures may need to be added before it can be finalized. */
public data class PartiallySignedWitnessInput(
override val txOut: TxOut,
override val nonWitnessUtxo: Transaction?,
override val sighashType: Int?,
override val partialSigs: Map,
override val derivationPaths: Map,
override val redeemScript: List?,
override val witnessScript: List?,
override val ripemd160: Set,
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
override val scriptWitness: ScriptWitness? = null
}
/** A fully signed segwit input. */
public data class FinalizedWitnessInput(
override val txOut: TxOut,
override val nonWitnessUtxo: Transaction?,
override val scriptWitness: ScriptWitness,
override val scriptSig: List?,
override val ripemd160: Set,
override val sha256: Set,
override val hash160: Set,
override val hash256: Set,
override val unknown: List
) : WitnessInput() {
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
}
}
/** An input spending a non-segwit output. */
public sealed class NonWitnessInput : Input() {
public abstract val inputTx: Transaction
public abstract val outputIndex: Int
public val amount: Satoshi by lazy { inputTx.txOut[outputIndex].amount }
override val nonWitnessUtxo: Transaction? by lazy { inputTx }
// 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(
override val inputTx: Transaction,
override val outputIndex: Int,
override val sighashType: Int?,
override val partialSigs: Map,
override val derivationPaths: Map,
override val redeemScript: List?,
override val ripemd160: Set,
override val sha256: Set,
override val hash160: Set,
override val hash256: Set,
override val unknown: List
) : NonWitnessInput() {
override val scriptSig: List? = null
override val scriptWitness: ScriptWitness? = null
}
/** A fully signed non-segwit input. */
public data class FinalizedNonWitnessInput(
override val inputTx: Transaction,
override val outputIndex: Int,
override val scriptSig: List,
override val ripemd160: Set,
override val sha256: Set,
override val hash160: Set,
override val hash256: Set,
override val unknown: List
) : NonWitnessInput() {
override val sighashType: Int? = null
override val partialSigs: Map = mapOf()
override val derivationPaths: Map = mapOf()
override val redeemScript: List? = null
override val witnessScript: List? = null
override val scriptWitness: ScriptWitness? = null
}
}
}
/** A PSBT output. A valid PSBT must contain one such output per output of the [[Global.tx]]. */
public sealed class Output {
// @formatter:off
/** Redeem script for this output (when using p2sh). */
public abstract val redeemScript: List?
/** Witness script for this output (when using p2wsh). */
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
/** A non-segwit output. */
public data class NonWitnessOutput(
override val redeemScript: List?,
override val derivationPaths: Map,
override val unknown: List
) : Output() {
override val witnessScript: List? = null
override val taprootInternalKey: XonlyPublicKey? = null
override val taprootDerivationPaths: Map = mapOf()
}
/** A segwit output. */
public data class WitnessOutput(
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
override val witnessScript: List? = null
}
}
public sealed class UpdateFailure {
public data class InvalidInput(val reason: String) : UpdateFailure()
public data class InvalidNonWitnessUtxo(val reason: String) : UpdateFailure()
public data class InvalidWitnessUtxo(val reason: String) : UpdateFailure()
public data class CannotCombine(val reason: String) : UpdateFailure()
public data class CannotJoin(val reason: String) : UpdateFailure()
public data class CannotUpdateInput(val index: Int, val reason: String) : UpdateFailure()
public data class CannotUpdateOutput(val index: Int, val reason: String) : UpdateFailure()
public data class CannotSignInput(val index: Int, val reason: String) : UpdateFailure()
public data class CannotFinalizeInput(val index: Int, val reason: String) : UpdateFailure()
public data class CannotExtractTx(val reason: String) : UpdateFailure()
}
public data class SignPsbtResult(val psbt: Psbt, val sig: ByteVector)
public sealed class ParseFailure {
public object InvalidMagicBytes : ParseFailure()
public object InvalidSeparator : ParseFailure()
public object DuplicateKeys : ParseFailure()
public data class InvalidPsbtVersion(val reason: String) : ParseFailure()
public data class UnsupportedPsbtVersion(val version: Long) : ParseFailure()
public data class InvalidGlobalTx(val reason: String) : ParseFailure()
public object GlobalTxMissing : ParseFailure()
public data class InvalidExtendedPublicKey(val reason: String) : ParseFailure()
public data class InvalidTxInput(val reason: String) : ParseFailure()
public data class InvalidTxOutput(val reason: String) : ParseFailure()
public object InvalidContent : ParseFailure()
}