What Is Bitcoin Script?
Bitcoin Script is a simple, stack-based, Turing-incomplete programming language. Every bitcoin payment is actually a small script program that defines the conditions under which the funds can be spent.
The key insight: outputs contain a "locking script" (scriptPubKey), and inputs provide an "unlocking script" (scriptSig) that satisfies it.
scriptPubKey is short for "script public key." It's stored in the transaction output, and it's called that because in the original Bitcoin payment type (P2PK), it literally contained a public key. Today it's more like a "locking condition," but the name stuck.
scriptSig is short for "script signature." It's stored in the transaction input, and it provides the proof (usually a signature) that satisfies the locking condition. In SegWit transactions, this data moved to a separate witness field, but the concept is the same.
Think of it like a padlock and key: the scriptPubKey is the padlock (set by the sender), and the scriptSig is the key (provided by the spender). The Bitcoin interpreter tries the key on the padlock. If it opens, the spend is valid.
scriptPubKey
The locking script. Stored in the output. Defines the spending conditions (e.g., "prove you own this public key").
scriptSig
The unlocking script. Provided in the input. Supplies the proof (e.g., a digital signature).
Stack Machine
No loops, no arbitrary jumps. Opcodes push/pop data from a stack. Evaluates to TRUE or FALSE.
By deliberately omitting loops, Bitcoin Script guarantees that every script terminates. This prevents denial-of-service attacks where a malicious script could run forever. Every node in the network must execute every script, so it must be deterministic and bounded.
How Script Executes
When validating an input, Bitcoin Core runs the script in this order:
For SegWit transactions, witness data replaces scriptSig, and for P2SH, there's an additional deserialization step.
Key Opcodes
There are around 200 opcodes, but most of Bitcoin operates on just a handful:
Stack Operations
Cryptographic Operations
Flow Control & Conditions
P2PKH: Pay to Public Key Hash
The classic Bitcoin payment type. "I'll pay whoever can prove they own this public key."
The Scripts
OP_DUP OP_HASH160 <pubkeyHash_20bytes> OP_EQUALVERIFY OP_CHECKSIG
<signature> <pubkey>
Execution Trace
1. Push <sig> โ [sig]
2. Push <pubkey> โ [sig, pubkey]
3. OP_DUP โ [sig, pubkey, pubkey]
4. OP_HASH160 โ [sig, pubkey, hash(pubkey)]
5. Push <pubkeyHash> โ [sig, pubkey, hash(pubkey), pubkeyHash]
6. OP_EQUALVERIFY โ [sig, pubkey] โ fails if hashes don't match
7. OP_CHECKSIG โ [TRUE] โ verifies signature against pubkey
Interactive: Step Through P2PKH Verification
Use the controls below to watch each opcode execute, one step at a time. The stack on the right updates live.
P2SH: Pay to Script Hash
Introduced in BIP 16. Allows the sender to pay to the hash of a script, with the full script revealed only when spending. Used for multisig, timelocks, and other complex conditions.
How It Works
OP_HASH160 <scriptHash_20bytes> OP_EQUAL
<signatures...> <redeemScript>
Bitcoin Core first validates that the serialized redeemScript matches the hash. Then it deserializes the redeemScript and executes it against the remaining stack data.
2-of-3 Multisig via P2SH
OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 OP_CHECKMULTISIG
OP_0 <sig1> <sig2> <serialized redeemScript>
Interactive: Step Through P2SH 2-of-3 Multisig
Watch how a 2-of-3 multisig P2SH script is verified. First the hash is checked, then the redeem script itself is executed.
SegWit Scripts (P2WPKH & P2WSH)
Segregated Witness (BIP 141) moved signatures out of scriptSig and into a separate witness field. This fixed transaction malleability and enabled new optimizations.
P2WPKH: Native SegWit Single-Key
OP_0 <20-byte pubkeyHash>
(empty)
[<signature>, <pubkey>]
The witness program version 0 + 20-byte hash tells Bitcoin Core to treat the witness as P2PKH-style data.
Interactive: Step Through P2WPKH Verification
SegWit moves the signature data to the witness field. The scriptSig is empty. Watch how the interpreter processes it:
P2WSH: Native SegWit Script Hash
OP_0 <32-byte SHA256(witnessScript)>
[<signatures...>, <witnessScript>]
Similar to P2SH but uses SHA-256 (32 bytes) instead of Hash160 (20 bytes) for stronger collision resistance.
Interactive: Step Through P2WSH 2-of-2 Multisig
P2WSH works like P2SH, but using the witness field and SHA-256 hashing:
Taproot (P2TR)
Taproot (BIP 341/342, activated 2021) is the most recent script upgrade. It uses witness version 1:
OP_1 <32-byte tweaked public key>
A Taproot output can be spent in two ways:
Key Path Spend
Provide a single Schnorr signature for the tweaked key. Cheapest and most private, looks identical to a single-sig payment.
Script Path Spend
Reveal a Merkle proof to one of potentially many embedded "tapscripts." Each branch can have different spending conditions.
Key Path Spend
[<schnorr_signature>]
The key path spend is the simplest and cheapest Taproot path, just a single Schnorr signature:
Script Path Spend
[<script_inputs...>, <tapscript>, <control_block>]
<pubkey> OP_CHECKSIG // example: single-sig branch
When the key path can't be used (e.g., one party in a multisig is offline), a script path reveals one branch of the Taproot tree:
When the key path is used (the common case), a Taproot transaction looks exactly like a simple single-signature payment, even if the key is actually an aggregated multisig key (MuSig2). Unused script paths are never revealed on-chain.
The Interpreter
Script execution happens in src/script/interpreter.cpp, primarily in the EvalScript() function:
- Reads opcodes one at a time from the script
- Maintains a main stack and an alt stack
- Tracks conditional nesting (OP_IF/OP_ELSE/OP_ENDIF depth)
- Enforces script size limits (10,000 bytes, 201 non-push opcodes)
- Returns a
ScriptErrorcode on failure
For signature checking, the interpreter calls into libsecp256k1 for the actual ECDSA/Schnorr verification.
Interactive: The Interpreter Pipeline
Click each stage to see what happens inside VerifyScript():
Script Flags
Script verification is controlled by flags that enable different rules. This is how soft forks are implemented, new rules are added as new flags:
Policy flags (used for mempool acceptance) are a superset of consensus flags; they're stricter. This means the mempool rejects scripts that are technically valid but could become invalid in future soft forks.