Script Validation (P2PKH-lite)

hard · bitcoin, script, crypto

Script Validation (P2PKH-lite)

You are the transaction gatekeeper for a toy Bitcoin-like node. A transaction input is valid only if its unlocking script (scriptSig) and locking script (scriptPubKey) execute successfully on a shared stack.

Implement a minimal script engine for P2PKH-like validation using HASH256 (double SHA-256) and P-256 ECDSA.

Script encoding

Scripts are byte slices containing either pushdata or opcodes.

Pushdata

  • A byte n with 1 <= n <= 75 means: push next n bytes.
  • If fewer than n bytes remain, script is malformed -> fail.

Opcodes (single byte)

  • 0x76 OP_DUP
  • 0xAA OP_HASH256
  • 0x88 OP_EQUALVERIFY
  • 0xAC OP_CHECKSIG

Any other opcode is invalid -> fail.

Simplified signature hash

For this problem:

S = SHA256(SHA256(tx.ID || uint32_le(inputIndex) || scriptPubKey))

All concatenations are raw byte concatenations. inputIndex must be encoded as 4-byte little-endian.

Types and function

type Outpoint struct {
    TxID  [32]byte
    Index uint32
}

type TxIn struct {
    Prev      Outpoint
    ScriptSig []byte
}

type TxOut struct {
    Value        uint64
    ScriptPubKey []byte
}

type Transaction struct {
    ID      [32]byte
    Inputs  []TxIn
    Outputs []TxOut
}

func EvalP2PKH(tx Transaction, inputIndex int, scriptSig []byte, scriptPubKey []byte) bool

Execution rules

  1. Reject immediately if inputIndex is out of range.
  2. Start with empty stack.
  3. Execute scriptSig, then scriptPubKey, on the same stack.
  4. Opcode behavior:
    • OP_DUP: duplicate top stack item.
    • OP_HASH256: replace top with HASH256(top).
    • OP_EQUALVERIFY: pop two items; fail if not equal.
    • OP_CHECKSIG: pop pubkey then signature; verify ECDSA over S; push 1 if valid else 0.
  5. Script succeeds iff stack is non-empty and top item is not all-zero bytes.

Crypto details

  • Curve: elliptic.P256().
  • Pubkey format: uncompressed 0x04 || X(32) || Y(32).
  • Treat pubkeys not exactly 65 bytes, wrong prefix, or not on curve as failed CHECKSIG verification (push 0).
  • Signature format: ASN.1 DER (ecdsa.VerifyASN1).

Scope and independent reconstruction

This is a generic interpreter for the stated subset; it does not enforce a P2PKH template or a clean stack. CHECKSIG failures, including invalid public keys, push 0 and continue. EQUALVERIFY mismatch fails immediately. A later push can therefore make a program with a failed CHECKSIG end truthy.

The digest trusts the supplied tx.ID. Changing tx.Outputs while keeping tx.ID, inputIndex and scriptPubKey unchanged does not change the digest. Deriving and checking an ID from transaction bytes is a surrounding system's responsibility.

Before coding, trace 02 76 aa 76: the first instruction pushes literal bytes 76aa, then DUP executes at offset 3. Explain why the aa byte is not HASH256. Use the lesson for the full P2PKH-lite trace, then reconstruct it independently.

Constraints

  • 0 <= len(scriptSig), len(scriptPubKey) <= 10000
  • 0 <= len(tx.Inputs) <= 100000

Common mistakes

  • Big-endian inputIndex in sighash.
  • Wrong pop order in OP_CHECKSIG.
  • Forgetting to reject malformed pushdata.
  • Accepting invalid pubkey point not on curve.
  • Treating any non-empty top as true (must reject all-zero top).
Run tests to see results
No issues detected
    Join Discord