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
| Aspect | Value |
|---|---|
| Status | Final |
| Layer | Bitcoin |
| Purpose | On-chain handling |
| Dependencies | BOLT-02, BOLT-03 |
Scenarios
| Scenario | Trigger | Resolution |
|---|---|---|
| Cooperative close | Both parties agree | Immediate funds |
| Force close (local) | You broadcast | CSV delay |
| Force close (remote) | Peer broadcasts | Immediate (their side) |
| Revoked state | Peer cheats | Justice transaction |
Monitoring Requirements
Nodes must monitor the blockchain for:
- Funding transaction: Confirmation count
- Commitment transactions: Any broadcast
- HTLC transactions: Preimage reveals
- 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:
- to_local output: Locked by CSV, wait
to_self_delayblocks - to_remote output: Immediately spendable by peer
- Offered HTLCs: Wait for timeout, then HTLC-timeout tx
- 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:
- Your to_remote output: Claim immediately
- Their HTLCs to you: Claim with preimage or wait for timeout
- 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 Type | Your Action | Deadline |
|---|---|---|
| Offered (outgoing) | Timeout after expiry | CLTV expiry + margin |
| Received (incoming) | Claim with preimage | Before 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)
Related BOLTs
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"
]
}