Skip to content

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.

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 over

correctlySpends() 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, and OP_CHECKLOCKTIMEVERIFY as 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.

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 match

The 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.