Script Validation
Lesson, slides, and applied problem sets.
View SlidesLesson
Script Validation (P2PKH-lite)
This module is where Bitcoin's "don't trust, verify" becomes executable. You will build a tiny script engine that behaves deterministically under malformed data, stack errors, and signature failures.
The challenge is not just crypto. It is the combination of:
- byte-level script parsing,
- strict stack-machine semantics,
- deterministic signature-hash construction, and
- fail-closed behavior on every invalid path.
Learning goals
- Parse mixed pushdata/opcode scripts without desynchronizing the byte stream.
- Execute P2PKH-limited opcodes with exact stack pop/push order.
- Build the exact simplified sighash used by the pack.
- Verify DER signatures over a parsed uncompressed P-256 public key.
- Diagnose failure modes quickly using stack-state checkpoints.
Scope of this module (important)
This is a toy but strict validator:
- Hash op is
HASH256(double SHA-256), not BitcoinHASH160. - Curve is
P-256, notsecp256k1. - Supported opcodes are only:
OP_DUP,OP_HASH256,OP_EQUALVERIFY,OP_CHECKSIG. - Any unknown opcode or malformed pushdata is immediate failure.
1) P2PKH execution flow at byte level
You execute two scripts in order against one shared stack:
scriptSigscriptPubKey
Typical bytes in this module:
scriptSig:
len(sig)(1 byte,1..75)sigbyteslen(pubkey)(1 byte, usually65)pubkeybytes (0x04 || X32 || Y32)
scriptPubKey (P2PKH-lite):
0x76OP_DUP0xAAOP_HASH2560x20push 32 bytes<32-byte pubkey hash>0x88OP_EQUALVERIFY0xACOP_CHECKSIG
Mental model
scriptSigprepares witness data (signature + pubkey).scriptPubKeyenforces lock conditions (hash match + signature check).
2) Parser contract: pushdata vs opcode
At byte script[i]:
- If
1 <= script[i] <= 75: this is pushdata lengthn.- Require
i+1+n <= len(script), else fail. - Push exact
nbytes to stack. - Advance by
1+n.
- Require
- Else treat as opcode byte.
- One-byte opcodes only in this module.
Why parser strictness matters
A single off-by-one in pushdata length changes where opcodes are read. That means different nodes could "execute" different programs over identical bytes.
Hidden-test aligned failures
Your parser must return false on:
- declared push length running past script end,
- unknown opcode values.
3) Stack semantics and exact pop order
OP_DUP (0x76)
- Requires at least 1 stack item.
- Duplicates top item (copy bytes, do not alias mutable backing unexpectedly).
OP_HASH256 (0xAA)
- Requires at least 1 stack item.
- Replaces top with
HASH256(top).
OP_EQUALVERIFY (0x88)
- Requires at least 2 items.
- Pops two items (
a=top,b=next). - Fails unless byte-equal.
- Pushes nothing on success.
OP_CHECKSIG (0xAC)
- Requires at least 2 items.
- Pops
pubkeyfrom top, thensignaturefrom next. - Verifies signature against simplified sighash.
- Pushes
1byte if valid, else0byte.
Final success rule
After both scripts:
- stack must be non-empty
- top item must be "truthy" (at least one non-zero byte)
Any parse error, stack underflow, invalid pubkey encoding, invalid curve point, or failed signature/hash check => return false.
4) Simplified sighash used in this pack
For tx, inputIndex, and current scriptPubKey:
S = HASH256(tx.ID || uint32_le(inputIndex) || scriptPubKey)
Implementation details:
tx.IDis 32 bytes.inputIndexis 4-byte little-endian.- Concatenate raw bytes in that exact order.
- Apply SHA-256 twice.
Why this exact format matters
If you accidentally use big-endian input index or hash only once, all signatures will fail even with correct keys.
5) Public key and signature verification contract
Pubkey parsing
Accept only uncompressed encoding:
- length exactly
65 - first byte
0x04 X= bytes[1:33],Y= bytes[33:65]- point must satisfy
elliptic.P256().IsOnCurve(X, Y)
Signature check
Use:
ecdsa.VerifyASN1(pub, sighash[:], sig)
Invalid DER signature, wrong key, or wrong sighash should produce false path by pushing 0 at OP_CHECKSIG and eventually failing truthiness.
6) End-to-end stack trace (worked)
Assume:
scriptSigpushes[sig, pub]scriptPubKeyisDUP HASH256 <expectedHash> EQUALVERIFY CHECKSIG
Initial stack: []
After scriptSig:
- push
sig->[sig] - push
pub->[sig, pub]
Execute scriptPubKey:
OP_DUP->[sig, pub, pub]OP_HASH256->[sig, pub, H(pub)]- push
<expectedHash>->[sig, pub, H(pub), expected] OP_EQUALVERIFY- pop
expected,H(pub) - if unequal -> fail
- if equal ->
[sig, pub]
- pop
OP_CHECKSIG- pop
pub, thensig - verify over
S - push
[1]if valid else[0]
- pop
End:
[1]=> success[0]or empty => failure
7) Near-solution architecture
Top-level evaluator
func EvalP2PKH(tx Transaction, inputIndex int, scriptSig, scriptPubKey []byte) bool {
if inputIndex < 0 || inputIndex >= len(tx.Inputs) {
return false
}
stack := make([][]byte, 0)
if !execScript(&stack, scriptSig, tx, inputIndex, scriptPubKey) {
return false
}
if !execScript(&stack, scriptPubKey, tx, inputIndex, scriptPubKey) {
return false
}
if len(stack) == 0 {
return false
}
return !isZero(stack[len(stack)-1])
}
Script executor skeleton
func execScript(stack *[][]byte, script []byte, tx Transaction, idx int, scriptPubKey []byte) bool {
for i := 0; i < len(script); {
op := script[i]
if op >= 1 && op <= 75 {
n := int(op)
if i+1+n > len(script) {
return false
}
data := make([]byte, n)
copy(data, script[i+1:i+1+n])
*stack = append(*stack, data)
i += 1 + n
continue
}
switch op {
case 0x76: // DUP
// underflow checks + copy
case 0xAA: // HASH256
// replace top with hash
case 0x88: // EQUALVERIFY
// pop two, compare, fail if mismatch
case 0xAC: // CHECKSIG
// pop pubkey then sig, verify, push 1/0
default:
return false
}
i++
}
return true
}
8) Failure matrix (what should return false)
inputIndexout of range.- malformed pushdata length.
- unknown opcode.
- stack underflow on any opcode.
- hash mismatch at
OP_EQUALVERIFY. - bad pubkey length or wrong prefix.
- pubkey point not on curve.
- invalid ASN.1 DER signature or signature mismatch.
- final top-of-stack all-zero or empty stack.
If your implementation fails hidden tests, check this matrix first.
9) Debugging checklist
- Parser position: log
iand decoded token kind. - Stack snapshots: log stack depth after each opcode.
- Pop order: ensure
CHECKSIGpops pubkey first, signature second. - Sighash bytes: verify byte order for
inputIndexis little-endian. - Curve check: reject off-curve points before verification.
- Truthiness: final top item is false only if all bytes are zero.
What you will build
- Byte parser for pushdata + opcode stream.
- Deterministic stack engine for P2PKH-lite opcodes.
- Simplified Bitcoin-style signature verification pipeline.
- Fail-closed behavior on malformed inputs.
Start by implementing parser + stack underflow guards, then add opcode behavior, then wire signature verification last.
Module Items
Script Validation (P2PKH-lite)
Script Validation Checkpoint
P2PKH execution and signatures.