The Blockchain Chief Bitcoin Book / Part IV: Advanced Features
Chapter 13

Advanced Topics

Miniscript's composable spending policies, output descriptors for wallet interoperability, the BIP 9 soft-fork deployment state machine, and Bitcoin's test networks.

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:

B Base Pushes nonzero (satisfied) or zero (dissatisfied) onto the stack. Required at the top level.
V Verify Pushes nothing on success; aborts on failure. Cannot be dissatisfied, only satisfied or script failure.
K Key Pushes a public key. Becomes a B-type when followed by OP_CHECKSIG.
W Wrapped Takes input from one below the stack top. Always either "OP_SWAP [B]" or an altstack pattern.

Type Properties

Beyond the four base types, miniscript tracks boolean properties that enable automated reasoning:

Composition Fragments

Miniscript provides a small set of combinators that can be composed into complex policies:

💡 Policy vs. Miniscript vs. Script

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:

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:

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):

DEFINEDInitial state. The deployment exists but hasn't started yet.
↓ past start_time
STARTEDMiners can signal support by setting the deployment's bit in the block version.
↓ threshold reached (95% of period)
LOCKED_INActivation is guaranteed. One more period before new rules take effect.
↓ period ends + min_activation_height
ACTIVENew consensus rules are enforced. Final state.
⚠️ FAILED State

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)

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:

CTransaction m_to_spend Virtual tx that commits to the block data (modified merkle root)
CTransaction m_to_sign Virtual tx whose signature proves block authority

Regtest (Regression Testing)

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

Phase 1: PRESYNCDownload headers and calculate cumulative work, but don't store them. Only keep 1 commitment bit per N headers (salted hash).
↓ cumulative work exceeds minimum threshold
Phase 2: REDOWNLOADDownload the same headers again. Verify them against the commitments stored in Phase 1. Only accept headers into permanent memory once they have enough verified commitments on top.
↓ sync complete
FINALAll headers are stored and verified. Normal operation begins.

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.

💡 CompressedHeader

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.