mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 01:40:55 +01:00
2024 10 31 taproot signing (#5767)
* core: Implement TaprootKeyPath signing * core: Rebase, remove isDummySignature * Empty commit to run CI
This commit is contained in:
parent
80be2f5989
commit
67bb3ceabd
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 =>
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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] = {
|
||||||
|
Loading…
Reference in New Issue
Block a user