Skip to content

The interpreter

Bitcoin4J has a complete Script interpreter, ported from the node software. We use it for two things: checking that a Transaction correctly spends the Outputs it claims, which the verifying a spend page covers, and running a script on its own to see what it does. The page on Bitcoin Script in the Developer Guide explains the execution model.

Interpreter.executeScript() runs one script against a stack that we provide. Without a Transaction there is nothing to sign over, so OP_CHECKSIG and its relatives throw an IllegalStateException, but everything else works.

// We run a script on its own, without a Transaction, and look at the stack afterwards.
Script script = new ScriptBuilder().number(2).number(3).op(OP_ADD).number(5).op(OP_EQUAL).build();
LinkedList<byte[]> stack = new LinkedList<>();
Interpreter.executeScript(null, 0, script, stack, Coin.ZERO,
EnumSet.of(Script.VerifyFlag.UTXO_AFTER_GENESIS));
byte[] top = stack.getLast(); // OP_EQUAL leaves 0x01 for true, an empty array for false

In the preceding example executeScript() takes six parameters:

  • the Transaction, or null when there is none,
  • the index of the Input being run, which only matters with a Transaction,
  • the script itself,
  • the stack, which the interpreter changes in place,
  • the amount of the Output being spent, again only needed for Signature checks,
  • the verify flags.

The stack is a LinkedList<byte[]> with the top of the stack at the end. A script “ends true” when the top item is non-zero.

To watch a script run, we pass a ScriptTraceCallback, which is called after each opcode with the program counter, the opcode, its name and both stacks.

Script script = new ScriptBuilder().number(2).number(3).op(OP_ADD).build();
List<String> steps = new ArrayList<>();
ScriptTraceCallback tracer = (pc, opcode, opcodeName, stack, altStack) ->
steps.add(pc + ": " + opcodeName + " (stack depth " + stack.size() + ")");
Interpreter.executeScript(null, 0, script, new LinkedList<>(), Coin.ZERO,
EnumSet.of(Script.VerifyFlag.UTXO_AFTER_GENESIS), null, tracer);

The seventh parameter is an optional Locking Script, which the interpreter only needs for SIGHASH_CHRONICLE Signatures. We pass null for it here.

Do note that the names given to the callback have no OP_ prefix (ADD, DUP), and small numbers appear as 2 and 3, even though the Javadoc shows OP_DUP as an example. It is a rather straightforward way of finding out where a custom script goes wrong.

The flags decide which rules the interpreter applies. Here is a summary of them:

Flag Effect
UTXO_AFTER_GENESIS The spent Output was created after Genesis: no limits on script, push or number size, restored opcodes, no P2SH, OP_CHECKLOCKTIMEVERIFY and OP_CHECKSEQUENCEVERIFY as no-ops
AFTER_CHRONICLE The Chronicle rules, including SIGHASH_CHRONICLE Signatures and the restored opcodes. For Transactions of version 2 and up, correctlySpends() also drops SIGPUSHONLY, CLEANSTACK, MINIMALDATA, MINIMALIF, LOW_S, NULLFAIL and NULLDUMMY
SIGHASH_FORKID Signatures must use the FORKID digest, as BSV requires
STRICTENC, DERSIG, LOW_S Signatures and Public Keys must be strictly encoded
NULLFAIL A failing Signature check must have been given an empty Signature
MINIMALDATA Pushes and numbers must use their shortest encoding
MINIMALIF OP_IF and OP_NOTIF arguments must be empty or 0x01
SIGPUSHONLY The Unlocking Script may only push data
CLEANSTACK Exactly one item may remain on the stack (only together with P2SH)
NULLDUMMY The extra item that OP_CHECKMULTISIG pops must be empty
P2SH Evaluate P2SH redeem scripts (pre-Genesis only)
CHECKLOCKTIMEVERIFY, CHECKSEQUENCEVERIFY Enforce those opcodes (pre-Genesis only)
DISCOURAGE_UPGRADABLE_NOPS Fail on the reserved OP_NOP opcodes
COMPRESSED_PUBKEYTYPE Public Keys must be compressed

Please note that MONOLITH_OPCODES exists in the enum for compatibility, but has no effect. The opcodes restored in May 2018 are always enabled.

For a spend on BSV today, Fixtures.FLAGS on the verifying a spend page is a sensible starting point. The pages on P2SH and Hodlocker show how the pre-Genesis flags change the outcome.

UTXO_AFTER_GENESIS makes a real difference to what a script may do. Here is one example of a limit that it lifts.

// UTXO_AFTER_GENESIS lifts the old limits, e.g. on the size of numbers.
// 0x0100000000 is a 5-byte number. Before Genesis, numbers were limited to 4 bytes.
Script script = Script.fromAsmString("0100000000 OP_1 OP_ADD");
assertThrows(ScriptException.class, () -> Interpreter.executeScript(null, 0, script,
new LinkedList<>(), Coin.ZERO, EnumSet.noneOf(Script.VerifyFlag.class)));
assertDoesNotThrow(() -> Interpreter.executeScript(null, 0, script,
new LinkedList<>(), Coin.ZERO, EnumSet.of(Script.VerifyFlag.UTXO_AFTER_GENESIS)));

Before Genesis, numbers were limited to 4 bytes, pushes to 520 bytes and scripts to 500 opcodes. After Genesis, those limits are gone, which is what makes larger contracts possible on BSV.

When a script fails, the interpreter throws a ScriptException, and getError() returns a ScriptError that names the rule that failed.

// Mallory tries to spend Alice's Output with her own key.
Transaction theft = new TransactionBuilder()
.spendFromTransaction(new TransactionSigner(SIGHASH, mallory), fundingTx, 0,
TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(mallory.getPublicKey()))
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(99_000))
.build(true);
ScriptException e = assertThrows(ScriptException.class,
() -> Fixtures.verifySpend(theft, 0, fundingTx.getOutputs().get(0)));
ScriptError error = e.getError(); // SCRIPT_ERR_EQUALVERIFY, as the Public Key hash does not match

The verifying a spend page has a table of the errors one meets most often and their usual cause. getMessage() adds a little more detail.