Standard Script Templates
At some point in Bitcoin’s history, the developers made the decision to restrict the types of Locking / Unlocking Script combinations that the Bitcoin network would accept to only a few commonly used ones. This, in addition to limiting the overall size of a script, meant that the potential application-development use-cases for Bitcoin were effectively restricted to those wherein one could work within these established limitations. Although the BitcoinSV network is no longer restricted in this manner, the language and monikers for these Transaction Templates remain.
In this section we will examine these most common Transaction Templates (also called Script Templates), and build real Transactions with them.
The examples follow on from Build a Transaction. Each one spends the Change Output of the Transaction we built there, which pays to our own Address, so we always start from a P2PKH Output that we control. To try them on your own RegTest Teranode, fund your Address from the faucet as we did there, and spend that Output instead.
Pay-to-Public-Key should sound familiar. As its name suggests, this script type puts a Public Key in the Locking Script. Let’s delve a little deeper into the workings of this method of locking and unlocking, and how we can use it from DartSV.
The P2PK Transaction format was the original canonical way of sending bitcoin between parties. As such, this influenced the naming of the input and output script. Today we still refer to Locking Scripts as scriptPubkey and Unlocking Scripts as scriptSig.
Locking Script (scriptPubkey)
Section titled “Locking Script (scriptPubkey)”The Locking Script contains a Public Key followed by an OP_CHECKSIG operation.
<pubkey> OP_CHECKSIGUnlocking Script (scriptSig)
Section titled “Unlocking Script (scriptSig)”The Unlocking Script requires only an ECDSA Signature.
<sig>Script execution
Section titled “Script execution”Script execution proceeds in the normal way by concatenating the scriptSig++scriptPubkey, and then executing the combined script as one.
<sig> <pubkey> OP_CHECKSIGCode example
Section titled “Code example”Right now we don’t have an existing P2PK UTXO to spend from. The faucet only pays to Addresses, i.e. to P2PKH, so we will create one ourselves, with a Transaction that:
- consumes an existing P2PKH Output (UTXO),
- sends some of the funds to a P2PK Output (UTXO).
P2PKH UTXO => P2PK UTXO
Section titled “P2PKH UTXO => P2PK UTXO”We spend our P2PKH change with the same P2PKHUnlockBuilder as before. The difference is in the Output: instead of spendToPKH(), we hand the builder a P2PKLockBuilder, which renders our P2PK Locking Script.
// previousTx: a Transaction with a P2PKH Output of ours at index 0final signer = DefaultTransactionSigner(sighash, privateKey);
final p2pkTx = TransactionBuilder() .spendFromTxnWithSigner(signer, previousTx, 0, TransactionInput.MAX_SEQ_NUMBER, P2PKHUnlockBuilder(privateKey.publicKey)) .spendToLockBuilder(P2PKLockBuilder(privateKey.publicKey), BigInt.from(100000)) // the P2PK Output .sendChangeToPKH(myAddress) .withFeePerKb(100) .build(true);
p2pkTx.outputs[1].script.toString(type: 'asm'); // <our Public Key> OP_CHECKSIGOur P2PK Output is at index 1, since DartSV puts the Change Output first. We broadcast this Transaction as before, with ./scripts/rpc.sh sendrawtransaction <hex>.
P2PK UTXO => P2PKH UTXO
Section titled “P2PK UTXO => P2PKH UTXO”To spend the P2PK Output, we need an Unlocking Script that holds the Signature and nothing else. This is the job of an unlocking script builder. The signer hands it the Signature, and the builder writes the script:
/// The Unlocking Script for P2PK: the Signature alone.class SignatureOnlyUnlocker extends UnlockingScriptBuilder { @override SVScript getScriptSig() { if (signatures.isEmpty) return SVScript(); // not signed yet return ScriptBuilder() .addData(Uint8List.fromList(HEX.decode(signatures[0].toTxFormat()))) .build(); }
@override void parse(SVScript script) {}}With that in place, the spend is rather straightforward. We send the whole Output, less the fee, back to a P2PKH Output of our own:
final spendTx = TransactionBuilder() .spendFromTxnWithSigner(signer, p2pkTx, 1, TransactionInput.MAX_SEQ_NUMBER, SignatureOnlyUnlocker()) // P2PK needs only the Signature .sendChangeToPKH(myAddress) // everything, less the fee, back to a P2PKH Output of ours .withFeePerKb(100) .build(true);Send it!
P2PKH (Pay to Public Key Hash) is the canonical bitcoin Transaction type. It is by far the most widely used method of sending coins from one party to another. Its Locking and Unlocking Scripts have the following patterns respectively:
Locking script (scriptPubkey)
Section titled “Locking script (scriptPubkey)”OP_DUP OP_HASH160 <Public Key Hash> OP_EQUALVERIFY OP_CHECKSIGUnlocking Script (scriptSig)
Section titled “Unlocking Script (scriptSig)”<Signature> <Public Key>Script execution
Section titled “Script execution”Combined for execution:
<Signature> <Public Key> OP_DUP OP_HASH160 <Public Key Hash> OP_EQUALVERIFY OP_CHECKSIG- The Signature and the Public Key are pushed onto the stack.
- OP_DUP duplicates the Public Key, and OP_HASH160 hashes the copy.
- The Public Key Hash from the Locking Script is pushed, and OP_EQUALVERIFY checks that the two hashes match. This proves the Public Key is the one the coins were sent to.
- OP_CHECKSIG checks the Signature against the Public Key, and so proves that the spender holds the Private Key.
Code example
Section titled “Code example”We have in fact already built a P2PKH Transaction. The Spending Transaction in Build a Transaction spends a P2PKH Output, and creates two more:
final bobAddress = Address('mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1');
// The signer holds our Private Key, and the Sighash type to sign with.final signer = DefaultTransactionSigner( SighashType.SIGHASH_ALL.value | SighashType.SIGHASH_FORKID.value, privateKey,);// The unlocker writes the Unlocking Script: <Signature> <Public Key>.final unlocker = P2PKHUnlockBuilder(privateKey.publicKey);
final spendingTx = TransactionBuilder() .spendFromTxnWithSigner(signer, faucetTx, 0, TransactionInput.MAX_SEQ_NUMBER, unlocker) // Output 0 is ours .spendToPKH(bobAddress, BigInt.from(500000)) // pay Bob half .sendChangeToPKH(myAddress) // and the rest, less the fee, back to us .withFeePerKb(100) // 100 satoshis per kilobyte .build(true);The two primary classes at work are P2PKHUnlockBuilder, which writes <Signature> <Public Key>, and P2PKHLockBuilder, which spendToPKH() and sendChangeToPKH() use behind the scenes to write the Locking Script for an Address.
The P2SH (Pay to Script Hash) Transaction type is a bit controversial. During its introduction, three years after the launch of the bitcoin network, key changes to how the bitcoin node executes scripts were introduced.
Since the Genesis upgrade, the Bitcoin SV network rejects Transactions that create new P2SH Outputs. P2SH Outputs created before Genesis can still be spent. This should not present a problem for the developer. Bitcoin SV has a very large script-size limit, and with the scaled network there really is no need to resort to the old P2SH method of script execution.
DartSV still lets us create both the Locking and the Unlocking Scripts, e.g. for reading or spending old Outputs. P2SHLockBuilder builds the Locking Script, and an Unlocking Script is built the same way as for any other script.
It should be noted that the P2SH script type also has its own Address format. Address formats are a wallet concern however, and it is important to remember that Addresses are never actually sent to the network. What ends up on the network is a cryptographic hash, from which one may reconstruct the Address.
Locking script (scriptPubkey)
Section titled “Locking script (scriptPubkey)”OP_HASH160 <hash of redeem script> OP_EQUALUnlocking script (scriptSig)
Section titled “Unlocking script (scriptSig)”<data the redeem script needs> <serialised redeem script>Script Execution
Section titled “Script Execution”P2SH script execution does not follow the usual procedure for executing scripts. Recall that ordinarily, the Script Interpreter will:
- concatenate the scriptSig and the scriptPubkey,
- execute the concatenated script, and evaluate the result of the execution as either success or failure.
P2SH script execution proceeds as follows:
- all of the scriptSig elements are pushed onto the stack (in P2SH the scriptSig contains only data items),
- a copy is made of the stack at this point,
- execution now proceeds as normal with the elements from the scriptPubkey, which checks that the last data item hashes to the script hash,
- upon successful completion of this execution, that last data item (the redeem script) is deserialised and executed as a script of its own, against the rest of the copied stack.
As the developer we therefore need to provide a serialised copy of the whole redeem script as the last data push in the scriptSig, preceded by whatever the redeem script itself needs to succeed, e.g. its Signatures.
The typical manner in which this pattern is used, is that the redeem script is agreed upon offline. The sender only ever sees its hash, and locks the coins to that. In order to spend the Output, the recipient:
- builds the data that the redeem script needs, e.g. the Signatures,
- appends the serialised redeem script,
- creates a Spending Transaction with this as the scriptSig.
While the P2SH method could be employed for arbitrary script execution, it has historically been used primarily for creating multisig Transactions. We will have a treatment on multisig Transactions in the following section.
Code example
Section titled “Code example”Since a new P2SH Output can’t be created on the network anymore, we will only show how one would use DartSV to create a P2SH Locking Script, and how to recognise a P2SH Address:
// The redeem script: here, a P2PK script.final redeemScript = ScriptBuilder() .addData(Uint8List.fromList(HEX.decode(privateKey.publicKey.toHex()))) .opCode(OpCodes.OP_CHECKSIG) .build();
final locker = P2SHLockBuilder.fromScript(redeemScript); // hashes the redeem script for uslocker.getScriptPubkey().toString(type: 'asm'); // OP_HASH160 <20-byte script hash> OP_EQUAL
final address = Address.fromScript(redeemScript, NetworkType.REGTEST);address.addressType == AddressType.SCRIPT_HASH; // trueA P2SH Address on regtest or testnet starts with a
2, and on mainnet with a3.
P2MS (Pay to Multisig) has had a bit of a rough road. Built around the OP_CHECKMULTISIG Script OpCode, it allows one to construct a spending condition that requires m-of-n Signatures. Motivated primarily by a need to preserve blockspace, and concerns over the size of the UTXO set that a bitcoin node has to maintain, P2MS was moved into a P2SH structure where pushing of Public Keys (Public Keys are relatively large) onto the blockchain could be deferred until spending time.
This means that from 2012 the canonical way of performing a MultiSig Transaction was to employ the P2SH locking/unlocking pattern. With the advent of Big Blocks, and a persistent low-fee-per-byte model on BitcoinSV, combined with the retirement of P2SH, P2MS has once again assumed its rightful place in the developer’s toolbox. P2MS is also referred to as bare multisig, because it is no longer wrapped and hidden inside a hex-encoded serialised data blob as per the P2SH method.
Locking Script (scriptPubkey)
Section titled “Locking Script (scriptPubkey)”The Locking Script for P2MS follows this general pattern:
OP_3 <pubKey1> <pubKey2> <pubKey3> <pubKey4> <pubKey5> OP_5 OP_CHECKMULTISIG- The first OpCode (OP_3 in the above case) designates the number of required Signatures.
- The participating Private Key holders are represented by the Public Keys that follow it.
- OP_5 in the above example is the total number of Public Keys available for checking.
- OP_CHECKMULTISIG closes out the scriptPubkey.
Unlocking Script (scriptSig)
Section titled “Unlocking Script (scriptSig)”The Unlocking Script for P2MS needs to provide the minimum number of required Signatures. The pattern is:
OP_0 <Sig> <Sig> <Sig>The OP_0 in the above example is a requirement due to a bug in OP_CHECKMULTISIG. The OP_CHECKMULTISIG code suffers from an off-by-one bug, wherein it will attempt to pop one more element off the stack than is required. The inclusion of an OpCode like OP_0 allows OP_CHECKMULTISIG to continue functioning in a non-breaking way.
Script Execution
Section titled “Script Execution”In the case of P2MS, script execution proceeds as it usually does: concatenation of scriptSig++scriptPubkey, followed by execution of the concatenated script.
OP_0 <Sig> <Sig> <Sig> OP_3 <pubKey1> <pubKey2> <pubKey3> <pubKey4> <pubKey5> OP_5 OP_CHECKMULTISIG- OP_0 results in an empty value being pushed onto the stack.
- All the Signatures, which are just data, are pushed onto the stack.
- OP_3 results in the value (3) being pushed onto the stack.
- All the Public Keys, which are just data, are pushed onto the stack.
- OP_5 results in the value (5) being pushed onto the stack.
- OP_CHECKMULTISIG executes, and
- pops the top of the stack to know how many Public Keys to pop next,
- pops (5) elements off the stack and saves them as Public Keys,
- pops an element (3) off the stack to learn how many Signatures are required,
- pops (3) elements off the stack and saves those values as Signatures,
- pops one more element off the stack, because of the bug (OP_0),
- checks that the (3) Signatures match (3) of the (5) Public Keys, in the same order as the keys appear in the script.
In the above example, we have a 3-of-5 MultiSig scheme. At least 3 Signatures are required out of a possible five.
A code example
Section titled “A code example”As with the P2PK Transaction earlier, we don’t have an existing P2MS UTXO to spend from, so we will first create one with a Transaction that:
- consumes an existing P2PKH Output (UTXO),
- sends some of the funds to a P2MS Output (UTXO).
P2PKH UTXO => P2MS UTXO
Section titled “P2PKH UTXO => P2MS UTXO”In order for us to create a multisig Output, we will need a number of Public Keys. We will construct the same 3-of-5 scheme as we used in the introduction to P2MS, with four new keys and our own.
final key1 = SVPrivateKey.fromWIF('L5MYU25rgrRy1MomVyzuSRuDB3s24CrP5ZXFSWEHDLvwKWVXJDnB');final key2 = SVPrivateKey.fromWIF('L25sS3jP3XyAgC9dxdSt9azucWcXruaGEDzwRQd4DASCnaEU9Muj');final key3 = SVPrivateKey.fromWIF('L1QKHjoft6JoUQb1LmZytZLPjjqam2ftmdtKaa8bw3tR8LDT3tV5');final key4 = SVPrivateKey.fromWIF('Ky7yMfxzsoHwsyB4GKp2TCfUzbBtziC2hcCSqvwGUet11tPXww54');
// 3-of-5: any three of these five keys can spend.final locker = P2MSLockBuilder( [key1.publicKey, key2.publicKey, key3.publicKey, key4.publicKey, privateKey.publicKey], 3);
final p2msTx = TransactionBuilder() .spendFromTxnWithSigner(DefaultTransactionSigner(sighash, privateKey), previousTx, 0, TransactionInput.MAX_SEQ_NUMBER, P2PKHUnlockBuilder(privateKey.publicKey)) .spendToLockBuilder(locker, BigInt.from(100000)) // the P2MS Output .sendChangeToPKH(myAddress) .withFeePerKb(100) .build(true);
p2msTx.outputs[1].script.toString(type: 'asm'); // OP_3 <pubkey> <pubkey> <pubkey> <pubkey> <pubkey> OP_5 OP_CHECKMULTISIGP2MSLockBuilder takes the Public Keys and the number of Signatures required. Do note that it sorts the keys, so their order in the script need not be the order we gave them in. Once again our P2MS Output is at index 1.
P2MS UTXO => P2PKH UTXO
Section titled “P2MS UTXO => P2PKH UTXO”To spend from a MultiSig UTXO, we create a P2MSUnlockBuilder and hand it to the builder, but this time without a signer. We then sign once for each of the three cosigning keys, and each signature is added to the unlocker as we go.
final unlocker = P2MSUnlockBuilder(); // collects the Signatures as we sign
final spendTx = TransactionBuilder() .spendFromTxn(p2msTx, 1, TransactionInput.MAX_SEQ_NUMBER, unlocker) // no signer yet .sendChangeToPKH(myAddress) .withFeePerKb(100) .build(false);
// OP_CHECKMULTISIG wants the Signatures in the same order as the keys in the script.final keyOrder = locker.publicKeys.map((k) => k.toHex()).toList();final cosigners = [key3, key2, key1] ..sort((a, b) => keyOrder.indexOf(a.publicKey.toHex()).compareTo(keyOrder.indexOf(b.publicKey.toHex())));
for (final key in cosigners) { DefaultTransactionSigner(sighash, key).sign(spendTx, p2msTx.outputs[1], 0); // adds one Signature}The order of the Signatures matters. OP_CHECKMULTISIG walks through the Signatures and the Public Keys together, so the Signatures must be in the same order as their keys in the Locking Script. This is why we sort our cosigners by the order of
locker.publicKeysbefore signing. Signatures in the wrong order fail verification.
Send it!

