Skip to content

Building scripts

Every Locking Script and Unlocking Script is a Script, which is a list of chunks. Each chunk is either an opcode or a push of data. The lock and unlock builders on the Transactions pages produce scripts for the standard types. When we need something else, we build the script ourselves with a ScriptBuilder. The page on Bitcoin Script in the Developer Guide explains how scripts execute.

Let’s build a P2PKH Locking Script by hand, one opcode at a time.

// import static org.twostack.bitcoin4j.script.ScriptOpCodes.*;
byte[] pubkeyHash = Utils.HEX.decode("94837d2d5d6106aa97db38957dcc294181ee91e9");
Script script = new ScriptBuilder()
.op(OP_DUP)
.op(OP_HASH160)
.data(pubkeyHash) // picks the right push opcode for the length
.op(OP_EQUALVERIFY)
.op(OP_CHECKSIG)
.build();
String asm = script.toAsmString();
String hex = Utils.HEX.encode(script.getProgram());

In the preceding example:

  • the opcodes are constants on ScriptOpCodes, which reads best with a static import,
  • data() pushes a byte array, and picks the right push opcode for its length (a direct push up to 75 bytes, then OP_PUSHDATA1, OP_PUSHDATA2 or OP_PUSHDATA4),
  • toAsmString() and getProgram() give us the script as text and as bytes.

ScriptBuilder also has static helpers for the common shapes, e.g. createP2PKHOutputScript(hash), createP2PKOutputScript(pubkey) and createOutputScript(address).

Numbers in Script are little-endian, with a sign bit. number() takes care of the encoding for us.

Script script = new ScriptBuilder()
.number(5) // 0..16 become OP_0..OP_16
.number(1000) // larger numbers become a minimal little-endian push
.op(OP_ADD)
.number(1005)
.op(OP_EQUAL)
.build();

The values 0 to 16 become the single-byte opcodes OP_0 to OP_16, -1 becomes OP_1NEGATE, and anything else becomes a minimal push.

data() follows the same minimal encoding. A one-byte array holding a value from 1 to 16 is written as the opcode OP_1 to OP_16, not as a push of one byte, since that is the only form the MINIMALDATA rule accepts.

// A one-byte push of 0x01..0x10 becomes the opcode OP_1..OP_16, as the minimal encoding requires.
Script script = new ScriptBuilder().data(new byte[]{0x05}).build();
assertArrayEquals(new byte[]{(byte) OP_5}, script.getProgram());
// The OP_RETURN template reads it back as the one byte we pushed.
Script dataScript = new ScriptBuilder().op(OP_FALSE).op(OP_RETURN).data(new byte[]{0x05}).build();
OpReturnScriptInfo info = (OpReturnScriptInfo) ScriptTemplateRegistry.getInstance().extractScriptInfo(dataScript);

Do note that the data builders on the data Outputs page use data() as well, so a one-byte buffer from 1 to 16 lands in their scripts as an opcode. Code of our own that reads such scripts chunk by chunk has to expect that.

Scripts reach us in three forms, and we can read all of them.

// ASM has opcodes by name and data as bare hex. It is the format bsv and DartSV use.
Script fromAsm = Script.fromAsmString("OP_DUP OP_HASH160 94837d2d5d6106aa97db38957dcc294181ee91e9 OP_EQUALVERIFY OP_CHECKSIG");
// The bitcoind test-vector format writes data pushes as <length> 0x<hex>.
Script fromBitcoind = Script.fromBitcoindString("OP_DUP OP_HASH160 20 0x94837d2d5d6106aa97db38957dcc294181ee91e9 OP_EQUALVERIFY OP_CHECKSIG");
// Raw bytes, e.g. from a Transaction Output.
Script fromBytes = new Script(Utils.HEX.decode("76a91494837d2d5d6106aa97db38957dcc294181ee91e988ac"));

All three give us the same Script. toAsmString() and toBitcoindString() write the two text forms back out.

getChunks() gives us the parsed chunks. A chunk with data is a push, and any other chunk is an opcode.

StringBuilder description = new StringBuilder();
for (ScriptChunk chunk : script.getChunks()) {
if (chunk.isPushData() && chunk.data != null) {
description.append("push ").append(chunk.data.length).append(" bytes; ");
} else {
description.append(ScriptOpCodes.getOpCodeName(chunk.opcode)).append("; ");
}
}

ScriptOpCodes converts between names and values in both directions. Please note that the names have no OP_ prefix.

int opcode = ScriptOpCodes.getOpCode("CHECKSIG"); // names without the OP_ prefix
String name = ScriptOpCodes.getOpCodeName(OP_CHECKSIG);

For the standard types, ScriptPattern has a test and an extractor for each.

Script script = ScriptBuilder.createOutputScript(address); // P2PKH for this Address
boolean isP2pkh = ScriptPattern.isP2PKH(script);
boolean isOpReturn = ScriptPattern.isOpReturn(script);
byte[] hash = ScriptPattern.extractHashFromP2PKH(script);
Script.ScriptType type = script.getScriptType(); // P2PKH, P2PK, P2SH, or null

getScriptType() returns P2PKH, P2PK or P2SH, and null for anything else. For more types, and to pull the fields out of them, we use the Script templates.