diff --git a/core-test/src/test/scala/org/bitcoins/core/wallet/signer/SignerTest.scala b/core-test/src/test/scala/org/bitcoins/core/wallet/signer/SignerTest.scala index 87c528fa66..b604eba176 100644 --- a/core-test/src/test/scala/org/bitcoins/core/wallet/signer/SignerTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/wallet/signer/SignerTest.scala @@ -2,10 +2,11 @@ package org.bitcoins.core.wallet.signer import org.bitcoins.core.crypto.{ BaseTxSigComponent, + TaprootTxSigComponent, WitnessTxSigComponentP2SH, WitnessTxSigComponentRaw } -import org.bitcoins.core.currency.{CurrencyUnits, Satoshis} +import org.bitcoins.core.currency.{Bitcoins, CurrencyUnits, Satoshis} import org.bitcoins.core.number.UInt32 import org.bitcoins.core.policy.Policy import org.bitcoins.core.protocol.script.* @@ -14,13 +15,14 @@ import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.script.PreExecutionScriptProgram import org.bitcoins.core.script.interpreter.ScriptInterpreter +import org.bitcoins.core.script.util.PreviousOutputMap import org.bitcoins.core.wallet.builder.{ RawTxSigner, StandardNonInteractiveFinalizer } import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.utxo.* -import org.bitcoins.crypto.ECDigitalSignature +import org.bitcoins.crypto.{ECDigitalSignature, ECPrivateKey, HashType} import org.bitcoins.testkitcore.gen.{ CreditingTxGen, GenUtil, @@ -178,7 +180,14 @@ class SignerTest extends BitcoinSUnitTest { o, Policy.standardFlags ) - case _: UnassignedWitnessScriptPubKey | _: TaprootScriptPubKey => ??? + case _: TaprootScriptPubKey => + TaprootTxSigComponent( + tx.asInstanceOf[WitnessTransaction], + UInt32(idx), + utxo.inputInfo.asInstanceOf[TaprootInputInfo].previousOutputMap, + Policy.standardFlags + ) + case _: UnassignedWitnessScriptPubKey => ??? case x @ (_: P2PKScriptPubKey | _: P2PKHScriptPubKey | _: P2PKWithTimeoutScriptPubKey | _: MultiSignatureScriptPubKey | _: WitnessCommitment | _: CSVScriptPubKey | _: CLTVScriptPubKey | @@ -213,10 +222,9 @@ class SignerTest extends BitcoinSUnitTest { utxos: Vector[InputSigningInfo[InputInfo]] ): Boolean = { val programs: Vector[PreExecutionScriptProgram] = - tx.inputs.zipWithIndex.toVector.map { - case (input: TransactionInput, idx: Int) => - val utxo = utxos.find(_.outPoint == input.previousOutput).get - createProgram(tx, idx, utxo) + tx.inputs.zipWithIndex.map { case (input: TransactionInput, idx: Int) => + val utxo = utxos.find(_.outPoint == input.previousOutput).get + createProgram(tx, idx, utxo) } ScriptInterpreter.runAllVerify(programs) } @@ -275,4 +283,51 @@ class SignerTest extends BitcoinSUnitTest { assert(verifyScripts(signedTx.get, creditingTxsInfos.toVector)) } } + + it must "sign a p2tr keypath utxo" in { + val privKey = ECPrivateKey.freshPrivateKey + val xonly = privKey.toXOnly + val spk = TaprootScriptPubKey(xonly) + val output = TransactionOutput(Bitcoins.one, spk) + val creditingTx = BaseTransaction( + version = TransactionConstants.version, + inputs = Vector.empty, + outputs = Vector(output), + lockTime = TransactionConstants.lockTime + ) + + val outPoint = TransactionOutPoint(creditingTx.txIdBE, 0) + val input = TransactionInput(outPoint, + ScriptSignature.empty, + TransactionConstants.sequence) + val output1 = TransactionOutput(Bitcoins.one, EmptyScriptPubKey) + val unsignedTx = WitnessTransaction( + version = TransactionConstants.version, + inputs = Vector(input), + outputs = Vector(output1), + lockTime = TransactionConstants.lockTime, + witness = EmptyWitness.fromN(1) + ) + + val previousOutputMap = PreviousOutputMap(Map(outPoint -> output)) + val inputInfo: TaprootKeyPathInputInfo = TaprootKeyPathInputInfo( + outPoint, + output.value, + spk, + previousOutputMap = previousOutputMap + ) + val sigParams = ECSignatureParams(inputInfo = inputInfo, + prevTransaction = creditingTx, + signer = privKey, + hashType = HashType.sigHashDefault) + + val sigComponent = TaprootKeyPathSigner.sign( + spendingInfo = sigParams.toScriptSignatureParams, + unsignedTx = unsignedTx, + spendingInfoToSatisfy = sigParams.toScriptSignatureParams) + + val result = + verifyScripts(sigComponent.transaction, utxos = Vector(sigParams)) + assert(result) + } } diff --git a/core/src/main/scala/org/bitcoins/core/crypto/TransactionSignatureCreator.scala b/core/src/main/scala/org/bitcoins/core/crypto/TransactionSignatureCreator.scala index 6883341365..ded4fbe001 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/TransactionSignatureCreator.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/TransactionSignatureCreator.scala @@ -94,6 +94,7 @@ sealed abstract class TransactionSignatureCreator { * level, a hardware wallet expects a scodec.bits.ByteVector as input, and * returns an [[ECDigitalSignature]] if it is able to sign the * scodec.bits.ByteVector's correctly. + * * @param sign * \- the implementation of the hardware wallet protocol to sign the * scodec.bits.ByteVector w/ the given public key @@ -121,11 +122,11 @@ sealed abstract class TransactionSignatureCreator { /** This is the same as createSig above, except the 'sign' function returns a * Future[ECDigitalSignature] */ - def createSig( + def createSig[Sig <: DigitalSignature]( spendingTransaction: Transaction, signingInfo: InputSigningInfo[InputInfo], - sign: (ByteVector, HashType) => Future[ECDigitalSignature], - hashType: HashType): Future[ECDigitalSignature] = { + sign: (ByteVector, HashType) => Future[Sig], + hashType: HashType): Future[Sig] = { val hash = TransactionSignatureSerializer.hashForSignature( spendingTransaction, diff --git a/core/src/main/scala/org/bitcoins/core/crypto/TxSigComponent.scala b/core/src/main/scala/org/bitcoins/core/crypto/TxSigComponent.scala index 7f1f522e99..4a44b2ccf5 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/TxSigComponent.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/TxSigComponent.scala @@ -58,6 +58,8 @@ object TxSigComponent { outputMap: PreviousOutputMap, flags: Seq[ScriptFlag] = Policy.standardFlags): TxSigComponent = { inputInfo match { + case kp: TaprootKeyPathInputInfo => + fromWitnessInput(kp, unsignedTx, flags) case segwit: SegwitV0NativeInputInfo => fromWitnessInput(segwit, unsignedTx, flags) case unassigned: UnassignedSegwitNativeInputInfo => @@ -113,6 +115,18 @@ object TxSigComponent { WitnessTxSigComponent(wtx, UInt32(idx), inputInfo.output, outputMap, flags) } + def fromWitnessInput( + info: TaprootKeyPathInputInfo, + unsignedTx: Transaction, + flags: Seq[ScriptFlag]): TaprootTxSigComponent = { + val inputIndex: UInt32 = UInt32(info.inputIndex) + val wtx = setTransactionWitness(info, unsignedTx) + TaprootTxSigComponent(transaction = wtx, + inputIndex = inputIndex, + outputMap = info.previousOutputMap, + flags = flags) + } + def fromWitnessInput( inputInfo: P2SHNestedSegwitV0InputInfo, unsignedTx: Transaction, diff --git a/core/src/main/scala/org/bitcoins/core/protocol/script/WitnessVersion.scala b/core/src/main/scala/org/bitcoins/core/protocol/script/WitnessVersion.scala index f9bc8d3954..273d4c0a42 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/script/WitnessVersion.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/script/WitnessVersion.scala @@ -82,23 +82,17 @@ case object WitnessVersion1 extends WitnessVersion { programBytes.size match { case 32 => // p2tr - if (scriptWitness.stack.isEmpty) { - Left(ScriptErrorWitnessProgramWitnessEmpty) - } else { - val rebuiltSPK = scriptWitness match { - case _: TaprootKeyPath => - Right(witnessSPK) - case sp: TaprootScriptPath => - Right(sp.script) - case _: TaprootUnknownPath => - Right(witnessSPK) - case w @ (EmptyScriptWitness | _: P2WPKHWitnessV0 | - _: P2WSHWitnessV0) => - sys.error( - s"Cannot rebuild witnessv1 with a non v1 witness, got=$w") - } - rebuiltSPK + val rebuiltSPK = scriptWitness match { + case _: TaprootKeyPath | EmptyScriptWitness => + Right(witnessSPK) + case sp: TaprootScriptPath => + Right(sp.script) + case _: TaprootUnknownPath => + Right(witnessSPK) + case w @ (_: P2WPKHWitnessV0 | _: P2WSHWitnessV0) => + sys.error(s"Cannot rebuild witnessv1 with a non v1 witness, got=$w") } + rebuiltSPK case _ => // witness version 1 programs need to be 32 bytes in size // this is technically wrong as this is dependent on a policy flag diff --git a/core/src/main/scala/org/bitcoins/core/protocol/transaction/InputUtil.scala b/core/src/main/scala/org/bitcoins/core/protocol/transaction/InputUtil.scala index 685837beb3..43b98d0a33 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/transaction/InputUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/transaction/InputUtil.scala @@ -82,7 +82,8 @@ object InputUtil { loop(conditional.nestedInputInfo +: newRemaining, accum) case _: P2WPKHV0InputInfo | _: UnassignedSegwitNativeInputInfo | _: P2PKInputInfo | _: P2PKHInputInfo | - _: MultiSignatureInputInfo | _: EmptyInputInfo => + _: MultiSignatureInputInfo | _: EmptyInputInfo | + _: TaprootKeyPathInputInfo => // none of these script types affect the sequence number of a tx so the defaultSequence is used val input = TransactionInput(spendingInfo.outPoint, diff --git a/core/src/main/scala/org/bitcoins/core/protocol/transaction/TxUtil.scala b/core/src/main/scala/org/bitcoins/core/protocol/transaction/TxUtil.scala index 582fd690c2..1ed4e28b0f 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/transaction/TxUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/transaction/TxUtil.scala @@ -5,6 +5,7 @@ import org.bitcoins.core.number.UInt32 import org.bitcoins.core.policy.Policy import org.bitcoins.core.protocol.script.* import org.bitcoins.core.script.control.OP_RETURN +import org.bitcoins.core.script.util.PreviousOutputMap import org.bitcoins.core.wallet.builder.{ AddWitnessDataFinalizer, RawTxBuilder, @@ -111,7 +112,8 @@ object TxUtil { currentLockTimeOpt) case _: P2WPKHV0InputInfo | _: UnassignedSegwitNativeInputInfo | _: P2PKInputInfo | _: P2PKHInputInfo | - _: MultiSignatureInputInfo | _: EmptyInputInfo => + _: MultiSignatureInputInfo | _: EmptyInputInfo | + _: TaprootKeyPathInputInfo => // none of these scripts affect the locktime of a tx loop(newRemaining, currentLockTimeOpt) } @@ -134,7 +136,9 @@ object TxUtil { outputs: Vector[TransactionOutput]): Transaction = { val dummySpendingInfos = utxos.map { inputInfo => val mockSigners = - inputInfo.pubKeys.take(inputInfo.requiredSigs).map(Sign.dummySign) + inputInfo.pubKeys + .take(inputInfo.requiredSigs) + .map(p => Sign.dummySign(p)) inputInfo.toSpendingInfo(EmptyTransaction, mockSigners, @@ -372,10 +376,30 @@ object TxUtil { } } + def inputIndexOpt( + inputInfo: InputInfo, + previousOutputMap: PreviousOutputMap): Option[Int] = { + previousOutputMap.zipWithIndex + .find(_._1._1 == inputInfo.outPoint) + .map(_._2) + } + + def inputIndex( + inputInfo: InputInfo, + previousOutputMap: PreviousOutputMap): Int = { + inputIndexOpt(inputInfo, previousOutputMap) match { + case Some(i) => i + case None => + throw new IllegalArgumentException( + s"The transaction did not contain the expected outPoint (${inputInfo.outPoint}), got $previousOutputMap") + } + } + /** Returns the index of the InputInfo in the transaction */ def inputIndexOpt(inputInfo: InputInfo, tx: Transaction): Option[Int] = { tx.inputs.zipWithIndex .find(_._1.previousOutput == inputInfo.outPoint) .map(_._2) } + } diff --git a/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala b/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala index f68cbf5ef1..befa959057 100644 --- a/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala +++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala @@ -913,7 +913,7 @@ object InputPSBTMap extends PSBTMapFactory[InputPSBTRecord, InputPSBTMap] { .sign(spendingInfo, unsignedTx) val utxos = spendingInfo.inputInfo match { - case _: UnassignedSegwitNativeInputInfo => + case _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo => Vector(WitnessUTXO(spendingInfo.output)) case _: RawInputInfo | _: P2SHNonSegwitInputInfo | _: SegwitV0NativeInputInfo | _: P2SHNestedSegwitV0InputInfo => @@ -971,7 +971,7 @@ object InputPSBTMap extends PSBTMapFactory[InputPSBTRecord, InputPSBTMap] { val builder = Vector.newBuilder[InputPSBTRecord] spendingInfo.inputInfo match { - case _: UnassignedSegwitNativeInputInfo => + case _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo => builder.+=(WitnessUTXO(spendingInfo.output)) case _: RawInputInfo | _: P2SHNonSegwitInputInfo | _: SegwitV0NativeInputInfo | _: P2SHNestedSegwitV0InputInfo => @@ -996,7 +996,7 @@ object InputPSBTMap extends PSBTMapFactory[InputPSBTRecord, InputPSBTMap] { case p2wsh: P2WSHV0InputInfo => builder.+=(WitnessScript(p2wsh.scriptWitness.redeemScript)) case _: RawInputInfo | _: P2WPKHV0InputInfo | - _: UnassignedSegwitNativeInputInfo => + _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo => () } diff --git a/core/src/main/scala/org/bitcoins/core/wallet/signer/Signer.scala b/core/src/main/scala/org/bitcoins/core/wallet/signer/Signer.scala index 819ef28256..afc1af0d37 100644 --- a/core/src/main/scala/org/bitcoins/core/wallet/signer/Signer.scala +++ b/core/src/main/scala/org/bitcoins/core/wallet/signer/Signer.scala @@ -34,7 +34,7 @@ sealed abstract class SignerUtils { val tx = spendingInfo.inputInfo match { case _: SegwitV0NativeInputInfo | _: P2SHNestedSegwitV0InputInfo | - _: UnassignedSegwitNativeInputInfo => + _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo => TxUtil.addWitnessData(unsignedTx, spendingInfo) case _: RawInputInfo | _: P2SHNonSegwitInputInfo => unsignedTx @@ -186,6 +186,10 @@ object BitcoinSigner extends SignerUtils { P2WPKHSigner.sign(spendingInfo, unsignedTx, spendingFrom(p2wpkh)) case pw2sh: P2WSHV0InputInfo => P2WSHSigner.sign(spendingInfo, unsignedTx, spendingFrom(pw2sh)) + case trk: TaprootKeyPathInputInfo => + TaprootKeyPathSigner.sign(spendingInfo, + unsignedTx, + spendingInfoToSatisfy = spendingFrom(trk)) case _: UnassignedSegwitNativeInputInfo => throw new UnsupportedOperationException("Unsupported Segwit version") } @@ -613,3 +617,70 @@ sealed abstract class ConditionalSigner extends Signer[ConditionalInputInfo] { } } object ConditionalSigner extends ConditionalSigner + +sealed abstract class TaprootKeyPathSigner + extends Signer[TaprootKeyPathInputInfo] { + + /** The method used to sign a bitcoin unspent transaction output that is + * potentially nested + * + * @param spendingInfo + * \- The information required for signing + * @param unsignedTx + * the external Transaction that needs an input signed + * @param isDummySignature + * \- do not sign the tx for real, just use a dummy signature this is + * useful for fee estimation + * @param spendingInfoToSatisfy + * \- specifies the NewSpendingInfo whose ScriptPubKey needs a + * ScriptSignature to be generated + * @return + */ + override def sign( + spendingInfo: ScriptSignatureParams[InputInfo], + unsignedTx: Transaction, + spendingInfoToSatisfy: ScriptSignatureParams[TaprootKeyPathInputInfo]) + : TaprootTxSigComponent = { + if (spendingInfoToSatisfy != spendingInfo) { + throw TxBuilderError.WrongSigner.exception + } else { + unsignedTx match { + case wtx: WitnessTransaction => + val signer = spendingInfoToSatisfy.signer + val inputIndex = spendingInfoToSatisfy.inputInfo.inputIndex + val hashType = spendingInfo.hashType + val unsignedTxWitness = TransactionWitness( + wtx.witness + .updated(inputIndex, + spendingInfoToSatisfy.inputInfo.scriptWitness) + .toVector) + + val unsignedWtx = wtx.copy(witness = unsignedTxWitness) + + val signature: SchnorrDigitalSignature = + doSign(unsignedTx, + spendingInfo, + signer.schnorrSignWithHashType, + hashType) + val scriptWitness = TaprootKeyPath(signature) + val signedTxWitness = + wtx.witness.updated(inputIndex, scriptWitness) + val signedTx = unsignedWtx.copy(witness = signedTxWitness) + + TaprootTxSigComponent( + transaction = signedTx, + inputIndex = UInt32(inputIndex), + outputMap = spendingInfoToSatisfy.inputInfo.previousOutputMap, + flags = flags) + + case btx: NonWitnessTransaction => + val wtx = WitnessTransaction.toWitnessTx(btx) + sign(spendingInfo, wtx, spendingInfoToSatisfy) + } + + } + } + +} + +object TaprootKeyPathSigner extends TaprootKeyPathSigner diff --git a/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputInfo.scala b/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputInfo.scala index 4e3ad32233..4de6478c39 100644 --- a/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputInfo.scala +++ b/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputInfo.scala @@ -14,7 +14,10 @@ import org.bitcoins.crypto.{ ECPublicKeyBytes, HashType, NetworkElement, - Sign + PublicKey, + SchnorrDigitalSignature, + Sign, + XOnlyPubKey } import scala.annotation.tailrec @@ -43,7 +46,7 @@ sealed trait InputInfo { def conditionalPath: ConditionalPath - def pubKeys: Vector[ECPublicKey] + def pubKeys: Vector[PublicKey] def requiredSigs: Int @@ -87,7 +90,7 @@ object InputInfo { def getRedeemScript(inputInfo: InputInfo): Option[ScriptPubKey] = { inputInfo match { case _: RawInputInfo | _: SegwitV0NativeInputInfo | - _: UnassignedSegwitNativeInputInfo => + _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo => None case info: P2SHInputInfo => Some(info.redeemScript) } @@ -99,6 +102,7 @@ object InputInfo { case info: SegwitV0NativeInputInfo => Some(info.scriptWitness) case info: P2SHNestedSegwitV0InputInfo => Some(info.scriptWitness) case info: UnassignedSegwitNativeInputInfo => Some(info.scriptWitness) + case info: TaprootKeyPathInputInfo => Some(info.scriptWitness) } } @@ -111,7 +115,8 @@ object InputInfo { case info: ConditionalInputInfo => info.hashPreImages case _: UnassignedSegwitNativeInputInfo | _: EmptyInputInfo | _: P2PKInputInfo | _: P2PKWithTimeoutInputInfo | - _: MultiSignatureInputInfo | _: P2WPKHV0InputInfo => + _: MultiSignatureInputInfo | _: P2WPKHV0InputInfo | + _: TaprootKeyPathInputInfo => Vector.empty } } @@ -200,7 +205,8 @@ object InputInfo { val dummyLowRHashType = ECDigitalSignature.dummyLowR.appendHashType(HashType.sigHashAll) info match { - case _: SegwitV0NativeInputInfo | _: UnassignedSegwitNativeInputInfo => + case _: SegwitV0NativeInputInfo | _: UnassignedSegwitNativeInputInfo | + _: TaprootKeyPathInputInfo => ScriptSigLenAndStackHeight(0, 0) case info: P2SHInputInfo => val serializedRedeemScript = ScriptConstant(info.redeemScript.asmBytes) @@ -263,6 +269,7 @@ object InputInfo { (stackHeightByteSize + redeemScriptSize + scriptSigLen).toInt case info: P2SHNestedSegwitV0InputInfo => maxWitnessLen(info.nestedInputInfo) + case _: TaprootKeyPathInputInfo => 65 // schnorr signature + hash type case _: UnassignedSegwitNativeInputInfo => throw new IllegalArgumentException( s"Cannot compute witness for unknown segwit InputInfo, got $info") @@ -351,6 +358,8 @@ object InputInfo { sealed trait RawInputInfo extends InputInfo { override def scriptPubKey: RawScriptPubKey override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty + + override def pubKeys: Vector[ECPublicKey] } object RawInputInfo { @@ -596,6 +605,8 @@ case class P2WPKHV0InputInfo( override def pubKeys: Vector[ECPublicKey] = Vector(pubKey) override def requiredSigs: Int = 1 + + override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty } case class P2WSHV0InputInfo( @@ -619,6 +630,8 @@ case class P2WSHV0InputInfo( override def pubKeys: Vector[ECPublicKey] = nestedInputInfo.pubKeys override def requiredSigs: Int = nestedInputInfo.requiredSigs + + override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty } case class UnassignedSegwitNativeInputInfo( @@ -630,6 +643,7 @@ case class UnassignedSegwitNativeInputInfo( pubKeys: Vector[ECPublicKey]) extends InputInfo { override def requiredSigs: Int = pubKeys.length + override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty } @@ -642,7 +656,13 @@ sealed trait P2SHInputInfo extends InputInfo { def nestedInputInfo: InputInfo - override def pubKeys: Vector[ECPublicKey] = nestedInputInfo.pubKeys + override def pubKeys: Vector[ECPublicKey] = { + val p = nestedInputInfo.pubKeys + require( + p.forall(_.isInstanceOf[ECPublicKey]), + s"Cannot have non ECPublicKey inside P2SHInputInfo, got=${nestedInputInfo.pubKeys}") + p.map(_.asInstanceOf[ECPublicKey]) + } override def requiredSigs: Int = nestedInputInfo.requiredSigs @@ -659,6 +679,8 @@ case class P2SHNonSegwitInputInfo( override val nestedInputInfo: RawInputInfo = RawInputInfo(outPoint, amount, redeemScript, conditionalPath, hashPreImages) + + override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty } case class P2SHNestedSegwitV0InputInfo( @@ -681,4 +703,38 @@ case class P2SHNestedSegwitV0InputInfo( scriptWitness, conditionalPath, hashPreImages) + + override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty +} + +sealed trait TaprootInputInfo extends InputInfo { + def scriptWitness: TaprootWitness + + override def scriptPubKey: TaprootScriptPubKey + + override def pubKeys: Vector[XOnlyPubKey] + + override def conditionalPath: ConditionalPath = ConditionalPath.NoCondition +} + +case class TaprootKeyPathInputInfo( + outPoint: TransactionOutPoint, + amount: CurrencyUnit, + scriptPubKey: TaprootScriptPubKey, + previousOutputMap: PreviousOutputMap) + extends TaprootInputInfo { + require( + previousOutputMap.outputMap.exists(_._2.scriptPubKey == scriptPubKey), + s"PreviousOutputMap did not contain spk we are spending=$scriptPubKey") + require( + previousOutputMap.outputMap.exists(_._1 == outPoint), + s"PreviousOutputMap did not contain outpoint we are spending=$outPoint") + override val requiredSigs: Int = 1 + + override def pubKeys: Vector[XOnlyPubKey] = Vector(scriptPubKey.pubKey) + + override def scriptWitness: TaprootWitness = TaprootKeyPath( + SchnorrDigitalSignature.dummy) + + def inputIndex: Int = TxUtil.inputIndex(this, previousOutputMap) } diff --git a/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputSigningInfo.scala b/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputSigningInfo.scala index 025b27e2c8..6ec57060bd 100644 --- a/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputSigningInfo.scala +++ b/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputSigningInfo.scala @@ -3,11 +3,19 @@ package org.bitcoins.core.wallet.utxo import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.protocol.script.{ SigVersionBase, + SigVersionTaprootKeySpend, SigVersionWitnessV0, SignatureVersion } -import org.bitcoins.core.protocol.transaction._ -import org.bitcoins.crypto.{HashType, Sign} +import org.bitcoins.core.protocol.transaction.* +import org.bitcoins.crypto.{ + ECPublicKey, + HashType, + PublicKey, + SchnorrPublicKey, + Sign, + XOnlyPubKey +} /** Stores the information required to generate a signature (ECSignatureParams) * or to generate a script signature (ScriptSignatureParams) for a given @@ -33,8 +41,13 @@ sealed trait InputSigningInfo[+InputType <: InputInfo] { .outputs(outPoint.vout.toInt)}) does match the corresponding value $amount" ) - private val keysToSignFor = inputInfo.pubKeys - require(signers.map(_.publicKey).forall(keysToSignFor.contains), + private val keysToSignFor = inputInfo.pubKeys.map { + case ec: ECPublicKey => ec.toXOnly + case s: SchnorrPublicKey => s.publicKey.toXOnly + case x: XOnlyPubKey => x.publicKey.toXOnly + case p: PublicKey => sys.error(s"Not supported=$p") + } + require(signers.map(_.publicKey.toXOnly).forall(keysToSignFor.contains), s"Cannot have signers that do not sign for one of $keysToSignFor") def outputReference: OutputReference = inputInfo.outputReference @@ -50,6 +63,7 @@ sealed trait InputSigningInfo[+InputType <: InputInfo] { SigVersionWitnessV0 case _: P2SHNonSegwitInputInfo | _: RawInputInfo => SigVersionBase + case _: TaprootKeyPathInputInfo => SigVersionTaprootKeySpend case i: InputInfo => sys.error(s"Cannot determine SigVersion for unsupported inputInfo=$i") } diff --git a/crypto/src/main/scala/org/bitcoins/crypto/Sign.scala b/crypto/src/main/scala/org/bitcoins/crypto/Sign.scala index 590c739594..0647cda2dd 100644 --- a/crypto/src/main/scala/org/bitcoins/crypto/Sign.scala +++ b/crypto/src/main/scala/org/bitcoins/crypto/Sign.scala @@ -215,7 +215,15 @@ trait Sign extends AsyncSign { final def schnorrSignWithHashType( dataToSign: ByteVector, hashType: HashType): SchnorrDigitalSignature = { - schnorrSign(dataToSign).appendHashType(hashType) + val sigNoHashType = schnorrSign(dataToSign) + if (hashType == HashType.sigHashDefault) { + // BIP341 states that if we use default sighash don't append hash type byte + // to avoid potential malleability + // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-21 + sigNoHashType + } else { + sigNoHashType.appendHashType(hashType) + } } } @@ -282,9 +290,16 @@ object Sign { ) } - def dummySign(publicKey: ECPublicKey): Sign = { + def dummySign(publicKey: PublicKey): Sign = { + val ecPubKey = publicKey match { + case ec: ECPublicKey => ec + case schnorr: SchnorrPublicKey => schnorr.publicKey + case xonly: XOnlyPubKey => xonly.publicKey + case x: PublicKey => + sys.error(s"Unsupported PublicKey type for dummySign, got=$x") + } constant(ECDigitalSignature.dummyLowR, - publicKey, + ecPubKey, SchnorrDigitalSignature.dummy) } } diff --git a/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/ScriptGenerators.scala b/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/ScriptGenerators.scala index 35650cf75d..d425c9e2e8 100644 --- a/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/ScriptGenerators.scala +++ b/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/ScriptGenerators.scala @@ -420,6 +420,7 @@ sealed abstract class ScriptGenerators { ), multiSigScriptPubKey, p2wpkhSPKV0, + witnessScriptPubKeyV1, unassignedWitnessScriptPubKey, conditionalScriptPubKey(defaultMaxDepth), multiSignatureWithTimeoutScriptPubKey @@ -465,7 +466,8 @@ sealed abstract class ScriptGenerators { p2shScriptPubKey.map(truncate), witnessCommitment, conditionalScriptPubKey(defaultMaxDepth), - multiSignatureWithTimeoutScriptPubKey + multiSignatureWithTimeoutScriptPubKey, + witnessScriptPubKeyV1 ) } diff --git a/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/WitnessGenerators.scala b/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/WitnessGenerators.scala index f7dbfa9de7..0f519c98fd 100644 --- a/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/WitnessGenerators.scala +++ b/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/WitnessGenerators.scala @@ -35,7 +35,7 @@ sealed abstract class WitnessGenerators { val spk = if (spkBytes.isEmpty) EmptyScriptPubKey else NonStandardScriptPubKey(cmpctSPK.bytes ++ spkBytes) P2WSHWitnessV0(spk,scriptSig) }*/ - Gen.oneOf(p2wpkhWitnessV0, p2wshWitnessV0) + Gen.oneOf(p2wpkhWitnessV0, p2wshWitnessV0, taprootWitness) } def taprootWitness: Gen[TaprootWitness] = {