示例#1
0
    def request_state_components(self):
        """
        Ask all our peers for any StateComponents that our Blockchain needs, and
        that we have not recently requested.

        """

        with self.lock:
            if not self.blockchain.state_available:
                # We need to ask for StateComponents from our peers until we get
                # all of them.
                for requested_component in \
                        self.blockchain.get_requested_state_components():

                    if (requested_component
                            in self.recently_requested_state_components):

                        # We just asked for this one
                        continue

                    logging.debug("Requesting StateComponent {} from all "
                                  "peers".format(
                                      bytes2string(requested_component)))

                    # Compose just one message per request
                    message = ["GETSTATE", bytes2string(requested_component)]

                    for connection in self.connections:
                        # Send it to all peers. Hopefully one or more of them
                        # will send us a StateComponent back.
                        connection.send_message(message)

                    # Remember having asked for this component
                    self.recently_requested_state_components.add(
                        requested_component)
示例#2
0
    def announce_block(self, block_hash):
        """
        Tell all of our connected peers about a block we have.
        """

        with self.lock:
            for connection in self.connections:
                # Send an INV message with the hash of the thing we have, in
                # case they want it.
                connection.send_message(["INV", bytes2string(block_hash)])
示例#3
0
    def announce_transaction(self, transaction_hash):
        """
        Tell all of our connected peers about a transaction we have.
        """

        with self.lock:
            for connection in self.connections:
                # Send a TXINV message with the hash of the thing we have, in
                # case they want it.
                connection.send_message(
                    ["TXINV", bytes2string(transaction_hash)])
    def process_inv_queue(self):
        """
        Download some blocks from the queue of blocks we know we need.

        """

        for _ in xrange(100):
            # Only ask for 100 blocks at a time.
            if len(self.inv_queue) > 0:
                # Get the bytestring hash of the next block to ask for
                block_hash = self.inv_queue.popleft()
                self.inv_queued.remove(block_hash)

                if (self.factory.peer.blockchain.needs_block(block_hash) and
                        block_hash not in self.block_queued):
                    # We need this block and don't have it in our queue to add
                    # to the blockchain, so get it. TODO: Check to see if we
                    # requested it recently.

                    self.send_message(["GETDATA", bytes2string(block_hash)])

        # Process the inv_queue again.
        self.inv_watcher = reactor.callLater(1, self.process_inv_queue)
示例#5
0
    def tick(self):
        """
        See if we have the optimal number of outgoing connections. If we have
        too few, add some.
        """

        with self.lock:

            # How many connections do we have right now?
            current_connections = len(self.connections)
            logging.info("Tick {} from localhost port {}: {} of {} "
                         "connections".format(self.tick_count, self.port,
                                              current_connections,
                                              self.optimal_connections))

            for connection in self.connections:
                logging.info(
                    "\tTo {} port {}".format(*connection.remote_address))

            logging.info("{} outgoing connections:".format(
                len(self.outgoing_hosts)))
            for host in self.outgoing_hosts:
                logging.info("\t To {}".format(host))

            logging.info("Blockchain height: {}".format(
                len(self.blockchain.hashes_by_height)))
            logging.info("Blocks known: {}".format(
                self.blockchain.get_block_count()))
            logging.info("Blocks pending: {}".format(
                len(self.blockchain.pending_callbacks)))
            logging.info("State available for verification and mining:"
                         " {}".format(self.blockchain.state_available))
            logging.info("Transactions pending: {}".format(
                len(self.blockchain.transactions)))

            # Calculate the average block time in seconds for the last few
            # blocks. It may be None, meaning we don't have enough history to
            # work it out.
            block_time = self.blockchain.get_average_block_time()
            if block_time is not None:
                logging.info(
                    "Average block time: {} seconds".format(block_time))

            logging.info("Blockchain disk usage: {} bytes".format(
                self.blockchain.get_disk_usage()))

            if self.tick_count % 60 == 0:
                # Also log some science stats every 60 ticks (by default, 1 hour)
                pybc.science.log_event("chain_height",
                                       len(self.blockchain.hashes_by_height))
                pybc.science.log_event("pending_transactions",
                                       len(self.blockchain.transactions))
                pybc.science.log_event("connections", current_connections)
                if block_time is not None:
                    # We have an average block time, so record that.
                    pybc.science.log_event("block_time", block_time)

                # We want to log how much space our databases (block store and
                # state) take.
                pybc.science.log_event("blockchain_usage",
                                       self.blockchain.get_disk_usage())
                pybc.science.log_filesize("blockchain_file_usage",
                                          self.blockchain.store_filename)

            # Request whatever state components we need, egardless if whether we
            # recently requested them, in case our download got stalled
            self.recently_requested_state_components = set()
            self.request_state_components()

            if (len(self.outgoing_hosts) < self.optimal_connections
                    and len(self.known_peers) > 0):
                # We don't have enough outgoing connections, but we do know some
                # peers.

                for i in range(
                        min(
                            self.connections_per_batch,
                            self.optimal_connections -
                            len(self.outgoing_hosts))):
                    # Try several connections in a batch.

                    for _ in range(self.optimal_connections -
                                   len(self.outgoing_hosts)):

                        # For each connection we want but don't have

                        # Find a peer we aren't connected to and connect to them
                        key = random.sample(self.known_peers, 1)[0]
                        if key.count(':'):
                            host = key.split(':')[0]
                        else:
                            host = key

                        # Try at most 100 times to find a host we aren't
                        # connected to, and which isn't trivially obviously us.
                        attempt = 1
                        while (host in self.outgoing_hosts
                               or host in self.incoming_hosts or host
                               == self.external_address) and attempt < 100:
                            # Try a new host
                            key = random.sample(self.known_peers, 1)[0]
                            if key.count(':'):
                                host = key.split(':')[0]
                            else:
                                host = key

                            # Increment attempt
                            attempt += 1

                        # TODO: This always makes two attempts at least

                        if attempt < 100:
                            # We found one!
                            # Connect to it.

                            # Get the port (and discard the last heard from
                            # time)
                            port, _ = self.known_peers[key]

                            # Connect to it.
                            self.connect(host, port)
                        else:
                            # No more things we can try connecting to
                            break

            # Throw out peers that are too old. First compile a list of their
            # hostnames.
            too_old = []

            for key, (port, last_seen) in six.iteritems(self.known_peers):
                if last_seen is None:
                    # This is a bootstrap peer. Don't remove it.
                    continue

                if time.time() - last_seen > self.peer_timeout:
                    # We haven't heard from/about this node recently enough.
                    too_old.append(key)

            # Now drop all the too old hosts
            for key in too_old:
                del self.known_peers[key]

            # Broadcast all our hosts.
            logging.info("{} known peers".format(len(self.known_peers)))
            for key, (port, last_seen) in six.iteritems(self.known_peers):
                if last_seen is None:
                    # This is a bootstrap peer, so don't announce it.
                    continue
                if key.count(':'):
                    host, _ = key.split(':')
                logging.debug("\tPeer {} port {} last seen {}".format(
                    host, port, time2string(last_seen)))
                for connection in self.connections:
                    connection.send_message(["ADDR", host, port, last_seen])

            if self.external_address is not None:
                # Broadcast ourselves, since we know our address.
                for connection in self.connections:
                    connection.send_message([
                        "ADDR", self.external_address, self.port,
                        int(time.time())
                    ])

            # Do we need to re-poll for blocks?
            if self.repoll:
                self.repoll = False
                logging.warning("Repolling due to unverifiable block.")

                # Compose just one message
                message = (["GETBLOCKS"] + [
                    bytes2string(block_hash)
                    for block_hash in self.blockchain.get_block_locator()
                ])
                for connection in self.connections:
                    connection.send_message(message)

            logging.info("Saving...")
            # Keep track of how long this takes.
            save_start = time.clock()

            if self.tick_count % 60 == 0:
                pybc.science.start_timer("sync")

            # Sync the blockchain to disk. This needs to lock the blockchain, so
            # the blockchain must never wait on the peer while holding its lock.
            # Hence the complicated deferred callback system. TODO: replace the
            # whole business with events.
            self.blockchain.sync()

            # Sync the known peers to disk
            self.known_peers.sync()

            if self.tick_count % 60 == 0:
                pybc.science.stop_timer("sync")

            # How long did it take?
            save_end = time.clock()

            logging.info("Saved to disk in {:.2} seconds".format(save_end -
                                                                 save_start))

            # Count this tick as having happened
            self.tick_count += 1

            # Tick again later
            reactor.callLater(self.tick_period, self.tick)

            logging.info("Tick complete.")
    def handle_message(self, parts):
        """
        Given a message as a list of string parts, handle it. Meant to run non-
        blocking in its own thread.

        Schedules any reply and any actions to be taken to be done in the main
        Twisted thread.
        """

        try:
            if len(parts) == 0:
                # Skip empty lines
                return

            if parts[0] == "NETWORK":
                # This is a network command, telling us the network and version
                # of the remote host. If we like them (i.e. they are equal to
                # ours), send back an acknowledgement with our network info.
                # Also start requesting peers and blocks from them.

                if (parts[1] == self.factory.peer.network and
                        int(parts[2]) == self.factory.peer.version):
                    # We like their network and version number.
                    # Send back a network OK command with our own.
                    self.send_message(["NETWORK-OK", self.factory.peer.network,
                                       self.factory.peer.version])

                    # Ask them for peers
                    self.send_message(["GETADDR"])

                    # Ask them for the blocks we need, given our list of block
                    # locator hashes.
                    self.send_message(["GETBLOCKS"] +
                                      [bytes2string(block_hash) for block_hash in
                                       self.factory.peer.blockchain.get_block_locator()])

                    # Send all the pending transactions
                    for transaction_hash, _ in \
                            self.factory.peer.blockchain.get_transactions():

                        self.send_message(["TXINV", bytes2string(
                            transaction_hash)])

                else:
                    # Nope, we don't like them.

                    # Disconnect
                    self.disconnect()

            elif parts[0] == "NETWORK-OK":
                # This is a network OK command, telling us that they want to
                # talk to us, and giving us their network and version number. If
                # we like their network and version number too, we can start
                # exchanging peer info.

                if (parts[1] == self.factory.peer.network and
                        int(parts[2]) == self.factory.peer.version):

                    # We like their network and version number. Send them a
                    # getaddr message requesting a list of peers. The next thing
                    # they give us might be something completely different, but
                    # that's OK; they ought to send some peers eventually.
                    self.send_message(["GETADDR"])

                    # Ask them for the blocks we need, given our list of block
                    # locator hashes.
                    self.send_message(["GETBLOCKS"] +
                                      [bytes2string(block_hash) for block_hash in
                                       self.factory.peer.blockchain.get_block_locator()])

                    # Send all the pending transactions
                    for transaction_hash, _ in \
                            self.factory.peer.blockchain.get_transactions():

                        self.send_message(["TXINV", bytes2string(
                            transaction_hash)])
                else:
                    # We don't like their network and version. Drop them.

                    # Disconnect
                    self.disconnect()

            elif parts[0] == "GETADDR":
                # They want a list of all known peers.
                # Send them ADDR messages, one per known peer.

                for host, port, time_seen in self.factory.peer.get_peers():
                    if time_seen is None:
                        # This is a bootstrap peer
                        continue
                    # Send the peer's host and port in an ADDR message
                    self.send_message(["ADDR", host, port, time_seen])
            elif parts[0] == "ADDR":
                # They claim that there is a peer. Tell our peer the host and port
                # and time seen.
                self.factory.peer.peer_seen(parts[1], int(parts[2]),
                                            int(parts[3]))
            elif parts[0] == "GETBLOCKS":
                # They gave us a block locator. Work out the blocks they need,
                # and send INV messages about them.

                # Decode all the hex hashes to bytestring
                block_hashes = [string2bytes(part) for part in parts[1:]]

                for needed_hash in \
                    self.factory.peer.blockchain.blocks_after_locator(
                        block_hashes):

                    # They need this hash. Send an INV message about it.
                    # TODO: consolidate and limit these.
                    self.send_message(["INV", bytes2string(needed_hash)])
            elif parts[0] == "INV":
                # They have a block. If we don't have it, ask for it.

                # TODO: allow advertising multiple things at once.

                # Decode the hash they have
                block_hash = string2bytes(parts[1])

                if (self.factory.peer.blockchain.needs_block(block_hash) and
                    block_hash not in self.inv_queued and
                        block_hash not in self.block_queued):

                    # We need this block, and it isn't in any queues.
                    self.inv_queue.append(block_hash)
                    self.inv_queued.add(block_hash)

            elif parts[0] == "GETDATA":
                # They want the data for a block. Send it to them if we have
                # it.

                # Decode the hash they want
                block_hash = string2bytes(parts[1])

                if self.factory.peer.blockchain.has_block(block_hash):
                    # Get the block to send
                    block = self.factory.peer.blockchain.get_block(block_hash)

                    # Send them the block. TODO: This encoding is terribly
                    # inefficient, but we can't send it as binary without them
                    # switching out of line mode, and they don't know to do that
                    # because messages queue.
                    self.send_message(["BLOCK", bytes2string(block.to_bytes())])
                else:
                    logging.error("Can't send missing block: '{}'".format(
                        bytes2string(block_hash)))

            elif parts[0] == "BLOCK":
                # They have sent us a block. Add it if it is valid.

                # Decode the block bytes
                block_bytes = string2bytes(parts[1])

                # Make a Block object
                block = Block.from_bytes(block_bytes)

                if block.block_hash() not in self.block_queued:
                    # Queue it if it's not already queued. Because of the set it
                    # can only be queued once.
                    self.block_queue.append(block)
                    self.block_queued.add(block.block_hash())

            elif parts[0] == "TXINV":
                # They have sent us a hash of a transaction that they have. If
                # we don't have it, we should get it and pass it on.

                # Decode the hash they have
                transaction_hash = string2bytes(parts[1])

                if not self.factory.peer.blockchain.has_transaction(
                        transaction_hash):

                    # We need this transaction!
                    self.send_message(["GETTX", parts[1]])
            elif parts[0] == "GETTX":
                # They want a transaction from our blockchain.

                # Decode the hash they want
                transaction_hash = string2bytes(parts[1])

                if self.factory.peer.blockchain.has_transaction(transaction_hash):
                    # Get the transaction to send
                    transaction = self.factory.peer.blockchain.get_transaction(
                        transaction_hash)

                    if transaction is not None:
                        # We have it (still). Send them the block transaction.
                        self.send_message(["TX", bytes2string(transaction)])
                    else:
                        logging.error("Lost transaction: '{}'".format(
                            bytes2string(transaction_hash)))
                else:
                    logging.error("Can't send missing transaction: '{}'".format(
                        bytes2string(transaction_hash)))
            elif parts[0] == "TX":
                # They have sent us a transaction. Add it to our blockchain.

                # Decode the transaction bytes
                transaction_bytes = string2bytes(parts[1])

                logging.debug("Incoming transaction.")

                if self.factory.peer.blockchain.transaction_valid_for_relay(
                        transaction_bytes):

                    # This is a legal transaction to accept from a peer (not
                    # something like a block reward).
                    logging.debug("Transaction acceptable from peer.")

                    # Give it to the blockchain as bytes. The blockchain can
                    # determine whether to forward it on or not and call the
                    # callback with transaction hash and transaction status
                    # (True or False).
                    self.factory.peer.send_transaction(
                        transaction_bytes)

            elif parts[0] == "GETSTATE":
                # They're asking us for a StateComponent from our State, with
                # the given hash. We just serve these like a dumb server.

                # We assume this is under our current State. If it's not, we'll
                # return a nort found message and make them start over again
                # from the top.

                # What StateComponent do they want?
                state_hash = string2bytes(parts[1])

                # Go get it
                state_component = \
                    self.factory.peer.blockchain.get_state_component(state_hash)

                if state_component is None:
                    # Complain we don't have that. They need to start over from
                    # the state hash in the latest block.
                    logging.warning("Peer requested nonexistent StateComponent "
                                    "{} vs. root hash {}".format(parts[1],
                                                                 bytes2string(
                                        self.factory.peer.blockchain.get_state_hash())))
                    self.send_message(["NOSTATE", bytes2string(state_hash)])
                else:

                    logging.debug("Fulfilling request for StateComponent "
                                  "{}".format(parts[1]))

                    # Pack up the StateComponent's data as bytes and send it
                    # along. They'll know which one it was when they hash it.
                    self.send_message(["STATE",
                                       bytes2string(state_component.data)])

            elif parts[0] == "NOSTATE":
                # They're saying they don't have a StateComponent we asked for.
                logging.warning("Peer says they do not have StateComponent: "
                                "{}".format(parts[1]))

            elif parts[0] == "STATE":
                # They're sending us a StateComponent we probably asked for.

                # Unpack the data
                component_bytestring = string2bytes(parts[1])

                # Give it to the Blockchain. It knows how to handle these
                # things. TODO: Maybe put a queue here, since this potentially
                # does the whole state rebuilding operation.
                self.factory.peer.blockchain.add_state_component(
                    component_bytestring)

                # Tell the peer to request more StateComponents.
                # TODO: This is going to blow up.
                self.factory.peer.request_state_components()

            elif parts[0] == "ERROR":
                # The remote peer didn't like something.
                # Print debugging output.
                logging.error("Error from remote peer: {}".format(" ".join(
                    parts[1:])))

            else:
                # They're trying to send a command we don't know about.
                # Complain.
                logging.error("Remote host tried unknown command {}".format(
                    parts[1]))
                self.send_message(["ERROR", parts[0]])

            if not self.incoming:
                # We processed a valid message from a peer we connected out to.
                # Record that we've seen them for anouncement purposes.
                self.factory.peer.peer_seen(self.remote_address[0],
                                            self.remote_address[1], int(time.time()))

        except BaseException:
            logging.error("Exception processing command: {}".format(parts))
            logging.error(traceback.format_exc())

            # Disconnect from people who send us garbage
            self.disconnect()