2024 10 31 taproot signing (#5767)

* core: Implement TaprootKeyPath signing

* core: Rebase, remove isDummySignature

* Empty commit to run CI
This commit is contained in:
Chris Stewart 2024-11-18 09:43:29 -06:00 committed by GitHub
parent 80be2f5989
commit 67bb3ceabd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 295 additions and 48 deletions

View File

@ -2,10 +2,11 @@ package org.bitcoins.core.wallet.signer
import org.bitcoins.core.crypto.{ import org.bitcoins.core.crypto.{
BaseTxSigComponent, BaseTxSigComponent,
TaprootTxSigComponent,
WitnessTxSigComponentP2SH, WitnessTxSigComponentP2SH,
WitnessTxSigComponentRaw 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.number.UInt32
import org.bitcoins.core.policy.Policy import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.script.* 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.psbt.PSBT
import org.bitcoins.core.script.PreExecutionScriptProgram import org.bitcoins.core.script.PreExecutionScriptProgram
import org.bitcoins.core.script.interpreter.ScriptInterpreter import org.bitcoins.core.script.interpreter.ScriptInterpreter
import org.bitcoins.core.script.util.PreviousOutputMap
import org.bitcoins.core.wallet.builder.{ import org.bitcoins.core.wallet.builder.{
RawTxSigner, RawTxSigner,
StandardNonInteractiveFinalizer StandardNonInteractiveFinalizer
} }
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.* import org.bitcoins.core.wallet.utxo.*
import org.bitcoins.crypto.ECDigitalSignature import org.bitcoins.crypto.{ECDigitalSignature, ECPrivateKey, HashType}
import org.bitcoins.testkitcore.gen.{ import org.bitcoins.testkitcore.gen.{
CreditingTxGen, CreditingTxGen,
GenUtil, GenUtil,
@ -178,7 +180,14 @@ class SignerTest extends BitcoinSUnitTest {
o, o,
Policy.standardFlags 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 | case x @ (_: P2PKScriptPubKey | _: P2PKHScriptPubKey |
_: P2PKWithTimeoutScriptPubKey | _: MultiSignatureScriptPubKey | _: P2PKWithTimeoutScriptPubKey | _: MultiSignatureScriptPubKey |
_: WitnessCommitment | _: CSVScriptPubKey | _: CLTVScriptPubKey | _: WitnessCommitment | _: CSVScriptPubKey | _: CLTVScriptPubKey |
@ -213,10 +222,9 @@ class SignerTest extends BitcoinSUnitTest {
utxos: Vector[InputSigningInfo[InputInfo]] utxos: Vector[InputSigningInfo[InputInfo]]
): Boolean = { ): Boolean = {
val programs: Vector[PreExecutionScriptProgram] = val programs: Vector[PreExecutionScriptProgram] =
tx.inputs.zipWithIndex.toVector.map { tx.inputs.zipWithIndex.map { case (input: TransactionInput, idx: Int) =>
case (input: TransactionInput, idx: Int) => val utxo = utxos.find(_.outPoint == input.previousOutput).get
val utxo = utxos.find(_.outPoint == input.previousOutput).get createProgram(tx, idx, utxo)
createProgram(tx, idx, utxo)
} }
ScriptInterpreter.runAllVerify(programs) ScriptInterpreter.runAllVerify(programs)
} }
@ -275,4 +283,51 @@ class SignerTest extends BitcoinSUnitTest {
assert(verifyScripts(signedTx.get, creditingTxsInfos.toVector)) 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)
}
} }

View File

@ -94,6 +94,7 @@ sealed abstract class TransactionSignatureCreator {
* level, a hardware wallet expects a scodec.bits.ByteVector as input, and * level, a hardware wallet expects a scodec.bits.ByteVector as input, and
* returns an [[ECDigitalSignature]] if it is able to sign the * returns an [[ECDigitalSignature]] if it is able to sign the
* scodec.bits.ByteVector's correctly. * scodec.bits.ByteVector's correctly.
*
* @param sign * @param sign
* \- the implementation of the hardware wallet protocol to sign the * \- the implementation of the hardware wallet protocol to sign the
* scodec.bits.ByteVector w/ the given public key * 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 /** This is the same as createSig above, except the 'sign' function returns a
* Future[ECDigitalSignature] * Future[ECDigitalSignature]
*/ */
def createSig( def createSig[Sig <: DigitalSignature](
spendingTransaction: Transaction, spendingTransaction: Transaction,
signingInfo: InputSigningInfo[InputInfo], signingInfo: InputSigningInfo[InputInfo],
sign: (ByteVector, HashType) => Future[ECDigitalSignature], sign: (ByteVector, HashType) => Future[Sig],
hashType: HashType): Future[ECDigitalSignature] = { hashType: HashType): Future[Sig] = {
val hash = val hash =
TransactionSignatureSerializer.hashForSignature( TransactionSignatureSerializer.hashForSignature(
spendingTransaction, spendingTransaction,

View File

@ -58,6 +58,8 @@ object TxSigComponent {
outputMap: PreviousOutputMap, outputMap: PreviousOutputMap,
flags: Seq[ScriptFlag] = Policy.standardFlags): TxSigComponent = { flags: Seq[ScriptFlag] = Policy.standardFlags): TxSigComponent = {
inputInfo match { inputInfo match {
case kp: TaprootKeyPathInputInfo =>
fromWitnessInput(kp, unsignedTx, flags)
case segwit: SegwitV0NativeInputInfo => case segwit: SegwitV0NativeInputInfo =>
fromWitnessInput(segwit, unsignedTx, flags) fromWitnessInput(segwit, unsignedTx, flags)
case unassigned: UnassignedSegwitNativeInputInfo => case unassigned: UnassignedSegwitNativeInputInfo =>
@ -113,6 +115,18 @@ object TxSigComponent {
WitnessTxSigComponent(wtx, UInt32(idx), inputInfo.output, outputMap, flags) 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( def fromWitnessInput(
inputInfo: P2SHNestedSegwitV0InputInfo, inputInfo: P2SHNestedSegwitV0InputInfo,
unsignedTx: Transaction, unsignedTx: Transaction,

View File

@ -82,23 +82,17 @@ case object WitnessVersion1 extends WitnessVersion {
programBytes.size match { programBytes.size match {
case 32 => case 32 =>
// p2tr // p2tr
if (scriptWitness.stack.isEmpty) { val rebuiltSPK = scriptWitness match {
Left(ScriptErrorWitnessProgramWitnessEmpty) case _: TaprootKeyPath | EmptyScriptWitness =>
} else { Right(witnessSPK)
val rebuiltSPK = scriptWitness match { case sp: TaprootScriptPath =>
case _: TaprootKeyPath => Right(sp.script)
Right(witnessSPK) case _: TaprootUnknownPath =>
case sp: TaprootScriptPath => Right(witnessSPK)
Right(sp.script) case w @ (_: P2WPKHWitnessV0 | _: P2WSHWitnessV0) =>
case _: TaprootUnknownPath => sys.error(s"Cannot rebuild witnessv1 with a non v1 witness, got=$w")
Right(witnessSPK)
case w @ (EmptyScriptWitness | _: P2WPKHWitnessV0 |
_: P2WSHWitnessV0) =>
sys.error(
s"Cannot rebuild witnessv1 with a non v1 witness, got=$w")
}
rebuiltSPK
} }
rebuiltSPK
case _ => case _ =>
// witness version 1 programs need to be 32 bytes in size // witness version 1 programs need to be 32 bytes in size
// this is technically wrong as this is dependent on a policy flag // this is technically wrong as this is dependent on a policy flag

View File

@ -82,7 +82,8 @@ object InputUtil {
loop(conditional.nestedInputInfo +: newRemaining, accum) loop(conditional.nestedInputInfo +: newRemaining, accum)
case _: P2WPKHV0InputInfo | _: UnassignedSegwitNativeInputInfo | case _: P2WPKHV0InputInfo | _: UnassignedSegwitNativeInputInfo |
_: P2PKInputInfo | _: P2PKHInputInfo | _: P2PKInputInfo | _: P2PKHInputInfo |
_: MultiSignatureInputInfo | _: EmptyInputInfo => _: MultiSignatureInputInfo | _: EmptyInputInfo |
_: TaprootKeyPathInputInfo =>
// none of these script types affect the sequence number of a tx so the defaultSequence is used // none of these script types affect the sequence number of a tx so the defaultSequence is used
val input = val input =
TransactionInput(spendingInfo.outPoint, TransactionInput(spendingInfo.outPoint,

View File

@ -5,6 +5,7 @@ import org.bitcoins.core.number.UInt32
import org.bitcoins.core.policy.Policy import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.script.* import org.bitcoins.core.protocol.script.*
import org.bitcoins.core.script.control.OP_RETURN import org.bitcoins.core.script.control.OP_RETURN
import org.bitcoins.core.script.util.PreviousOutputMap
import org.bitcoins.core.wallet.builder.{ import org.bitcoins.core.wallet.builder.{
AddWitnessDataFinalizer, AddWitnessDataFinalizer,
RawTxBuilder, RawTxBuilder,
@ -111,7 +112,8 @@ object TxUtil {
currentLockTimeOpt) currentLockTimeOpt)
case _: P2WPKHV0InputInfo | _: UnassignedSegwitNativeInputInfo | case _: P2WPKHV0InputInfo | _: UnassignedSegwitNativeInputInfo |
_: P2PKInputInfo | _: P2PKHInputInfo | _: P2PKInputInfo | _: P2PKHInputInfo |
_: MultiSignatureInputInfo | _: EmptyInputInfo => _: MultiSignatureInputInfo | _: EmptyInputInfo |
_: TaprootKeyPathInputInfo =>
// none of these scripts affect the locktime of a tx // none of these scripts affect the locktime of a tx
loop(newRemaining, currentLockTimeOpt) loop(newRemaining, currentLockTimeOpt)
} }
@ -134,7 +136,9 @@ object TxUtil {
outputs: Vector[TransactionOutput]): Transaction = { outputs: Vector[TransactionOutput]): Transaction = {
val dummySpendingInfos = utxos.map { inputInfo => val dummySpendingInfos = utxos.map { inputInfo =>
val mockSigners = val mockSigners =
inputInfo.pubKeys.take(inputInfo.requiredSigs).map(Sign.dummySign) inputInfo.pubKeys
.take(inputInfo.requiredSigs)
.map(p => Sign.dummySign(p))
inputInfo.toSpendingInfo(EmptyTransaction, inputInfo.toSpendingInfo(EmptyTransaction,
mockSigners, 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 */ /** Returns the index of the InputInfo in the transaction */
def inputIndexOpt(inputInfo: InputInfo, tx: Transaction): Option[Int] = { def inputIndexOpt(inputInfo: InputInfo, tx: Transaction): Option[Int] = {
tx.inputs.zipWithIndex tx.inputs.zipWithIndex
.find(_._1.previousOutput == inputInfo.outPoint) .find(_._1.previousOutput == inputInfo.outPoint)
.map(_._2) .map(_._2)
} }
} }

View File

@ -913,7 +913,7 @@ object InputPSBTMap extends PSBTMapFactory[InputPSBTRecord, InputPSBTMap] {
.sign(spendingInfo, unsignedTx) .sign(spendingInfo, unsignedTx)
val utxos = spendingInfo.inputInfo match { val utxos = spendingInfo.inputInfo match {
case _: UnassignedSegwitNativeInputInfo => case _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo =>
Vector(WitnessUTXO(spendingInfo.output)) Vector(WitnessUTXO(spendingInfo.output))
case _: RawInputInfo | _: P2SHNonSegwitInputInfo | case _: RawInputInfo | _: P2SHNonSegwitInputInfo |
_: SegwitV0NativeInputInfo | _: P2SHNestedSegwitV0InputInfo => _: SegwitV0NativeInputInfo | _: P2SHNestedSegwitV0InputInfo =>
@ -971,7 +971,7 @@ object InputPSBTMap extends PSBTMapFactory[InputPSBTRecord, InputPSBTMap] {
val builder = Vector.newBuilder[InputPSBTRecord] val builder = Vector.newBuilder[InputPSBTRecord]
spendingInfo.inputInfo match { spendingInfo.inputInfo match {
case _: UnassignedSegwitNativeInputInfo => case _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo =>
builder.+=(WitnessUTXO(spendingInfo.output)) builder.+=(WitnessUTXO(spendingInfo.output))
case _: RawInputInfo | _: P2SHNonSegwitInputInfo | case _: RawInputInfo | _: P2SHNonSegwitInputInfo |
_: SegwitV0NativeInputInfo | _: P2SHNestedSegwitV0InputInfo => _: SegwitV0NativeInputInfo | _: P2SHNestedSegwitV0InputInfo =>
@ -996,7 +996,7 @@ object InputPSBTMap extends PSBTMapFactory[InputPSBTRecord, InputPSBTMap] {
case p2wsh: P2WSHV0InputInfo => case p2wsh: P2WSHV0InputInfo =>
builder.+=(WitnessScript(p2wsh.scriptWitness.redeemScript)) builder.+=(WitnessScript(p2wsh.scriptWitness.redeemScript))
case _: RawInputInfo | _: P2WPKHV0InputInfo | case _: RawInputInfo | _: P2WPKHV0InputInfo |
_: UnassignedSegwitNativeInputInfo => _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo =>
() ()
} }

View File

@ -34,7 +34,7 @@ sealed abstract class SignerUtils {
val tx = spendingInfo.inputInfo match { val tx = spendingInfo.inputInfo match {
case _: SegwitV0NativeInputInfo | _: P2SHNestedSegwitV0InputInfo | case _: SegwitV0NativeInputInfo | _: P2SHNestedSegwitV0InputInfo |
_: UnassignedSegwitNativeInputInfo => _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo =>
TxUtil.addWitnessData(unsignedTx, spendingInfo) TxUtil.addWitnessData(unsignedTx, spendingInfo)
case _: RawInputInfo | _: P2SHNonSegwitInputInfo => case _: RawInputInfo | _: P2SHNonSegwitInputInfo =>
unsignedTx unsignedTx
@ -186,6 +186,10 @@ object BitcoinSigner extends SignerUtils {
P2WPKHSigner.sign(spendingInfo, unsignedTx, spendingFrom(p2wpkh)) P2WPKHSigner.sign(spendingInfo, unsignedTx, spendingFrom(p2wpkh))
case pw2sh: P2WSHV0InputInfo => case pw2sh: P2WSHV0InputInfo =>
P2WSHSigner.sign(spendingInfo, unsignedTx, spendingFrom(pw2sh)) P2WSHSigner.sign(spendingInfo, unsignedTx, spendingFrom(pw2sh))
case trk: TaprootKeyPathInputInfo =>
TaprootKeyPathSigner.sign(spendingInfo,
unsignedTx,
spendingInfoToSatisfy = spendingFrom(trk))
case _: UnassignedSegwitNativeInputInfo => case _: UnassignedSegwitNativeInputInfo =>
throw new UnsupportedOperationException("Unsupported Segwit version") throw new UnsupportedOperationException("Unsupported Segwit version")
} }
@ -613,3 +617,70 @@ sealed abstract class ConditionalSigner extends Signer[ConditionalInputInfo] {
} }
} }
object ConditionalSigner extends ConditionalSigner 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

View File

@ -14,7 +14,10 @@ import org.bitcoins.crypto.{
ECPublicKeyBytes, ECPublicKeyBytes,
HashType, HashType,
NetworkElement, NetworkElement,
Sign PublicKey,
SchnorrDigitalSignature,
Sign,
XOnlyPubKey
} }
import scala.annotation.tailrec import scala.annotation.tailrec
@ -43,7 +46,7 @@ sealed trait InputInfo {
def conditionalPath: ConditionalPath def conditionalPath: ConditionalPath
def pubKeys: Vector[ECPublicKey] def pubKeys: Vector[PublicKey]
def requiredSigs: Int def requiredSigs: Int
@ -87,7 +90,7 @@ object InputInfo {
def getRedeemScript(inputInfo: InputInfo): Option[ScriptPubKey] = { def getRedeemScript(inputInfo: InputInfo): Option[ScriptPubKey] = {
inputInfo match { inputInfo match {
case _: RawInputInfo | _: SegwitV0NativeInputInfo | case _: RawInputInfo | _: SegwitV0NativeInputInfo |
_: UnassignedSegwitNativeInputInfo => _: UnassignedSegwitNativeInputInfo | _: TaprootKeyPathInputInfo =>
None None
case info: P2SHInputInfo => Some(info.redeemScript) case info: P2SHInputInfo => Some(info.redeemScript)
} }
@ -99,6 +102,7 @@ object InputInfo {
case info: SegwitV0NativeInputInfo => Some(info.scriptWitness) case info: SegwitV0NativeInputInfo => Some(info.scriptWitness)
case info: P2SHNestedSegwitV0InputInfo => Some(info.scriptWitness) case info: P2SHNestedSegwitV0InputInfo => Some(info.scriptWitness)
case info: UnassignedSegwitNativeInputInfo => 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 info: ConditionalInputInfo => info.hashPreImages
case _: UnassignedSegwitNativeInputInfo | _: EmptyInputInfo | case _: UnassignedSegwitNativeInputInfo | _: EmptyInputInfo |
_: P2PKInputInfo | _: P2PKWithTimeoutInputInfo | _: P2PKInputInfo | _: P2PKWithTimeoutInputInfo |
_: MultiSignatureInputInfo | _: P2WPKHV0InputInfo => _: MultiSignatureInputInfo | _: P2WPKHV0InputInfo |
_: TaprootKeyPathInputInfo =>
Vector.empty Vector.empty
} }
} }
@ -200,7 +205,8 @@ object InputInfo {
val dummyLowRHashType = val dummyLowRHashType =
ECDigitalSignature.dummyLowR.appendHashType(HashType.sigHashAll) ECDigitalSignature.dummyLowR.appendHashType(HashType.sigHashAll)
info match { info match {
case _: SegwitV0NativeInputInfo | _: UnassignedSegwitNativeInputInfo => case _: SegwitV0NativeInputInfo | _: UnassignedSegwitNativeInputInfo |
_: TaprootKeyPathInputInfo =>
ScriptSigLenAndStackHeight(0, 0) ScriptSigLenAndStackHeight(0, 0)
case info: P2SHInputInfo => case info: P2SHInputInfo =>
val serializedRedeemScript = ScriptConstant(info.redeemScript.asmBytes) val serializedRedeemScript = ScriptConstant(info.redeemScript.asmBytes)
@ -263,6 +269,7 @@ object InputInfo {
(stackHeightByteSize + redeemScriptSize + scriptSigLen).toInt (stackHeightByteSize + redeemScriptSize + scriptSigLen).toInt
case info: P2SHNestedSegwitV0InputInfo => case info: P2SHNestedSegwitV0InputInfo =>
maxWitnessLen(info.nestedInputInfo) maxWitnessLen(info.nestedInputInfo)
case _: TaprootKeyPathInputInfo => 65 // schnorr signature + hash type
case _: UnassignedSegwitNativeInputInfo => case _: UnassignedSegwitNativeInputInfo =>
throw new IllegalArgumentException( throw new IllegalArgumentException(
s"Cannot compute witness for unknown segwit InputInfo, got $info") s"Cannot compute witness for unknown segwit InputInfo, got $info")
@ -351,6 +358,8 @@ object InputInfo {
sealed trait RawInputInfo extends InputInfo { sealed trait RawInputInfo extends InputInfo {
override def scriptPubKey: RawScriptPubKey override def scriptPubKey: RawScriptPubKey
override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty
override def pubKeys: Vector[ECPublicKey]
} }
object RawInputInfo { object RawInputInfo {
@ -596,6 +605,8 @@ case class P2WPKHV0InputInfo(
override def pubKeys: Vector[ECPublicKey] = Vector(pubKey) override def pubKeys: Vector[ECPublicKey] = Vector(pubKey)
override def requiredSigs: Int = 1 override def requiredSigs: Int = 1
override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty
} }
case class P2WSHV0InputInfo( case class P2WSHV0InputInfo(
@ -619,6 +630,8 @@ case class P2WSHV0InputInfo(
override def pubKeys: Vector[ECPublicKey] = nestedInputInfo.pubKeys override def pubKeys: Vector[ECPublicKey] = nestedInputInfo.pubKeys
override def requiredSigs: Int = nestedInputInfo.requiredSigs override def requiredSigs: Int = nestedInputInfo.requiredSigs
override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty
} }
case class UnassignedSegwitNativeInputInfo( case class UnassignedSegwitNativeInputInfo(
@ -630,6 +643,7 @@ case class UnassignedSegwitNativeInputInfo(
pubKeys: Vector[ECPublicKey]) pubKeys: Vector[ECPublicKey])
extends InputInfo { extends InputInfo {
override def requiredSigs: Int = pubKeys.length override def requiredSigs: Int = pubKeys.length
override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty
} }
@ -642,7 +656,13 @@ sealed trait P2SHInputInfo extends InputInfo {
def nestedInputInfo: 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 override def requiredSigs: Int = nestedInputInfo.requiredSigs
@ -659,6 +679,8 @@ case class P2SHNonSegwitInputInfo(
override val nestedInputInfo: RawInputInfo = override val nestedInputInfo: RawInputInfo =
RawInputInfo(outPoint, amount, redeemScript, conditionalPath, hashPreImages) RawInputInfo(outPoint, amount, redeemScript, conditionalPath, hashPreImages)
override def previousOutputMap: PreviousOutputMap = PreviousOutputMap.empty
} }
case class P2SHNestedSegwitV0InputInfo( case class P2SHNestedSegwitV0InputInfo(
@ -681,4 +703,38 @@ case class P2SHNestedSegwitV0InputInfo(
scriptWitness, scriptWitness,
conditionalPath, conditionalPath,
hashPreImages) 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)
} }

View File

@ -3,11 +3,19 @@ package org.bitcoins.core.wallet.utxo
import org.bitcoins.core.currency.CurrencyUnit import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.script.{ import org.bitcoins.core.protocol.script.{
SigVersionBase, SigVersionBase,
SigVersionTaprootKeySpend,
SigVersionWitnessV0, SigVersionWitnessV0,
SignatureVersion SignatureVersion
} }
import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.protocol.transaction.*
import org.bitcoins.crypto.{HashType, Sign} import org.bitcoins.crypto.{
ECPublicKey,
HashType,
PublicKey,
SchnorrPublicKey,
Sign,
XOnlyPubKey
}
/** Stores the information required to generate a signature (ECSignatureParams) /** Stores the information required to generate a signature (ECSignatureParams)
* or to generate a script signature (ScriptSignatureParams) for a given * 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" .outputs(outPoint.vout.toInt)}) does match the corresponding value $amount"
) )
private val keysToSignFor = inputInfo.pubKeys private val keysToSignFor = inputInfo.pubKeys.map {
require(signers.map(_.publicKey).forall(keysToSignFor.contains), 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") s"Cannot have signers that do not sign for one of $keysToSignFor")
def outputReference: OutputReference = inputInfo.outputReference def outputReference: OutputReference = inputInfo.outputReference
@ -50,6 +63,7 @@ sealed trait InputSigningInfo[+InputType <: InputInfo] {
SigVersionWitnessV0 SigVersionWitnessV0
case _: P2SHNonSegwitInputInfo | _: RawInputInfo => case _: P2SHNonSegwitInputInfo | _: RawInputInfo =>
SigVersionBase SigVersionBase
case _: TaprootKeyPathInputInfo => SigVersionTaprootKeySpend
case i: InputInfo => case i: InputInfo =>
sys.error(s"Cannot determine SigVersion for unsupported inputInfo=$i") sys.error(s"Cannot determine SigVersion for unsupported inputInfo=$i")
} }

View File

@ -215,7 +215,15 @@ trait Sign extends AsyncSign {
final def schnorrSignWithHashType( final def schnorrSignWithHashType(
dataToSign: ByteVector, dataToSign: ByteVector,
hashType: HashType): SchnorrDigitalSignature = { 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, constant(ECDigitalSignature.dummyLowR,
publicKey, ecPubKey,
SchnorrDigitalSignature.dummy) SchnorrDigitalSignature.dummy)
} }
} }

View File

@ -420,6 +420,7 @@ sealed abstract class ScriptGenerators {
), ),
multiSigScriptPubKey, multiSigScriptPubKey,
p2wpkhSPKV0, p2wpkhSPKV0,
witnessScriptPubKeyV1,
unassignedWitnessScriptPubKey, unassignedWitnessScriptPubKey,
conditionalScriptPubKey(defaultMaxDepth), conditionalScriptPubKey(defaultMaxDepth),
multiSignatureWithTimeoutScriptPubKey multiSignatureWithTimeoutScriptPubKey
@ -465,7 +466,8 @@ sealed abstract class ScriptGenerators {
p2shScriptPubKey.map(truncate), p2shScriptPubKey.map(truncate),
witnessCommitment, witnessCommitment,
conditionalScriptPubKey(defaultMaxDepth), conditionalScriptPubKey(defaultMaxDepth),
multiSignatureWithTimeoutScriptPubKey multiSignatureWithTimeoutScriptPubKey,
witnessScriptPubKeyV1
) )
} }

View File

@ -35,7 +35,7 @@ sealed abstract class WitnessGenerators {
val spk = if (spkBytes.isEmpty) EmptyScriptPubKey else NonStandardScriptPubKey(cmpctSPK.bytes ++ spkBytes) val spk = if (spkBytes.isEmpty) EmptyScriptPubKey else NonStandardScriptPubKey(cmpctSPK.bytes ++ spkBytes)
P2WSHWitnessV0(spk,scriptSig) P2WSHWitnessV0(spk,scriptSig)
}*/ }*/
Gen.oneOf(p2wpkhWitnessV0, p2wshWitnessV0) Gen.oneOf(p2wpkhWitnessV0, p2wshWitnessV0, taprootWitness)
} }
def taprootWitness: Gen[TaprootWitness] = { def taprootWitness: Gen[TaprootWitness] = {