Bitcoin Script: Understanding Bitcoin's Programming Language
A developer's guide to Bitcoin Script: opcodes, standard scripts, and the limits of Bitcoin programmability.
Every Bitcoin transaction contains a small program. Not a smart contract in the Ethereum sense, but a script: a stack-based, intentionally limited language that defines who can spend a given output and under what conditions. Bitcoin Script is the reason Bitcoin can do more than simple transfers. It enables hash time-locked contracts, timelocks, multisignature wallets, and the spending conditions that power every Layer 2 protocol built on Bitcoin.
Understanding Script is essential for anyone building on Bitcoin. This article walks through how Script works, what it can and cannot do, and why its limitations are a feature rather than a bug.
A Stack-Based Execution Model
Bitcoin Script is a stack-based programming language loosely inspired by Forth. It uses reverse Polish notation: operands are pushed onto a stack, and operators pop them off, perform computation, and push results back. There are no variables, no named functions, and no stored state. Each script executes once, returning either true or false.
A Bitcoin UTXO (unspent transaction output) is locked by a scriptPubKey (also called the locking script). To spend it, the spender provides a scriptSig (the unlocking script). The Bitcoin node concatenates the unlocking script with the locking script and runs them. If the stack ends with a truthy value on top, the spend is valid.
Why stack-based? Stack machines are simple to implement, simple to verify, and leave no ambiguity about execution order. Every node in the Bitcoin network must agree on whether a transaction is valid. A stack machine makes deterministic execution straightforward, which is exactly what a consensus system requires.
Script execution step by step
Consider the simplest useful script: a Pay-to-Public-Key-Hash (P2PKH) transaction. The locking script stored on the blockchain looks like this:
OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIGThe unlocking script provided by the spender is:
<signature> <pubKey>Execution proceeds left to right. The signature and public key are pushed onto the stack. OP_DUP duplicates the top item (the public key). OP_HASH160 hashes it. The resulting hash is compared against the expected hash with OP_EQUALVERIFY. Finally, OP_CHECKSIG verifies the signature against the public key. If all checks pass, the script succeeds and the UTXO can be spent.
Common Opcodes
Bitcoin Script includes roughly 100 active opcodes. Many are straightforward stack manipulation or arithmetic operations. Here are the most important ones for understanding how Bitcoin transactions work:
| Opcode | Function | Use case |
|---|---|---|
| OP_CHECKSIG | Verify a signature against a public key | Standard single-sig spending |
| OP_CHECKMULTISIG | Verify M-of-N signatures | Multisig wallets |
| OP_HASH160 | SHA-256 then RIPEMD-160 | P2PKH address derivation |
| OP_EQUAL / OP_EQUALVERIFY | Compare top two stack items | Hash preimage checks, P2SH validation |
| OP_CHECKLOCKTIMEVERIFY (CLTV) | Fail if block height or time not reached | Absolute timelocks |
| OP_CHECKSEQUENCEVERIFY (CSV) | Fail if relative time not elapsed | Relative timelocks, payment channels |
| OP_IF / OP_ELSE / OP_ENDIF | Conditional branching | HTLCs, multi-path spending conditions |
| OP_RETURN | Mark output as provably unspendable | Data embedding, on-chain metadata |
| OP_DROP | Remove top stack item | Cleaning up after conditional checks |
| OP_CHECKSIGADD | Increment counter if signature valid (Tapscript) | Efficient multisig in Taproot |
The timelock opcodes (CLTV and CSV) are particularly important for Layer 2 protocols. They enable time-dependent spending conditions that form the foundation of payment channels, hash time-locked contracts, and dispute resolution mechanisms.
Standard Script Types
While Bitcoin Script is flexible enough to express arbitrary spending conditions, the network only relays transactions using recognized standard script templates. This is a policy choice, not a consensus rule: non-standard scripts are valid if mined, but most nodes will not propagate them. The standard types have evolved over time, each improving on the last.
P2PKH: Pay-to-Public-Key-Hash
The original standard script type, used since Bitcoin's early days. The locking script contains a hash of the recipient's public key. The spender must provide the full public key and a valid signature. P2PKH addresses start with "1" (mainnet). This format works but is verbose: the unlocking script must include both the public key and signature, consuming block space.
P2SH: Pay-to-Script-Hash
Introduced in BIP 16 (2012), P2SH moves complexity from the locking script to the unlocking script. Instead of embedding the full script in the output, the sender only includes a hash of the redeem script. The spender must provide the actual script (which must hash to the expected value) along with the data needed to satisfy it. P2SH addresses start with "3".
P2SH enabled practical multisig wallets because senders no longer needed to know the details of the spending conditions. Any arbitrarily complex script could be hidden behind a simple hash.
P2WPKH and P2WSH: SegWit native scripts
Segregated Witness (activated in 2017) separated signature data from the transaction structure, fixing transaction malleability and enabling a discount on witness data. P2WPKH is the SegWit equivalent of P2PKH; P2WSH is the SegWit equivalent of P2SH. Both use bech32 addresses starting with "bc1q".
The witness discount means that signature and script data costs 0.25 weight units per byte instead of 1. This incentivizes moving complexity into the witness, making advanced scripts cheaper to use.
P2TR: Pay-to-Taproot
Taproot, activated in November 2021 via BIP 341, represents the most significant upgrade to Bitcoin Script since SegWit. P2TR outputs can be spent in two ways: a key path (a single Schnorr signature) or a script path (revealing one of potentially many scripts encoded in a Merkle tree).
The key path is indistinguishable from a simple single-signature spend, regardless of how many parties or conditions were actually involved. This means a 3-of-5 multisig that resolves cooperatively looks identical on-chain to a single-key payment. Only when participants disagree and must use the script path do the underlying conditions become visible.
Privacy through uniformity: With Taproot, cooperative spends (the common case) all look the same on-chain. An observer cannot distinguish a simple payment from a complex contract that happened to close cooperatively. This is a significant privacy improvement for multisig users, Lightning channel closes, and other scripted transactions.
Script type comparison
| Script type | Address prefix | Introduced | Key features |
|---|---|---|---|
| P2PKH | 1... | 2009 | Original format, widely supported |
| P2SH | 3... | 2012 | Hash-based, hides script complexity from sender |
| P2WPKH | bc1q... | 2017 | SegWit, witness discount, fixes malleability |
| P2WSH | bc1q... | 2017 | SegWit script hash, cheaper complex scripts |
| P2TR | bc1p... | 2021 | Schnorr, Merkle script trees, key/script path spending |
Why Script Is Intentionally Limited
Bitcoin Script is not Turing-complete. It has no loops, no recursion, and no ability to reference external state. Scripts cannot read other transactions, query block headers, or maintain persistent storage. Every script execution is a pure function: given these inputs, is this spend authorized?
These limitations are deliberate. Satoshi Nakamoto disabled several opcodes early in Bitcoin's history (including OP_CAT and OP_MUL) after discovering potential denial-of-service vectors. The design philosophy prioritizes safety over expressiveness:
- No loops means every script terminates in bounded time. Nodes can validate any transaction without risk of infinite execution.
- No external state means scripts are fully deterministic. Two nodes running the same script on the same inputs always get the same result.
- No persistent storage means the UTXO set remains the only source of truth. There is no "contract state" to synchronize across the network.
- Limited arithmetic prevents overflow attacks and keeps validation costs predictable.
The consequence is that Bitcoin Script can express conditions on who can spend and when, but it cannot express conditions on where funds go next. This is the fundamental difference between Bitcoin's spending conditions and Ethereum's smart contracts: Bitcoin scripts guard inputs, not outputs.
Disabled and reserved opcodes
Several opcodes that existed in early Bitcoin were disabled because they introduced vulnerabilities or unbounded computation. OP_CAT (concatenate two stack items), OP_MUL (multiply), OP_LSHIFT, and OP_RSHIFT were all disabled. Any transaction containing a disabled opcode is invalid by consensus, regardless of execution path. Some of these opcodes are now being reconsidered: OP_CAT in particular has been proposed for re-enablement through covenant proposals like BIP 347, which would allow it within Tapscript.
Tapscript: Script for the Taproot Era
Taproot did not just add a new address format: it introduced Tapscript, a modified version of Bitcoin Script with several important changes. Tapscript is defined in BIP 342 and applies to scripts executed via the Taproot script path.
Key Tapscript changes
- OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY are replaced by OP_CHECKSIGADD, which is more efficient for batch validation of multiple signatures.
- All signature operations use Schnorr signatures instead of ECDSA, enabling key aggregation and threshold signing schemes like FROST and MuSig2.
- Script size limits are relaxed. The 10,000-byte script size limit from legacy Script is removed, allowing larger and more complex scripts in the Taproot witness.
- Unknown opcodes are treated as OP_SUCCESS, which makes future soft-fork upgrades easier. New opcodes can be assigned to currently-undefined values without invalidating existing scripts.
The OP_SUCCESS mechanism is particularly important for Bitcoin's evolution. It means new opcodes can be added through soft forks rather than hard forks, since old nodes will simply treat transactions using new opcodes as valid (anyone-can-spend). This is a cleaner upgrade path than the OP_NOP approach used for CLTV and CSV.
Miniscript: Safer Script Composition
Raw Bitcoin Script is powerful but error-prone. Miniscript is a structured subset of Script developed by Pieter Wuille, Andrew Poelstra, and Sanket Kanjalkar at Blockstream. It provides a type system and composition rules that make it possible to reason about scripts formally.
Miniscript is not a new language: it compiles to standard Bitcoin Script. Its value is in what it prevents. With raw Script, it is easy to write a spending condition that appears correct but has subtle bugs: an unreachable branch, an off-by-one in a threshold, or an unintended anyone-can-spend path. Miniscript makes these classes of bugs structurally impossible.
What Miniscript enables
- Automated analysis of spending conditions: given a Miniscript, tools can enumerate every possible spending path and verify that each requires the intended set of keys or conditions.
- Composition: multiple parties can independently specify their required conditions and combine them into a single script with provable properties.
- Optimal witness generation: given a set of available keys and hash preimages, Miniscript wallets can automatically construct the minimal-cost witness for spending.
- Compatibility with PSBTs (Partially Signed Bitcoin Transactions): Miniscript-aware signers can determine exactly which signatures they need to provide without understanding the full script.
For example, a vault policy like "spendable by Alice immediately, or by Bob after 30 days, or by Alice and Bob together at any time" can be expressed in Miniscript as:
or(pk(Alice),or(and(pk(Bob),older(4320)),and(pk(Alice),pk(Bob))))This compiles to optimized Bitcoin Script, and analysis tools can verify that no spending path exists that was not intended. Output descriptors (defined in BIP 380) build on Miniscript to provide a standard way of describing wallet configurations.
Bitcoin Script in Layer 2 Protocols
The most sophisticated uses of Bitcoin Script today are not in simple payments: they are in the spending conditions that secure Layer 2 protocols. Payment channels, HTLCs, and dispute resolution mechanisms are all implemented as Bitcoin scripts.
Payment channels
Payment channels work by creating a funding transaction (a 2-of-2 multisig on-chain) and then exchanging off-chain commitment transactions that spend from it. Each commitment transaction uses timelocks and revocation keys to ensure that only the latest state can be claimed. If a counterparty publishes an old state, the other party can use the revocation key to claim all funds via a justice transaction.
These mechanisms rely entirely on Bitcoin Script: OP_CHECKSIG for signature verification, OP_CHECKSEQUENCEVERIFY for relative timelocks, and OP_IF/OP_ELSE for branching between the cooperative and dispute paths.
HTLCs
Hash time-locked contracts are the building block of cross-channel and cross-chain atomic payments. An HTLC script has two spending paths: reveal a hash preimage before a deadline, or reclaim funds after the deadline expires. The script uses OP_HASH160 and OP_EQUALVERIFY for the hash check, combined with OP_CHECKLOCKTIMEVERIFY or OP_CHECKSEQUENCEVERIFY for the timeout path.
The evolution from HTLCs to PTLCs (point time-locked contracts) uses Schnorr signature properties instead of hash preimages, improving privacy by making payment routing unlinkable across hops.
Bitcoin Script vs Ethereum's EVM
Comparing Bitcoin Script to Ethereum's Virtual Machine illustrates two fundamentally different philosophies for blockchain programmability. Bitcoin optimizes for verifiability and safety. Ethereum optimizes for expressiveness and flexibility.
| Property | Bitcoin Script | Ethereum EVM |
|---|---|---|
| Computation model | Stack-based, no loops | Stack-based, Turing-complete |
| State | Stateless (UTXO conditions only) | Stateful (persistent contract storage) |
| Execution trigger | Spending a UTXO | Calling a contract function |
| Termination | Always terminates (no loops) | Gas limit enforces termination |
| Composability | Limited (scripts are independent) | High (contracts call other contracts) |
| Attack surface | Narrow (limited opcodes) | Wide (reentrancy, overflow, etc.) |
| Upgrade path | New opcodes via soft fork | EIPs, hard forks for EVM changes |
| Programmability | Spending conditions only | Arbitrary computation |
The EVM's expressiveness comes at a cost. Reentrancy attacks, integer overflow bugs, and unintended contract interactions have led to billions of dollars in losses across the Ethereum ecosystem. Bitcoin Script's limited surface area makes entire categories of bugs structurally impossible. You cannot have a reentrancy attack in a language that does not support calls to external code.
That said, Bitcoin Script's limitations are real. It cannot enforce conditions on transaction outputs (where funds go after spending), which means constructs like covenants require new opcodes or clever workarounds. The ongoing covenant debate reflects the tension between keeping Script safe and making Bitcoin more programmable at the base layer.
The Covenant Frontier
Covenants would allow scripts to impose conditions on future transactions: restricting where coins can be sent, enforcing spending schedules, or creating vault-like constructs directly on Layer 1. Several proposals are under discussion:
- OP_CTV (BIP 119): enables pre-committed transaction templates, useful for vaults and batched payments.
- OP_CAT (BIP 347): re-enables stack item concatenation in Tapscript, which can be combined with other opcodes to build covenant-like behavior.
- OP_VAULT (BIP 345): purpose-built opcodes for creating secure vaults with recovery paths.
- MATT (Merklize All The Things): uses Merkle tree commitments to enable general-purpose covenant logic.
None of these proposals have been activated. The Bitcoin development community moves cautiously on consensus changes, particularly ones that expand Script's capabilities. Each proposal is evaluated not just on its utility but on its potential for misuse: could it enable censorship of specific coins? Could it make fungibility analysis easier? These concerns reflect Bitcoin's conservative design philosophy.
Extending Programmability Off-Chain
Rather than waiting for new opcodes, Layer 2 protocols can extend Bitcoin's programmability without changing Script at all. The pattern is consistent: use Script for what it does well (enforcing spending conditions for dispute resolution) and move complex logic off-chain.
Spark takes this approach. Smart contract-like behavior happens off-chain through cooperative signing between users and operators. Bitcoin Script is only involved when participants disagree and need to settle disputes on-chain. In the cooperative case, a Spark transfer looks like any other Taproot key-path spend: a single Schnorr signature that reveals nothing about the off-chain logic that produced it.
This design philosophy treats Bitcoin Script as a court of last resort rather than a runtime environment. The base layer enforces property rights and dispute resolution. Everything else: transfer logic, token operations, conditional payments: happens above the script layer, where complexity does not burden every node in the network.
Script as security, not execution: Layer 2 protocols like Spark and the Lightning Network use Bitcoin Script to guarantee that users can always recover their funds on-chain. The script path exists for disputes. The cooperative path (key-path spending) is where normal operation happens: faster, cheaper, and more private.
Writing and Testing Scripts
For developers looking to work with Bitcoin Script directly, several tools and libraries make the process more accessible:
- rust-miniscript: the reference Miniscript implementation, with compilation, analysis, and signing support.
- Output descriptors: Bitcoin Core's standard for describing wallet configurations, built on Miniscript.
- btcdeb: a Script debugger that lets you step through script execution instruction by instruction.
- Miniscript playground: Pieter Wuille's web tool for compiling and analyzing Miniscript policies.
When building applications that create custom scripts, Miniscript should be the default choice over raw Script. The safety guarantees and tooling support make it practical to build complex spending conditions with confidence that they behave as intended.
Looking Ahead
Bitcoin Script sits at the intersection of Bitcoin's most important tension: safety versus expressiveness. The language's limitations have kept Bitcoin remarkably secure for over 15 years. Those same limitations drive the innovation happening at Layer 2, where protocols work around Script's constraints rather than lobbying to remove them.
The trajectory is clear. Tapscript made the language more extensible without making it less safe. Miniscript made it more usable without making it more complex. Proposed opcodes like OP_CTV and OP_CAT could enable new classes of on-chain logic while preserving Script's core properties. And Layer 2 protocols continue to push the boundary of what is possible with the opcodes that already exist.
For developers, the practical takeaway is this: Bitcoin Script is not a limitation to work around. It is a foundation to build on. Understanding how it works, what it guarantees, and where its boundaries lie is the starting point for building anything meaningful on Bitcoin.
This article is for educational purposes only. It does not constitute financial or investment advice. Bitcoin and Layer 2 protocols involve technical and financial risk. Always do your own research and understand the tradeoffs before using any protocol.

