Menu
BOLT-bolt-05 Final

BOLT-05: On-Chain Handling

Lightning on-chain transaction handling. Force close procedures, HTLC resolution, and penalty transactions.

Type Basis of Lightning Technology
Number bolt-05
Status Final
Authors Lightning Network Developers
Original https://github.com/lightning/bolts/blob/master/05-onchain.md
Requires

BOLT-05: On-Chain Transaction Handling

BOLT-05 specifies how nodes handle Lightning-related transactions that appear on the blockchain. This includes force closes, HTLC resolution, and penalty enforcement.

Specification Summary

AspectValue
StatusFinal
LayerBitcoin
PurposeOn-chain handling
DependenciesBOLT-02, BOLT-03

Scenarios

ScenarioTriggerResolution
Cooperative closeBoth parties agreeImmediate funds
Force close (local)You broadcastCSV delay
Force close (remote)Peer broadcastsImmediate (their side)
Revoked statePeer cheatsJustice transaction

Monitoring Requirements

Nodes must monitor the blockchain for:

  1. Funding transaction: Confirmation count
  2. Commitment transactions: Any broadcast
  3. HTLC transactions: Preimage reveals
  4. Revoked states: Fraud detection
def monitor_channel(channel_id, funding_txid):
    """Monitor blockchain for channel activity."""
    while channel.state != 'closed':
        # Check for any spend of funding output
        spend = blockchain.get_spend(funding_txid, funding_output_index)

        if spend:
            tx = blockchain.get_transaction(spend.txid)
            handle_closing_transaction(channel_id, tx)

Force Close Handling

When You Force Close

You broadcast your commitment transaction:

  1. to_local output: Locked by CSV, wait to_self_delay blocks
  2. to_remote output: Immediately spendable by peer
  3. Offered HTLCs: Wait for timeout, then HTLC-timeout tx
  4. Received HTLCs: If you have preimage, HTLC-success tx
def handle_local_force_close(channel):
    """Process your own force close."""
    # Broadcast commitment
    broadcast(channel.local_commitment)

    # Schedule to_local spend after delay
    schedule_after_blocks(
        channel.to_self_delay,
        lambda: claim_to_local(channel)
    )

    # Handle HTLCs
    for htlc in channel.offered_htlcs:
        schedule_at_block(
            htlc.cltv_expiry,
            lambda: timeout_htlc(channel, htlc)
        )

    for htlc in channel.received_htlcs:
        if have_preimage(htlc.payment_hash):
            claim_htlc_success(channel, htlc)

When Peer Force Closes

Peer broadcasts their commitment transaction:

  1. Your to_remote output: Claim immediately
  2. Their HTLCs to you: Claim with preimage or wait for timeout
  3. Check for revocation: Is this an old state?
def handle_remote_force_close(channel, their_commitment):
    """Process peer's force close."""
    # Check if revoked
    revocation_secret = find_revocation_secret(their_commitment)
    if revocation_secret:
        broadcast_justice(channel, revocation_secret)
        return

    # Claim immediate outputs
    claim_to_remote(channel, their_commitment)

    # Handle HTLCs (roles inverted)
    for htlc in their_commitment.htlcs_to_us:
        if have_preimage(htlc.payment_hash):
            claim_htlc(channel, htlc)

Revoked State Detection

Identifying Revoked Commitments

Each commitment has a unique per_commitment_point. Store all revealed revocation secrets:

class RevocationStore:
    def __init__(self):
        self.secrets = {}  # commitment_number -> secret

    def add_secret(self, commitment_num: int, secret: bytes):
        self.secrets[commitment_num] = secret

    def find_secret(self, commitment_tx) -> Optional[bytes]:
        """Check if transaction uses a revoked commitment."""
        commitment_num = extract_commitment_number(commitment_tx)
        return self.secrets.get(commitment_num)

Justice Transaction

When peer broadcasts revoked state, claim ALL funds:

def broadcast_justice(channel, revocation_secret):
    """Claim all channel funds after peer's fraud."""
    # Derive revocation key
    revocation_key = derive_revocation_privkey(
        channel.revocation_basepoint_secret,
        channel.per_commitment_point,
        revocation_secret
    )

    # Claim to_local output (now ours via revocation)
    claim_with_revocation(
        channel.commitment_output_to_local,
        revocation_key
    )

    # Claim all HTLCs via revocation path
    for htlc in channel.all_htlcs:
        claim_htlc_with_revocation(htlc, revocation_key)

HTLC Resolution

HTLC States on Force Close

HTLC TypeYour ActionDeadline
Offered (outgoing)Timeout after expiryCLTV expiry + margin
Received (incoming)Claim with preimageBefore expiry

HTLC Timeout (Offered)

def timeout_offered_htlc(channel, htlc):
    """Claim back offered HTLC after timeout."""
    current_block = blockchain.height()

    if current_block >= htlc.cltv_expiry:
        # Build HTLC-timeout transaction
        htlc_timeout_tx = build_htlc_timeout(
            commitment_tx=channel.commitment,
            htlc=htlc,
            local_sig=sign_htlc_timeout(channel, htlc),
            remote_sig=htlc.remote_sig  # From commitment_signed
        )
        broadcast(htlc_timeout_tx)

HTLC Success (Received)

def claim_received_htlc(channel, htlc, preimage):
    """Claim received HTLC with preimage."""
    # Must claim before timeout!
    if blockchain.height() >= htlc.cltv_expiry - SAFETY_MARGIN:
        raise TooLate("HTLC close to expiry")

    htlc_success_tx = build_htlc_success(
        commitment_tx=channel.commitment,
        htlc=htlc,
        preimage=preimage,
        local_sig=sign_htlc_success(channel, htlc),
        remote_sig=htlc.remote_sig
    )
    broadcast(htlc_success_tx)

Timing Considerations

Safety Margins

# Minimum blocks before HTLC expiry to claim
HTLC_CLAIM_MARGIN = 6

# Blocks to wait after seeing commitment before acting
CONFIRMATION_MARGIN = 6

# Maximum time to watch for peer's preimage
PREIMAGE_WATCH_BLOCKS = 100

Deadline Tracking

def track_htlc_deadlines(channel):
    """Monitor HTLC resolution deadlines."""
    for htlc in channel.htlcs:
        if htlc.is_offered:
            # We can claim after expiry
            deadline = htlc.cltv_expiry
            action = "timeout"
        else:
            # We must claim before expiry
            deadline = htlc.cltv_expiry - HTLC_CLAIM_MARGIN
            action = "success" if have_preimage(htlc) else "fail"

        schedule_deadline(deadline, htlc, action)

Anchor Output Handling

With anchor outputs, commitment transactions may have low fees:

def bump_commitment_fee(commitment_tx, target_feerate):
    """CPFP the commitment transaction via anchor."""
    anchor_output = find_anchor_output(commitment_tx)

    # Create child transaction spending anchor
    bump_tx = Transaction()
    bump_tx.add_input(commitment_tx.txid, anchor_output.index)
    bump_tx.add_output(sweep_address, anchor_output.value - fee)

    # Set fee to make combined feerate meet target
    required_fee = calculate_cpfp_fee(
        parent_weight=commitment_tx.weight,
        child_weight=bump_tx.weight,
        target_feerate=target_feerate
    )

    broadcast(bump_tx)

Watchtower Integration

Delegate breach monitoring to watchtower:

def register_with_watchtower(watchtower, channel):
    """Register channel with watchtower for breach detection."""
    for commitment_num, secret in channel.revocation_secrets:
        # Create encrypted justice transaction blob
        blob = create_watchtower_blob(
            channel=channel,
            commitment_num=commitment_num,
            revocation_secret=secret
        )

        # Hint allows watchtower to detect breach
        hint = sha256(commitment_txid_pattern)[:16]

        watchtower.register(hint, blob)

Machine-Readable Summary

{
  "bolt": "05",
  "title": "On-Chain Transaction Handling",
  "status": "final",
  "scenarios": [
    "cooperative-close",
    "local-force-close",
    "remote-force-close",
    "revoked-state"
  ],
  "key_concepts": [
    "justice-transaction",
    "htlc-resolution",
    "csv-delays",
    "anchor-cpfp"
  ]
}