2021-09-07 06:09:15 +02:00
#! /usr/bin/python3
from pyln . spec . bolt7 import ( channel_announcement , channel_update ,
node_announcement )
2021-09-07 06:09:16 +02:00
from pyln . proto import ShortChannelId , PublicKey
2021-09-07 22:55:21 +02:00
from typing import Any , Dict , List , Optional , Union , cast
2021-09-07 06:09:15 +02:00
import io
import struct
# These duplicate constants in lightning/common/gossip_store.h
2022-05-03 19:42:31 +02:00
GOSSIP_STORE_VERSIONS = [ 0x09 , 0x0a ]
2021-09-07 06:09:15 +02:00
GOSSIP_STORE_LEN_DELETED_BIT = 0x80000000
GOSSIP_STORE_LEN_PUSH_BIT = 0x40000000
2022-05-04 15:48:59 +02:00
GOSSIP_STORE_LEN_RATELIMIT_BIT = 0x20000000
2021-09-07 06:09:15 +02:00
GOSSIP_STORE_LEN_MASK = ( ~ ( GOSSIP_STORE_LEN_PUSH_BIT
2022-05-04 15:48:59 +02:00
| GOSSIP_STORE_LEN_DELETED_BIT
| GOSSIP_STORE_LEN_RATELIMIT_BIT ) )
2021-09-07 06:09:15 +02:00
# These duplicate constants in lightning/gossipd/gossip_store_wiregen.h
WIRE_GOSSIP_STORE_PRIVATE_CHANNEL = 4104
WIRE_GOSSIP_STORE_PRIVATE_UPDATE = 4102
WIRE_GOSSIP_STORE_DELETE_CHAN = 4103
WIRE_GOSSIP_STORE_ENDED = 4105
2021-09-07 22:54:57 +02:00
WIRE_GOSSIP_STORE_CHANNEL_AMOUNT = 4101
2021-09-07 06:09:15 +02:00
class GossipStoreHeader ( object ) :
def __init__ ( self , buf : bytes ) :
length , self . crc , self . timestamp = struct . unpack ( ' >III ' , buf )
self . deleted = ( length & GOSSIP_STORE_LEN_DELETED_BIT ) != 0
self . length = ( length & GOSSIP_STORE_LEN_MASK )
2021-09-07 06:09:16 +02:00
class GossmapHalfchannel ( object ) :
""" One direction of a GossmapChannel. """
2021-09-07 22:55:21 +02:00
def __init__ ( self , channel : ' GossmapChannel ' , direction : int ,
2021-09-07 22:55:21 +02:00
timestamp : int , cltv_expiry_delta : int ,
2021-09-07 06:09:16 +02:00
htlc_minimum_msat : int , htlc_maximum_msat : int ,
fee_base_msat : int , fee_proportional_millionths : int ) :
2021-09-07 22:56:06 +02:00
2021-09-07 22:55:21 +02:00
self . channel = channel
self . direction = direction
2021-09-07 22:56:06 +02:00
self . source = channel . node1 if direction == 0 else channel . node2
self . destination = channel . node2 if direction == 0 else channel . node1
2021-09-07 06:09:16 +02:00
self . timestamp : int = timestamp
self . cltv_expiry_delta : int = cltv_expiry_delta
self . htlc_minimum_msat : int = htlc_minimum_msat
self . htlc_maximum_msat : Optional [ int ] = htlc_maximum_msat
self . fee_base_msat : int = fee_base_msat
self . fee_proportional_millionths : int = fee_proportional_millionths
2021-09-07 22:55:21 +02:00
def __repr__ ( self ) :
return " GossmapHalfchannel[ {} x {} ] " . format ( str ( self . channel . scid ) , self . direction )
2021-09-07 06:09:16 +02:00
2021-09-07 22:55:21 +02:00
class GossmapNodeId ( object ) :
2021-09-07 22:56:06 +02:00
def __init__ ( self , buf : Union [ bytes , str ] ) :
2021-09-07 22:56:06 +02:00
if isinstance ( buf , str ) :
buf = bytes . fromhex ( buf )
2021-09-07 22:55:21 +02:00
if len ( buf ) != 33 or ( buf [ 0 ] != 2 and buf [ 0 ] != 3 ) :
raise ValueError ( " {} is not a valid node_id " . format ( buf . hex ( ) ) )
self . nodeid = buf
def to_pubkey ( self ) - > PublicKey :
return PublicKey ( self . nodeid )
def __eq__ ( self , other ) :
if not isinstance ( other , GossmapNodeId ) :
return False
2021-09-07 22:56:06 +02:00
return self . nodeid . __eq__ ( other . nodeid )
2021-09-07 22:55:21 +02:00
2021-09-07 22:56:06 +02:00
def __lt__ ( self , other ) :
if not isinstance ( other , GossmapNodeId ) :
raise ValueError ( f " Cannot compare GossmapNodeId with { type ( other ) } " )
return self . nodeid . __lt__ ( other . nodeid ) # yes, that works
2021-09-07 22:55:21 +02:00
def __hash__ ( self ) :
return self . nodeid . __hash__ ( )
def __repr__ ( self ) :
return " GossmapNodeId[ {} ] " . format ( self . nodeid . hex ( ) )
2021-09-07 22:55:21 +02:00
@classmethod
def from_str ( cls , s : str ) :
2021-09-07 22:55:21 +02:00
if s . startswith ( ' 0x ' ) :
s = s [ 2 : ]
2021-09-07 22:56:06 +02:00
if len ( s ) != 66 :
2021-09-07 22:55:21 +02:00
raise ValueError ( f " { s } is not a valid hexstring of a node_id " )
2021-09-07 22:55:21 +02:00
return cls ( bytes . fromhex ( s ) )
2021-09-07 22:55:21 +02:00
2021-09-07 06:09:15 +02:00
class GossmapChannel ( object ) :
2021-09-07 06:09:16 +02:00
""" A channel: fields of channel_announcement are in .fields, optional updates are in .updates_fields, which can be None if there has been no channel update. """
2021-09-07 06:09:15 +02:00
def __init__ ( self ,
2021-09-07 06:09:15 +02:00
fields : Dict [ str , Any ] ,
2021-09-07 06:09:15 +02:00
announce_offset : int ,
scid ,
2021-09-07 22:55:21 +02:00
node1 : ' GossmapNode ' ,
node2 : ' GossmapNode ' ,
2021-09-07 06:09:15 +02:00
is_private : bool ) :
2021-09-07 06:09:15 +02:00
self . fields = fields
2021-09-07 06:09:15 +02:00
self . announce_offset = announce_offset
self . is_private = is_private
self . scid = scid
2021-09-07 22:55:21 +02:00
self . node1 = node1
self . node2 = node2
2021-09-07 06:09:15 +02:00
self . updates_fields : List [ Optional [ Dict [ str , Any ] ] ] = [ None , None ]
2021-09-07 06:09:15 +02:00
self . updates_offset : List [ Optional [ int ] ] = [ None , None ]
2021-09-07 22:54:57 +02:00
self . satoshis = None
2021-09-07 22:55:21 +02:00
self . half_channels : List [ Optional [ GossmapHalfchannel ] ] = [ None , None ]
2021-09-07 06:09:16 +02:00
2021-09-07 22:56:02 +02:00
def _update_channel ( self ,
direction : int ,
fields : Dict [ str , Any ] ,
off : int ) :
2021-09-07 06:09:16 +02:00
self . updates_fields [ direction ] = fields
2021-09-07 22:55:21 +02:00
self . updates_offset [ direction ] = off
2021-09-07 06:09:16 +02:00
2021-09-07 22:55:21 +02:00
half = GossmapHalfchannel ( self , direction ,
fields [ ' timestamp ' ] ,
2021-09-07 06:09:16 +02:00
fields [ ' cltv_expiry_delta ' ] ,
fields [ ' htlc_minimum_msat ' ] ,
fields . get ( ' htlc_maximum_msat ' , None ) ,
fields [ ' fee_base_msat ' ] ,
fields [ ' fee_proportional_millionths ' ] )
self . half_channels [ direction ] = half
2021-09-07 22:56:06 +02:00
def get_direction ( self , direction : int ) :
2021-09-07 06:09:16 +02:00
""" returns the GossmapHalfchannel if known by channel_update """
if not 0 < = direction < = 1 :
raise ValueError ( " direction can only be 0 or 1 " )
return self . half_channels [ direction ]
2021-09-07 22:55:21 +02:00
def __repr__ ( self ) :
return " GossmapChannel[ {} ] " . format ( str ( self . scid ) )
2021-09-07 06:09:15 +02:00
class GossmapNode ( object ) :
2021-09-07 06:09:15 +02:00
""" A node: fields of node_announcement are in .announce_fields, which can be None of there has been no node announcement.
. channels is a list of the GossmapChannels attached to this node .
"""
2021-09-07 22:56:06 +02:00
def __init__ ( self , node_id : Union [ GossmapNodeId , bytes , str ] ) :
2021-09-07 22:56:06 +02:00
if isinstance ( node_id , bytes ) or isinstance ( node_id , str ) :
node_id = GossmapNodeId ( node_id )
2021-09-07 06:09:15 +02:00
self . announce_fields : Optional [ Dict [ str , Any ] ] = None
2021-09-07 22:55:21 +02:00
self . announce_offset : Optional [ int ] = None
self . channels : List [ GossmapChannel ] = [ ]
2021-09-07 06:09:15 +02:00
self . node_id = node_id
2021-09-07 22:55:21 +02:00
def __repr__ ( self ) :
return " GossmapNode[ {} ] " . format ( self . node_id . nodeid . hex ( ) )
2021-09-07 22:56:06 +02:00
def __eq__ ( self , other ) :
if not isinstance ( other , GossmapNode ) :
return False
return self . node_id . __eq__ ( other . node_id )
def __lt__ ( self , other ) :
if not isinstance ( other , GossmapNode ) :
raise ValueError ( f " Cannot compare GossmapNode with { type ( other ) } " )
return self . node_id . __lt__ ( other . node_id )
2021-09-07 06:09:15 +02:00
class Gossmap ( object ) :
""" Class to represent the gossip map of the network """
def __init__ ( self , store_filename : str = " gossip_store " ) :
self . store_filename = store_filename
self . store_file = open ( store_filename , " rb " )
self . store_buf = bytes ( )
2021-09-07 22:55:21 +02:00
self . nodes : Dict [ GossmapNodeId , GossmapNode ] = { }
2021-09-07 06:09:15 +02:00
self . channels : Dict [ ShortChannelId , GossmapChannel ] = { }
2021-09-07 22:55:21 +02:00
self . _last_scid : Optional [ str ] = None
2022-05-03 19:42:31 +02:00
version = self . store_file . read ( 1 ) [ 0 ]
if version not in GOSSIP_STORE_VERSIONS :
raise ValueError ( " Invalid gossip store version {} " . format ( version ) )
2021-09-07 06:09:15 +02:00
self . bytes_read = 1
self . refresh ( )
def _new_channel ( self ,
2021-09-07 06:09:15 +02:00
fields : Dict [ str , Any ] ,
2021-09-07 06:09:15 +02:00
announce_offset : int ,
2021-09-07 06:09:15 +02:00
scid : ShortChannelId ,
2021-09-07 22:55:21 +02:00
node1 : GossmapNode ,
node2 : GossmapNode ,
2021-09-07 06:09:15 +02:00
is_private : bool ) :
2021-09-07 06:09:15 +02:00
c = GossmapChannel ( fields , announce_offset ,
2021-09-07 22:55:21 +02:00
scid , node1 , node2 ,
2021-09-07 06:09:15 +02:00
is_private )
2021-09-07 22:54:57 +02:00
self . _last_scid = scid
2021-09-07 06:09:15 +02:00
self . channels [ scid ] = c
2021-09-07 22:55:21 +02:00
node1 . channels . append ( c )
node2 . channels . append ( c )
2021-09-07 06:09:15 +02:00
2021-09-07 06:09:15 +02:00
def _del_channel ( self , scid : ShortChannelId ) :
2021-09-07 06:09:15 +02:00
c = self . channels [ scid ]
2021-09-07 22:56:06 +02:00
del self . channels [ scid ]
c . node1 . channels . remove ( c )
c . node2 . channels . remove ( c )
2021-09-07 06:09:15 +02:00
# Beware self-channels n1-n1!
2021-09-07 22:56:06 +02:00
if len ( c . node1 . channels ) == 0 and c . node1 != c . node2 :
del self . nodes [ c . node1 . node_id ]
if len ( c . node2 . channels ) == 0 :
del self . nodes [ c . node2 . node_id ]
2021-09-07 06:09:15 +02:00
2021-09-07 22:56:02 +02:00
def _add_channel ( self , rec : bytes , off : int , is_private : bool ) :
2021-09-07 06:09:15 +02:00
fields = channel_announcement . read ( io . BytesIO ( rec [ 2 : ] ) , { } )
2021-09-07 22:55:21 +02:00
# Add nodes one the fly
node1_id = GossmapNodeId ( fields [ ' node_id_1 ' ] )
node2_id = GossmapNodeId ( fields [ ' node_id_2 ' ] )
if node1_id not in self . nodes :
self . nodes [ node1_id ] = GossmapNode ( node1_id )
if node2_id not in self . nodes :
self . nodes [ node2_id ] = GossmapNode ( node2_id )
2021-09-07 06:09:15 +02:00
self . _new_channel ( fields , off ,
ShortChannelId . from_int ( fields [ ' short_channel_id ' ] ) ,
2021-09-07 22:55:21 +02:00
self . get_node ( node1_id ) , self . get_node ( node2_id ) ,
2021-09-07 06:09:15 +02:00
is_private )
2021-09-07 22:54:57 +02:00
def _set_channel_amount ( self , rec : bytes ) :
""" Sets channel capacity of last added channel """
sats , = struct . unpack ( " >Q " , rec [ 2 : ] )
self . channels [ self . _last_scid ] . satoshis = sats
2021-09-07 06:09:16 +02:00
def get_channel ( self , short_channel_id : ShortChannelId ) :
""" Resolves a channel by its short channel id """
2021-09-07 22:55:21 +02:00
if isinstance ( short_channel_id , str ) :
2021-09-07 06:09:16 +02:00
short_channel_id = ShortChannelId . from_str ( short_channel_id )
return self . channels . get ( short_channel_id )
2021-09-07 22:55:21 +02:00
def get_node ( self , node_id : Union [ GossmapNodeId , str ] ) :
2021-09-07 06:09:16 +02:00
""" Resolves a node by its public key node_id """
2021-09-07 22:55:21 +02:00
if isinstance ( node_id , str ) :
2021-09-07 06:09:16 +02:00
node_id = GossmapNodeId . from_str ( node_id )
2021-09-07 22:55:21 +02:00
return self . nodes . get ( cast ( GossmapNodeId , node_id ) )
2021-09-07 06:09:16 +02:00
2021-09-07 22:56:02 +02:00
def _update_channel ( self , rec : bytes , off : int ) :
2021-09-07 06:09:15 +02:00
fields = channel_update . read ( io . BytesIO ( rec [ 2 : ] ) , { } )
direction = fields [ ' channel_flags ' ] & 1
2021-09-07 06:09:15 +02:00
c = self . channels [ ShortChannelId . from_int ( fields [ ' short_channel_id ' ] ) ]
2021-09-07 22:56:02 +02:00
c . _update_channel ( direction , fields , off )
2021-09-07 06:09:15 +02:00
2021-09-07 22:56:02 +02:00
def _add_node_announcement ( self , rec : bytes , off : int ) :
2021-09-07 06:09:15 +02:00
fields = node_announcement . read ( io . BytesIO ( rec [ 2 : ] ) , { } )
2021-09-07 06:09:16 +02:00
node_id = GossmapNodeId ( fields [ ' node_id ' ] )
self . nodes [ node_id ] . announce_fields = fields
self . nodes [ node_id ] . announce_offset = off
2021-09-07 06:09:15 +02:00
def reopen_store ( self ) :
2021-09-07 06:09:15 +02:00
""" FIXME: Implement! """
2021-09-07 06:09:15 +02:00
assert False
2021-09-07 22:56:02 +02:00
def _remove_channel_by_deletemsg ( self , rec : bytes ) :
2021-09-07 22:56:06 +02:00
scidint , = struct . unpack ( " >Q " , rec [ 2 : ] )
scid = ShortChannelId . from_int ( scidint )
2021-09-07 06:09:15 +02:00
# It might have already been deleted when we skipped it.
if scid in self . channels :
self . _del_channel ( scid )
def _pull_bytes ( self , length : int ) - > bool :
""" Pull bytes from file into our internal buffer """
if len ( self . store_buf ) < length :
self . store_buf + = self . store_file . read ( length
- len ( self . store_buf ) )
return len ( self . store_buf ) > = length
def _read_record ( self ) - > Optional [ bytes ] :
""" If a whole record is not in the file, returns None.
If deleted , returns empty . """
if not self . _pull_bytes ( 12 ) :
return None
hdr = GossipStoreHeader ( self . store_buf [ : 12 ] )
if not self . _pull_bytes ( 12 + hdr . length ) :
return None
self . bytes_read + = len ( self . store_buf )
ret = self . store_buf [ 12 : ]
self . store_buf = bytes ( )
if hdr . deleted :
ret = bytes ( )
return ret
def refresh ( self ) :
""" Catch up with any changes to the gossip store """
while True :
off = self . bytes_read
rec = self . _read_record ( )
# EOF?
if rec is None :
break
# Deleted?
if len ( rec ) == 0 :
continue
rectype , = struct . unpack ( " >H " , rec [ : 2 ] )
if rectype == channel_announcement . number :
2021-09-07 22:56:02 +02:00
self . _add_channel ( rec , off , False )
2021-09-07 06:09:15 +02:00
elif rectype == WIRE_GOSSIP_STORE_PRIVATE_CHANNEL :
2021-09-07 22:56:02 +02:00
self . _add_channel ( rec [ 2 + 8 + 2 : ] , off + 2 + 8 + 2 , True )
2021-09-07 22:54:57 +02:00
elif rectype == WIRE_GOSSIP_STORE_CHANNEL_AMOUNT :
self . _set_channel_amount ( rec )
2021-09-07 06:09:15 +02:00
elif rectype == channel_update . number :
2021-09-07 22:56:02 +02:00
self . _update_channel ( rec , off )
2021-09-07 06:09:15 +02:00
elif rectype == WIRE_GOSSIP_STORE_PRIVATE_UPDATE :
2021-09-07 22:56:02 +02:00
self . _update_channel ( rec [ 2 + 2 : ] , off + 2 + 2 )
2021-09-07 06:09:15 +02:00
elif rectype == WIRE_GOSSIP_STORE_DELETE_CHAN :
2021-09-07 22:56:02 +02:00
self . _remove_channel_by_deletemsg ( rec )
2021-09-07 06:09:15 +02:00
elif rectype == node_announcement . number :
2021-09-07 22:56:02 +02:00
self . _add_node_announcement ( rec , off )
2021-09-07 06:09:15 +02:00
elif rectype == WIRE_GOSSIP_STORE_ENDED :
self . reopen_store ( )
else :
continue