Script Validation

Work through examples, try the reasoning yourself, then put it into practice.

View Slides

Before you start

  • Trace byte slices, stacks and the preceding transaction API.

What you will practice

  • Distinguish pushed bytes from executable opcodes.
  • Trace immediate failures, false values and final stack truth.
  • Identify which fields the supplied signature digest actually binds.
In this lesson

Lesson

6 min read

Script validation: parse bytes, execute a stack, inspect the commitment

A signature check can be correct while the surrounding authorization is wrong. It may verify the wrong bytes, or the script may overwrite a false result. This module asks you to trace all three layers rather than treat CHECKSIG as a magic transaction-validity flag.

Preparation: byte slices, a last-in-first-out stack, double SHA-256, and the transaction types from the preceding module. You will track the instruction cursor, derive a P2PKH-lite stack trace, and identify exactly what the supplied signature digest commits to. Implement Script Validation afterward.

The course machine and its limits

EvalP2PKH(tx, inputIndex, scriptSig, scriptPubKey) checks the index, executes two byte slices on one initially empty stack, then tests its final top item. Despite its name, the function does not enforce a P2PKH template. It accepts any program using this small instruction set. The conventional template below makes a signature result decisive by leaving it on top at the end.

This is a course machine: HASH256 (double SHA-256), P-256 keys, four opcodes, and truth defined as any nonzero byte. It omits Bitcoin's other script rules, secp256k1, HASH160, and transaction signature-hash formats. Do not connect this interpreter to a real Bitcoin network.

Follow the instruction cursor

At index i, a byte n in 1..75 declares n literal bytes. It is followed by data, not another instruction. Require n bytes after the length before slicing; advance by 1+n. All other bytes must match a supported opcode.

Trace the program 02 76 aa 76 (hex):

Cursor before Action Stack afterward, bottom first Next cursor
0 Push two literal bytes [76aa] 3
3 Execute DUP [76aa,76aa] 4

The aa at offset 2 is data, so HASH256 never executes. Reading every byte as an opcode would run a different program. 02 76 is truncated and must fail; 00 is unsupported rather than an empty push in this subset.

Derive the stack transitions

Instruction Needs Effect
DUP (76) 1 item Copy top and append it
HASH256 (aa) 1 item Replace top with its 32-byte hash
EQUALVERIFY (88) 2 items Pop both; fail immediately if unequal
CHECKSIG (ac) 2 items Pop pubkey then signature; push 01 if valid, 00 otherwise

Underflow, malformed pushdata and unsupported opcodes fail execution immediately. EQUALVERIFY failure is also immediate. CHECKSIG failure pushes a value and continues. Invalid public-key encoding or an invalid signature makes that value zero. This distinction matters when instructions follow CHECKSIG.

For the usual scriptSig pushes [sig,pub], execute locking script DUP HASH256 <expectedHash> EQUALVERIFY CHECKSIG:

Step Stack
Enter locking script [sig,pub]
DUP [sig,pub,pub]
HASH256 [sig,pub,H(pub)]
Push expected [sig,pub,H(pub),expected]
EQUALVERIFY succeeds [sig,pub]
CHECKSIG [01] for a valid signature, otherwise [00]

The order is not arbitrary. Reversing pubkey/signature pops feeds each decoder the wrong bytes. EQUALVERIFY consumes the hash copies, preserving the original pubkey for CHECKSIG.

What exactly is signed?

The course digest is:

H(tx.ID || uint32_le(inputIndex) || scriptPubKey)

The preimage has 36 + len(scriptPubKey) bytes. Index 258 contributes 02 01 00 00, not the text 258 or big-endian bytes. The 65-byte public key is 04 || X32 || Y32, where coordinates are big-endian fixed-width values; validate it on P-256. The signature uses ASN.1 DER; Go's ecdsa.VerifyASN1 checks it against the supplied hash.

Crucially, this API trusts tx.ID. It does not serialize tx.Outputs or recompute the ID. If an output value changes while ID, index, and locking script remain unchanged, the digest remains identical. A previously valid signature still verifies. An earlier checkpoint incorrectly claimed output mutation alone must fail; that would require a binding this function does not establish.

A surrounding system must derive a trustworthy transaction commitment from its specified serialization before relying on it. Changing tx.ID generally changes the digest, but the function alone cannot prove that the ID represents these outputs. Nor does it check UTXO existence, double spending, fees, or subsidy.

A false signature is not always a false program

Push an invalid signature and invalid key, then execute CHECKSIG followed by 01 01 (push the byte 01). The final stack is [00,01], so this generic machine returns true. It does not require a single stack item, and it does not require CHECKSIG to be the final instruction.

This is the declared evaluator behavior, not permission to accept arbitrary locking scripts as a P2PKH authorization policy. The template's last CHECKSIG ensures its result is the final truth test. Conversely, an EQUALVERIFY mismatch cannot be rescued by a later push because execution has already failed.

Truthy is also different from nonempty: [00,00] as one item is false, while [00,80] is true under this course's any-nonzero rule. Do not silently import a different protocol's numeric truth rules.

Reconstruct and diagnose

Predict without running code:

  1. 01 07 76 88 runs from an empty stack. Does it succeed?
  2. Change it to 01 07 01 08 88 01 01. Can the last push rescue it?
  3. A valid signature's output amount changes but its tx.ID does not. Is failure guaranteed by this evaluator?
  4. An implementation clears the stack between the two scripts. Where does the usual P2PKH-lite trace first fail?
Check the reasoning
  1. Push 07, duplicate, compare and pop both. Execution finishes with an empty stack, so the final result is false despite successful equality.
  2. No. Unequal values cause immediate EQUALVERIFY failure.
  3. No. The preimage still contains the same ID, index and locking bytes.
  4. At DUP: the signature and public key supplied by scriptSig were lost.

Independent variation: design a program with two final items where only the older item is zero. Explain why the result is true, then state an additional policy a caller would need if it wanted to require exactly one true item.

Implement the parser and stack first, then the digest and verification. Check malformed programs separately from invalid signatures and final false results. The checkpoint uses fresh traces and commitment questions before you proceed to block validation.


Practice sequence

Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.

Join Discord