Skip to content

Verifying a spend

The Interpreter runs an Input’s Unlocking Script together with the Locking Script it spends, just as a node does. Running it before broadcasting catches wrong keys, wrong amounts and malformed scripts locally, before they ever reach the network.

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

correctlySpends takes the Unlocking Script, the Locking Script, the Transaction, the Input index, the verification flags and the amount of the Output being spent. It returns normally when the spend is valid, and throws a ScriptException when it is not.

// spentOutput: the Output this Input spends (from the funding Transaction).
bool inputIsValid(Transaction tx, int index, TransactionOutput spentOutput) {
try {
Interpreter().correctlySpends(
tx.inputs[index].script!,
spentOutput.script,
tx,
index,
{VerifyFlag.SIGHASH_FORKID, VerifyFlag.UTXO_AFTER_GENESIS},
Coin.ofSat(spentOutput.satoshis),
);
return true;
} on ScriptException catch (e) {
print('input $index rejected: ${e.error}'); // e.g. ScriptError.SCRIPT_ERR_EVAL_FALSE
return false;
}
}

We verify each Input separately, each against the Output it spends. This means we need the spent Outputs’ Locking Scripts and amounts. A Transaction does not contain them, so we either keep them from when we built it, or fetch them along with the funding Transactions.

The amount matters. With SIGHASH_FORKID the Signature commits to it, so passing a different amount fails even for a correctly signed Input.

The flags {VerifyFlag.SIGHASH_FORKID, VerifyFlag.UTXO_AFTER_GENESIS} apply the current Bitcoin SV rules to an Output created after Genesis. The Interpreter page lists the other flags.

ScriptException.error is a ScriptError value naming the rule that failed, and cause is a message.

try {
Interpreter().correctlySpends(tx.inputs[0].script!, fundingTx.outputs[0].script, tx, 0,
{VerifyFlag.SIGHASH_FORKID, VerifyFlag.UTXO_AFTER_GENESIS}, Coin.ofSat(fundingTx.outputs[0].satoshis));
} on ScriptException catch (e) {
e.error; // ScriptError.SCRIPT_ERR_EQUALVERIFY: the public key does not hash to the address
e.cause; // a human-readable message
}

The common errors are:

  • SCRIPT_ERR_EQUALVERIFY: in P2PKH, the Public Key does not match the Address.
  • SCRIPT_ERR_EVAL_FALSE: the scripts ran but left false, often because of a bad Signature.
  • SCRIPT_ERR_INVALID_STACK_OPERATION: the Unlocking Script is missing items, often because the Input was never signed.

On failure the Interpreter prints a stack trace to the console before throwing. That output is noise, not a second error.

Transaction.verify() runs the checks that need no context. It complements the Interpreter, but it does not replace it.

// Context-free checks: Inputs and Outputs present, amounts in range, no duplicate Inputs.
// It does not check Signatures or scripts.
try {
tx.verify();
} on VerificationException catch (e) {
print(e.cause);
}