Skip to content

The script interpreter

Interpreter is a full Bitcoin SV Script Interpreter. Its main entry point, correctlySpends, checks one Input against the Output it spends. Verifying a spend covers that for signed Transactions. On this page we look at scripts in general.

The examples use the keys and the fundingTransaction helper described in About the examples.

The Interpreter always runs in the context of a spending Transaction, even when the script checks no Signature. We therefore build one around the scripts:

// Locking Script: whoever supplies two numbers that add up to 5 can spend.
final lockingScript = SVScript.fromString('OP_ADD OP_5 OP_EQUAL');
final fundingTx = fundingTransaction(lockingScript, BigInt.from(1000));
final unlockingScript = ScriptBuilder().number(2).number(3).build();
final spendTx = TransactionBuilder()
.spendFromTxn(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, DefaultUnlockBuilder.fromScript(unlockingScript))
.spendToPKH(aliceKey.toAddress(networkType: NetworkType.TEST), BigInt.from(900))
.build(false);
Interpreter().correctlySpends(unlockingScript, lockingScript, spendTx, 0,
{VerifyFlag.SIGHASH_FORKID, VerifyFlag.UTXO_AFTER_GENESIS}, Coin.ofSat(BigInt.from(1000)));

The flags select the consensus and policy rules. We pass them as a Set<VerifyFlag>.

Flag Effect
SIGHASH_FORKID Signatures use the FORKID digest. Needed for any Bitcoin SV Signature.
UTXO_AFTER_GENESIS The Output was created after Genesis: no script size or opcode limits, OP_RETURN ends the script successfully, and P2SH is not evaluated.
AFTER_CHRONICLE The Chronicle rules: reinstated opcodes, larger numbers, and relaxed malleability rules for Transaction version 2 and above.
P2SH Evaluate P2SH redeem scripts (pre-Genesis Outputs only).
STRICTENC, DERSIG, LOW_S, NULLFAIL, NULLDUMMY, MINIMALDATA, MINIMALIF, SIGPUSHONLY, CLEANSTACK Policy rules on encodings and stack contents, as in the node.
CHECKLOCKTIMEVERIFY, CHECKSEQUENCEVERIFY Enable those opcodes (pre-Genesis).

For an Output created after Genesis, {SIGHASH_FORKID, UTXO_AFTER_GENESIS} matches what the network checks. Add AFTER_CHRONICLE for Transactions under the Chronicle rules.

The Chronicle upgrade reinstated opcodes such as OP_2MUL, OP_2DIV, OP_SUBSTR, OP_LEFT, OP_RIGHT, OP_LSHIFTNUM, OP_RSHIFTNUM, OP_VER, OP_VERIF and OP_VERNOTIF. They only run with AFTER_CHRONICLE:

// OP_2MUL is one of the opcodes the Chronicle upgrade reinstated.
final preChronicle = {VerifyFlag.SIGHASH_FORKID, VerifyFlag.UTXO_AFTER_GENESIS};
final postChronicle = {...preChronicle, VerifyFlag.AFTER_CHRONICLE};
// With preChronicle: throws ScriptException (SCRIPT_ERR_DISABLED_OPCODE).
// With postChronicle: 3 * 2 == 6, the spend is valid.
Interpreter().correctlySpends(unlockingScript, lockingScript, spendTx, 0, postChronicle, Coin.ofSat(BigInt.from(1000)));

Setting traceCallback lets us see each step: the chunk index, the opcode, and copies of the main and alt stacks after that step. This is the tool to reach for when debugging a script that fails.

final steps = <String>[];
final interpreter = Interpreter()
..traceCallback = (int chunkIndex, int opcode, List<List<int>> stack, List<List<int>> altStack) {
steps.add('${OpCodes.getOpCodeName(opcode)} -> stack depth ${stack.length}');
};
interpreter.correctlySpends(unlockingScript, lockingScript, spendTx, 0,
{VerifyFlag.SIGHASH_FORKID, VerifyFlag.UTXO_AFTER_GENESIS}, Coin.ofSat(BigInt.from(1000)));
// steps: [OP_2 -> stack depth 1, OP_3 -> stack depth 2, OP_ADD -> stack depth 1,
// OP_5 -> stack depth 2, OP_EQUAL -> stack depth 1]

Failures throw a ScriptException, whose error field names the rule (ScriptError.SCRIPT_ERR_EVAL_FALSE, SCRIPT_ERR_DISABLED_OPCODE, …). The Interpreter also prints a stack trace to the console when it throws.