Example #1
0
 def __init__(self, id=None, udpPort=4000, dataStore=None, routingTable=None, networkProtocol=None):
     if id != None:
         self.id = id
     else:
         self.id = self._generateID()
     self.port = udpPort
     
     if networkProtocol == None:
         self._protocol = protocol.KademliaProtocol(self)
     else:
         self._protocol = networkProtocol
     
     self._joinDeferred = None
     self.contactsList = []
     self.dataStore = DictDataStore()
Example #2
0
 def __init__(self, id, client_string='ssl:%s:%d'):
     """
     Initialises the object representing the node with the given id.
     """
     # The node's ID within the distributed hash table.
     self.id = id
     # The routing table stores information about other nodes on the DHT.
     self._routing_table = RoutingTable(id)
     # The local key/value store containing data held by this node.
     self._data_store = DictDataStore()
     # A dictionary of IDs for messages pending a response and associated
     # deferreds to be fired when a response is completed.
     self._pending = {}
     # The template string to use when initiating a connection to another
     # node on the network.
     self._client_string = client_string
     # The version of Drogulus that this node implements.
     self.version = get_version()
     log.msg('Initialised node with id: %r' % self.id)
Example #3
0
class Node(object):
    """
    This class represents a single local node in the DHT encapsulating its
    presence in the network.

    All interactions with the DHT network by a client application are
    performed via this class (or a subclass).
    """

    def __init__(self, id, client_string='ssl:%s:%d'):
        """
        Initialises the object representing the node with the given id.
        """
        # The node's ID within the distributed hash table.
        self.id = id
        # The routing table stores information about other nodes on the DHT.
        self._routing_table = RoutingTable(id)
        # The local key/value store containing data held by this node.
        self._data_store = DictDataStore()
        # A dictionary of IDs for messages pending a response and associated
        # deferreds to be fired when a response is completed.
        self._pending = {}
        # The template string to use when initiating a connection to another
        # node on the network.
        self._client_string = client_string
        # The version of Drogulus that this node implements.
        self.version = get_version()
        log.msg('Initialised node with id: %r' % self.id)

    def join(self, seed_nodes=None):
        """
        Causes the Node to join the DHT network. This should be called before
        any other DHT operations. The seed_nodes argument must be a list of
        already known contacts describing existing nodes on the network.
        """
        if not seed_nodes:
            raise ValueError('Seed nodes required for node to join network')
        for contact in seed_nodes:
            self._routing_table.add_contact(contact)
        # Looking up the node's ID on the network will populate the routing
        # table with fresh nodes as well as tell us who our nearest neighbours
        # are.

        # TODO: Add callback to kick off refresh of k-buckets in future..?
        raise Exception('FIX ME!')
        # Ensure the refresh of k-buckets is set up properly.
        return NodeLookup(self.id, FindNode, self)

    def message_received(self, message, protocol):
        """
        Handles incoming messages.
        """
        # Update the routing table.
        peer = protocol.transport.getPeer()
        other_node = Contact(message.node, peer.host, peer.port,
                             message.version, time.time())
        log.msg('Message received from %s' % other_node)
        log.msg(message)
        self._routing_table.add_contact(other_node)
        # Sort on message type and pass to handler method. Explicit > implicit.
        if isinstance(message, Ping):
            self.handle_ping(message, protocol)
        elif isinstance(message, Pong):
            self.handle_pong(message)
        elif isinstance(message, Store):
            self.handle_store(message, protocol, other_node)
        elif isinstance(message, FindNode):
            self.handle_find_node(message, protocol)
        elif isinstance(message, FindValue):
            self.handle_find_value(message, protocol)
        elif isinstance(message, Error):
            self.handle_error(message, protocol, other_node)
        elif isinstance(message, Value):
            self.handle_value(message, other_node)
        elif isinstance(message, Nodes):
            self.handle_nodes(message)

    def send_message(self, contact, message):
        """
        Sends a message to the specified contact, adds it to the _pending
        dictionary and ensures it times-out after the correct period. If an
        error occurs the deferred's errback is called.
        """
        d = defer.Deferred()
        # open network call.
        client_string = self._client_string % (contact.address, contact.port)
        client = clientFromString(reactor, client_string)
        connection = client.connect(DHTFactory(self))
        # Ensure the connection will potentially time out.
        connection_timeout = reactor.callLater(constants.RPC_TIMEOUT,
                                               connection.cancel)

        def on_connect(protocol):
            # Cancel pending connection_timeout if it's still active.
            if connection_timeout.active():
                connection_timeout.cancel()
            # Send the message and add a timeout for the response.
            protocol.sendMessage(message)
            self._pending[message.uuid] = d
            reactor.callLater(constants.RESPONSE_TIMEOUT, response_timeout,
                              message, protocol, self)

        def on_error(error):
            log.msg('***** ERROR ***** interacting with %s' % contact)
            log.msg(error)
            self._routing_table.remove_contact(message.node)
            if message.uuid in self._pending:
                del self._pending[message.uuid]
            d.errback(error)

        connection.addCallback(on_connect)
        connection.addErrback(on_error)
        return d

    def trigger_deferred(self, message, error=False):
        """
        Given a message, will attempt to retrieve the deferred and trigger it
        with the appropriate callback or errback.
        """
        if message.uuid in self._pending:
            deferred = self._pending[message.uuid]
            if error:
                error.message = message
                deferred.errback(error)
            else:
                deferred.callback(message)
            # Remove the called deferred from the _pending dictionary.
            del self._pending[message.uuid]

    def handle_ping(self, message, protocol):
        """
        Handles an incoming Ping message. Returns a Pong message using the
        referenced protocol object.
        """
        pong = Pong(message.uuid, self.id, self.version)
        protocol.sendMessage(pong, True)

    def handle_pong(self, message):
        """
        Handles an incoming Pong message.
        """
        self.trigger_deferred(message)

    def handle_store(self, message, protocol, sender):
        """
        Handles an incoming Store message. Checks the provenance and timeliness
        of the message before storing locally. If there is a problem, removes
        the untrustworthy peer from the routing table. Otherwise, at
        REPLICATE_INTERVAL minutes in the future, the local node will attempt
        to replicate the Store message elsewhere in the DHT if such time is
        <= the message's expiry time.

        Sends a Pong message if successful otherwise replies with an
        appropriate Error.
        """
        # Check provenance
        is_valid, err_code = validate_message(message)
        if is_valid:
            # Ensure the node doesn't already have a more up-to-date version
            # of the value.
            current = self._data_store.get(message.key, False)
            if current and (message.timestamp < current.timestamp):
                # The node already has a later version of the value so
                # return an error.
                details = {
                    'new_timestamp': '%d' % current.timestamp
                }
                raise ValueError(8, constants.ERRORS[8], details,
                                 message.uuid)
            # Good to go, so store value.
            self._data_store.set_item(message.key, message)
            # Reply with a pong so the other end updates its routing table.
            pong = Pong(message.uuid, self.id, self.version)
            protocol.sendMessage(pong, True)
            # At some future time attempt to replicate the Store message
            # around the network IF it is within the message's expiry time.
            raise Exception("FIX ME!")
            # Need to check that callLater is called as part of the tests.
            reactor.callLater(constants.REPLICATE_INTERVAL,
                              self.republish, message)
        else:
            # Remove from the routing table.
            log.msg('Problem with Store command: %d - %s' %
                    (err_code, constants.ERRORS[err_code]))
            self._routing_table.blacklist(sender)
            # Return an error.
            details = {
                'message': 'You have been blacklisted.'
            }
            raise ValueError(err_code, constants.ERRORS[err_code], details,
                             message.uuid)

    def handle_find_node(self, message, protocol):
        """
        Handles an incoming FindNode message. Finds the details of up to K
        other nodes closer to the target key that *this* node knows about.
        Responds with a "Nodes" message containing the list of matching
        nodes.
        """
        target_key = message.key
        # List containing tuples of information about the matching contacts.
        other_nodes = [(n.id, n.address, n.port, n.version) for n in
                       self._routing_table.find_close_nodes(target_key)]
        result = Nodes(message.uuid, self.id, other_nodes, self.version)
        protocol.sendMessage(result, True)

    def handle_find_value(self, message, protocol):
        """
        Handles an incoming FindValue message. If the local node contains the
        value associated with the requested key replies with an appropriate
        "Value" message. Otherwise, responds with details of up to K other
        nodes closer to the target key that the local node knows about. In
        this case a "Nodes" message containing the list of matching nodes is
        sent to the caller.
        """
        match = self._data_store.get(message.key, False)
        if match:
            result = Value(message.uuid, self.id, match.key, match.value,
                           match.timestamp, match.expires, match.public_key,
                           match.name, match.meta, match.sig, match.version)
            protocol.sendMessage(result, True)
        else:
            self.handle_find_node(message, protocol)

    def handle_error(self, message, protocol, sender):
        """
        Handles an incoming Error message. Currently, this simply logs the
        error and closes the connection. In future this *may* remove the
        sender from the routing table (depending on the error).
        """
        # TODO: Handle error 8 (out of date data)
        log.msg('***** ERROR ***** from %s' % sender)
        log.msg(message)

    def handle_value(self, message, sender):
        """
        Handles an incoming Value message containing a value retrieved from
        another node on the DHT. Ensures the message is valid and calls the
        referenced deferred to signal the arrival of the value.

        TODO: How to handle invalid messages and errback the deferred.
        """
        # Check provenance
        is_valid, err_code = validate_message(message)
        if is_valid:
            self.trigger_deferred(message)
        else:
            log.msg('Problem with incoming Value: %d - %s' %
                    (err_code, constants.ERRORS[err_code]))
            log.msg(message)
            # Remove the remote node from the routing table.
            self._routing_table.remove_contact(sender.id, True)
            error = ValueError(constants.ERRORS[err_code])
            self.trigger_deferred(message, error)

    def handle_nodes(self, message):
        """
        Handles an incoming Nodes message containing information about other
        nodes on the network that are close to a requested key.
        """
        self.trigger_deferred(message)

    def send_ping(self, contact):
        """
        Sends a ping request to the given contact and returns a deferred
        that is fired when the reply arrives or an error occurs.
        """
        new_uuid = str(uuid4())
        ping = Ping(new_uuid, self.id, self.version)
        return self.send_message(contact, ping)

    def send_store(self, contact, public_key, name, value, timestamp, expires,
                   meta, signature):
        """
        Sends a Store message to the given contact. The value contained within
        the message is stored against a key derived from the public_key and
        name. Furthermore, the message is cryptographically signed using the
        value, timestamp, expires, name and meta values.
        """
        uuid = str(uuid4())
        compound_key = construct_key(public_key, name)
        store = Store(uuid, self.id, compound_key, value, timestamp, expires,
                      public_key, name, meta, signature, self.version)
        return self.send_message(contact, store)

    def send_find(self, contact, target, message_type):
        """
        Sends a Find[Node|Value] message to the given contact with the
        intention of obtaining information at the given target key. The type of
        find message is specified by message_type.
        """
        new_uuid = str(uuid4())
        find_message = message_type(new_uuid, self.id, target, self.version)
        deferred = self.send_message(contact, find_message)
        return (new_uuid, deferred)

    def _process_lookup_result(self, nearest_nodes, public_key, name, value,
                               timestamp, expires, meta, signature, length):
        """
        Given a list of nearest nodes will return a list of send_store based
        deferreds for the item to be stored in the DHT. The list will contain
        up to "length" number of deferreds.
        """
        list_of_deferreds = []
        for contact in nearest_nodes[:length]:
            deferred = self.send_store(contact, public_key, name, value,
                                       timestamp, expires, meta, signature)
            list_of_deferreds.append(deferred)
        return list_of_deferreds

    def replicate(self, public_key, name, value, timestamp, expires, meta,
                  signature, duplicate):
        """
        Will replicate args to "duplicate" number of nodes in the distributed
        hash table. Returns a deferred that will fire with a list of send_store
        deferreds when "duplicate" number of closest nodes have been
        identified.

        Obviously, the list can be turned in to a deferred_list to fire when
        the store commands have completed.

        Even if "duplicate" is > K no more than K items will be contained
        within the list result.
        """
        if duplicate < 1:
            # Guard to ensure meaningful duplication count.
            raise ValueError('Duplication count may not be less than 1')

        result = defer.Deferred()
        compound_key = construct_key(public_key, name)
        lookup = NodeLookup(compound_key, FindNode, self)

        def on_success(nodes):
            """
            A list of close nodes have been found so send store messages to
            the "duplicate" closest number of them and fire the "result"
            deferred with the resulting DeferredList of pending deferreds.
            """
            deferreds = self._process_lookup_result(nodes, public_key, name,
                                                    value, timestamp, expires,
                                                    meta, signature, duplicate)
            result.callback(deferreds)

        def on_error(error):
            """
            Catch all for errors during the lookup phase. Simply pass them on
            via the "result" deferred.
            """
            result.errback(error)

        lookup.addCallback(on_success)
        lookup.addErrback(on_error)
        return result

    def retrieve(self, key):
        """
        Given a key, will try to retrieve associated value from the distributed
        hash table. Returns a deferred that will fire when the operation is
        complete or failed.

        As the original Kademlia explains:

        "For caching purposes, once a lookup succeeds, the requesting node
        stores the <key, value> pair at the closest node it observed to the
        key that did not return the value."

        This method adds a callback to the NodeLookup to achieve this end.
        """
        lookup = NodeLookup(key, FindValue, self)

        def cache(result):
            """
            Called once the lookup succeeds in order to store the item at the
            node closest to the key that did not return the value.
            """
            caching_contact = None
            for candidate in lookup.shortlist:
                if candidate in lookup.contacted:
                    caching_contact = candidate
                    break
            if caching_contact:
                log.msg("Caching to %r" % caching_contact)
                self.send_store(caching_contact, result.public_key,
                                result.name, result.value, result.timestamp,
                                result.expires, result.meta, result.sig)
            return result

        lookup.addCallback(cache)
        return lookup

    def republish(self, message):
        """
        Will check and republish a locally stored message to the wider network.

        From the original Kademlia paper:

        "To ensure the persistence of key-value pairs, nodes must periodically
        republish keys. Otherwise, two phenomena may cause lookups for valid
        keys to fail. First, some of the k nodes that initially get a key-value
        pair when it is published may leave the network. Second, new nodes may
        join the network with IDs closer to some published key than the nodes
        on which the key-value pair was originally published. In both cases,
        the nodes with a key-value pair must republish it so as once again to
        ensure it is available on the k nodes closest to the key.

        To compensate for nodes leaving the network, Kademlia republishes each
        key-value pair once an hour. A naive implementation of this strategy
        would require many messages - each of up to k nodes storing a key-value
        pair would perform a node lookup followed by k - 1 STORE RPCs every
        hour. Fortunately, the republish process can be heavily optimized.
        First, when a node receives a STORE RPC for a given key-value pair, it
        assumes the RPC was also issued to the other k - 1 closest nodes, and
        thus the recipient will not republish the key-value pair in the next
        hour. This ensures that as long as republication intervals are not
        exactly synchronized, only one node will republish a given key-value
        pair every hour.

        A second optimization avoids performing node lookups before
        republishing keys. As described in Section 2.4, to handle unbalanced
        trees, nodes split k-buckets as required to ensure they have complete
        knowledge of a surrounding subtree with at least k nodes. If, before
        republishing key-value pairs, a node u refreshes all k-buckets in this
        subtree of k nodes, it will automatically be able to figure out the
        k closest nodes to a given key. These bucket refreshes can be amortized
        over the republication of many keys.

        To see why a node lookup is unnecessary after u refreshes buckets in
        the sub-tree of size >= k, it is necessary to consider two cases. If
        the key being republished falls in the ID range of the subtree, then
        since the subtree is of size at least k and u has complete knowledge of
        the subtree, clearly u must know the k closest nodes to the key. If,
        on the other hand, the key lies outside the subtree, yet u was one of
        the k closest nodes to the key, it must follow that u's k-buckets for
        intervals closer to the key than the subtree all have fewer than k
        entries. Hence, u will know all nodes in these k-buckets, which
        together with knowledge of the subtree will include the k closest nodes
        to the key.

        When a new node joins the system, it must store any key-value pair to
        which is is one of the k closest. Existing nodes, by similarly
        exploiting complete knowledge of their surrounding subtrees, will know
        which key-value pairs the new node should store. Any node learning of a
        new node therefore issues STORE RPCs to transfer relevant key-value
        pairs to the new node. To avoid redundant STORE RPCs, however, a node
        only transfers a key-value pair if it's [sic] own ID is closer to the
        key than are the IDs of other nodes."

        Messages are only republished if the following requirements are met:

        * They still exist in the local data store.
        * They have not expired.
        * They have not been updated for REPLICATE_INTERVAL seconds.
        """
        pass
Example #4
0
class Node(object):
    """
    This class represents a single local node in the DHT encapsulating its
    presence in the network.

    All interactions with the DHT network by a client application are
    performed via this class (or a subclass).
    """

    def __init__(self, id, client_string='ssl:%s:%d'):
        """
        Initialises the object representing the node with the given id.
        """
        # The node's ID within the distributed hash table.
        self.id = id
        # The routing table stores information about other nodes on the DHT.
        self._routing_table = RoutingTable(id)
        # The local key/value store containing data held by this node.
        self._data_store = DictDataStore()
        # A dictionary of IDs for messages pending a response and associated
        # deferreds to be fired when a response is completed.
        self._pending = {}
        # The template string to use when initiating a connection to another
        # node on the network.
        self._client_string = client_string
        # The version of Drogulus that this node implements.
        self.version = get_version()
        log.msg('Initialised node with id: %r' % self.id)

    def join(self, seed_nodes=None):
        """
        Causes the Node to join the DHT network. This should be called before
        any other DHT operations. The seedNodes argument contains a list of
        tuples describing existing nodes on the network in the form of their
        IP address and port.
        """
        pass

    def message_received(self, message, protocol):
        """
        Handles incoming messages.
        """
        # Update the routing table.
        peer = protocol.transport.getPeer()
        other_node = Contact(message.node, peer.host, peer.port,
                             message.version, time.time())
        log.msg('Message received from %s' % other_node)
        log.msg(message)
        self._routing_table.add_contact(other_node)
        # Sort on message type and pass to handler method. Explicit > implicit.
        if isinstance(message, Ping):
            self.handle_ping(message, protocol)
        elif isinstance(message, Pong):
            self.handle_pong(message)
        elif isinstance(message, Store):
            self.handle_store(message, protocol, other_node)
        elif isinstance(message, FindNode):
            self.handle_find_node(message, protocol)
        elif isinstance(message, FindValue):
            self.handle_find_value(message, protocol)
        elif isinstance(message, Error):
            self.handle_error(message, protocol, other_node)
        elif isinstance(message, Value):
            self.handle_value(message, other_node)
        elif isinstance(message, Nodes):
            self.handle_nodes(message)

    def send_message(self, contact, message):
        """
        Sends a message to the specified contact, adds it to the _pending
        dictionary and ensures it times-out after the correct period. If an
        error occurs the deferred's errback is called.
        """
        d = defer.Deferred()
        # open network call.
        client_string = self._client_string % (contact.address, contact.port)
        client = clientFromString(reactor, client_string)
        connection = client.connect(DHTFactory(self))
        # Ensure the connection will potentially time out.
        connection_timeout = reactor.callLater(constants.RPC_TIMEOUT,
                                               connection.cancel)

        def on_connect(protocol):
            # Cancel pending connection_timeout if it's still active.
            if connection_timeout.active():
                connection_timeout.cancel()
            # Send the message and add a timeout for the response.
            protocol.sendMessage(message)
            self._pending[message.uuid] = d
            reactor.callLater(constants.RESPONSE_TIMEOUT, response_timeout,
                              message, protocol, self)

        def on_error(error):
            log.msg('***** ERROR ***** interacting with %s' % contact)
            log.msg(error)
            self._routing_table.remove_contact(message.node)
            if message.uuid in self._pending:
                del self._pending[message.uuid]
            d.errback(error)

        connection.addCallback(on_connect)
        connection.addErrback(on_error)
        return d

    def trigger_deferred(self, message, error=False):
        """
        Given a message, will attempt to retrieve the deferred and trigger it
        with the appropriate callback or errback.
        """
        if message.uuid in self._pending:
            deferred = self._pending[message.uuid]
            if error:
                error.message = message
                deferred.errback(error)
            else:
                deferred.callback(message)
            # Remove the called deferred from the _pending dictionary.
            del self._pending[message.uuid]

    def handle_ping(self, message, protocol):
        """
        Handles an incoming Ping message. Returns a Pong message using the
        referenced protocol object.
        """
        pong = Pong(message.uuid, self.id, self.version)
        protocol.sendMessage(pong, True)

    def handle_pong(self, message):
        """
        Handles an incoming Pong message.
        """
        self.trigger_deferred(message)

    def handle_store(self, message, protocol, sender):
        """
        Handles an incoming Store message. Checks the provenance and timeliness
        of the message before storing locally. If there is a problem, removes
        the untrustworthy peer from the routing table. Otherwise, at
        REPLICATE_INTERVAL minutes in the future, the local node will attempt
        to replicate the Store message elsewhere in the DHT if such time is
        <= the message's expiry time.

        Sends a Pong message if successful otherwise replies with an
        appropriate Error.
        """
        # Check provenance
        is_valid, err_code = validate_message(message)
        if is_valid:
            # Ensure the node doesn't already have a more up-to-date version
            # of the value.
            current = self._data_store.get(message.key, False)
            if current and (message.timestamp < current.timestamp):
                # The node already has a later version of the value so
                # return an error.
                details = {
                    'new_timestamp': '%d' % current.timestamp
                }
                raise ValueError(8, constants.ERRORS[8], details,
                                 message.uuid)
            # Good to go, so store value.
            self._data_store.set_item(message.key, message)
            # Reply with a pong so the other end updates its routing table.
            pong = Pong(message.uuid, self.id, self.version)
            protocol.sendMessage(pong, True)
            # At some future time attempt to replicate the Store message
            # around the network IF it is within the message's expiry time.
            reactor.callLater(constants.REPLICATE_INTERVAL,
                              self.replicate, message)
        else:
            # Remove from the routing table.
            log.msg('Problem with Store command: %d - %s' %
                    (err_code, constants.ERRORS[err_code]))
            self._routing_table.blacklist(sender)
            # Return an error.
            details = {
                'message': 'You have been blacklisted.'
            }
            raise ValueError(err_code, constants.ERRORS[err_code], details,
                             message.uuid)

    def handle_find_node(self, message, protocol):
        """
        Handles an incoming FindNode message. Finds the details of up to K
        other nodes closer to the target key that *this* node knows about.
        Responds with a "Nodes" message containing the list of matching
        nodes.
        """
        target_key = message.key
        # List containing tuples of information about the matching contacts.
        other_nodes = [(n.id, n.address, n.port, n.version) for n in
                       self._routing_table.find_close_nodes(target_key)]
        result = Nodes(message.uuid, self.id, other_nodes, self.version)
        protocol.sendMessage(result, True)

    def handle_find_value(self, message, protocol):
        """
        Handles an incoming FindValue message. If the local node contains the
        value associated with the requested key replies with an appropriate
        "Value" message. Otherwise, responds with details of up to K other
        nodes closer to the target key that the local node knows about. In
        this case a "Nodes" message containing the list of matching nodes is
        sent to the caller.
        """
        match = self._data_store.get(message.key, False)
        if match:
            result = Value(message.uuid, self.id, match.key, match.value,
                           match.timestamp, match.expires, match.public_key,
                           match.name, match.meta, match.sig, match.version)
            protocol.sendMessage(result, True)
        else:
            self.handle_find_node(message, protocol)

    def handle_error(self, message, protocol, sender):
        """
        Handles an incoming Error message. Currently, this simply logs the
        error and closes the connection. In future this *may* remove the
        sender from the routing table (depending on the error).
        """
        # TODO: Handle error 8 (out of date data)
        log.msg('***** ERROR ***** from %s' % sender)
        log.msg(message)

    def handle_value(self, message, sender):
        """
        Handles an incoming Value message containing a value retrieved from
        another node on the DHT. Ensures the message is valid and calls the
        referenced deferred to signal the arrival of the value.

        TODO: How to handle invalid messages and errback the deferred.
        """
        # Check provenance
        is_valid, err_code = validate_message(message)
        if is_valid:
            self.trigger_deferred(message)
        else:
            log.msg('Problem with incoming Value: %d - %s' %
                    (err_code, constants.ERRORS[err_code]))
            log.msg(message)
            # Remove the remote node from the routing table.
            self._routing_table.remove_contact(sender.id, True)
            error = ValueError(constants.ERRORS[err_code])
            self.trigger_deferred(message, error)

    def handle_nodes(self, message):
        """
        Handles an incoming Nodes message containing information about other
        nodes on the network that are close to a requested key.
        """
        self.trigger_deferred(message)

    def send_ping(self, contact):
        """
        Sends a ping request to the given contact and returns a deferred
        that is fired when the reply arrives or an error occurs.
        """
        new_uuid = str(uuid4())
        ping = Ping(new_uuid, self.id, self.version)
        return self.send_message(contact, ping)

    def send_store(self, contact, public_key, name, value, timestamp, expires,
                   meta, signature):
        """
        Sends a Store message to the given contact. The value contained within
        the message is stored against a key derived from the public_key and
        name. Furthermore, the message is cryptographically signed using the
        value, timestamp, expires, name and meta values.
        """
        uuid = str(uuid4())
        compound_key = construct_key(public_key, name)
        store = Store(uuid, self.id, compound_key, value, timestamp, expires,
                      public_key, name, meta, signature, self.version)
        return self.send_message(contact, store)

    def send_find(self, contact, target, message_type):
        """
        Sends a Find[Node|Value] message to the given contact with the
        intention of obtaining information at the given target key. The type of
        find message is specified by message_type.
        """
        new_uuid = str(uuid4())
        find_message = message_type(new_uuid, self.id, target, self.version)
        deferred = self.send_message(contact, find_message)
        return (new_uuid, deferred)

    def _process_lookup_result(self, nearest_nodes, public_key, name, value,
                               timestamp, expires, meta, signature, length):
        """
        Given a list of nearest nodes will return a list of send_store based
        deferreds for the item to be stored in the DHT. The list will contain
        up to "length" number of deferreds.
        """
        list_of_deferreds = []
        for contact in nearest_nodes[:length]:
            deferred = self.send_store(contact, public_key, name, value,
                                       timestamp, expires, meta, signature)
            list_of_deferreds.append(deferred)
        return list_of_deferreds

    def replicate(self, public_key, name, value, timestamp, expires, meta,
                  signature, duplicate):
        """
        Will replicate args to "duplicate" number of nodes in the distributed
        hash table. Returns a deferred that will fire with a list of send_store
        deferreds when "duplicate" number of closest nodes have been
        identified.

        Obviously, the list can be turned in to a deferred_list to fire when
        the store commands have completed.

        Even if "duplicate" is > K no more than K items will be contained
        within the list result.
        """
        if duplicate < 1:
            # Guard to ensure meaningful duplication count.
            raise ValueError('Duplication count may not be less than 1')

        result = defer.Deferred()
        compound_key = construct_key(public_key, name)
        lookup = NodeLookup(compound_key, FindNode, self)

        def on_success(nodes):
            """
            A list of close nodes have been found so send store messages to
            the "duplicate" closest number of them and fire the "result"
            deferred with the resulting DeferredList of pending deferreds.
            """
            deferreds = self._process_lookup_result(nodes, public_key, name,
                                                    value, timestamp, expires,
                                                    meta, signature, duplicate)
            result.callback(deferreds)

        def on_error(error):
            """
            Catch all for errors during the lookup phase. Simply pass them on
            via the "result" deferred.
            """
            result.errback(error)

        lookup.addCallback(on_success)
        lookup.addErrback(on_error)
        return result

    def retrieve(self, key):
        """
        Given a key, will try to retrieve associated value from the distributed
        hash table. Returns a deferred that will fire when the operation is
        complete or failed.

        As the original Kademlia explains:

        "For caching purposes, once a lookup succeeds, the requesting node
        stores the <key, value> pair at the closest node it observed to the
        key that did not return the value."

        This method adds a callback to the NodeLookup to achieve this end.
        """
        lookup = NodeLookup(key, FindValue, self)

        def cache(result):
            """
            Called once the lookup succeeds in order to store the item at the
            node closest to the key that did not return the value.
            """
            caching_contact = None
            for candidate in lookup.shortlist:
                if candidate in lookup.contacted:
                    caching_contact = candidate
                    break
            if caching_contact:
                log.msg("Caching to %r" % caching_contact)
                self.send_store(caching_contact, result.public_key,
                                result.name, result.value, result.timestamp,
                                result.expires, result.meta, result.sig)
            return result

        lookup.addCallback(cache)
        return lookup
Example #5
0
class Node(object):
    """
    This class represents a single local node in the DHT encapsulating its
    presence in the network.

    All interactions with the DHT network by a client application are
    performed via this class (or a subclass).
    """

    def __init__(self, id, client_string='ssl:%s:%d'):
        """
        Initialises the object representing the node with the given id.
        """
        # The node's ID within the distributed hash table.
        self.id = id
        # The routing table stores information about other nodes on the DHT.
        self._routing_table = RoutingTable(id)
        # The local key/value store containing data held by this node.
        self._data_store = DictDataStore()
        # A dictionary of IDs for messages pending a response and associated
        # deferreds to be fired when a response is completed.
        self._pending = {}
        # The template string to use when initiating a connection to another
        # node on the network.
        self._client_string = client_string
        # The version of Drogulus that this node implements.
        self.version = get_version()
        log.msg('Initialised node with id: %r' % self.id)

    def join(self, seed_nodes=None):
        """
        Causes the Node to join the DHT network. This should be called before
        any other DHT operations. The seedNodes argument contains a list of
        tuples describing existing nodes on the network in the form of their
        IP address and port.
        """
        pass

    def message_received(self, message, protocol):
        """
        Handles incoming messages.
        """
        # Update the routing table.
        peer = protocol.transport.getPeer()
        other_node = Contact(message.node, peer.host, peer.port,
                             message.version, time.time())
        log.msg('Message received from %s' % other_node)
        log.msg(message)
        self._routing_table.add_contact(other_node)
        # Sort on message type and pass to handler method. Explicit > implicit.
        if isinstance(message, Ping):
            self.handle_ping(message, protocol)
        elif isinstance(message, Pong):
            self.handle_pong(message)
        elif isinstance(message, Store):
            self.handle_store(message, protocol, other_node)
        elif isinstance(message, FindNode):
            self.handle_find_node(message, protocol)
        elif isinstance(message, FindValue):
            self.handle_find_value(message, protocol)
        elif isinstance(message, Error):
            self.handle_error(message, protocol, other_node)
        elif isinstance(message, Value):
            self.handle_value(message, other_node)
        elif isinstance(message, Nodes):
            self.handle_nodes(message)

    def send_message(self, contact, message):
        """
        Sends a message to the specified contact, adds it to the _pending
        dictionary and ensures it times-out after the correct period. If an
        error occurs the deferred's errback is called.
        """
        d = defer.Deferred()
        # open network call.
        client_string = self._client_string % (contact.address, contact.port)
        client = clientFromString(reactor, client_string)
        connection = client.connect(DHTFactory(self))
        # Ensure the connection will potentially time out.
        connection_timeout = reactor.callLater(constants.RPC_TIMEOUT,
                                               connection.cancel)

        def on_connect(protocol):
            # Cancel pending connection_timeout if it's still active.
            if connection_timeout.active():
                connection_timeout.cancel()
            # Send the message and add a timeout for the response.
            protocol.sendMessage(message)
            self._pending[message.uuid] = d
            reactor.callLater(constants.RESPONSE_TIMEOUT, response_timeout,
                              message, protocol, self)

        def on_error(error):
            log.msg('***** ERROR ***** connecting to %s' % contact)
            log.msg(error)
            self._routing_table.remove_contact(message.node)
            d.errback(error)

        connection.addCallbacks(on_connect, on_error)
        return d

    def trigger_deferred(self, message, error=False):
        """
        Given a message, will attempt to retrieve the deferred and trigger it
        with the appropriate callback or errback.
        """
        if message.uuid in self._pending:
            deferred = self._pending[message.uuid]
            if error:
                error.message = message
                deferred.errback(error)
            else:
                deferred.callback(message)
            # Remove the called deferred from the _pending dictionary.
            del self._pending[message.uuid]

    def handle_ping(self, message, protocol):
        """
        Handles an incoming Ping message. Returns a Pong message using the
        referenced protocol object.
        """
        pong = Pong(message.uuid, self.id, self.version)
        protocol.sendMessage(pong, True)

    def handle_pong(self, message):
        """
        Handles an incoming Pong message.
        """
        self.trigger_deferred(message)

    def handle_store(self, message, protocol, sender):
        """
        Handles an incoming Store message. Checks the provenance and timeliness
        of the message before storing locally. If there is a problem, removes
        the untrustworthy peer from the routing table. Otherwise, at
        REPLICATE_INTERVAL minutes in the future, the local node will attempt
        to replicate the Store message elsewhere in the DHT if such time is
        <= the message's expiry time.

        Sends a Pong message if successful otherwise replies with an
        appropriate Error.
        """
        # Check provenance
        is_valid, err_code = validate_message(message)
        if is_valid:
            # Ensure the node doesn't already have a more up-to-date version
            # of the value.
            current = self._data_store.get(message.key, False)
            if current and (message.timestamp < current.timestamp):
                # The node already has a later version of the value so
                # return an error.
                details = {
                    'new_timestamp': '%d' % current.timestamp
                }
                raise ValueError(8, constants.ERRORS[8], details,
                                 message.uuid)
            # Good to go, so store value.
            self._data_store.set_item(message.key, message)
            # Reply with a pong so the other end updates its routing table.
            pong = Pong(message.uuid, self.id, self.version)
            protocol.sendMessage(pong, True)
            # At some future time attempt to replicate the Store message
            # around the network IF it is within the message's expiry time.
            reactor.callLater(constants.REPLICATE_INTERVAL,
                              self.send_replicate, message)
        else:
            # Remove from the routing table.
            log.msg('Problem with Store command: %d - %s' %
                    (err_code, constants.ERRORS[err_code]))
            self._routing_table.remove_contact(sender.id, True)
            # Return an error.
            details = {
                'message': 'You have been removed from remote routing table.'
            }
            raise ValueError(err_code, constants.ERRORS[err_code], details,
                             message.uuid)

    def handle_find_node(self, message, protocol):
        """
        Handles an incoming FindNode message. Finds the details of up to K
        other nodes closer to the target key that *this* node knows about.
        Responds with a "Nodes" message containing the list of matching
        nodes.
        """
        target_key = message.key
        # List containing tuples of information about the matching contacts.
        other_nodes = [(n.id, n.address, n.port, n.version) for n in
                       self._routing_table.find_close_nodes(target_key)]
        result = Nodes(message.uuid, self.id, other_nodes, self.version)
        protocol.sendMessage(result, True)

    def handle_find_value(self, message, protocol):
        """
        Handles an incoming FindValue message. If the local node contains the
        value associated with the requested key replies with an appropriate
        "Value" message. Otherwise, responds with details of up to K other
        nodes closer to the target key that the local node knows about. In
        this case a "Nodes" message containing the list of matching nodes is
        sent to the caller.
        """
        match = self._data_store.get(message.key, False)
        if match:
            result = Value(message.uuid, self.id, match.key, match.value,
                           match.timestamp, match.expires, match.public_key,
                           match.name, match.meta, match.sig, match.version)
            protocol.sendMessage(result, True)
        else:
            self.handle_find_node(message, protocol)

    def handle_error(self, message, protocol, sender):
        """
        Handles an incoming Error message. Currently, this simply logs the
        error and closes the connection. In future this *may* remove the
        sender from the routing table (depending on the error).
        """
        # TODO: Handle error 8 (out of date data)
        log.msg('***** ERROR ***** from %s' % sender)
        log.msg(message)

    def handle_value(self, message, sender):
        """
        Handles an incoming Value message containing a value retrieved from
        another node on the DHT. Ensures the message is valid and calls the
        referenced deferred to signal the arrival of the value.

        TODO: How to handle invalid messages and errback the deferred.
        """
        # Check provenance
        is_valid, err_code = validate_message(message)
        if is_valid:
            self.trigger_deferred(message)
        else:
            log.msg('Problem with incoming Value: %d - %s' %
                    (err_code, constants.ERRORS[err_code]))
            log.msg(message)
            # Remove the remote node from the routing table.
            self._routing_table.remove_contact(sender.id, True)
            error = ValueError(constants.ERRORS[err_code])
            self.trigger_deferred(message, error)

    def handle_nodes(self, message):
        """
        Handles an incoming Nodes message containing information about other
        nodes on the network that are close to a requested key.
        """
        self.trigger_deferred(message)

    def iterative_lookup(self, key, message_class):
        """
        A generic lookup function for finding nodes or values within the
        distributed hash table. Takes a key that either references a value or
        location in the hash-space. This function returns a deferred that will
        fire wth the found value or a set of peers in the DHT that are close to
        the key. The message class should be either FindNode or FindValue.
        """
        pass

    def send_ping(self, contact):
        """
        Sends a ping request to the given contact and returns a deferred
        that is fired when the reply arrives or an error occurs.
        """
        new_uuid = str(uuid4())
        ping = Ping(new_uuid, self.id, self.version)
        return self.send_message(contact, ping)

    def send_store(self, private_key, public_key, name, value,
                   timestamp, expires, meta):
        """
        Sends a Store message to the given contact. The value contained within
        the message is stored against a key derived from the public_key and
        name. Furthermore, the message is cryptographically signed using the
        value, timestamp, expires, name and meta values.
        """
        new_uuid = str(uuid4())
        signature = generate_signature(value, timestamp, expires, name, meta,
                                       private_key)
        compound_key = construct_key(public_key, name)
        new_store = Store(new_uuid, self.id, compound_key, value, timestamp,
                          expires, public_key, name, meta, signature,
                          self.version)
        return self.send_replicate(new_store)

    def send_replicate(self, store_message):
        """
        Sends an existing valid Store message (that will probably have
        originated from a third party) to another peer on the network for the
        purposes of replication / spreading popular values.
        """
        # Check for expiry time..?
        # Find closest node...
        """
        new_uuid = str(uuid4())
        store = Store(new_uuid, self.id, store_message.key,
                      store_message.value, store_message.timestamp,
                      store_message.expires, store_message.public_key,
                      store_message.name, store_message.meta,
                      store_message.sig, self.version)
        return self.send_message(contact, store)"""

    def send_find_node(self, contact, id):
        """
        Sends a FindNode message to the given contact with the intention of
        obtaining contact information about the node with the specified id.
        """
        pass

    def send_find_value(self, contact, key):
        """
        Sends a FindValue message to the given contact with the intention of
        obtaining the value associated with the specified key.
        """
        pass
Example #6
0
class StaticTupleSpacePeer():
    """ Enables tuples to be stored locally, and in turn allows non-local tuples to be located at
        static network locations provided as input at start-up 
    """
    def __init__(self, id=None, udpPort=4000, dataStore=None, routingTable=None, networkProtocol=None):
        if id != None:
            self.id = id
        else:
            self.id = self._generateID()
        self.port = udpPort
        
        if networkProtocol == None:
            self._protocol = protocol.KademliaProtocol(self)
        else:
            self._protocol = networkProtocol
        
        self._joinDeferred = None
        self.contactsList = []
        self.dataStore = DictDataStore()
        

    def put(self, sTuple, originalPublisherID=None):
        """ Used to write a tuple or serialized (string) data into a tuple space
        
        @note: This method is generally called "out" in tuple space literature,
               but is renamed to "put" in this implementation to match the 
               renamed "in"/"get" method (see the description for C{get()}).
        
        @param sTuple: The tuple to write into the static tuple space (it
                       is named "sTuple" to avoid a conflict with the Python
                       C{tuple} data type).
        @type sTuple: tuple
        
        @rtype: twisted.internet.defer.Deferred
        """
        
        if isinstance(sTuple, tuple):
            # Parse the tuple
            sData = (sTuple[0], sTuple[1])
            ownerID = sTuple[2]
            
            # Serialize the data
            tupleValue = cPickle.dumps(sData)
        elif isinstance(sTuple, str):
            tupleValue = sTuple
            ownerID = originalPublisherID
        else:
            raise DataFormatError("Error, expected a tuple or a serialized string as input")
        
        # TODO: may need to implement more advanced tuple space, based on data types
        #       for now tuples need to match exactly (element value as well as element order)     
        
        # Generate a hash of the data to be stored        
        h = hashlib.sha1()
        h.update(tupleValue)
        mainKey = h.digest()
        
        originallyPublished = 0        
        now = int(time.time())
        
#        print 'publishing :'
#        print 'key ' + str((mainKey,)) 
#        print 'value' + tupleValue 
        
        # TODO: Check if the tuple has already been stored, also check that other peers tuples don't replace
        # locally stored tuples
        self.dataStore.setItem(mainKey, tupleValue, now, originallyPublished, ownerID)
        
        df = defer.Deferred() 
        # invoke call-back now
        df.callback(tupleValue)
        
        return df

    
    def get(self, template):
        """ Reads and removes (consumes) a tuple from the tuple space (blocking)
        
        @type template: tuple
        
        @note: This method is generally called "in" in tuple space literature,
               but is renamed to "get" in this implementation to avoid
               a conflict with the Python C{in} keyword.
        @return: a matching tuple,  or None if no matching tuples were found
        """
        
        # TODO: consider to implement blocking mechanism that waits until a tuple is found
        # TODO: consider to implement mechanism that removes the tuple (resource marshalling)
        
     
        return self.findTuple(template)

    
    def getIfExists(self, template, getListenerTuple=False):
        """ Reads and removes (consumes) a tuple from the tuple space (non-blocking)
        
        @type template: tuple
        
        @param getListenerTuple: If set to True, look for a I{listener tuple}
                                 for this template; this is typically used
                                 to remove event handlers.
        @type getListenerTuple: bool
        
        @return: a matching tuple,  or None if no matching tuples were found
        
        @note: This method is generally called "in" in tuple space literature,
               but is renamed to "get" in this implementation to avoid
               a conflict with the Python C{in} keyword.
        """
        return self.findTuple(template)
    
    
    def read(self, template, numberOfResults=1):
        """ Non-destructively reads a tuple in the tuple space (blocking)
        
        This operation is similar to "get" (or "in") in that the peer builds a
        template and waits for a matching tuple in the tuple space. Upon
        finding a matching tuple, however, it copies it, leaving the original
        tuple in the tuple space.
        
        @note: This method is named "rd" in some other implementations.
        
        @param numberOfResults: The maximum number of matching tuples to return.
                                If set to 1 (default), return the tuple itself,
                                otherwise return a list of tuples. If set to 0
                                or lower, return all results.
        @type numberOfResults: int
        
        @return: a matching tuple, or list of tuples (if C{numberOfResults} is
                 not set to 1, or None if no matching tuples were found
        @rtype: twisted.internet.defer.Deferred
        """
        returnedTuple = self.findTuple(template)
        
        if returnedTuple != None:
            #returnTuples = []
            #returnTuples.append(returnedTuple)
            #return returnTuples
            return returnedTuple
        else:
            return None

    
    def readIfExists(self, template, numberOfResults=1):
        """ Non-destructively reads a tuple in the tuple space (non-blocking)
        
        This operation is similar to "get" (or "in") in that the peer builds a
        template and waits for a matching tuple in the tuple space. Upon
        finding a matching tuple, however, it copies it, leaving the original
        tuple in the tuple space.
        
        @note: This method is named "rd" in some other implementations.
        
        @param numberOfResults: The maximum number of matching tuples to return.
                                If set to 1 (default), return the tuple itself,
                                otherwise return a list of tuples. If set to 0
                                or lower, return all results.
        @type numberOfResults: int
        
        @return: a matching tuple, or list of tuples (if C{numberOfResults} is
                 not set to 1, or None if no matching tuples were found
        @rtype: twisted.internet.defer.Deferred
        """
        returnedTuple = self.findTuple(template)
        
        if returnedTuple != None:
            #returnTuples = []
            #returnTuples.append(returnedTuple)
            #return returnTuples
            return returnedTuple
        else:
            return None

    @rpcmethod
    def findTuple(self, value):
        """ Used to search the dataStore for a tuple, if invoked locally it 
            searches this peers datastore. If it is invoked via RPC it will 
            search for the tuple at the remote peer
        
            @param value: The tuple to search for  
            
            return: a matching tuple,  or None if no matching tuples were found
        """
        
        if isinstance(value, tuple):
            # parse the tuple 
            dataTuple = (value[0], value[1])
            
            # Serialize the tuple
            serialValue = cPickle.dumps(dataTuple)
        elif isinstance(value, str):
            serialValue = value
        else:
            raise DataFormatError("Error, expected a tuple or a serialized string as input")
        
        # print 'searching for ' + serialValue
        
        # Generate a hash of the value       
        h = hashlib.sha1()
        h.update(serialValue)
        mainKey = h.digest()
        
        keys = self.dataStore.keys()
        
        try:
            dataStoreTuple = self.dataStore.__getitem__(mainKey)
        except Exception:
            
            # The data wasn't found, so return None
            return None
        
        
        # Note, returning value given as input since it was found in the data store
        # and it is in tuple format, not serialized as in the data store
        # TODO: Implement mechanism to handle tuple type (resource/handler)
        if len(value) == 5:
            extraArgs = []
            if value[3] == None:
                extraArgs.append('')
            else:
                extraArgs.append(value[3])
            if value[4] == None:
                extraArgs.append('')
            else:
                extraArgs.append(value[4])
            
            returnTuple = (value[0], value[1], self.dataStore.originalPublisherID(mainKey), extraArgs[0], extraArgs[1])
        else:
            returnTuple = (value[0], value[1], self.dataStore.originalPublisherID(mainKey))
        return returnTuple
        
    
    @rpcmethod 
    def getOwnedTuples(self):
        """ Used to obtain all of the tuples owned by this peer via RPC, 
                        
            @return: a list containing all of the tuples and their owner ID's 
                     in the following format [(ownerID, tuple1), ..., (ownerID, tuple n)]
            @rtype: list
        """
        
        dataStoreKeys = self.dataStore.keys()
        tuples = []
        
        for key in dataStoreKeys:
            if (self.dataStore.originalPublisherID(key) == self.id):           
                idTuple = []
                idTuple.append(self.id)
                idTuple.append(self.dataStore.__getitem__(key))
                tuples.append(idTuple)
        
        return tuples
    
    @rpcmethod
    def getAllTuples(self):
        """ Used to obtain all of the tuples stored at a remote peer via RPC, 
                       
            @return: a list containing all of the tuples and their owner ID's 
                     in the following format [(ownerID, tuple1), ..., (ownerID, tuple n)]
            @rtype: list
        """
        dataStoreKeys = self.dataStore.keys()
        tuples = []
        
        for key in dataStoreKeys:
            idTuple = []
            idTuple.append(self.dataStore.originalPublisherID(key))
            idTuple.append(self.dataStore.__getitem__(key))
            tuples.append(idTuple)
            
        return tuples
            
    def findContact(self, contactID):
        """ Used to search for a contact inside of this peers contactList
            @return: a contact if it was found
        """
        
        #print 'searching for ' + str((contactID,))
        for contact in self.contactsList:
            #print 'contact ' + str((contact.id,))
                       
            if contact.id == contactID:
                return contact
            
    
    def refreshDataStore(self):
        """ Refreshs the datastore, ensuring that the tuples obtained from remote peers are still valid and that
            those peers are still alive """
        
        # TODO: Implement this
    
    def joinNetwork(self, knownNodeAddresses=None):
        """ 
        Causes the peer to join the static network; A list of known contacts addresses is given as input.
        Each of these contacts are polled to see if they are alive. 
        
        @param knownNodeAddresses: A sequence of tuples containing IP address
                                   information for existing nodes on the
                                   Kademlia network, in the format:
                                   C{(<ip address>, (udp port>)}
        @type knownNodeAddresses: tuple
        
        @return: Deferred, will call-back once join has completed
        @rtype: twisted.internet.defer.Deferred
        """
        def addContact(responseTuple):
            """ adds a contact once it has responded to the remote procedure call """
            responseMsg = responseTuple[0]
            originatingAddress = responseTuple[1]
            
            contactID = responseTuple[0].nodeID
            #print 'id ' + str((responseMsg.nodeID,))
            #print 'response ' + str(responseMsg.response)            
            #print 'address ' + str(originatingAddress)
            
            if contactID != None:
                activeContact = Contact(contactID, originatingAddress[0], originatingAddress[1], self._protocol)
                self.contactsList.append(activeContact)
                
                
                                
                # Check for an errorMessage in the responseMsg
                if isinstance(responseMsg, ErrorMessage):
                    self._joinDeferred.errback(failure.Failure(Exception('Error response from RPC call: ' + str(responseMsg.response))))
                    #print 'Error response from RPC call: ' + str(responseMsg.response)
                else:
                    # Check if any tuples were returned by this contact
                    response = responseMsg.response
                    if isinstance(response, list):
                        # obtain the tuples and their owner ID's
                        for item in response:
                            # Place this item into the local data store
                            ownerID = item[0]
                            tupleValue = item[1]
                            self.put(tupleValue, ownerID)
                            
#                        print 'received tuples'
#                        print response
#                        
#                        localTuples = self.getAllTuples()
#                        print 'locally stored tuples'
#                        for tuple in localTuples:
#                            print str(tuple)
                    else:
                        self._joinDeferred.errback(failure.Failure(Exception('RPC response from contact invalid, expected a list')))
                        
                # Check if all the contacts have been reached
                if len(self.contactsList) == len(knownNodeAddresses):
                    # invoke joinDeferred callback to signal that join has completed
                    self._joinDeferred.callback(self.contactsList)
        
        def checkInitStatus(error): 
            """ Invoked when RPC attempt to contact fails
                @type failure: twisted.python.failure.Failure 
            """
            error.trap(protocol.TimeoutError)
            deadContactID = error.getErrorMessage()
            
            # TODO: Log this error
            #print 'Error, communication with contact failed!'
            
            # Remove this contact from the tentativeContacts list
            badContacts = []
            for cont in tentativeContacts:
                if cont.id == deadContactID:
                    badContacts.append(cont)
            for cont in badContacts:
                # TODO: log this error
                #print 'dead contact removed!' 
                tentativeContacts.remove(cont)
          
            # Check if all the other contacts have responded (Thus join completed)
            if (len(self.contactsList) > 0) and  \
                (len(self.contactsList) == (len(knownNodeAddresses) - (len(knownNodeAddresses) - len(tentativeContacts)))):
                # invoke joinDeferred errback to signal that join did not complete successfully
                self._joinDeferred.errback(failure.Failure(Exception('Not all contacts responded')))
            # Check if all of the contacts did not respond
            elif len(tentativeContacts) == 0:
                # invoke joinDeferred errback to signal that join did not complete successfully
                self._joinDeferred.errback(failure.Failure(Exception('None of the contacts could be reached')))
                # TODO: log this error
                
        # Prepare the underlying Kademlia protocol
        self._listeningPort = twisted.internet.reactor.listenUDP(self.port, self._protocol) #IGNORE:E1101
                   
        self._joinDeferred = defer.Deferred() 
        tentativeContacts = []
        
        # Create temporary contact information for the list of addresses of known nodes
        if knownNodeAddresses != None:
            bootstrapContacts = []
            for address, port in knownNodeAddresses:
                contact = Contact(self._generateID(), address, port, self._protocol)
                
                tentativeContacts.append(contact)
                                                
                # Check that the contact exists, and obtain its actual id and a list of all the tuples stored 
                # by this contact
                rpcMethod = getattr(contact, 'getOwnedTuples')
                df = rpcMethod(rawResponse=True)
                df.addCallback(addContact)
                df.addErrback(checkInitStatus)
        # if no known contacts, just call-back without trying to connect to peers
        else:
            self._joinDeferred.callback(None)
                                
                #self.contactsList.append(contact)
        
        # TODO: schedule a call to a statusCheckMethod which ensures that contacts are active
            
    def _iterativeFind(self, contactID):
        """ Used in Distributed Hash Table, still accessed by current mobilIVR system, thus just 
            here as a blank for interface purposes
        """
        emptyList = []
        return emptyList
    
    def addContact(self, contact):
        """ add a contact """
        
    def removeContact(self, contact):
        """ remove the contact """        
    
    def _generateID(self):
        """ Generates a 160-bit pseudo-random identifier
        
        @return: A globally unique 160-bit pseudo-random identifier
        @rtype: str
        """
        hash = hashlib.sha1()
        hash.update(str(random.getrandbits(255)))
        return hash.digest()