BOLT-bolt-07 Final
BOLT-07: Gossip Protocol
Lightning Network gossip protocol specification. Channel announcements, node announcements, and network topology discovery.
| Type | Basis of Lightning Technology |
| Number | bolt-07 |
| Status | Final |
| Authors | Lightning Network Developers |
| Original | https://github.com/lightning/bolts/blob/master/07-routing-gossip.md |
| Requires |
BOLT-07: Gossip Protocol
BOLT-07 defines how nodes share information about the network topology. This gossip propagates channel and node information, enabling pathfinding.
Specification Summary
| Aspect | Value |
|---|---|
| Status | Final |
| Layer | Network |
| Purpose | Topology discovery |
| Dependencies | BOLT-01 |
Message Types
| Type | Name | Purpose |
|---|---|---|
| 256 | channel_announcement | New channel |
| 257 | node_announcement | Node metadata |
| 258 | channel_update | Channel parameters |
| 261 | query_channel_range | Request channels |
| 262 | reply_channel_range | Channel list |
| 263 | query_short_channel_ids | Request details |
| 264 | reply_short_channel_ids_end | End of response |
Channel Announcement
Announces a new public channel to the network.
Message Format (type 256)
┌─────────────────────────────────────────────┐
│ node_signature_1 (64 bytes) │
│ node_signature_2 (64 bytes) │
│ bitcoin_signature_1 (64 bytes) │
│ bitcoin_signature_2 (64 bytes) │
├─────────────────────────────────────────────┤
│ features (variable) │
│ chain_hash (32 bytes) │
│ short_channel_id (8 bytes) │
│ node_id_1 (33 bytes) │
│ node_id_2 (33 bytes) │
│ bitcoin_key_1 (33 bytes) │
│ bitcoin_key_2 (33 bytes) │
└─────────────────────────────────────────────┘
Short Channel ID
Compact channel identifier:
┌────────────────────────────────────────────┐
│ block_height (3 bytes) │ tx_index (3 bytes)│ output_index (2 bytes) │
└────────────────────────────────────────────┘
def encode_short_channel_id(block: int, tx: int, output: int) -> int:
return (block << 40) | (tx << 16) | output
def decode_short_channel_id(scid: int) -> tuple:
block = (scid >> 40) & 0xFFFFFF
tx = (scid >> 16) & 0xFFFFFF
output = scid & 0xFFFF
return (block, tx, output)
# Format: 123x456x0
def format_scid(scid: int) -> str:
block, tx, output = decode_short_channel_id(scid)
return f"{block}x{tx}x{output}"
Verification
Nodes must verify:
- Funding transaction exists at short_channel_id
- Output is 2-of-2 multisig of bitcoin_key_1 and bitcoin_key_2
- All signatures are valid
- node_id_1 < node_id_2 (lexicographically)
Node Announcement
Broadcasts node metadata to the network.
Message Format (type 257)
┌─────────────────────────────────────────────┐
│ signature (64 bytes) │
├─────────────────────────────────────────────┤
│ features (variable) │
│ timestamp (4 bytes) │
│ node_id (33 bytes) │
│ rgb_color (3 bytes) │
│ alias (32 bytes) │
│ addresses (variable) │
└─────────────────────────────────────────────┘
Address Types
| Type | Description |
|---|---|
| 1 | IPv4 (4 bytes + 2-byte port) |
| 2 | IPv6 (16 bytes + 2-byte port) |
| 3 | Tor v2 (deprecated) |
| 4 | Tor v3 (35 bytes) |
| 5 | DNS hostname |
RGB Color
Node color for visualization:
rgb = bytes.fromhex("3399ff") # Blue
# Displayed in explorers as node color
Alias
32-byte UTF-8 alias (padded with zeros):
alias = "MyNode".encode('utf-8').ljust(32, b'\x00')
Channel Update
Updates channel routing parameters.
Message Format (type 258)
┌─────────────────────────────────────────────┐
│ signature (64 bytes) │
├─────────────────────────────────────────────┤
│ chain_hash (32 bytes) │
│ short_channel_id (8 bytes) │
│ timestamp (4 bytes) │
│ message_flags (1 byte) │
│ channel_flags (1 byte) │
│ cltv_expiry_delta (2 bytes) │
│ htlc_minimum_msat (8 bytes) │
│ fee_base_msat (4 bytes) │
│ fee_proportional_millionths (4 bytes) │
│ htlc_maximum_msat (8 bytes, if flag set) │
└─────────────────────────────────────────────┘
Channel Flags
| Bit | Name | Meaning |
|---|---|---|
| 0 | direction | 0 = node_1→node_2, 1 = node_2→node_1 |
| 1 | disabled | Channel is disabled |
Message Flags
| Bit | Name | Meaning |
|---|---|---|
| 0 | htlc_maximum | htlc_maximum_msat field present |
Fee Calculation
From channel_update fields:
def calculate_fee(amount_msat: int, update: ChannelUpdate) -> int:
"""Calculate routing fee for forwarding."""
return update.fee_base_msat + (amount_msat * update.fee_proportional_millionths // 1_000_000)
Gossip Queries
query_channel_range (type 261)
Request channels in a block range:
┌─────────────────────────────────────────────┐
│ chain_hash (32 bytes) │
│ first_blocknum (4 bytes) │
│ number_of_blocks (4 bytes) │
│ [tlvs] │
└─────────────────────────────────────────────┘
reply_channel_range (type 262)
Response with channel list:
┌─────────────────────────────────────────────┐
│ chain_hash (32 bytes) │
│ first_blocknum (4 bytes) │
│ number_of_blocks (4 bytes) │
│ sync_complete (1 byte) │
│ len (2 bytes) │
│ encoded_short_ids (len bytes) │
│ [tlvs] │
└─────────────────────────────────────────────┘
query_short_channel_ids (type 263)
Request specific channel details:
┌─────────────────────────────────────────────┐
│ chain_hash (32 bytes) │
│ len (2 bytes) │
│ encoded_short_ids (len bytes) │
│ [tlvs] │
└─────────────────────────────────────────────┘
Timestamp Rules
Update Priority
Newer timestamps replace older:
def should_replace(existing: int, new: int) -> bool:
# Newer timestamp wins
if new > existing:
return True
# Same timestamp, keep existing
return False
Rate Limiting
Nodes should rate-limit:
- node_announcement: ~1 per 30 seconds
- channel_update: ~1 per 60 seconds per channel
Agent Implementation
Tracking Network State
class NetworkGraph:
def __init__(self):
self.channels = {} # scid -> ChannelAnnouncement
self.nodes = {} # pubkey -> NodeAnnouncement
self.updates = {} # (scid, direction) -> ChannelUpdate
def process_channel_announcement(self, msg: bytes):
ann = parse_channel_announcement(msg)
if verify_channel_announcement(ann):
self.channels[ann.short_channel_id] = ann
def process_node_announcement(self, msg: bytes):
ann = parse_node_announcement(msg)
if ann.timestamp > self.nodes.get(ann.node_id, 0):
self.nodes[ann.node_id] = ann
def process_channel_update(self, msg: bytes):
update = parse_channel_update(msg)
key = (update.short_channel_id, update.direction)
if update.timestamp > self.updates.get(key, {}).get('timestamp', 0):
self.updates[key] = update
def get_channel_fee(self, scid: int, direction: int) -> int:
update = self.updates.get((scid, direction))
if not update:
return None
return update.fee_base_msat, update.fee_proportional_millionths
Gossip Synchronization
async def sync_gossip(peer, graph):
"""Synchronize gossip with peer."""
# Get our current highest block
our_height = graph.get_highest_block()
# Query new channels
await peer.send(query_channel_range(
first_blocknum=our_height - 1000, # Some overlap
number_of_blocks=10000
))
# Process replies
while True:
msg = await peer.receive()
if msg.type == 262: # reply_channel_range
for scid in msg.short_channel_ids:
if scid not in graph.channels:
# Request full announcement
await peer.send(query_short_channel_ids([scid]))
if msg.sync_complete:
break
Related BOLTs
Machine-Readable Summary
{
"bolt": "07",
"title": "Gossip Protocol",
"status": "final",
"messages": [
{"type": 256, "name": "channel_announcement"},
{"type": 257, "name": "node_announcement"},
{"type": 258, "name": "channel_update"},
{"type": 261, "name": "query_channel_range"},
{"type": 262, "name": "reply_channel_range"},
{"type": 263, "name": "query_short_channel_ids"}
],
"key_concepts": [
"short-channel-id",
"network-topology",
"fee-announcements"
]
}