Miniscript
Bitcoin Script is powerful but dangerous. A single misplaced opcode can make funds permanently unspendable or accidentally anyone-can-spend. Miniscript is a structured subset of Script that enables composable, analyzable spending conditions without these foot-guns.
What Miniscript Solves
Raw Bitcoin Script has no standard tooling for answering critical questions: "Is this script safe?" "What's the worst-case spending cost?" "Can a third party malleate the witness?" Miniscript provides a type system and composition rules that answer all of these at compile time.
The Type System: B, V, K, W
Every miniscript expression has one of four base types, describing how it interacts with the stack:
Type Properties
Beyond the four base types, miniscript tracks boolean properties that enable automated reasoning:
- z (zero-arg): consumes exactly 0 stack elements
- o (one-arg): consumes exactly 1 stack element
- d (dissatisfiable): there's an easy way to construct a dissatisfaction
- u (unit): satisfaction pushes exactly 1 (not just nonzero)
- e (expression): the dissatisfaction is nonmalleable
- f (forced): dissatisfactions always require a signature
- s (safe): satisfactions always require a signature
- m (nonmalleable): for every distinct set of available keys, there is exactly one valid satisfaction
Composition Fragments
Miniscript provides a small set of combinators that can be composed into complex policies:
and_v(X, Y): both X and Y must be satisfiedor_b(X, Y): either X or Y (or both, but optimized for one)thresh(k, X1, X2, ...): at least k of the sub-expressionsolder(n): relative timelock of n blocksafter(n): absolute timelock (block height or time)pk_k(key): a raw public key checkmulti_a(k, key1, key2, ...): k-of-n multisig (Tapscript version)
Users write a human-readable policy like or(pk(A), and(pk(B), older(144))). A compiler converts this to the optimal miniscript expression, which maps directly to Bitcoin Script. The compiler chooses the most weight-efficient encoding automatically.
Tapscript Support
Miniscript supports both P2WSH (SegWit v0) and Tapscript (SegWit v1) contexts. The key differences in Tapscript mode are: multi_a replaces multi (using individual OP_CHECKSIGADD instead of OP_CHECKMULTISIG), and script size limits are relaxed (no 10,000-byte limit in Tapscript).
Satisfaction Algorithm
Given a miniscript and a set of available keys/preimages, the ProduceInput() function determines the optimal witness. It works bottom-up: each node calculates both its cheapest satisfaction and its cheapest dissatisfaction. Parent nodes combine these to find the globally optimal witness. If no valid satisfaction exists, the algorithm reports failure rather than producing an invalid witness.
Output Descriptors
Output descriptors are a standardized language for describing which scripts a wallet watches or can sign for. They replace the old "import a private key" model with something structured, deterministic, and exportable.
Why Descriptors Matter
Before descriptors, wallet backup meant backing up a key pool and hoping you covered all address types. With descriptors, a wallet is fully defined by a small set of descriptor strings. You can export them, import them into another wallet implementation, and get the exact same set of addresses.
Descriptor Syntax
A descriptor is a function-call-like expression that describes how to derive scriptPubKeys:
wpkh(xpub.../0/*): native SegWit single-sig; derive child keys at index *tr(internal_key, {script_tree}): Taproot with internal key and optional script treewsh(multi(2, key1, key2, key3)): 2-of-3 multisig wrapped in P2WSHwsh(sortedmulti(2, key1, key2)): multisig with keys sorted lexicographicallycombo(key): all output types for a given key (legacy, P2SH-segwit, native segwit)raw(hex): raw scriptPubKey (for non-standard scripts)
Checksums
Every descriptor has an 8-character checksum appended after a #, using a BCH-based error-detecting code. This catches typos and ensures the descriptor was transmitted correctly: wpkh([d34db33f/84'/0'/0']xpub.../0/*)#checksum.
DescriptorCache
Deriving extended public keys at each index is expensive. DescriptorCache stores three kinds of cached xpubs:
- Parent xpubs: the xpub at the key expression position (pre-derivation)
- Last hardened xpubs: the xpub at the last hardened derivation step (for ranged descriptors)
- Derived xpubs: fully derived xpubs at specific child indexes (the final result)
This cache is serialized with the wallet, so re-opening a wallet doesn't require re-deriving thousands of keys.
Descriptors + Miniscript
Since Bitcoin Core 26.0, descriptors can contain miniscript expressions: wsh(and_v(v:pk(A), older(144))). This bridges the gap between the wallet's key management (descriptors) and advanced spending policies (miniscript). The wallet can then automatically produce witnesses for any policy it has the keys to satisfy.
Soft Fork Deployment (BIP 9)
BIP 9 defines how new consensus rules are activated on the Bitcoin network using version bits in block headers. It allows multiple soft forks to be deployed simultaneously, each using a different bit position.
The State Machine
Each deployment follows a five-state finite state machine, evaluated once per retarget period (2016 blocks):
If the timeout time passes without the threshold being reached, the deployment enters the FAILED state. This is also a final state, meaning the activation window has closed. A new deployment (with a different bit) would be needed to try again.
Block Version Encoding
BIP 9 blocks use version numbers with the top 3 bits set to 001 (the value 0x20000000), leaving 29 bits available for signaling. Each deployment is assigned one bit position (0-28). Miners signal readiness by setting that bit to 1.
Buried Deployments
Once a soft fork has been active for a long time, its BIP 9 activation logic is replaced with a simple height check. These are called "buried deployments." For example, SegWit activation is hardcoded as active at height 481,824 rather than checking version bits. This simplifies the code and removes the dependency on historical block versions.
VersionBitsCache
The VersionBitsCache class caches deployment states per retarget period to avoid recomputing them on every block. It also computes the correct block version for miners (ComputeBlockVersion) and checks for unknown activations that might indicate the node needs upgrading.
Test Networks
Bitcoin Core supports multiple networks for development and testing, each with its own genesis block, address prefixes, and network magic bytes.
Testnet (testnet3 / testnet4)
- Public network mirroring mainnet's behavior, but with worthless coins
- Lower difficulty target; blocks can be mined with consumer hardware
- Special difficulty reset rule: if no block is found for 20 minutes, the difficulty drops to 1
- Testnet4 (introduced in Bitcoin Core 28.0) fixes known testnet3 issues, including time-warp attacks
Signet (BIP 325)
Signet is a centrally-signed test network. Instead of proof-of-work, blocks must include a valid signature from a designated key (the "challenge script"). This provides a stable, predictable test environment:
- No reorgs: the signet authority produces blocks in order
- Consistent block times: blocks arrive on schedule, unlike testnet's wild difficulty swings
- Custom signets: anyone can create their own signet with their own challenge script
- Default signet:
-signetconnects to the community-run signet (started at block 0 in 2020)
Regtest (Regression Testing)
- Fully local network: no connections to other nodes
- Instant block generation:
generatetoaddress 1 <addr>mines a block immediately - Difficulty is always 1: any hash satisfies the target
- Used extensively in Bitcoin Core's functional test suite (over 300 test scripts)
- All soft forks are active from block 1 by default
Headers Sync Anti-DoS
When a new node joins the network, it must download ~900,000+ block headers. An attacker could waste the node's memory by sending millions of low-work headers. The HeadersSyncState class implements a two-phase strategy to prevent this.
The Problem
Each CBlockIndex object stored in memory costs ~100 bytes. An attacker sending 10 million fake low-difficulty headers would consume ~1 GB of RAM. Before this defense was added, new nodes were vulnerable to this memory exhaustion attack.
Two-Phase Download
Memory Efficiency
During PRESYNC, the node uses a bitdeque to store commitments, using only 1 bit per N headers. For the entire Bitcoin blockchain (900k+ headers), this amounts to a few kilobytes of temporary memory per peer, rather than the 90+ MB that storing all headers would require.
During sync, headers are stored without the hashPrevBlock field (which is redundant since headers arrive in order). This CompressedHeader type saves 32 bytes per header, further reducing memory usage during the redownload phase.