#!/usr/bin/env python3

import random # For simulation, not cryptography!
import math
import sys
import logging

import network
import dirauth
import relay

import nacl.hash

class VanillaCreatedExtendedHandler:
    """A handler for VanillaCreatedCircuitCell and
    VanillaExtendedCircuitCell cells."""

    def __init__(self, channelmgr, ntor, expecteddesc):
        self.channelmgr = channelmgr
        self.ntor = ntor
        self.expecteddesc = expecteddesc
        self.onionkey = expecteddesc.descdict['onionkey']
        self.idkey = expecteddesc.descdict['idkey']

    def received_cell(self, circhandler, cell):
        secret = self.ntor.verify(cell.ntor_reply, self.onionkey, self.idkey)
        enckey = nacl.hash.sha256(secret + b'upstream')
        deckey = nacl.hash.sha256(secret + b'downstream')
        circhandler.add_crypt_layer(enckey, deckey)
        if len(circhandler.circuit_descs) == 0:
            # This was a VanillaCreatedCircuitCell
            circhandler.replace_celltype_handler(
                    relay.VanillaCreatedCircuitCell, None)
        else:
            # This was a VanillaExtendedCircuitCell
            circhandler.replace_celltype_handler(
                    relay.VanillaExtendedCircuitCell, None)
        circhandler.circuit_descs.append(self.expecteddesc)

        # Are we done building the circuit?
        if len(circhandler.circuit_descs) == 3:
            # Yes!
            return

        nexthop = None
        while nexthop is None:
            nexthop = self.channelmgr.relaypicker.pick_weighted_relay()
            if nexthop.descdict['addr'] in \
                    [ desc.descdict['addr'] \
                        for desc in circhandler.circuit_descs ]:
                nexthop = None

        # Construct the VanillaExtendCircuitCell
        ntor = relay.NTor(self.channelmgr.perfstats)
        ntor_request = ntor.request()
        circextendmsg = relay.VanillaExtendCircuitCell(
                nexthop.descdict['addr'], ntor_request)

        # Set up the reply handler
        circhandler.replace_celltype_handler(
                relay.VanillaExtendedCircuitCell,
                VanillaCreatedExtendedHandler(self.channelmgr, ntor, nexthop))

        # Send the cell
        circhandler.send_cell(circextendmsg)

class TelescopingCreatedHandler:
    """A handler for TelescopingCreatedCircuitCell cells; this will only always
    communicate with the client's guard."""

    def __init__(self, channelmgr, ntor):
        self.channelmgr = channelmgr
        self.ntor = ntor
        if type(self.channelmgr.guard) is dirauth.RelayDescriptor:
            guardd = self.channelmgr.guard.descdict
        else:
            guardd = self.channelmgr.guard.snipdict
        self.onionkey = guardd["onionkey"]
        self.idkey = guardd["idkey"]

    def received_cell(self, circhandler, cell):
        logging.debug("Received cell in TelescopingCreatedHandler")

        secret = self.ntor.verify(cell.ntor_reply, self.onionkey, self.idkey)
        enckey = nacl.hash.sha256(secret + b'upstream')
        deckey = nacl.hash.sha256(secret + b'downstream')
        circhandler.add_crypt_layer(enckey, deckey)

        circhandler.replace_celltype_handler(relay.TelescopingCreatedCircuitCell, None)

        circhandler.circuit_descs.append(self.channelmgr.guard)

        nexthopidx = None
        while nexthopidx is None:
            nexthopidx = self.channelmgr.relaypicker.pick_weighted_relay_index()
            #print("WARNING: Unimplemented! Need to check if this idx is in the list of circhandlers idxs")
            # TODO verify we don't need to do the above

        # Construct the TelescopingExtendCircuitCell
        ntor = relay.NTor(self.channelmgr.perfstats)
        ntor_request = ntor.request()
        circextendmsg = relay.TelescopingExtendCircuitCell(
                nexthopidx, ntor_request)

        # Set up the reply handler
        circhandler.replace_celltype_handler(
                relay.TelescopingExtendedCircuitCell,
                TelescopingExtendedHandler(self.channelmgr, ntor))

        # Send the cell
        circhandler.send_cell(circextendmsg)


class TelescopingExtendedHandler:
    """A handler for TelescopingExtendedCircuitCell cells."""

    def __init__(self, channelmgr, ntor):
        self.channelmgr = channelmgr
        self.ntor = ntor

    def received_cell(self, circhandler, cell):
        logging.debug("Received cell in TelescopingExtendedHandler")

        # Validate the SNIP
        dirauth.SNIP.verify(cell.snip, self.channelmgr.consensus,
                network.thenetwork.dirauthkeys()[0],
                self.channelmgr.perfstats)

        onionkey = cell.snip.snipdict['onionkey']
        idkey = cell.snip.snipdict['idkey']

        secret = self.ntor.verify(cell.ntor_reply, onionkey, idkey)
        enckey = nacl.hash.sha256(secret + b'upstream')
        deckey = nacl.hash.sha256(secret + b'downstream')
        circhandler.add_crypt_layer(enckey, deckey)

        circhandler.replace_celltype_handler(
                    relay.TelescopingExtendedCircuitCell, None)
        circhandler.circuit_descs.append(cell.snip)

        # Are we done building the circuit?
        #logging.warning("we may need another circhandler structure for snips")
        if len(circhandler.circuit_descs) == 3:
            # Yes!
            return

        nexthopidx = self.channelmgr.relaypicker.pick_weighted_relay_index()

        # Construct the VanillaExtendCircuitCell
        ntor = relay.NTor(self.channelmgr.perfstats)
        ntor_request = ntor.request()
        circextendmsg = relay.TelescopingExtendCircuitCell(
                nexthopidx, ntor_request)

        # Set up the reply handler
        circhandler.replace_celltype_handler(
                relay.TelescopingExtendedCircuitCell,
                TelescopingExtendedHandler(self.channelmgr, ntor))

        # Send the cell
        circhandler.send_cell(circextendmsg)

class SinglePassCreatedHandler:
    """A handler for SinglePassCreatedCircuitCell cells."""

    def __init__(self, channelmgr, ntor, client_key):
        self.channelmgr = channelmgr
        self.ntor = ntor
        self.client_key = client_key

    def received_cell(self, circhandler, cell):
        # We should only get one relay.SinglePassCreatedCircuitCell per
        # circuit
        circhandler.replace_celltype_handler(relay.SinglePassCreatedCircuitCell, None)

        # The circuit always starts with the guard
        circhandler.circuit_descs.append(self.channelmgr.guard)

        # Process each layer of the message
        blinding_keys = []
        while cell is not None:
            lasthop = circhandler.circuit_descs[-1]
            if type(lasthop) is dirauth.RelayDescriptor:
                lasthopd = lasthop.descdict
            else:
                lasthopd = lasthop.snipdict
            onionkey = lasthopd["onionkey"]
            idkey = lasthopd["idkey"]
            pathselkey = lasthopd["pathselkey"]
            if cell.enc is None:
                secret = self.ntor.verify(cell.ntor_reply, onionkey, idkey)
                enckey = nacl.hash.sha256(secret + b'upstream')
                deckey = nacl.hash.sha256(secret + b'downstream')
                circhandler.add_crypt_layer(enckey, deckey)
                cell = None
            else:
                secret = self.ntor.verify(cell.ntor_reply, onionkey,
                        idkey, b'circuit')
                enckey = nacl.hash.sha256(secret + b'upstream')
                deckey = nacl.hash.sha256(secret + b'downstream')
                createdkey = nacl.hash.sha256(secret + b'created')
                circhandler.add_crypt_layer(enckey, deckey)
                (snip, vrfout, nextlayer) = cell.enc.decrypt(createdkey)
                # Check the signature on the SNIP
                dirauth.SNIP.verify(snip, self.channelmgr.consensus,
                        network.thenetwork.dirauthkeys()[0],
                        self.channelmgr.perfstats)
                # Compute the index, check the VRF, ensure the SNIP is
                # the correct one
                pathsel_rand, next_blindkey = relay.Sphinx.client(
                        self.client_key, blinding_keys,
                        onionkey, b'pathsel',
                        nextlayer is None, self.channelmgr.perfstats)
                if nextlayer is not None:
                    blinding_keys.append(next_blindkey)

                try:
                    index = int.from_bytes(relay.VRF.check_output(pathselkey,
                                        pathsel_rand, vrfout,
                                        self.channelmgr.perfstats)[:4],
                                    'big', signed=False)
                except ValueError as e:
                    circhandler.close()
                    raise ValueError(str(e.args) + str(lasthopd))

                indexrange = snip.snipdict["range"]
                if index < indexrange[0] or index >= indexrange[1]:
                    logging.error("Incorrect SNIP received")

                circhandler.circuit_descs.append(snip)
                cell = nextlayer


class ClientChannelManager(relay.ChannelManager):
    """The subclass of ChannelManager for clients."""

    def __init__(self, myaddr, dirauthaddrs, perfstats):
        super().__init__(myaddr, dirauthaddrs, perfstats)
        self.guardaddr = None
        self.guard = None

    def get_consensus_from_fallbackrelay(self):
        """Download a fresh consensus from a random fallbackrelay."""
        fb = network.thenetwork.getfallbackrelay()
        logging.debug("Chose fallback %s", fb)
        if network.thenetwork.womode == network.WOMode.VANILLA:
            if self.consensus is not None and \
                    len(self.consensus.consdict['relays']) > 0:
                self.send_msg(relay.RelayGetConsensusDiffMsg(), fb.netaddr)
            else:
                self.send_msg(relay.RelayGetConsensusMsg(), fb.netaddr)
        else:
            self.send_msg(relay.RelayGetConsensusMsg(), fb.netaddr)

    def ensure_guard_vanilla(self):
        """Ensure that we have a channel to a guard (Vanilla Onion
        Routing version)."""
        while True:
            if self.guardaddr is None:
                # Pick a guard from the consensus
                self.guard = self.relaypicker.pick_weighted_relay()
                self.guardaddr = self.guard.descdict['addr']

            self.test_guard_connection()

            if self.guardaddr is not None:
                break

        logging.debug('chose guard=%s', self.guardaddr)

    def test_guard_connection(self):
            # Connect to the guard
            try:
                self.get_channel_to(self.guardaddr)
            except network.NetNoServer:
                # Our guard is gone
                self.guardaddr = None
                self.guard = None

    def ensure_guard_walking_onions(self):
        """Ensure we have a channel to a guard (Walking Onions version).

        For the first implementation, we assume an out-of-band mechanism
        that just simply hands us a guard; we don't count the number of
        operations or bandwidth as this operation in practice occurs
        infrequently."""

        while True:
            if self.guardaddr is None:
                #randomly sample a guard
                #logging.warning("Unimplemented! guard should be selected from any relays.")
                self.guard = self.relaypicker.pick_weighted_relay()
                # here, we have a SNIP instead of a relay descriptor
                self.guardaddr = self.guard.snipdict['addr']

            self.test_guard_connection()

            if self.guardaddr is not None:
                break

        # Ensure we have the current descriptor for the guard
        # Note that self.guard may be a RelayDescriptor or a SNIP,
        # depending on how we got it
        if type(self.guard) is dirauth.RelayDescriptor:
            guardepoch = self.guard.descdict["epoch"]
        else:
            guardepoch = self.guard.snipdict["epoch"]
        if guardepoch != network.thenetwork.getepoch():
            guardchannel = self.get_channel_to(self.guardaddr)
            guardchannel.send_msg(relay.RelayGetDescMsg())

        logging.debug('chose guard=%s', self.guardaddr)

    def ensure_guard(self):
        """Ensure that we have a channel to a guard."""
        if network.thenetwork.womode == network.WOMode.VANILLA:
            self.ensure_guard_vanilla()
            return
        # At this point, we are either in Telescoping or Single-Pass mode

        self.ensure_guard_walking_onions()

    def new_circuit_vanilla(self):
        """Create a new circuit from this client. (Vanilla Onion Routing
        version)"""

        # Get our channel to the guard
        guardchannel = self.get_channel_to(self.guardaddr)

        # Allocate a new circuit id on it
        circid, circhandler = guardchannel.new_circuit()

        # Construct the VanillaCreateCircuitMsg
        ntor = relay.NTor(self.perfstats)
        ntor_request = ntor.request()
        circcreatemsg = relay.VanillaCreateCircuitMsg(circid, ntor_request)

        # Set up the reply handler
        circhandler.replace_celltype_handler(
                relay.VanillaCreatedCircuitCell,
                VanillaCreatedExtendedHandler(self, ntor, self.guard))

        # Send the message
        guardchannel.send_msg(circcreatemsg)

        return circhandler

    def new_circuit_telescoping(self):
        """Create a new circuit from this client (Telescoping Walking Onions
        version). If an error occurs and the circuit is deleted from the guard
        channel, return None, otherwise, return the circuit handler."""

        # Get our channel to the guard
        guardchannel = self.get_channel_to(self.guardaddr)

        # Allocate a new circuit id on it
        circid, circhandler = guardchannel.new_circuit()

        # Construct the TelescopingCreateCircuitMsg
        ntor = relay.NTor(self.perfstats)
        ntor_request = ntor.request()
        circcreatemsg = relay.TelescopingCreateCircuitMsg(circid, ntor_request)

        # Set up the reply handler
        circhandler.replace_celltype_handler(
                relay.TelescopingCreatedCircuitCell,
                TelescopingCreatedHandler(self, ntor))

        # Send the message
        guardchannel.send_msg(circcreatemsg)

        # Check to make sure the circuit is open before sending it- if there
        # was an error when establishing it, the circuit could already be
        # closed.
        if not guardchannel.is_circuit_open(circid):
            logging.debug("Circuit was already closed, not sending bytes. circid: " + str(circid))
            return None

        guard = circhandler.circuit_descs[0]
        if type(guard) is dirauth.RelayDescriptor:
            guardd = guard.descdict
        else:
            guardd = guard.snipdict
        if guardd["addr"] == circhandler.circuit_descs[2].snipdict["addr"]:
            logging.debug("circuit in a loop")
            circhandler.close()
            circhandler = None

        return circhandler

    def new_circuit_singlepass(self):
        """Create a new circuit from this client (Single-Pass Walking Onions
        version). If an error occurs and the circuit is deleted from the guard
        channel, return None, otherwise, return the circuit handler."""

        # Get our channel to the guard
        guardchannel = self.get_channel_to(self.guardaddr)

        # Allocate a new circuit id on it
        circid, circhandler = guardchannel.new_circuit()

        # first, create the path-selection key used for Sphinx
        client_pathsel_key = nacl.public.PrivateKey.generate()
        self.perfstats.keygens += 1

        # Construct the SinglePassCreateCircuitMsg
        ntor = relay.NTor(self.perfstats)
        ntor_request = ntor.request()
        circcreatemsg = relay.SinglePassCreateCircuitMsg(circid, ntor_request,
                client_pathsel_key.public_key)

        # Set up the reply handler
        circhandler.replace_celltype_handler(
                relay.SinglePassCreatedCircuitCell,
                SinglePassCreatedHandler(self, ntor, client_pathsel_key))

        # Send the message
        guardchannel.send_msg(circcreatemsg)

        # Check to make sure the circuit is open before sending it- if there
        # was an error when establishing it, the circuit could already be
        # closed.
        if not guardchannel.is_circuit_open(circid):
            logging.debug("Circuit was already closed, not sending bytes. circid: " + str(circid))
            return None

        # In Single-Pass Walking Onions, we need to check whether the
        # circuit got into a loop (guard equals exit); each node will
        # refuse to extend to itself, so this is the only possible loop
        # in a circuit of length 3
        guard = circhandler.circuit_descs[0]
        if type(guard) is dirauth.RelayDescriptor:
            guardd = guard.descdict
        else:
            guardd = guard.snipdict
        if guardd["addr"] == circhandler.circuit_descs[2].snipdict["addr"]:
            logging.debug("circuit in a loop")
            circhandler.close()
            circhandler = None

        return circhandler

    def new_circuit(self):
        """Create a new circuit from this client."""
        circhandler = None

        # If an error occured, circhandler will still be None, so we should
        # try again.
        while circhandler is None:
            if network.thenetwork.womode == network.WOMode.VANILLA:
                circhandler = self.new_circuit_vanilla()
            elif network.thenetwork.womode == network.WOMode.TELESCOPING:
                circhandler = self.new_circuit_telescoping()
            elif network.thenetwork.womode == network.WOMode.SINGLEPASS:
                circhandler = self.new_circuit_singlepass()

        return circhandler

    def received_msg(self, msg, peeraddr, channel):
        """Callback when a NetMsg not specific to a circuit is
        received."""
        logging.debug("Client %s received msg %s from %s" % (self.myaddr, msg, peeraddr))
        if isinstance(msg, relay.RelayConsensusMsg) or \
                isinstance(msg, relay.RelayConsensusDiffMsg):
            self.relaypicker = dirauth.Consensus.verify(msg.consensus,
                    network.thenetwork.dirauthkeys(), self.perfstats)
            self.consensus = msg.consensus
        elif isinstance(msg, relay.RelayDescMsg):
            dirauth.RelayDescriptor.verify(msg.desc, self.perfstats)
            self.guard = msg.desc
        else:
            return super().received_msg(msg, peeraddr, channel)

    def received_cell(self, circid, cell, peeraddr, channel):
        """Callback with a circuit-specific cell is received."""
        logging.debug("Client %s received cell on circ %d: %s from %s" % (self.myaddr, circid, cell, peeraddr))
        if isinstance(msg, relay.CloseCell):
            logging.debug("Log: Client received close cell; closing circuit")
            # TODO close cell
        return super().received_cell(circid, cell, peeraddr, channel)


class Client:
    """A class representing a Tor client."""

    def __init__(self, dirauthaddrs):
        # Get a network address for client-side use only (do not bind it
        # to the network)
        self.netaddr = network.NetAddr()
        self.perfstats = network.PerfStats(network.EntType.CLIENT)
        self.perfstats.name = "Client at %s" % self.netaddr
        self.perfstats.is_bootstrapping = True
        self.channelmgr = ClientChannelManager(self.netaddr, dirauthaddrs,
                self.perfstats)

        # Register for epoch tick notifications
        network.thenetwork.wantepochticks(self, True)

    def terminate(self):
        """Quit this client."""

        # Stop listening for epoch ticks
        network.thenetwork.wantepochticks(self, False)

        # Close relay connections
        self.channelmgr.terminate()

    def get_consensus(self):
        """Fetch a new consensus."""

        # We're going to want a new consensus from our guard.  In order
        # to get that, we'll need a channel to our guard.  In order to
        # get that, we'll need a guard address.  In order to get that,
        # we'll need a consensus (uh, oh; in that case, fetch the
        # consensus from a fallback relay).


        guardaddr = self.channelmgr.guardaddr
        guardchannel = None
        if guardaddr is not None:
            try:
                guardchannel = self.channelmgr.get_channel_to(guardaddr)
            except network.NetNoServer:
                guardaddr = None

        if guardchannel is None:
            logging.debug("In bootstrapping mode")
            self.channelmgr.get_consensus_from_fallbackrelay()
            logging.debug('client consensus=%s', self.channelmgr.consensus)
            return

        if network.thenetwork.womode == network.WOMode.VANILLA:
            if self.channelmgr.consensus is not None and len(self.channelmgr.consensus.consdict['relays']) > 0:
                guardchannel.send_msg(relay.RelayGetConsensusDiffMsg())
                logging.debug('got consensus diff, client consensus=%s', self.channelmgr.consensus)
                return

        # At this point, we are in one of the following scenarios:
            # 1. This is a walking onions protocol, and the client fetches the
            #    complete consensus each epoch
            # 2. This is Vanilla Onion Routing and the client doesn't have a
            #    consensus and needs to bootstrap it.

        guardchannel.send_msg(relay.RelayGetConsensusMsg())
        logging.debug('client consensus=%s', self.channelmgr.consensus)

    def newepoch(self, epoch):
        """Callback that fires at the start of each epoch"""

        # We'll need a new consensus
        self.get_consensus()

        # If we don't have a guard, pick one and make a channel to it
        self.channelmgr.ensure_guard()

if __name__ == '__main__':
    perfstats = network.PerfStats(network.EntType.NONE)
    totsent = 0
    totrecv = 0
    dirasent = 0
    dirarecv = 0
    relaysent = 0
    relayrecv = 0
    clisent = 0
    clirecv = 0

    if len(sys.argv) < 3:
        print("Must pass in network mode and snip auth mode!")
        print("Network options are vanilla, telescoping, or single-pass.")
        print("SNIP auth options are merkle or threshold.")
        sys.exit(0)

    logging.basicConfig(level=logging.DEBUG)

    womode = network.WOMode[sys.argv[1].upper()]
    snipauthmode = network.SNIPAuthMode[sys.argv[2].upper()]
    network.thenetwork.set_wo_style(womode, snipauthmode)

    # Initialize the (non-cryptographic) random seed
    random.seed(1)

    # Start some dirauths
    numdirauths = 9
    dirauthaddrs = []
    dirauths = []
    for i in range(numdirauths):
        dira = dirauth.DirAuth(i, numdirauths)
        dirauths.append(dira)
        dirauthaddrs.append(dira.netaddr)

    # Start some relays
    numrelays = 100
    relays = []
    for i in range(numrelays):
        # Relay bandwidths (at least the ones fast enough to get used)
        # in the live Tor network (as of Dec 2019) are well approximated
        # by (200000-(200000-25000)/3*log10(x)) where x is a
        # uniform integer in [1,2500]
        x = random.randint(1,2500)
        bw = int(200000-(200000-25000)/3*math.log10(x))
        relays.append(relay.Relay(dirauthaddrs, bw, 0))

    # The fallback relays are a hardcoded list of about 5% of the
    # relays, used by clients for bootstrapping
    numfallbackrelays = int(numrelays * 0.05) + 1
    fallbackrelays = random.sample(relays, numfallbackrelays)
    for r in fallbackrelays:
        r.set_is_fallbackrelay()
    network.thenetwork.setfallbackrelays(fallbackrelays)

    # Tick the epoch
    network.thenetwork.nextepoch()

    dirauth.Consensus.verify(dirauth.DirAuth.consensus, network.thenetwork.dirauthkeys(), perfstats)

    print('ticked; epoch=', network.thenetwork.getepoch())

    relays[3].channelmgr.send_msg(relay.RelayRandomHopMsg(30), relays[5].netaddr)

    # See what channels exist and do a consistency check
    for r in relays:
        print("%s: %s" % (r.netaddr, [ str(k) for k in r.channelmgr.channels.keys()]))
        raddr = r.netaddr
        for ad, ch in r.channelmgr.channels.items():
            if ch.peer.channelmgr.myaddr != ad:
                print('address mismatch:', raddr, ad, ch.peer.channelmgr.myaddr)

            if ch.peer.channelmgr.channels[raddr].peer is not ch:
                print('asymmetry:', raddr, ad, ch, ch.peer.channelmgr.channels[raddr].peer)

    # Start some clients
    numclients = 1
    clients = []
    for i in range(numclients):
        clients.append(Client(dirauthaddrs))

    # Tick the epoch
    network.thenetwork.nextepoch()

    # See what channels exist and do a consistency check
    for c in clients:
        print("%s: %s" % (c.netaddr, [ str(k) for k in c.channelmgr.channels.keys()]))
        caddr = c.netaddr
        for ad, ch in c.channelmgr.channels.items():
            if ch.peer.channelmgr.myaddr != ad:
                print('address mismatch:', caddr, ad, ch.peer.channelmgr.myaddr)

            if ch.peer.channelmgr.channels[caddr].peer is not ch:
                print('asymmetry:', caddr, ad, ch, ch.peer.channelmgr.channels[caddr].peer)

    # Pick a bunch of bw-weighted random relays and look at the
    # distribution
    for i in range(100):
        r = relays[0].channelmgr.relaypicker.pick_weighted_relay()
        if network.thenetwork.womode == network.WOMode.VANILLA:
            print("relay",r.descdict["addr"])
        else:
            print("relay",r.snipdict["addr"])

    relays[3].terminate()
    relaysent += relays[3].perfstats.bytes_sent
    relayrecv += relays[3].perfstats.bytes_received
    del relays[3]

    # Tick the epoch
    network.thenetwork.nextepoch()

    circs = []
    for i in range(20):
        circ = clients[0].channelmgr.new_circuit()
        if circ is None:
            sys.exit("ERR: Client unable to create circuits")
        circs.append(circ)
        circ.send_cell(relay.StringCell("hello world circuit %d" % i))

    # Tick the epoch
    network.thenetwork.nextepoch()

    # See what channels exist and do a consistency check
    for r in relays:
        print("%s: %s" % (r.netaddr, [ str(k) + str([ck for ck in r.channelmgr.channels[k].circuithandlers.keys()]) for k in r.channelmgr.channels.keys()]))
        raddr = r.netaddr
        for ad, ch in r.channelmgr.channels.items():
            if ch.peer.channelmgr.myaddr != ad:
                print('address mismatch:', raddr, ad, ch.peer.channelmgr.myaddr)

            if ch.peer.channelmgr.channels[raddr].peer is not ch:
                print('asymmetry:', raddr, ad, ch, ch.peer.channelmgr.channels[raddr].peer)

    # See what channels exist and do a consistency check
    for c in clients:
        print("%s: %s" % (c.netaddr, [ str(k) + str([ck for ck in c.channelmgr.channels[k].circuithandlers.keys()]) for k in c.channelmgr.channels.keys()]))
        caddr = c.netaddr
        for ad, ch in c.channelmgr.channels.items():
            if ch.peer.channelmgr.myaddr != ad:
                print('address mismatch:', caddr, ad, ch.peer.channelmgr.myaddr)

            if ch.peer.channelmgr.channels[caddr].peer is not ch:
                print('asymmetry:', caddr, ad, ch, ch.peer.channelmgr.channels[caddr].peer)

            if ch.circuithandlers.keys() != \
                    ch.peer.channelmgr.channels[caddr].circuithandlers.keys():
                print('circuit asymmetry:', caddr, ad, ch.peer.channelmgr.myaddr)

    for c in circs:
        c.close()

    for d in dirauths:
        print(d.perfstats)
        dirasent += d.perfstats.bytes_sent
        dirarecv += d.perfstats.bytes_received
    print("DirAuths sent=%s recv=%s" % (dirasent, dirarecv))
    totsent += dirasent
    totrecv += dirarecv
    for r in relays:
        print(r.perfstats)
        relaysent += r.perfstats.bytes_sent
        relayrecv += r.perfstats.bytes_received
    print("Relays sent=%s recv=%s" % (relaysent, relayrecv))
    totsent += relaysent
    totrecv += relayrecv
    for c in clients:
        print(c.perfstats)
        clisent += c.perfstats.bytes_sent
        clirecv += c.perfstats.bytes_received
    print("Client sent=%s recv=%s" % (clisent, clirecv))
    totsent += clisent
    totrecv += clirecv

    print("Total sent=%s recv=%s" % (totsent, totrecv))
