Verifying a spend
The builder’s checks only look at amounts. Whether an Input actually unlocks the Output it spends (the
right key, a valid Signature, a script that ends true) is decided by running the scripts. The Interpreter
does that the way a node does, offline. We use it in tests, and before we broadcast anything built with a
custom script.
Checking one Input
Section titled “Checking one Input”int inputIndex = 0;TransactionOutput spentOutput = fundingTx.getOutputs().get(0);
Set<Script.VerifyFlag> flags = EnumSet.of( Script.VerifyFlag.SIGHASH_FORKID, // BSV Signatures Script.VerifyFlag.UTXO_AFTER_GENESIS); // the Output was created after the Genesis upgrade
new Interpreter().correctlySpends( spendingTx.getInputs().get(inputIndex).getScriptSig(), // Unlocking Script spentOutput.getScript(), // Locking Script spendingTx, inputIndex, flags, Coin.valueOf(spentOutput.getAmount().longValue())); // the spent amount, which FORKID signs overcorrectlySpends() returns nothing when the spend is valid, and throws a ScriptException when it is not. It
works on a copy of the Transaction, so it never changes ours. We check each Input separately, against the
Output it spends.
The flags select the rules. Two of them are needed for any BSV spend:
SIGHASH_FORKID, since Signatures use (and must use) the FORKID digest.UTXO_AFTER_GENESIS, since the spent Output was created after the Genesis upgrade, so the Genesis rules apply. That means large scripts and numbers, restored opcodes, no P2SH, andOP_CHECKLOCKTIMEVERIFYas a no-op.
Fixtures.FLAGS, which we use on the other pages, adds the Signature-encoding checks that nodes apply as
policy (STRICTENC, DERSIG, LOW_S, NULLFAIL). The interpreter page
lists the rest, including AFTER_CHRONICLE.
Pass the amount of the spent Output. The FORKID digest commits to it, so the default of zero (from the five-argument overload) makes every valid FORKID Signature fail.
When it fails
Section titled “When it fails”ScriptException.getError() tells us why.
// 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 matchThe common errors, and their usual cause:
ScriptError |
Usual cause |
|---|---|
SCRIPT_ERR_EQUALVERIFY |
P2PKH: the Public Key does not match the Address |
SCRIPT_ERR_SIG_NULLFAIL, SCRIPT_ERR_EVAL_FALSE |
A Signature over the wrong script, amount or Transaction state, or a Transaction that changed after signing |
SCRIPT_ERR_MUST_USE_FORKID |
Signed without SigHashType.FORKID |
SCRIPT_ERR_SIG_DER |
A Signature without its Sighash byte, or not DER |
SCRIPT_ERR_INVALID_STACK_OPERATION |
The Unlocking Script is empty or short, i.e. the Input was never signed |
To watch the script run step by step, we trace it, as shown on the interpreter page.

