BOLT-09: Feature Bit Assignments
BOLT-09 defines the feature bits used in Lightning to negotiate capabilities between nodes. Features enable protocol evolution while maintaining backward compatibility.
Specification Summary
| Aspect | Value |
|---|
| Status | Final (living document) |
| Layer | Protocol |
| Purpose | Capability negotiation |
| Dependencies | BOLT-01 |
Feature Bit Semantics
Even/Odd Convention
| Bit Type | Meaning | If Unknown |
|---|
| Even (0, 2, 4…) | Required | Must disconnect |
| Odd (1, 3, 5…) | Optional | May ignore |
Feature Pairs
Each feature uses two consecutive bits:
- Bit N (even): “I require this feature”
- Bit N+1 (odd): “I understand this feature”
def feature_required(feature_bit: int) -> int:
"""Get required (even) bit for feature."""
return feature_bit & ~1
def feature_optional(feature_bit: int) -> int:
"""Get optional (odd) bit for feature."""
return feature_bit | 1
Feature Contexts
Features apply in different contexts:
| Context | Where Used |
|---|
I | Init message (connection) |
N | Node announcement |
C | Channel announcement |
C- | Channel type (not announced) |
9 | Invoice feature bits |
B | BOLT11 invoice |
Assigned Features
Core Protocol Features
| Bits | Name | Context | Description |
|---|
| 0/1 | option_data_loss_protect | IN | Safer channel reestablish |
| 4/5 | option_upfront_shutdown_script | IN | Pre-agreed shutdown address |
| 6/7 | gossip_queries | IN | Gossip query support |
| 8/9 | var_onion_optin | IN9 | TLV onion payloads |
| 10/11 | gossip_queries_ex | IN | Extended gossip queries |
| 12/13 | option_static_remotekey | IN | Static remote key |
Payment Features
| Bits | Name | Context | Description |
|---|
| 14/15 | payment_secret | IN9 | Payment secret required |
| 16/17 | basic_mpp | IN9 | Multi-path payments |
| 18/19 | option_support_large_channel | IN | Wumbo channels (>16M sats) |
Channel Features
| Bits | Name | Context | Description |
|---|
| 20/21 | option_anchor_outputs | IN | Anchor outputs |
| 22/23 | option_anchors_zero_fee_htlc_tx | IN | Zero-fee anchor HTLCs |
| 24/25 | option_shutdown_anysegwit | IN | Any SegWit shutdown |
Advanced Features
| Bits | Name | Context | Description |
|---|
| 26/27 | option_dual_fund | IN | Dual-funded channels |
| 44/45 | option_scid_alias | IN | SCID aliases |
| 46/47 | option_zeroconf | IN | Zero-conf channels |
Invoice Features
| Bits | Name | Context | Description |
|---|
| 8/9 | var_onion_optin | B | Variable onion |
| 14/15 | payment_secret | B | Required payment secret |
| 16/17 | basic_mpp | B | MPP support |
| 48/49 | payment_metadata | B | Payment metadata |
Feature Negotiation
During Connection
def negotiate_features(local: bytes, remote: bytes) -> bytes:
"""Determine active features for connection."""
# Check for unknown required features
for i in range(0, len(remote) * 8, 2):
if has_bit(remote, i) and not understand_feature(i):
raise UnknownRequiredFeature(i)
# Intersection of supported features
result = bytes(max(len(local), len(remote)))
for i in range(len(result) * 8):
if has_bit(local, i) and has_bit(remote, i):
set_bit(result, i)
return result
Channel Type Negotiation
def negotiate_channel_type(local_features, remote_features):
"""Determine channel type from features."""
# Prefer anchors_zero_fee if both support
if supports(local_features, 22) and supports(remote_features, 22):
return ChannelType.ANCHORS_ZERO_FEE
# Fall back to basic anchors
if supports(local_features, 20) and supports(remote_features, 20):
return ChannelType.ANCHORS
# Static remote key
if supports(local_features, 12) and supports(remote_features, 12):
return ChannelType.STATIC_REMOTEKEY
return ChannelType.LEGACY
Feature Dependencies
Some features require others:
| Feature | Requires |
|---|
basic_mpp | payment_secret, var_onion_optin |
option_anchors_zero_fee_htlc_tx | option_anchor_outputs |
option_scid_alias | (recommended with option_zeroconf) |
def validate_features(features: bytes) -> bool:
"""Check feature dependencies."""
if has_feature(features, 16): # basic_mpp
if not has_feature(features, 14): # payment_secret
return False
if not has_feature(features, 8): # var_onion
return False
return True
Implementation
Feature Vector Encoding
def encode_features(features: dict) -> bytes:
"""Encode feature dict to bytes."""
max_bit = max(features.keys()) if features else 0
num_bytes = (max_bit + 8) // 8
result = bytearray(num_bytes)
for bit, enabled in features.items():
if enabled:
byte_idx = bit // 8
bit_idx = bit % 8
result[byte_idx] |= (1 << bit_idx)
return bytes(result)
def decode_features(data: bytes) -> dict:
"""Decode feature bytes to dict."""
features = {}
for i in range(len(data) * 8):
if data[i // 8] & (1 << (i % 8)):
features[i] = True
return features
Checking Features
class FeatureVector:
def __init__(self, data: bytes):
self.data = data
def has_feature(self, bit: int) -> bool:
"""Check if feature bit is set."""
byte_idx = bit // 8
if byte_idx >= len(self.data):
return False
return bool(self.data[byte_idx] & (1 << (bit % 8)))
def requires(self, feature: int) -> bool:
"""Check if feature is required (even bit)."""
return self.has_feature(feature & ~1)
def supports(self, feature: int) -> bool:
"""Check if feature is supported (either bit)."""
return self.has_feature(feature & ~1) or self.has_feature(feature | 1)
Common Feature Sets
Modern Node (2024+)
MODERN_NODE_FEATURES = {
1: True, # data_loss_protect (optional)
9: True, # var_onion (optional)
13: True, # static_remotekey (optional)
15: True, # payment_secret (optional)
17: True, # basic_mpp (optional)
23: True, # anchors_zero_fee (optional)
45: True, # scid_alias (optional)
}
Modern Invoice
INVOICE_FEATURES = {
9: True, # var_onion
15: True, # payment_secret
17: True, # basic_mpp
}
Machine-Readable Summary
{
"bolt": "09",
"title": "Feature Bit Assignments",
"status": "final",
"key_features": [
{"bits": [0,1], "name": "data_loss_protect"},
{"bits": [8,9], "name": "var_onion_optin"},
{"bits": [12,13], "name": "static_remotekey"},
{"bits": [14,15], "name": "payment_secret"},
{"bits": [16,17], "name": "basic_mpp"},
{"bits": [22,23], "name": "anchors_zero_fee_htlc_tx"}
],
"contexts": ["I", "N", "C", "B", "9"]
}