DEV Community

Aturo Phil
Aturo Phil

Posted on

BIP-110 Explained for Developers: How Bitcoin Soft Forks Actually Work

Bitcoin Development Series, Part 1 of 4

Bitcoin is heading toward an August 2026 deadline for BIP-110, a proposed temporary softfork that would restrict Ordinals-style arbitrary data from being embedded in transactions for one year. As of today, miner signaling sits at effectively zero. The proposal is almost certainly going to fail but the mechanics of why and how are worth understanding if you work anywhere near the Bitcoin protocol.

This post walks through how soft fork activation works, what BIP-110 specifically proposes, and how to inspect miner signaling yourself with code.


What Is a Soft Fork?

A soft fork is a backward-compatible change to Bitcoin's consensus rules. Nodes running old software still accept blocks from nodes running the new rules — but not vice versa. This is what makes soft forks safer than hard forks in a permissionless network: you do not force everyone to upgrade on day one.

Hard forks, by contrast, change rules in a way that causes old nodes to reject new blocks entirely. They require near-universal coordination, which is why Bitcoin has avoided them.

How Soft Fork Activation Works: BIP 9

The dominant activation mechanism used since 2016 is defined in BIP 9. The process works like this:

  1. A proposal is assigned a version bit (bit 0–28) in the block header's nVersion field.
  2. Miners signal readiness by setting that bit in blocks they produce.
  3. Activation requires 95% of blocks in a 2,016-block retarget window to signal support.
  4. There is a starttime and a timeout. If the threshold is not met before timeout, the proposal fails and is discarded.
# Simplified BIP 9 state machine logic

THRESHOLD = 0.95          # 95% of blocks in a retarget window
WINDOW     = 2016         # one retarget period

def check_activation(signaling_blocks: int, total_blocks: int) -> str:
    ratio = signaling_blocks / total_blocks
    if ratio >= THRESHOLD:
        return "LOCKED_IN"   # activates after one more window
    return "STARTED"         # still counting

print(check_activation(1914, 2016))   # => LOCKED_IN  (95%)
print(check_activation(1800, 2016))   # => STARTED    (89.3%)
print(check_activation(5, 2016))      # => STARTED    (0.25%) — current BIP-110 situation
Enter fullscreen mode Exit fullscreen mode

BIP-110 uses a variant of this with a hard deadline instead of an ongoing timeout. If signaling does not lock in before the August activation window closes, the proposal is considered rejected.


What BIP-110 Actually Proposes

BIP-110 is titled "Reduced Data Temporary Softfork." It proposes to cap the amount of arbitrary data that can be pushed via witness scripts to 42 bytes per input — effectively making Ordinals-style inscriptions impossible for a period of one year.

The mechanism targets the OP_FALSE OP_IF ... OP_ENDIF envelope that Ordinals uses to embed data in the tapscript witness. Under BIP-110, a transaction would be invalid during the restriction window if any witness stack item exceeds the cap.

# Standard Ordinals inscription envelope (what BIP-110 targets)
OP_FALSE
OP_IF
  OP_PUSH "ord"           # protocol tag
  OP_PUSH 1
  OP_PUSH "text/plain"    # content type
  OP_PUSH 0
  OP_PUSH <arbitrary data up to 4MB per input>
OP_ENDIF
<actual spend script>
Enter fullscreen mode Exit fullscreen mode

Under BIP-110, the <arbitrary data> push beyond 42 bytes would cause the transaction to be rejected by upgraded nodes. Non-upgraded nodes would still accept it which is what makes this a soft fork rather than a hard fork.


Checking Miner Signaling On-Chain

You can inspect signaling using Bitcoin Core's RPC interface or a block explorer API. Here is how to do it with Python and the bitcoin-rpc library:

pip install python-bitcoinrpc
Enter fullscreen mode Exit fullscreen mode
from bitcoinrpc.authproxy import AuthServiceProxy
import struct

RPC_URL = "http://rpcuser:rpcpassword@127.0.0.1:8332"
BIP110_BIT = 4   # assigned version bit for BIP-110

def get_rpc():
    return AuthServiceProxy(RPC_URL)

def check_version_bit(nversion: int, bit: int) -> bool:
    """
    BIP 9 signals when nVersion has bit N set
    and the high bits match 0x20000000.
    """
    BIP9_TOP_MASK  = 0xE0000000
    BIP9_TOP_BITS  = 0x20000000
    if (nversion & BIP9_TOP_MASK) != BIP9_TOP_BITS:
        return False
    return bool(nversion & (1 << bit))

def scan_signaling(num_blocks: int = 2016) -> dict:
    rpc = get_rpc()
    tip_hash  = rpc.getbestblockhash()
    tip       = rpc.getblock(tip_hash)
    tip_height = tip["height"]

    signaling = 0
    total     = 0
    block_hash = tip_hash

    for _ in range(num_blocks):
        block   = rpc.getblock(block_hash)
        nversion = block["version"]

        if check_version_bit(nversion, BIP110_BIT):
            signaling += 1
        total += 1

        block_hash = block.get("previousblockhash")
        if not block_hash:
            break

    return {
        "blocks_scanned":    total,
        "signaling":         signaling,
        "not_signaling":     total - signaling,
        "signal_percentage": round((signaling / total) * 100, 2),
        "threshold_needed":  95.0,
        "locked_in":         (signaling / total) >= 0.95,
    }

result = scan_signaling(2016)
print(result)
# Current output (approximately):
# {'blocks_scanned': 2016, 'signaling': 5, 'not_signaling': 2011,
#  'signal_percentage': 0.25, 'threshold_needed': 95.0, 'locked_in': False}
Enter fullscreen mode Exit fullscreen mode

If you do not run a full node, you can query a public API instead:

import httpx

def check_signaling_via_api(num_blocks: int = 100) -> dict:
    """
    Uses mempool.space API — no node required.
    """
    signaling = 0

    # Get recent block heights
    resp   = httpx.get("https://mempool.space/api/blocks")
    blocks = resp.json()

    for block in blocks[:num_blocks]:
        nversion = block["version"]
        if check_version_bit(nversion, BIP110_BIT):
            signaling += 1

    return {
        "sample_size":       len(blocks[:num_blocks]),
        "signaling":         signaling,
        "signal_percentage": round((signaling / len(blocks[:num_blocks])) * 100, 2),
    }

print(check_signaling_via_api())
Enter fullscreen mode Exit fullscreen mode

Why Saylor and Adam Back Oppose It

The opposition from Michael Saylor and Adam Back is not purely technical it is about governance precedent. Their argument: Bitcoin's consensus layer should not be used to resolve application-level disputes. Ordinals are a use of witness space that Taproot technically permits. If the community dislikes it, the correct response is social (pools refusing transactions) or economic (higher fee competition), not a consensus rule change.

The concern is that BIP-110 passing would establish a precedent where any sufficiently disliked use of block space can be banned by a temporary softfork which is a much larger surface area than the original dispute.


What Happens If BIP-110 Fails

If the August window closes without 95% signaling, BIP-110 is discarded with no effect on the chain. The Consensus Cleanup effort (being split into independent BIPs per the latest Bitcoin Optech discussion) continues as a separate track, focused on fixing genuine protocol vulnerabilities rather than restricting applications.

The Ordinals debate will continue but through fee pressure and mining pool policy, not consensus rules.


Further Reading


Next in this series: How Ordinals Actually Work Under the Hood — the commit-reveal pattern, witness encoding, and a full inscription walkthrough with code.

Top comments (0)