Script Validation
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore 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 readScript 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:
01 07 76 88runs from an empty stack. Does it succeed?- Change it to
01 07 01 08 88 01 01. Can the last push rescue it? - A valid signature's output amount changes but its tx.ID does not. Is failure guaranteed by this evaluator?
- An implementation clears the stack between the two scripts. Where does the usual P2PKH-lite trace first fail?
Check the reasoning
- Push 07, duplicate, compare and pop both. Execution finishes with an empty stack, so the final result is false despite successful equality.
- No. Unequal values cause immediate EQUALVERIFY failure.
- No. The preimage still contains the same ID, index and locking bytes.
- 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.
- Step 1
Script Validation (P2PKH-lite)
- Step 2
Script Validation Checkpoint
Trace cursor and stack state, then inspect the exact signature commitment.