Exemple #1
0
 def __init__(self, peermgr, config):
     self.peermgr = peermgr
     self.config = config
     self.chainmgr = ChainProxy()
     self.CLIENT_ID = self.config.get('network', 'client_id') or self.CLIENT_ID 
Exemple #2
0
class WireProtocol(object):

    """
    Translates between the network and the local data
    https://github.com/ethereum/wiki/wiki/%5BEnglish%5D-Wire-Protocol

    stateless!
    """

    cmd_map = dict(((0x00, 'Hello'),
                   (0x01, 'Disconnect'),
                   (0x02, 'Ping'),
                   (0x03, 'Pong'),
                   (0x10, 'GetPeers'),
                   (0x11, 'Peers'),
                   (0x12, 'Transactions'),
                   (0x13, 'Blocks'),
                   (0x14, 'GetChain'),
                   (0x15, 'NotInChain'),
                   (0x16, 'GetTransactions')))
    cmd_map_by_name = dict((v, k) for k, v in cmd_map.items())

    disconnect_reasons_map = dict((
        ('Disconnect requested', 0x00),
        ('TCP sub-system error', 0x01),
        ('Bad protocol', 0x02),
        ('Useless peer', 0x03),
        ('Too many peers', 0x04),
        ('Already connected', 0x05),
        ('Wrong genesis block', 0x06),
        ('Incompatible network protocols', 0x07),
        ('Client quitting', 0x08)))
    disconnect_reasons_map_by_id = \
        dict((v, k) for k, v in disconnect_reasons_map.items())

    SYNCHRONIZATION_TOKEN = 0x22400891 # as sent by Ethereum(++)/v0.3.11/brew/Darwin/unknown
    PROTOCOL_VERSION = 0x08
    NETWORK_ID = 0
    CLIENT_ID = 'Ethereum(py)/0.0.1'
    CAPABILITIES = 0x01 + 0x02 + 0x04  # node discovery + transaction relaying

    def __init__(self, peermgr, config):
        self.peermgr = peermgr
        self.config = config
        self.chainmgr = ChainProxy()
        self.CLIENT_ID = self.config.get('network', 'client_id') or self.CLIENT_ID 

    def rcv_packet(self, peer, packet):
        """
        Though TCP provides a connection-oriented medium, Ethereum nodes communicate
        in terms of packets. These packets are formed as a 4-byte synchronisation token
        (0x22400891), a 4-byte "payload size", to be interpreted as a big-endian integer
        and finally an N-byte RLP-serialised data structure, where N is the aforementioned
        "payload size". To be clear, the payload size specifies the number of bytes in the
        packet ''following'' the first 8.
        """

        # check header
        if not idec(packet[:4]) == self.SYNCHRONIZATION_TOKEN:
            logger.warn('check header failed, skipping message, sync token was {0}'
                        .format(idec(packet[:4])))
            return

        # unpack message
        payload_len = idec(packet[4:8])
        # assert 8 + payload_len <= len(packet) # this get's sometimes raised!?
        data = lrlp_decode(packet[8:8 + payload_len])

        # check cmd
        if (not len(data)) or (idec(data[0]) not in self.cmd_map):
            logger.warn('check cmd failed')
            return self.send_Disconnect(peer, reason='Bad protocol')

        # good peer
        peer.last_valid_packet_received = time.time()

        cmd_id = idec(data.pop(0))
        func_name = "rcv_%s" % self.cmd_map[cmd_id]
        if not hasattr(self, func_name):
            logger.warn('unknown cmd \'{0}\''.format(func_name))
            return
            """
            return self.send_Disconnect(
                peer,
                reason='Incompatible network protocols')
            raise NotImplementedError('%s not implmented')
            """
        # check Hello was sent

        # call the correspondig method
        return getattr(self, func_name)(peer, data)

    def send_packet(self, peer, data):
        """
        4-byte synchronisation token, (0x22400891),
        a 4-byte "payload size", to be interpreted as a big-endian integer
        an N-byte RLP-serialised data structure
        """
        payload = rlp.encode(list_ienc(data))
        packet = ienc4(self.SYNCHRONIZATION_TOKEN)
        packet += ienc4(len(payload))
        packet += payload
        peer.send_packet(packet)

    def send_Hello(self, peer):
        # assert we did not sent hello yet
        payload = [0x00,
                   self.PROTOCOL_VERSION,
                   self.NETWORK_ID,
                   self.CLIENT_ID,
                   self.config.getint('network', 'listen_port'),
                   self.CAPABILITIES,
                   self.config.get('wallet', 'pub_key')
                   ]
        self.send_packet(peer, payload)

        peer.hello_sent = True

    def rcv_Hello(self, peer, data):
        """
        [0x00, PROTOCOL_VERSION, NETWORK_ID, CLIENT_ID, CAPABILITIES, LISTEN_PORT, NODE_ID]
        First packet sent over the connection, and sent once by both sides.
        No other messages may be sent until a Hello is received.
        PROTOCOL_VERSION is one of:
            0x00 for PoC-1;
            0x01 for PoC-2;
            0x07 for PoC-3.
            0x08 sent by Ethereum(++)/v0.3.11/brew/Darwin/unknown
        NETWORK_ID should be 0.
        CLIENT_ID Specifies the client software identity, as a human-readable string
                    (e.g. "Ethereum(++)/1.0.0").
        CAPABILITIES specifies the capabilities of the client as a set of flags;
                    presently three bits are used:
                    0x01 for peers discovery, 0x02 for transaction relaying, 0x04 for block-chain querying.
        LISTEN_PORT specifies the port that the client is listening on
                    (on the interface that the present connection traverses).
                    If 0 it indicates the client is not listening.
        NODE_ID is optional and specifies a 512-bit hash, (potentially to be used as public key)
                    that identifies this node.

        [574621841, 116, 'Hello', '\x08', '', 'Ethereum(++)/v0.3.11/brew/Darwin/unknown', '\x07', 'v_', "\xc5\xfe\xc6\xea\xe4TKvz\x9e\xdc\xa7\x01\xf6b?\x7fB\xe7\xfc(#t\xe9}\xafh\xf3Ot'\xe5u\x07\xab\xa3\xe5\x95\x14 |P\xb0C\xa2\xe4jU\xc8z|\x86\xa6ZV!Q6\x82\xebQ$4+"]
        [574621841, 27, 'Hello', '\x08', '\x00', 'Ethereum(py)/0.0.1', 'vb', '\x07']
        """

        # check compatibility
        if idec(data[0]) != self.PROTOCOL_VERSION:
            return self.send_Disconnect(
                peer,
                reason='Incompatible network protocols')

        if idec(data[1]) != self.NETWORK_ID:
            return self.send_Disconnect(peer, reason='Wrong genesis block')

        """
        spec has CAPABILITIES after PORT, CPP client the other way round. emulating the latter
        https://github.com/ethereum/cpp-ethereum/blob/master/libethereum/PeerNetwork.cpp#L144
        """

        # TODO add to known peers list
        peer.hello_received = True
        if len(data) == 6:
            peer.node_id = data[5]

        # reply with hello if not send
        if not peer.hello_sent:
            self.send_Hello(peer)

    def send_Ping(self, peer):
        """
        [0x02]
        Requests an immediate reply of Pong from the peer.
        """
        self.send_packet(peer, [0x02])

    def rcv_Ping(self, peer, data):
        self.send_Pong(peer)

    def send_Pong(self, peer):
        """
        [0x03]
        Reply to peer's Ping packet.
        """
        #self.send_packet(peer, [0x03])
        #self.chainmgr.pingpong(True)

    def rcv_Pong(self, peer, data):
        self.send_GetTransactions(peer) # FIXME

    def send_Disconnect(self, peer, reason=None):
        """
        [0x01, REASON]
        Inform the peer that a disconnection is imminent;
        if received, a peer should disconnect immediately.
        When sending, well-behaved hosts give their peers a fighting chance
        (read: wait 2 seconds) to disconnect to before disconnecting themselves.
        REASON is an optional integer specifying one of a number of reasons
        """
        logger.info(
            'sending {0} disconnect because {1}'.format(repr(peer), reason))
        assert not reason or reason in self.disconnect_reasons_map
        payload = [0x01]
        if reason:
            payload.append(self.disconnect_reasons_map[reason])
        self.send_packet(peer, payload)
        # end connection
        time.sleep(2)
        self.peermgr.remove_peer(peer)

    def rcv_Disconnect(self, peer, data):
        if len(data):
            reason = self.disconnect_reasons_map_by_id[idec(data[0])]
            logger.info('{0} sent disconnect, {1} '.format(repr(peer), reason))
        self.peermgr.remove_peer(peer)

    def rcv_GetPeers(self, peer, data):
        """
        [0x10]
        Request the peer to enumerate some known peers for us to connect to.
        This should include the peer itself.
        """
        self.send_Peers(peer)

    def send_GetPeers(self, peer):
        self.send_packet(peer, [0x10])

    def rcv_Peers(self, peer, data):
        """
        [0x11, [IP1, Port1, Id1], [IP2, Port2, Id2], ... ]
        Specifies a number of known peers. IP is a 4-byte array 'ABCD' that
        should be interpreted as the IP address A.B.C.D. Port is a 2-byte array
        that should be interpreted as a 16-bit big-endian integer.
        Id is the 512-bit hash that acts as the unique identifier of the node.

        IPs look like this: ['6', '\xcc', '\n', ')']
        """
        for ip, port, pid in data:
            assert isinstance(ip, list)
            ip = '.'.join(str(ord(b or '\x00')) for b in ip)
            port = idec(port)
            logger.debug('received peer address: {0}:{1}'.format(ip, port))
            self.peermgr.add_peer_address(ip, port, pid)

    def send_Peers(self, peer):
        data = [0x11]
        for ip, port, pid in self.peermgr.get_known_peer_addresses():
            ip = list((chr(int(x)) for x in ip.split('.')))
            data.append([ip, port, pid])
        if len(data) > 1:
            self.send_packet(peer, data)  # FIXME, what?

    def rcv_Blocks(self, peer, data):
        """
        [0x13, [block_header, transaction_list, uncle_list], ... ]
        Specify (a) block(s) that the peer should know about. The items in the list
        (following the first item, 0x13) are blocks in the format described in the
        main Ethereum specification.
        """
        
        # FIXME, currently just dumps data
        for e in data:
            header, transaction_list, uncle_list = e
            logger.debug('received block:  parent:{0}'.format(header[0].encode('hex')))


    def rcv_Transactions(self, peer, data):
        """
        [0x12, [nonce, receiving_address, value, ... ], ... ]
        Specify (a) transaction(s) that the peer should make sure is included on 
        its transaction queue. The items in the list (following the first item 0x12) 
        are transactions in the format described in the main Ethereum specification.
        """
        logger.info('received transactions', len(data), peer)
        self.chainmgr.addTransactions(data)


    def send_Transactions(self, peer, transaction_list):
        data = [0x12] + [transaction_list]
        self.send_packet(peer, data) 

    def rcv_GetTransactions(self, peer):
        """
        [0x16]
        Request the peer to send all transactions currently in the queue. 
        See Transactions.
        """
        logger.debug('received get_transaction', peer)
        self.chainmgr.request_transactions(peer.id())

    def send_GetTransactions(self, peer):
        logger.info('asking for transactions')
        self.send_packet(peer, [0x16]) 

    def _broadcast(self, method, data):
        for peer in self.peermgr.connected_peers:
            method(peer, data)

    def process_chainmanager_queue(self):

        while True:
            msg = self.chainmgr.pop_message()
            if not msg:
                return
            cmd, data = msg[0], msg[1:]
            logger.debug('%r received %s datalen:%d' % (self, cmd, len(data)))
            
            if cmd == "send_transactions":
                transaction_list = data[0]
                peer = self.peermgr.get_peer_by_id(data[1])
                if peer:
                    self.send_Transactions(peer, transaction_list)
                else: # broadcast
                    self._broadcast(self.send_Transactions, transaction_list)
            
            elif cmd == 'pingpong':
                reply = data[0]
                logger.debug('%r received pingpong(reply=%r)' % (self, reply))
                if reply:
                    self.chainmgr.pingpong()
            else:
                raise Exception('unknown commad')