python ethereum 代码分析

python ethereum 代码分析 《1》

python 版本以太坊

项目地址
https://github.com/ethereum/pyethapp
https://github.com/ethereum/pyethereum
https://github.com/ethereum/pydevp2p
其中 pyethapp 依赖pyethereum 和 pydevp2p。pyethereum主要包括对block的处理,对transaction的处理以及以太坊虚拟机部分;pydevp2p 是p2p网络库,主要包括节点发现协议(Node Discovery)的实现,p2p通信协议的实现,并定义了protocol基类和service基类

pydevp2p 模块


代码整体结构

python ethereum 代码分析_第1张图片

1.协议基类BaseProtocol 查看代码

子类协议继承BaseProtocol中的command类
来实现对不同command进行处理。
receive_packet方法

    def receive_packet(self, packet):
        cmd_name = self.cmd_by_id[packet.cmd_id]
        cmd = getattr(self, '_receive_' + cmd_name)
        try:
            cmd(packet)
        except ProtocolError as e:
            log.debug('protocol exception, stopping', error=e, peer=self.peer)
            self.stop()

receive_packet 通过cmd_id 来获取到对应的command类从而获得该command类的receive方法,调用该方法处理数据包packet
BaseProtocol子类协议中所有command类的方法在协议实例化的时候通过调用_setup方法 被添加到BaseProtocol子类实例中

    def _setup(self):

        # collect commands
        klasses = [k for k in self.__class__.__dict__.values()
                   if isinstance(k, type) and issubclass(k, self.command) and k != self.command]
        assert len(set(k.cmd_id for k in klasses)) == len(klasses)

        def create_methods(klass):
            instance = klass()

            def receive(packet):
                "decode rlp, create dict, call receive"
                assert isinstance(packet, Packet)
                instance.receive(proto=self, data=klass.decode_payload(packet.payload))

            def create(*args, **kargs):
                "get data, rlp encode, return packet"
                res = instance.create(self, *args, **kargs)
                payload = klass.encode_payload(res)
                return Packet(self.protocol_id, klass.cmd_id, payload=payload)

            def send(*args, **kargs):
                "create and send packet"
                packet = create(*args, **kargs)
                self.send_packet(packet)

            return receive, create, send, instance.receive_callbacks

        for klass in klasses:
            receive, create, send, receive_callbacks = create_methods(klass)
            setattr(self, '_receive_' + klass.__name__, receive)
            setattr(self, 'receive_' + klass.__name__ + '_callbacks', receive_callbacks)
            setattr(self, 'create_' + klass.__name__, create)
            setattr(self, 'send_' + klass.__name__, send)

        self.cmd_by_id = dict((klass.cmd_id, klass.__name__) for klass in klasses)

2.启动peermanager Service

基类BaseService,一个service对应一个WireProtocol
先看下客户端启动时用到哪些服务:
ethapp启动的服务
其中NodeDiscovery(节点发现服务), PeerManager(节点管理服务)在pydevp2p 模块中实现。
注册服务并启动

    for service in services + contrib_services:
        assert issubclass(service, BaseService)
        if service.name not in app.config['deactivated_services'] + [AccountsService.name]:
            assert service.name not in app.services
            service.register_with_app(app)
            assert hasattr(app.services, service.name)

    # start app
    log.info('starting')
    app.start()

app.start() 中调用了所有service的start方法
先看peermanager的start方法

    def start(self):
        log.info('starting peermanager')
        # try upnp nat
        self.nat_upnp = add_portmap(
            self.config['p2p']['listen_port'],
            'TCP',
            'Ethereum DEVP2P Peermanager'
        )
        # start a listening server
        log.info('starting listener', addr=self.listen_addr)
        self.server.set_handle(self._on_new_connection)
        self.server.start()
        super(PeerManager, self).start()
        gevent.spawn_later(0.001, self._bootstrap, self.config['p2p']['bootstrap_nodes'])
        gevent.spawn_later(1, self._discovery_loop)

gevent.spawn_later(0.001, self._bootstrap, self.config[‘p2p’][‘bootstrap_nodes’]) 将_bootstrap方法加入协程调度队列,_bootstrap方法将初始化节点(bootstrap_nodes)生成peer对象并开始监听,如果没有bootstrap_nodes,则节点无法加入网络。
gevent.spawn_later(1, self._discovery_loop) 启动节点发现服务

self.server.start()启动gevent的StreamServer并用_on_new_connection函数处理新链接

    def _on_new_connection(self, connection, address):
        log.debug('incoming connection', connection=connection)
        peer = self._start_peer(connection, address)
        # Explicit join is required in gevent >= 1.1.
        # See: https://github.com/gevent/gevent/issues/594
        # and http://www.gevent.org/whatsnew_1_1.html#compatibility
        peer.join()

    def _start_peer(self, connection, address, remote_pubkey=None):
        # create peer
        peer = Peer(self, connection, remote_pubkey=remote_pubkey)
        peer.link(on_peer_exit)
        log.debug('created new peer', peer=peer, fno=connection.fileno())
        self.peers.append(peer)
        # loop
        peer.start()
        log.debug('peer started', peer=peer, fno=connection.fileno())
        assert not connection.closed
        return peer

3.新建连接

接受到一个新连接后,新建一个peer节点对象,peer.start()开始对该节点进行监听
peer对象开始监听
将接收的数据加入队列
当数据完整后,序列化为packet对象加入packet队列,
_run_decoded_packets加入协程调度

    def _run_decoded_packets(self):
        # handle decoded packets
        while not self.is_stopped:
            self._handle_packet(self.mux.packet_queue.get())  # get_packet blocks

_run_decoded_packets获得调度后,_handle_packet来处理packet

    def _handle_packet(self, packet):
        assert isinstance(packet, Packet)
        try:
            protocol, cmd_id = self.protocol_cmd_id_from_packet(packet)
            log.debug('recv packet', peer=self, cmd=protocol.cmd_by_id[cmd_id], protocol=protocol.name, orig_cmd_id=packet.cmd_id)
            packet.cmd_id = cmd_id  # rewrite
            protocol.receive_packet(packet)
        except UnknownCommandError as e:
            log.debug('received unknown cmd', peer=self, error=e, packet=packet)
        except Exception as e:
            log.debug('failed to handle packet', peer=self, error=e)
            self.stop()

_handle_packet从数据包packet中获得该数据包的协议protocol和协议中的cmd_id,之后由cmd_id来定位到该协议具体的函数,
protocol.receive_packet(packet) 调用协议的receive_packet方法。
这里的protocol实例是p2p_protocol实例,peer在初始化的时候实例化了p2p_protocol实例。查看代码

    def receive_packet(self, packet):
        cmd_name = self.cmd_by_id[packet.cmd_id]
        cmd = getattr(self, '_receive_' + cmd_name)
        try:
            cmd(packet)
        except ProtocolError as e:
            log.debug('protocol exception, stopping', error=e, peer=self.peer)
            self.stop()

receive_packet 通过cmd_id定位到对应的处理函数
先看看p2p_protocol协议都有哪些command,
1.hello:第一个数据包,接收双方都只有收到hello后才能继续发送其他消息
2.disconnect:收到这个数据包后,peer应该立即断开连接
3.ping:接收方收到ping数据包时,应该发送pong给对方;发送方发送ping包要求对方立即返回pong
4.pong:接收方确认收到回包,确认该peer对象在线

假设上面的receive_packet收到的是一个hello数据包
处理hello数据包
peer处理hello

    def receive_hello(self, proto, version, client_version_string, capabilities,
                      listen_port, remote_pubkey):
        log.debug('received hello', proto=proto, version=version,
                  client_version=client_version_string, capabilities=capabilities)
        assert isinstance(remote_pubkey, bytes)
        assert len(remote_pubkey) == 64
        if self.remote_pubkey_available:
            assert self.remote_pubkey == remote_pubkey
        self.hello_received = True

        # enable backwards compatibility for legacy peers
        if version < 5:
            self.offset_based_dispatch = True
            max_window_size = 2**32  # disable chunked transfers

        # call peermanager
        #校验是否超出最大节点连接数,是否已经连接过该节点
        agree = self.peermanager.on_hello_received(
            proto, version, client_version_string, capabilities, listen_port, remote_pubkey)
        if not agree:
            return

        self.remote_client_version = client_version_string
        self.remote_pubkey = remote_pubkey
        self.remote_capabilities = capabilities

        # register in common protocols
        log.debug('connecting services', services=self.peermanager.wired_services)
        remote_services = dict()#该节点可用的services
        for name, version in capabilities:
            if not name in remote_services:
                remote_services[name] = []
            remote_services[name].append(version)#添加该节点可用的service
        for service in sorted(self.peermanager.wired_services, key=operator.attrgetter('name')):
            proto = service.wire_protocol
            assert isinstance(service, WiredService)
            assert isinstance(proto.name, bytes)
            if proto.name in remote_services:
                if proto.version in remote_services[proto.name]:
                    if service != self.peermanager:  # p2p protocol already registered
                        #为该peer节点注册其其他可用service
                        self.connect_service(service)
                else:
                    log.debug('wrong version', service=proto.name, local_version=proto.version,
                              remote_version=remote_services[proto.name])
                    self.report_error('wrong version')

4.节点发现协议

python ethereum 的节点发现协议类似kademlia协议
app启动的时候也启动了节点发现服务NodeDiscovery
先看NodeDiscovery是怎么发现和存储节点的:
NodeDiscovery service中包含的kademlia协议实例维护了一个路由表

RoutingTable路由表中包含一个KBucket列表,节点Node存储在KBucket中的nodes列表中。
Kbucket:
1.每个KBucket的nodes列表最大长度k是常量,这里是16
2.每个KBucket中最新通信过的节点会被更新到列表的尾部
3.当一个KBucket的nodes列表达到最大长度,则该KBucket分裂为两个新的KBucket并添加到路由表RoutingTable中。
4. KBucket初始化的时候指定该KBucket节点id值大小的范围,往该KBucket中添加新节点时,节点id值在该范围之内才可以添加

class KBucket(object):
    k = k_bucket_size
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.nodes = []

5.一个KBucket的nodes列表是根据节点id值大小排列的,id值大的在尾部,id值小的在头部。
6.节点的id值范围是0~2 ** 256 - 1 ,也就是说节点id有256位(二进制表示)
7. 路由表初始化的时候只有一个KBucket,该KBucket中节点取值范围是0~2 ** 256-1,当该KBucket的nodes列表长度超过k(k=16)的时候,该KBucket分裂为两个kbucket,并更新到RoutingTable的buckets中

class RoutingTable(object):

    def __init__(self, node):
        self.this_node = node
        self.buckets = [KBucket(0, k_max_node_id)]

Node:
1.node 代表区块链网络中的一个节点,节点的id值从该节点的pubkey转换而来
2.节点与节点的距离不是物理距离而是逻辑距离,节点与节点的距离用异或来计算,例如节点“000…101”和节点 “000…111”的距离为 000…101 ^ 000…111 = 000…010 = 2

节点的查找:
在路由表中查找离指定节点target_id最近的一些节点,neighbours函数返回当前路由表中离指定节点距离最近的k个节点,默认k=16

查找目标节点target_id的流程:
1.在本地路由表中查找离target_id最近的3个节点,并向每个节点发送请求要求节点返回该节点知道的离target_id最近的16个节点。
2.本地节点收到回包,获得某个节点返回的离target_id最近的16个节点
3.本地节点在收到的16个节点中选出3个离target_id最近的节点来与本地路由表中离target_id最近的节点来进行比较,如果选出来的3个节点中的节点离target_id更近,则向该节点发送请求要求节点返回该节点知道的离target_id最近的16个节点。
4.重复第二步直到收敛到离离target_id最近的节点
这个收敛的过程参考https://wenku.baidu.com/view/ee91580216fc700abb68fcae.html

先看这里面实现的kademlia协议:
kademlia协议中定义了对数据包的处理函数
1.ping :ping指定节点,并期望获得pong回包
2.recv_ping :收到ping包,更新路由表状态,调用send_pong
3.recv_pong : 收到pong包,更新路由表状态
4.find_node :在当前路由表中选取离自己最近的k_find_concurrency(查询并发量,全局参数,默认为3,类似原生kademlia协议中的alpha 参数)个节点,对他们发送 “find_node”数据包,并期望每个节点会发送离指定节点target_id最近的k(k=16)个节点的信息给自己
5.recv_find_node: 收到find_node数据包后,节点返回离指定节点target_id距离最近的k(k=16)个节点的信息
6.recv_neighbours :节点发送find_node数据包给最近的3个节点后,其中某个节点返回离指定节点target_id最近的k个节点,调用recv_neighbours 函数处理该数据包

    def recv_neighbours(self, remote, neighbours):
        """
        if one of the neighbours is closer than the closest known neighbour
            if not timed out
                query closest node for neighbours
        add all nodes to the list
        """
        assert isinstance(neighbours, list)
        log.debug('recv neighbours', remoteid=remote, num=len(neighbours), local=self.this_node,
                  neighbours=neighbours)
        neighbours = [n for n in neighbours if n != self.this_node]
        #选出自己路由表中还没有的节点
        neighbours = [n for n in neighbours if n not in self.routing]

        # we don't map requests to responses, thus forwarding to all FIXME
        for nodeid, timeout in self._find_requests.items():
            assert is_integer(nodeid)
            #对节点重新按距离排序,neighbours是离指定的随机节点最近的k个节点,这里用于排序的节点id(nodeid)是之前发送find_node数据包时指定的target_id
            closest = sorted(neighbours, key=operator.methodcaller('id_distance', nodeid))
            #节点响应没有超时的话
            if time.time() < timeout:
                closest_known = self.routing.neighbours(nodeid)
                #选出自己路由表中离target_id最近的节点
                closest_known = closest_known[0] if closest_known else None
                assert closest_known != self.this_node
                # send find_node requests to k_find_concurrency closests
                #选出接收到数据包中的离target_id最近的3个节点
                for close_node in closest[:k_find_concurrency]:
                    if not closest_known or \
                            close_node.id_distance(nodeid) < closest_known.id_distance(nodeid):
                        log.debug('forwarding find request', closest=close_node,
                                  closest_known=closest_known)
                        #如果接收到的数据包中某节点离target_id更近,则继续发送find_node数据包给该节点
                        self.wire.send_find_node(close_node, nodeid)

        # add all nodes to the list
        for node in neighbours:
            if node != self.this_node:
                self.ping(node)

其中remote为返回该信息的节点, neighbours为该节点返回的k个节点
_find_requests中设定了请求超时时间

整个查找节点的过程是一个递归和收敛的过程,在查找节点的过程中同时更新路由表,查询最后收敛到离目标节点target_id最近的节点上,这里有三种情况(这里只是本人观点仅供参考):
1.最近的节点即使target_id,target_id存在并且成功找到,整个查找过程时间复杂度O(log n)
2.离target_id最近的节点中也没有target_id的信息,及target_id对应的节点不存在或者不在线
3.target_id存在但是由于节点知道的节点数量不够,导致无法收敛到最近的节点。
参考https://wenku.baidu.com/view/ee91580216fc700abb68fcae.html Kademlia协议需要确保每个节点知道其各子树的至少一个节点,只要这些子树非空。在这个前提下,每个节点都可以通过ID值来找到任何一个节点。
这一点目前没看出来以太坊的区块链网络是如何保证的

现在再看节点发现服务NodeDiscovery:
开启UDP数据包监听端口,节点发现服务是基于UDP协议的
_handle_packet处理数据包
receive数据包,根据command_id定位到具体处理函数

NodeDiscovery 发现的节点是怎么添加到peermanager 服务中的peers列表里面的呢:
peermanager启动时_discovery_loop函数添加新发现的节点

    def _discovery_loop(self):
        log.info('waiting for bootstrap')
        gevent.sleep(self.discovery_delay)
        while not self.is_stopped:
            try:
                num_peers, min_peers = self.num_peers(), self.config['p2p']['min_peers']
                #从app实例中获取kademlia protocol实例
                kademlia_proto = self.app.services.discovery.protocol.kademlia
                if num_peers < min_peers:
                    log.debug('missing peers', num_peers=num_peers,
                              min_peers=min_peers, known=len(kademlia_proto.routing))
                    #随机生成一个target_id
                    nodeid = kademlia.random_nodeid()
                    #在本地路由表中选3个离target_id最近的节点发送find_node数据包
                    kademlia_proto.find_node(nodeid)  # fixme, should be a task
                    #协程让出cpu,等待结果
                    gevent.sleep(self.discovery_delay)  # wait for results
                    #从本地路由表中取16个离target_id最近的节点
                    neighbours = kademlia_proto.routing.neighbours(nodeid, 2)
                    if not neighbours:
                        gevent.sleep(self.connect_loop_delay)
                        continue
                    node = random.choice(neighbours)
                    if node.pubkey in self.remote_pubkeys():
                        gevent.sleep(self.discovery_delay)
                        continue
                    log.debug('connecting random', node=node)
                    local_pubkey = crypto.privtopub(decode_hex(self.config['node']['privkey_hex']))
                    if node.pubkey == local_pubkey:
                        continue
                    if node.pubkey in [p.remote_pubkey for p in self.peers]:
                        continue
                    #随机选择一个节点,建立连接并实例化为peer对象
                    self.connect((node.address.ip, node.address.tcp_port), node.pubkey)
            except AttributeError:
                # TODO: Is this the correct thing to do here?
                log.error("Discovery service not available.")
                break
            except Exception as e:
                log.error("discovery failed", error=e, num_peers=num_peers, min_peers=min_peers)
            gevent.sleep(self.connect_loop_delay)

        evt = gevent.event.Event()
        evt.wait()

eee

总结:

pydevp2p 模块主要负责底层的网络交互,其中包括PeerManager service负责p2p通信,和NodeDiscovery service负责发现节点


你可能感兴趣的:(ethereum)