Adding New Script Types
Adding a New ScriptPubKey Type
In this document, we will describe how to add new script implementations and types in Bitcoin-S. We will use the following script template example which we have called P2PK with Timeout to illustrate the process:
OP_IF
<Public Key>
OP_ELSE
<Timeout> OP_CHECKSEQUENCEVERIFY OP_DROP
<Timeout Public Key>
OP_ENDIF
OP_CHECKSIG
Here is the actual pull request in which a very similar ScriptPubKey
is implemented in Bitcoin-S.
Please note that this document only explains how to add new RawScriptPubKey
s which are the subset of ScriptPubKey
s which are fully described by their raw scripts. This is to say that this guide will not help in implementing a new segwit version, but should be helpful for most anything else.
It is also important to note that all new scripts should be implemented as if they are to appear on-chain without any P2SH or P2WSH. Bitcoin-S already supports conversions from raw on-chain scripts to these formats in the constructors for the script hash schemes which does not require extra support for new script types.
Step 0: Design Philosophy
Bitcoin-S strives to have script types defined in such a way that they can be easily composed and reused. Before going through this guide and implementing a really large script template type, try to decompose your script into smaller re-usable pieces.
Also remember to consider what existing pieces you can use. For example, LockTimeScriptPubKey
s are implemented in such a way that any other RawScriptPubKey
can be given a time lock by nesting it within a LockTimeScriptPubKey
subtype. Likewise, ConditionalScriptPubKey
s are built to allow any other RawScriptPubKey
type to populate both the OP_IF/OP_NOTIF
case and the OP_ELSE
case.
Step 1: Create a New ScriptPubKey Trait
Go to ScriptPubKey.scala
and add a new trait:
sealed trait P2PKWithTimeoutScriptPubKey extends RawScriptPubKey
You will then want to add all of the relevant accessor methods. For our case of P2PKWithTimeout, this will mean giving access to the public key, timeout, and timeout public key. Lastly, you will want to add a scaladoc. In total, we get the following result:
/** The type for ScriptPubKeys of the form:
* OP_IF
* <Public Key>
* OP_ELSE
* <Timeout> OP_CHECKSEQUENCEVERIFY OP_DROP
* <Timeout Public Key>
* OP_ENDIF
* OP_CHECKSIG
*/
sealed trait P2PKWithTimeoutScriptPubKey extends RawScriptPubKey {
lazy val pubKey: ECPublicKey =
ECPublicKey.fromBytes(asm(2).bytes)
lazy val lockTime: ScriptNumber = ScriptNumber.fromBytes(asm(5).bytes)
lazy val timeoutPubKey: ECPublicKey =
ECPublicKey.fromBytes(asm(9).bytes)
}
Step 2: Create Companion Object
We now need a companion object which will fulfill four functionalities for us:
- Contain a concrete
Impl
class for our SPK type- This simply means creating a
private case class
wrappingasm: Vector[ScriptToken]
- This simply means creating a
- Create a
fromAsm
constructor- This should be a simple call to
buildScript
which is inherited fromScriptFactory
- This should be a simple call to
- Create a logical constructor from Bitcoin-S types
- This means creating an
apply
method that takes in logical BItcoin-S types and constructs asm - Note that this may require the use of
BitcoinScriptUtil.calculatePushOp
- This means creating an
- Create an ASM filter which will detect if a given
Vector[ScriptToken]
corresponds to our type
This looks like the following:
object P2PKWithTimeoutScriptPubKey
extends ScriptFactory[P2PKWithTimeoutScriptPubKey] {
private case class P2PKWithTimeoutScriptPubKeyImpl(asm: Vector[ScriptToken])
extends P2PKWithTimeoutScriptPubKey
override def fromAsm(asm: Seq[ScriptToken]): P2PKWithTimeoutScriptPubKey = {
buildScript(
asm = asm.toVector,
constructor = P2PKWithTimeoutScriptPubKeyImpl.apply,
invariant = isP2PKWithTimeoutScriptPubKey,
errorMsg = s"Given asm was not a P2PKWithTimeoutScriptPubKey, got $asm"
)
}
def apply(
pubKey: ECPublicKey,
lockTime: ScriptNumber,
timeoutPubKey: ECPublicKey): P2PKWithTimeoutScriptPubKey = {
val timeoutAsm = CSVScriptPubKey(lockTime, EmptyScriptPubKey).asm.toVector
val pubKeyAsm = BitcoinScriptUtil
.calculatePushOp(pubKey.bytes)
.toVector ++ Vector(ScriptConstant(pubKey.bytes))
val timeoutPubKeyAsm = BitcoinScriptUtil
.calculatePushOp(timeoutPubKey.bytes)
.toVector ++ Vector(ScriptConstant(timeoutPubKey.bytes))
P2PKWithTimeoutScriptPubKeyImpl(
Vector(Vector(OP_IF),
pubKeyAsm,
Vector(OP_ELSE),
timeoutAsm,
timeoutPubKeyAsm,
Vector(OP_ENDIF, OP_CHECKSIG)).flatten
)
}
def isP2PKWithTimeoutScriptPubKey(asm: Seq[ScriptToken]): Boolean = {
if (asm.length == 12) {
val pubKey = ECPublicKey.fromBytes(asm(2).bytes)
val lockTimeTry = Try(ScriptNumber.fromBytes(asm(5).bytes))
val timeoutPubKey = ECPublicKey.fromBytes(asm(9).bytes)
lockTimeTry match {
case Success(lockTime) =>
asm == P2PKWithTimeoutScriptPubKey(pubKey, lockTime, timeoutPubKey).asm
case Failure(_) => false
}
} else {
false
}
}
}
Step 3: Add to Relevant fromAsm Methods
We now need to ensure that ScriptPubKey.fromAsm(p2pkWithTimeoutSPK.asm)
returns our type. Since P2PKWithTimeoutScriptPubKey extends RawScriptPubKey
, this means we must add to RawScriptPubKey.fromAsm
. Note that order in this function's match
can matter. Since our type is more specific than any other currently existing type, we put our new case
at the top:
asm match {
case Nil => EmptyScriptPubKey
case _ if P2PKWithTimeoutScriptPubKey.isP2PKWithTimeoutScriptPubKey(asm) =>
P2PKWithTimeoutScriptPubKey.fromAsm(asm)
//...
}
Step 4: Create a ScriptSignature If Necessary
Often times a new ScriptSignature
type will be necessary when introducing a new ScriptPubKey
type. When this is the case, the procedure for adding a new ScriptSignature
is more or less identical to steps 1 and 2 above. Here is what this looks like for P2PKScriptPubKey
(note, this is not P2PKWithTimeoutScriptPubKey
):
/**
* Represents a pay to public key script signature
* https://bitcoin.org/en/developer-guide#pubkey
* Signature script: <sig>
*/
sealed trait P2PKScriptSignature extends ScriptSignature {
/** PubKey scriptSignatures only have one signature */
def signature: ECDigitalSignature = signatures.head
/** The digital signatures inside of the scriptSig */
def signatures: Seq[ECDigitalSignature] = {
Seq(ECDigitalSignature(BitcoinScriptUtil.filterPushOps(asm).head.hex))
}
override def toString = s"P2PKScriptSignature($signature)"
}
object P2PKScriptSignature extends ScriptFactory[P2PKScriptSignature] {
private case class P2PKScriptSignatureImpl(
override val asm: Vector[ScriptToken])
extends P2PKScriptSignature
def fromAsm(asm: Seq[ScriptToken]): P2PKScriptSignature = {
buildScript(asm.toVector,
P2PKScriptSignatureImpl(_),
isP2PKScriptSignature(_),
"The given asm tokens were not a p2pk script sig: " + asm)
}
def apply(signature: ECDigitalSignature): P2PKScriptSignature = {
val pushOps = BitcoinScriptUtil.calculatePushOp(signature.bytes)
val signatureConstant = ScriptConstant(signature.bytes)
val asm = pushOps ++ Seq(signatureConstant)
P2PKScriptSignature.fromAsm(asm)
}
/** P2PK scriptSigs always have the pattern [pushop, digitalSignature] */
def isP2PKScriptSignature(asm: Seq[ScriptToken]): Boolean = asm match {
case Seq(_: BytesToPushOntoStack, _: ScriptConstant) => true
case _ => false
}
}
However, it is sometimes not necessary to create a new ScriptSignature
type for every new ScriptPubKey
. This is because we want to maintain unique representations for every ScriptSignature
, and it turns out that in our case of P2PKWithTimeoutScriptPubKey
, script signatures are of the form
<boolean> <signautre>
which is already represented by ConditionalScriptSignature
. When this happens, you only need to create an object
for your new type, and then follow step 2 above, skipping the first part (adding an Impl case class
):
object P2PKWithTimeoutScriptSignature
extends ScriptFactory[ConditionalScriptSignature] {
override def fromAsm(asm: Seq[ScriptToken]): ConditionalScriptSignature = {
buildScript(
asm.toVector,
ConditionalScriptSignature.fromAsm,
isP2PKWithTimeoutScriptSignature,
s"The given asm tokens were not a P2PKWithTimeoutScriptSignature, got $asm"
)
}
def apply(
beforeTimeout: Boolean,
signature: ECDigitalSignature): ConditionalScriptSignature = {
ConditionalScriptSignature(P2PKScriptSignature(signature), beforeTimeout)
}
def isP2PKWithTimeoutScriptSignature(asm: Seq[ScriptToken]): Boolean = {
P2PKScriptSignature.isP2PKScriptSignature(asm.dropRight(1)) && ConditionalScriptSignature
.isValidConditionalScriptSig(asm)
}
}
Remember that in all of them above, ScriptSignature
s are written as if they are to appear on-chain in transaction inputs rather than transaction witnesses, since Bitcoin-S supports turning any raw ScriptSignature
into a P2WSHWitness
without requiring explicit support for new script types.
Step 5: Add to ScriptSignature.fromAsm If Applicable
If you added a new ScriptSignature
type in the previous step, you must add a case
to the match
statement in ScriptSignature.fromAsm
at the bottom of ScriptSignature.scala
. For P2PKScriptSignature
(note that this does not apply to P2PKWithTimeoutScriptSignature
since there is no new unique type for this ScriptSignature
), this looks like:
tokens match {
//...
case _ if P2PKScriptSignature.isP2PKScriptSignature(tokens) =>
P2PKScriptSignature.fromAsm(tokens)
//...
}
Step 6: Create Relevant BitcoinUTXOSpendingInfo
BitcoinUTXOSpendingInfo
is the Bitcoin-S data structure for the information required to spend from a given ScriptPubKey
. Hence, when defining new script types, it is important to define how they are spent as well so that they can be useful.
There are three distinct kinds of scripts when it comes to signing in Bitcoin-S: scripts that have nesting (such as ConditionalScriptPubKey
, LockTimeScriptPubKey
), scripts without nesting but which involve multiple signing keys (such as MultiSignatureScriptPubKey
), and scripts without nesting and which require only one key for signing (such as P2PKWithTimeoutScriptPubKey
, P2PKHScriptPubKey
). We will cover each of these cases in turn, starting with the last case as it applies to our example of P2PKWithTimeout
.
Non-Nested Single-Key Spending Info
This is the easiest case and only requires creating a new case class
in UTXOSpendingInfo.scala
which extends RawScriptUTXOSpendingInfoFull with RawScriptUTXOSpendingInfoSingle
and which contains in its parameters, all of the info required for spending. Make sure to also validate any data in these parameters using require
statements. Make sure to override
both requiredSigs: Int = 1
and conditionalPath: ConditionalPath
to be either whatever is needed based on your parameters, or ConditionalPath.NoConditionsLeft
if your script does not use any conditionals. Here is what this looks like for P2PKWithTimeout
:
case class P2PKWithTimeoutSpendingInfo(
outPoint: TransactionOutPoint,
amount: CurrencyUnit,
scriptPubKey: P2PKWithTimeoutScriptPubKey,
override val signer: Sign,
hashType: HashType,
isBeforeTimeout: Boolean)
extends RawScriptUTXOSpendingInfoFull
with RawScriptUTXOSpendingInfoSingle {
require(
scriptPubKey.pubKey == signer.publicKey || scriptPubKey.timeoutPubKey == signer.publicKey,
"Signer pubkey must match ScriptPubKey")
override val requiredSigs: Int = 1
override def conditionalPath: ConditionalPath =
if (isBeforeTimeout) {
ConditionalPath.nonNestedTrue
} else {
ConditionalPath.nonNestedFalse
}
}
Non-Nested Multi-Key Spending Info
For new script types which require multiple keys to spend, we must make two separate case class
es, one for normal spending (which extends RawScriptUTXOSpendingInfoFull
) and one for signing a transaction with a single key (which extends RawScriptUTXOSpendingInfoSingle
). We then make a sealed trait
for the both of them to extend. In this trait
, you want to make sure to override def scriptPubKey
to have your new ScriptPubKey
type as well as overriding any other members of RawScriptUTXOSpendingInfo
which are common to both classes. In total, this all works out to the following for MutliSignatureScriptPubKey
:
sealed trait MultiSignatureSpendingInfo extends RawScriptUTXOSpendingInfo {
override def conditionalPath: ConditionalPath =
ConditionalPath.NoConditionsLeft
override def scriptPubKey: MultiSignatureScriptPubKey
}
case class MultiSignatureSpendingInfoSingle(
outPoint: TransactionOutPoint,
amount: CurrencyUnit,
scriptPubKey: MultiSignatureScriptPubKey,
signer: Sign,
hashType: HashType
) extends MultiSignatureSpendingInfo
with RawScriptUTXOSpendingInfoSingle
case class MultiSignatureSpendingInfoFull(
outPoint: TransactionOutPoint,
amount: CurrencyUnit,
scriptPubKey: MultiSignatureScriptPubKey,
private val signersWithPossibleExtra: Vector[Sign],
hashType: HashType
) extends RawScriptUTXOSpendingInfoFull
with MultiSignatureSpendingInfo {
require(signersWithPossibleExtra.length >= scriptPubKey.requiredSigs,
s"Not enough signers!: $this")
override val requiredSigs: Int = scriptPubKey.requiredSigs
override val signers: Vector[Sign] =
signersWithPossibleExtra.take(requiredSigs)
override def toSingle(signerIndex: Int): MultiSignatureSpendingInfoSingle = {
MultiSignatureSpendingInfoSingle(outPoint,
amount,
scriptPubKey,
signers(signerIndex),
hashType)
}
/** @inheritdoc */
override def toSingles: Vector[MultiSignatureSpendingInfoSingle] = {
signers.map { signer =>
MultiSignatureSpendingInfoSingle(outPoint,
amount,
scriptPubKey,
signer,
hashType)
}
}
}
Nested Spending Info
This case is very similar to the above where we need to create two case classes one for normal spending (which extends RawScriptUTXOSpendingInfoFull
) and one for signing a transaction with a single key (which extends RawScriptUTXOSpendingInfoSingle
). As well as needing to create a sealed trait
for them both to extend which overrides scriptPubKey
as well as any other commonalities from the sub-classes. The one new thing in the nested case is that we must create a def nestedSpendingInfo: RawScriptUTXOSpendingInfo
in our trait
and make sure to override signers
and requiredSigs
from this nestedSpendingInfo
in the subclass extending RawScriptUTXOSpendingInfoFull
. For the case of spending LockTimeScriptPubKey
s, this looks like the following:
sealed trait LockTimeSpendingInfo extends RawScriptUTXOSpendingInfo {
override def scriptPubKey: LockTimeScriptPubKey
def nestedSpendingInfo: RawScriptUTXOSpendingInfo
}
case class LockTimeSpendingInfoSingle(
outPoint: TransactionOutPoint,
amount: CurrencyUnit,
scriptPubKey: LockTimeScriptPubKey,
signer: Sign,
hashType: HashType,
conditionalPath: ConditionalPath
) extends LockTimeSpendingInfo
with RawScriptUTXOSpendingInfoSingle {
override val nestedSpendingInfo: RawScriptUTXOSpendingInfoSingle = {
RawScriptUTXOSpendingInfoSingle(outPoint,
amount,
scriptPubKey.nestedScriptPubKey,
signer,
hashType,
conditionalPath)
}
}
case class LockTimeSpendingInfoFull(
outPoint: TransactionOutPoint,
amount: CurrencyUnit,
scriptPubKey: LockTimeScriptPubKey,
private val signersWithPossibleExtra: Vector[Sign],
hashType: HashType,
conditionalPath: ConditionalPath
) extends RawScriptUTXOSpendingInfoFull
with LockTimeSpendingInfo {
override val nestedSpendingInfo: RawScriptUTXOSpendingInfoFull = {
RawScriptUTXOSpendingInfoFull(outPoint,
amount,
scriptPubKey.nestedScriptPubKey,
signersWithPossibleExtra,
hashType,
conditionalPath)
}
override val signers: Vector[Sign] = nestedSpendingInfo.signers
override val requiredSigs: Int = nestedSpendingInfo.requiredSigs
}
Step 7: Add to Relevant Apply Methods
Now that we have created our new RawScriptUTXOSpendingInfoFull
and RawScriptUTXOSpendingInfoSingle
, possibly in the same class, we need to add them to the general-purpose spending info constructors. This means adding a case
to both RawScriptUTXOSpendingInfoSingle.apply
and RawScriptUTXOSpendingInfoFull.apply
for your new ScriptPubKey
type which constructs your relevant RawScriptUTXOSpendingInfoSingle
and RawScriptUTXOSpendingInfoFull
from generic types (given as parameters in the apply
methods). For P2PKWithTimeout
, both of these cases look like the following:
scriptPubKey match {
//...
case p2pkWithTimeout: P2PKWithTimeoutScriptPubKey =>
conditionalPath.headOption match {
case None =>
throw new IllegalArgumentException(
"ConditionalPath must be specified for P2PKWithTimeout")
case Some(beforeTimeout) =>
P2PKWithTimeoutSpendingInfo(outPoint,
amount,
p2pkWithTimeout,
signer,
hashType,
beforeTimeout)
}
//...
}
Step 8: Create a Signer
We must now add signing functionality for our new script type within Signer.scala
. Once again, we have three different cases depending on your new script type.
Non-Nested Single-Key Spending Info
For this case, all we must do is create a new class which extends RawSingleKeyBitcoinSigner
and implements keyAndSigToScriptSig
. For P2PKWithTimeout
this looks like the following:
sealed abstract class P2PKWithTimeoutSigner
extends RawSingleKeyBitcoinSigner[P2PKWithTimeoutSpendingInfo] {
override def keyAndSigToScriptSig(
key: ECPublicKey,
sig: ECDigitalSignature,
spendingInfo: P2PKWithTimeoutSpendingInfo): ScriptSignature = {
P2PKWithTimeoutScriptSignature(spendingInfo.isBeforeTimeout, sig)
}
}
object P2PKWithTimeoutSigner extends P2PKWithTimeoutSigner
Non-Nested Multi-Key Spending Info
In this case we must create a new BitcoinSignerSingle
and a normal BitcoinSignerFull
, the latter of which requires implementing the sign
function. For MultiSignature
this looks like the following:
sealed abstract class MultiSigSignerSingle
extends BitcoinSignerSingle[MultiSignatureSpendingInfoSingle]
object MultiSigSignerSingle extends MultiSigSignerSingle
sealed abstract class MultiSigSigner
extends BitcoinSignerFull[MultiSignatureSpendingInfoFull] {
override def sign(
spendingInfo: UTXOSpendingInfoFull,
unsignedTx: Transaction,
isDummySignature: Boolean,
spendingInfoToSatisfy: MultiSignatureSpendingInfoFull)(
implicit ec: ExecutionContext): Future[TxSigComponent] = {
val (_, output, inputIndex, _) =
relevantInfo(spendingInfo, unsignedTx)
val keysAndSigsF: Seq[Future[PartialSignature]] = spendingInfoToSatisfy.toSingles.zipWithIndex.map {
case (infoSingle, index) =>
MultiSigSignerSingle
.signSingle(spendingInfo.toSingle(index),
unsignedTx,
isDummySignature,
infoSingle)
}
val signaturesF = Future.sequence(keysAndSigsF).map(_.map(_.signature))
val scriptSigF = signaturesF.map { sigs =>
MultiSignatureScriptSignature(sigs)
}
updateScriptSigInSigComponent(unsignedTx,
inputIndex.toInt,
output,
scriptSigF)
}
}
object MultiSigSigner extends MultiSigSigner
Nested Spending Info
When signing for a nested script structure, we must create a new BitcoinSignerSingle
and a normal BitcoinSignerFull
. For the single signer, you will likely only need to delegate with a call to BitcoinSignerSingle.signSingle
because we only need a signature which will not likely require anything other than doing the signing on spendingInfoToSatisfy.nestedSpendingInfo
. For the full signer, you will also need to make a delegating call with the nestedSpendingInfo
to BitcoinSigner.sign
, but you may also need to do whatever else is needed with the nested result to construct a correct ScriptSignature
. For ConditionalScriptSignature
, this all looks like:
sealed abstract class ConditionalSignerSingle
extends BitcoinSignerSingle[ConditionalSpendingInfoSingle] {
override def signSingle(
spendingInfo: UTXOSpendingInfoSingle,
unsignedTx: Transaction,
isDummySignature: Boolean,
spendingInfoToSatisfy: ConditionalSpendingInfoSingle)(
implicit ec: ExecutionContext): Future[PartialSignature] = {
BitcoinSignerSingle.signSingle(spendingInfo,
unsignedTx,
isDummySignature,
spendingInfoToSatisfy.nestedSpendingInfo)
}
}
object ConditionalSignerSingle extends ConditionalSignerSingle
/** Delegates to get a ScriptSignature for the case being
* spent and then adds an OP_TRUE or OP_FALSE
*/
sealed abstract class ConditionalSigner
extends BitcoinSignerFull[ConditionalSpendingInfoFull] {
override def sign(
spendingInfo: UTXOSpendingInfoFull,
unsignedTx: Transaction,
isDummySignature: Boolean,
spendingInfoToSatisfy: ConditionalSpendingInfoFull)(
implicit ec: ExecutionContext): Future[TxSigComponent] = {
val (_, output, inputIndex, _) = relevantInfo(spendingInfo, unsignedTx)
val missingOpSigComponentF = BitcoinSigner.sign(
spendingInfo,
unsignedTx,
isDummySignature,
spendingInfoToSatisfy.nestedSpendingInfo)
val scriptSigF = missingOpSigComponentF.map { sigComponent =>
ConditionalScriptSignature(sigComponent.scriptSignature,
spendingInfoToSatisfy.condition)
}
updateScriptSigInSigComponent(unsignedTx,
inputIndex.toInt,
output,
scriptSigF)
}
}
object ConditionalSigner extends ConditionalSigner
Step 9: Add to BitcoinSigner.sign
We must now add the new signing functionality from the previous step to the general-purpose signing functions by adding a new case
for your new ScriptPubKey
type in the match
within BitcoinSigner.sign
. In the case of P2PKWithTimeout
, this looks like:
spendingInfoToSatisfy match {
//...
case p2pKWithTimeout: P2PKWithTimeoutSpendingInfo =>
P2PKWithTimeoutSigner.sign(spendingInfo,
unsignedTx,
isDummySignature,
p2pKWithTimeout)
//...
}
We have now fully implemented the new script type! But have we done it correctly? We must now add the new script type to the Bitcoin-S test framework so that our scripts get added to existing Bitcoin-S property-based tests.
Step 10: Add to ScriptGenerators
The first step to adding our new script type to Bitcoin-S property-based tests is creating generators for our new ScriptPubKey
and ScriptSignature
types in ScriptGenerators.scala
.
It is important to note that in the current Bitcoin-S generator framework for ScriptPubKey
s, all conditionals always spend only their OP_TRUE
cases.
ScriptPubKey Generator
Let's start by creating a generator for our ScriptPubKey
, this generator should also return the private keys that were used to create the ScriptPubKey
. To construct this Gen
, you will likely need to use other generators for the internal structures in your script such as keys and lock times. For P2PKWithTimeout
this looks like:
def p2pkWithTimeoutScriptPubKey: Gen[
(P2PKWithTimeoutScriptPubKey, Seq[ECPrivateKey])] =
for {
privKey <- CryptoGenerators.privateKey
timeoutPrivKey <- CryptoGenerators.privateKey
lockTime <- NumberGenerator.timeLockScriptNumbers
} yield {
(P2PKWithTimeoutScriptPubKey(privKey.publicKey,
lockTime,
timeoutPrivKey.publicKey),
Vector(privKey, timeoutPrivKey))
}
Note that the private key used in the OP_TRUE
case is the head
of the Seq[ECPrivateKey]
returned. This makes it possible for tests that only spend the OP_TRUE
case to find the correct key, as it is expected to be the first one.
We must now add this Gen
to all of the following def
s in ScriptGenerators.scala
: randomNonP2SHScriptPubKey, scriptPubKey, nonWitnessScriptPubKey, nonConditionalRawScriptPubKey, rawScriptPubKey
, and if your ScriptPubKey
has no lock times, you must also add the above Gen
to nonConditionalNonLocktimeRawScriptPubKey, nonLocktimeRawScriptPubKey
as well.
ScriptSignature Generator
We must also create a generator for our ScriptSignature
type, even if we did not introduce a new ScriptSignature
type (in our example of P2PKWithTimeout
we use a specific form of ConditionalScriptSignature
). Once again you will likely need to use other existing generators. For P2PKWithTimeoutScriptSignature
, this looks like:
def p2pkWithTimeoutScriptSignature: Gen[ConditionalScriptSignature] =
for {
privKey <- CryptoGenerators.privateKey
hash <- CryptoGenerators.doubleSha256Digest
hashType <- CryptoGenerators.hashType
signature = ECDigitalSignature.fromBytes(
privKey.sign(hash).bytes ++ ByteVector.fromByte(hashType.byte))
beforeTimeout <- NumberGenerator.bool
} yield P2PKWithTimeoutScriptSignature(beforeTimeout, signature)
We now add this Gen
to scriptSignature: Gen[ScriptSignature]
as well as adding a case for our new ScriptPubKey
type in pickCorrespondingScriptSignature
which should return our new ScriptSignature
generator. If our ScriptPubKey
does not have any lock times, you should also add this script signature Gen
to nonLockTimeConditionalScriptSignature
and randomNonLockTimeScriptSig
.
ScriptPubKey with Paired ScriptSignature Generator
Lastly, we need to construct a generator that returns both a ScriptPubKey
and a ScriptSignature
signing that that ScriptPubKey
. All keys used in signing should also be returned. This all should be done by using the above ScriptPubKey
generator, then constructing an ScriptSignature
for your type where all actual signatures are EmptyDigitalSignature
s. A SpendingInfoFull
should then be constructed for the generated ScriptPubKey
(using the private keys generated in the same line). Finally, a TxSignatureComponent
should be created by using the new Signer
for our script type. From this TxSignatureComponent
, a ScriptSignature
is readily available. For P2PKWithTimeout
, this generator looks like:
def signedP2PKWithTimeoutScriptSignature: Gen[
(ConditionalScriptSignature, P2PKWithTimeoutScriptPubKey, ECPrivateKey)] =
for {
(spk, privKeys) <- p2pkWithTimeoutScriptPubKey
hashType <- CryptoGenerators.hashType
} yield {
val privKey = privKeys.head
val emptyScriptSig = P2PKWithTimeoutScriptSignature(beforeTimeout = true,
EmptyDigitalSignature)
val (creditingTx, outputIndex) =
TransactionGenerators.buildCreditingTransaction(spk)
val (spendingTx, inputIndex) = TransactionGenerators
.buildSpendingTransaction(creditingTx, emptyScriptSig, outputIndex)
val spendingInfo = P2PKWithTimeoutSpendingInfo(
TransactionOutPoint(creditingTx.txIdBE, inputIndex),
creditingTx.outputs(outputIndex.toInt).value,
spk,
privKey,
hashType,
isBeforeTimeout = true)
val txSigComponentF = P2PKWithTimeoutSigner.sign(spendingInfo,
spendingTx,
isDummySignature = false)
val txSigComponent = Await.result(txSigComponentF, timeout)
val signedScriptSig =
txSigComponent.scriptSignature.asInstanceOf[ConditionalScriptSignature]
(signedScriptSig, spk, privKey)
}
I strongly advise you also look at at least one other Gen
of this kind before writing your own.
Step 11: Add to CreditingTxGen
Now that we have generators constructed for ScriptPubKey
s, ScriptSignature
s and their pairings completed, we will create a generator for our type's SpendingInfoFull
. This should usually be as simple as mapping on the ScriptPubKey
generator in ScriptGenerators
and calling build
(within CreditinTxGen.scala
). We then also create another generator which returns lists of SpendingInfo
s generated by the previous Gen
. For P2PKWithTimeout
, this looks like:
def p2pkWithTimeoutOutput: Gen[BitcoinUTXOSpendingInfoFull] = {
ScriptGenerators.p2pkWithTimeoutScriptPubKey.flatMap { p2pkWithTimeout =>
build(p2pkWithTimeout._1, Seq(p2pkWithTimeout._2.head), None, None)
}
}
def p2pkWithTimeoutOutputs: Gen[Seq[BitcoinUTXOSpendingInfoFull]] = {
Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2pkWithTimeoutOutput))
}
We must then add our output Gen
to one of cltvOutputGens
or nonCLTVOutputGens
depending on whether the ScriptPubKey
type has CLTVs (absolute lock times) or not. We must also add our output Gen
to nonP2SHOutput
, and also to nonSHOutput
and nonP2WSHOutput
in the case that your ScriptPubKey
type has no CLTVs.
Step 12: Fix all Non-Exhaustive Matches
All we have left is to clean up our code and make sure that nothing has been missed. Within an sbt
terminal, you should run the following sequence of commands:
clean
project coreTest
test:compile
This should have quite a lengthy output but we are only interested in any compiler errors there may be, as well as non-exhaustive match compiler warnings. You should first fix any compiler errors you encounter, and then you can get the warnings again by running clean
and then running test:compile
again.
The warnings we're interested in should look something like this:
[warn] /home/nkohen/Desktop/SuredBits/bitcoin-s-core/testkit/src/main/scala/org/bitcoins/testkit/core/gen/ScriptGenerators.scala:524:59: match may not be exhaustive.
[warn] It would fail on the following input: P2PKWithTimeoutScriptPubKeyImpl(_)
[warn] scriptPubKey: ScriptPubKey): Gen[ScriptSignature] = scriptPubKey match {
[warn] ^
[warn] one warning found
You may get these warnings for your new ScriptSignature
type as well. These are places where the compiler expects there to be defined functionality in a pattern match where one of our new types is a possibility, but for which no functionality is defined. You must go to each of these warnings and add a case
for the relevant new type, or add this new type to an existing case when applicable.
Step 13: Run tests and debug
Lastly, once everything is compiling nicely, all that is left is to run tests and debug. While within an sbt
terminal session, run the following two commands to run the relevant tests:
project coreTest
test
If all tests pass we are all done! If you encounter any test failures, you can re-run individual tests using the testOnly
command which must be given the full name of the test you wish to run (these names should be at the bottom of the testing output and look something like org.bitcoins.core.script.interpreter.ScriptInterpreterTest
).