示例#1
0
class ConsoleTools(object):
    def __init__(self, raiden_service, discovery, settle_timeout, reveal_timeout):
        self._chain = raiden_service.chain
        self._raiden = raiden_service
        self._api = RaidenAPI(raiden_service)
        self._discovery = discovery
        self.settle_timeout = settle_timeout
        self.reveal_timeout = reveal_timeout
        self.deposit = self._api.deposit

    def create_token(
            self,
            initial_alloc=10 ** 6,
            name='raidentester',
            symbol='RDT',
            decimals=2,
            timeout=60,
            gasprice=GAS_PRICE,
            auto_register=True):
        """ Create a proxy for a new HumanStandardToken (ERC20), that is
        initialized with Args(below).
        Per default it will be registered with 'raiden'.

        Args:
            initial_alloc (int): amount of initial tokens.
            name (str): human readable token name.
            symbol (str): token shorthand symbol.
            decimals (int): decimal places.
            timeout (int): timeout in seconds for creation.
            gasprice (int): gasprice for the creation transaction.
            auto_register (boolean): if True(default), automatically register
                the token with raiden.

        Returns:
            token_address_hex: the hex encoded address of the new token/token.
        """
        contract_path = get_contract_path('HumanStandardToken.sol')
        # Deploy a new ERC20 token
        token_proxy = self._chain.client.deploy_solidity_contract(
            self._raiden.address, 'HumanStandardToken',
            compile_file(contract_path),
            dict(),
            (initial_alloc, name, decimals, symbol),
            contract_path=contract_path,
            gasprice=gasprice,
            timeout=timeout)
        token_address_hex = hexlify(token_proxy.address)
        if auto_register:
            self.register_token(token_address_hex)
        print("Successfully created {}the token '{}'.".format(
            'and registered ' if auto_register else ' ',
            name
        ))
        return token_address_hex

    def register_token(self, token_address_hex):
        """ Register a token with the raiden token manager.

        Args:
            token_address_hex (string): a hex encoded token address.

        Returns:
            channel_manager: the channel_manager contract_proxy.
        """
        # Add the ERC20 token to the raiden registry
        token_address = safe_address_decode(token_address_hex)
        self._raiden.default_registry.add_token(token_address)

        # Obtain the channel manager for the token
        channel_manager = self._raiden.default_registry.manager_by_token(token_address)

        # Register the channel manager with the raiden registry
        self._raiden.register_channel_manager(channel_manager.address)
        return channel_manager

    def open_channel_with_funding(
            self,
            token_address_hex,
            peer_address_hex,
            amount,
            settle_timeout=None,
            reveal_timeout=None):
        """ Convenience method to open a channel.

        Args:
            token_address_hex (str): hex encoded address of the token for the channel.
            peer_address_hex (str): hex encoded address of the channel peer.
            amount (int): amount of initial funding of the channel.
            settle_timeout (int): amount of blocks for the settle time (if None use app defaults).
            reveal_timeout (int): amount of blocks for the reveal time (if None use app defaults).

        Return:
            netting_channel: the (newly opened) netting channel object.
        """
        # Check, if peer is discoverable
        peer_address = safe_address_decode(peer_address_hex)
        token_address = safe_address_decode(token_address_hex)
        try:
            self._discovery.get(peer_address)
        except KeyError:
            print('Error: peer {} not found in discovery'.format(peer_address_hex))
            return

        self._api.open(
            token_address,
            peer_address,
            settle_timeout=settle_timeout,
            reveal_timeout=reveal_timeout,
        )

        return self._api.deposit(token_address, peer_address, amount)

    def channel_stats_for(self, token_address_hex, peer_address_hex, pretty=False):
        """ Collect information about sent and received transfers
        between yourself and your peer for the given token.

        Args:
            token_address_hex (string): hex encoded address of the token
            peer_address_hex (string): hex encoded address of the peer
            pretty (boolean): if True, print a json representation instead of returning a dict

        Returns:
            stats (dict): collected stats for the channel or None if pretty

        """
        peer_address = safe_address_decode(peer_address_hex)
        token_address = safe_address_decode(token_address_hex)

        # Get the token
        token = self._chain.token(token_address)

        # Obtain the token manager
        graph = self._raiden.token_to_channelgraph[token_address]
        assert graph

        # Get the channel
        channel = graph.partneraddress_to_channel[peer_address]
        assert channel

        # Collect data
        stats = dict(
            transfers=dict(
                received=[t.transferred_amount for t in channel.received_transfers],
                sent=[t.transferred_amount for t in channel.sent_transfers],
            ),
            channel=(channel
                     if not pretty
                     else hexlify(channel.external_state.netting_channel.address)),
            lifecycle=dict(
                opened_at=channel.external_state.opened_block or 'not yet',
                can_transfer=channel.can_transfer,
                closed_at=channel.external_state.closed_block or 'not yet',
                settled_at=channel.external_state.settled_block or 'not yet',
            ),
            funding=channel.external_state.netting_channel.detail(),
            token=dict(
                our_balance=token.balance_of(self._raiden.address),
                partner_balance=token.balance_of(peer_address),
                name=token.proxy.name(),
                symbol=token.proxy.symbol(),
            ),
        )
        stats['funding']['our_address'] = hexlify(stats['funding']['our_address'])
        stats['funding']['partner_address'] = hexlify(stats['funding']['partner_address'])
        if not pretty:
            return stats
        else:
            print(json.dumps(stats, indent=2, sort_keys=True))

    def show_events_for(self, token_address_hex, peer_address_hex):
        """ Find all EVM-EventLogs for a channel.

        Args:
            token_address_hex (string): hex encoded address of the token
            peer_address_hex (string): hex encoded address of the peer

        Returns:
            events (list)
        """
        token_address = safe_address_decode(token_address_hex)
        peer_address = safe_address_decode(peer_address_hex)

        graph = self._raiden.token_to_channelgraph[token_address]
        assert graph

        channel = graph.partneraddress_to_channel[peer_address]
        netcontract_address = channel.external_state.netting_channel.address
        assert netcontract_address

        netting_channel = self._chain.netting_channel(netcontract_address)
        return events.netting_channel_events(self._chain.client, netting_channel)

    def wait_for_contract(self, contract_address_hex, timeout=None):
        """ Wait until a contract is mined

        Args:
            contract_address_hex (string): hex encoded address of the contract
            timeout (int): time to wait for the contract to get mined

        Returns:
            True if the contract got mined, false otherwise
        """
        contract_address = safe_address_decode(contract_address_hex)
        start_time = time.time()
        result = self._raiden.chain.client.call(
            'eth_getCode',
            contract_address,
            'latest',
        )

        current_time = time.time()
        while result == '0x':
            if timeout and start_time + timeout > current_time:
                return False

            result = self._raiden.chain.client.call(
                'eth_getCode',
                contract_address,
                'latest',
            )
            gevent.sleep(0.5)

            current_time = time.time()

        return result != '0x'
示例#2
0
class ConnectionManager:
    """The ConnectionManager provides a high level abstraction for connecting to a
    Token network.

    Note:
        It is initialized with 0 funds; a connection to the token network
        will be only established _after_ calling `connect(funds)`
    """
    # XXX Hack: for bootstrapping, the first node on a network opens a channel
    # with this address to become visible.
    BOOTSTRAP_ADDR_HEX = b'2' * 40
    BOOTSTRAP_ADDR = unhexlify(BOOTSTRAP_ADDR_HEX)

    def __init__(self, raiden, token_address, channelgraph):
        self.lock = Semaphore()
        self.raiden = raiden
        self.api = RaidenAPI(raiden)
        self.channelgraph = channelgraph
        self.token_address = token_address
        self.funds = 0
        self.initial_channel_target = 0
        self.joinable_funds_target = 0

    def connect(self,
                funds,
                initial_channel_target=3,
                joinable_funds_target=.4):
        """Connect to the network.
        Use this to establish a connection with the token network.

        Subsequent calls to `connect` are allowed, but will only affect the spendable
        funds and the connection strategy parameters for the future. `connect` will not
        close any channels.

        Note: the ConnectionManager does not discriminate manually opened channels from
        automatically opened ones. If the user manually opened channels, those deposit
        amounts will affect the funding per channel and the number of new channels opened.

        Args:
            funds (int): the amount of tokens spendable for this
            ConnectionManager.
            initial_channel_target (int): number of channels to open immediately
            joinable_funds_target (float): amount of funds not initially assigned
        """
        if funds <= 0:
            raise ValueError('connecting needs a positive value for `funds`')

        if self.token_address in self.raiden.message_handler.blocked_tokens:
            self.raiden.message_handler.blocked_tokens.pop(self.token_address)

        self.initial_channel_target = initial_channel_target
        self.joinable_funds_target = joinable_funds_target

        open_channels = self.open_channels
        # there are already channels open
        if len(open_channels):
            log.debug(
                'connect() called on an already joined token network',
                token_address=pex(self.token_address),
                open_channels=len(open_channels),
                sum_deposits=self.sum_deposits,
                funds=funds,
            )

        if len(self.channelgraph.graph.nodes()) == 0:
            with self.lock:
                log.debug('bootstrapping token network.')
                # make ourselves visible
                self.api.open(self.token_address,
                              ConnectionManager.BOOTSTRAP_ADDR)

        with self.lock:
            # set our available funds
            self.funds = funds
            # try to fullfill our connection goal
            self._add_new_partners()

    def leave_async(self):
        """ Async version of `leave()`
        """
        leave_result = AsyncResult()
        gevent.spawn(self.leave).link(leave_result)
        return leave_result

    def leave(self, only_receiving=True):
        """ Leave the token network.
        This implies closing all channels and waiting for all channels to be settled.
        """
        # set leaving state
        if self.token_address not in self.raiden.message_handler.blocked_tokens:
            self.raiden.message_handler.blocked_tokens.append(
                self.token_address)
        if self.initial_channel_target > 0:
            self.initial_channel_target = 0

        closed_channels = self.close_all(only_receiving)
        self.wait_for_settle(closed_channels)
        return closed_channels

    def close_all(self, only_receiving=True):
        """ Close all channels in the token network.
        Note: By default we're just discarding all channels we haven't received anything.
        This potentially leaves deposits locked in channels after `closing`. This is "safe"
        from an accounting point of view (deposits can not be lost), but may still be
        undesirable from a liquidity point of view (deposits will only be freed after
        manually closing or after the partner closed the channel).

        If only_receiving is False then we close and settle all channels irrespective of them
        having received transfers or not.
        """
        with self.lock:
            self.initial_channel_target = 0
            channels_to_close = (self.receiving_channels[:]
                                 if only_receiving else self.open_channels[:])
            for channel in channels_to_close:
                # FIXME: race condition, this can fail if channel was closed externally
                self.api.close(self.token_address, channel.partner_address)
            return channels_to_close

    def wait_for_settle(self, closed_channels):
        """Wait for all closed channels of the token network to settle.
        Note, that this does not time out.
        """
        not_settled_channels = [
            channel for channel in closed_channels
            if not channel.state != CHANNEL_STATE_SETTLED
        ]
        while any(c.state != CHANNEL_STATE_SETTLED
                  for c in not_settled_channels):
            # wait for events to propagate
            gevent.sleep(self.raiden.alarm.wait_time)
        return True

    def join_channel(self, partner_address, partner_deposit):
        """Will be called, when we were selected as channel partner by another
        node. It will fund the channel with up to the partner's deposit, but
        not more than remaining funds or the initial funding per channel.

        If the connection manager has no funds, this is a noop.
        """
        # not initialized
        if self.funds <= 0:
            return
        # in leaving state
        if self.leaving_state:
            return
        with self.lock:
            remaining = self.funds_remaining
            initial = self.initial_funding_per_partner
            joining_funds = min(partner_deposit, remaining, initial)
            if joining_funds <= 0:
                return

            self.api.deposit(self.token_address, partner_address,
                             joining_funds)
            log.debug('joined a channel!',
                      funds=joining_funds,
                      me=pex(self.raiden.address),
                      partner=pex(partner_address))

    def retry_connect(self):
        """Will be called when new channels in the token network are detected.
        If the minimum number of channels was not yet established, it will try
        to open new channels.

        If the connection manager has no funds, this is a noop.
        """
        # not initialized
        if self.funds <= 0:
            return
        # in leaving state
        if self.leaving_state:
            return
        with self.lock:
            if self.funds_remaining <= 0:
                return
            if len(self.open_channels) >= self.initial_channel_target:
                return

            # try to fullfill our connection goal
            self._add_new_partners()

    def _add_new_partners(self):
        """ This opens channels with a number of new partners according to the
        connection strategy parameter `self.initial_channel_target`.
        Each new channel will receive `self.initial_funding_per_partner` funding. """
        # this could be a subsequent call, or some channels already open
        new_partner_count = max(
            0, self.initial_channel_target - len(self.open_channels))
        for partner in self.find_new_partners(new_partner_count):
            self._open_and_deposit(partner, self.initial_funding_per_partner)

    def _open_and_deposit(self, partner, funding_amount):
        """ Open a channel with `partner` and deposit `funding_amount` tokens.

        If the channel was already opened (a known race condition),
        this skips the opening and only deposits.
        """
        try:
            self.api.open(self.token_address, partner)
        # this can fail because of a race condition, where the channel partner opens first
        except DuplicatedChannelError:
            log.info('partner opened channel first')

        channelgraph = self.raiden.token_to_channelgraph[self.token_address]
        if partner not in channelgraph.partneraddress_to_channel:
            self.raiden.poll_blockchain_events()

        if partner not in channelgraph.partneraddress_to_channel:
            log.error(
                'Opening new channel failed; channel already opened, '
                'but partner not in channelgraph',
                partner=pex(partner),
                token_address=pex(self.token_address),
            )
        else:
            try:
                self.api.deposit(
                    self.token_address,
                    partner,
                    funding_amount,
                )
            except AddressWithoutCode:
                log.warn(
                    'connection manager: channel closed just after it was created'
                )
            except TransactionThrew:
                log.exception('connection manager: deposit failed')

    def find_new_partners(self, number):
        """Search the token network for potential channel partners.

        Args:
            number (int): number of partners to return
        """
        known = set(c.partner_address for c in self.open_channels)
        known = known.union({self.__class__.BOOTSTRAP_ADDR})
        known = known.union({self.raiden.address})
        available = set(self.channelgraph.graph.nodes()) - known

        available = self._select_best_partners(available)
        log.debug('found {} partners'.format(len(available)))
        return available[:number]

    def _select_best_partners(self, partners):
        # FIXME: use a proper selection strategy
        # https://github.com/raiden-network/raiden/issues/576
        return list(partners)

    @property
    def initial_funding_per_partner(self):
        """The calculated funding per partner depending on configuration and
        overall funding of the ConnectionManager.
        """
        if self.initial_channel_target:
            return int(self.funds * (1 - self.joinable_funds_target) /
                       self.initial_channel_target)
        else:
            return 0

    @property
    def wants_more_channels(self):
        """True, if funds available and the `initial_channel_target` was not yet
        reached.
        """
        if self.token_address in self.raiden.message_handler.blocked_tokens:
            return False
        return (self.funds_remaining > 0
                and len(self.open_channels) < self.initial_channel_target)

    @property
    def funds_remaining(self):
        """The remaining funds after subtracting the already deposited amounts.
        """
        if self.funds > 0:
            remaining = self.funds - self.sum_deposits
            assert isinstance(remaining, int)
            return remaining
        return 0

    @property
    def open_channels(self):
        """Shorthand for getting our open channels in this token network.
        """
        return [
            channel for channel in self.api.get_channel_list(
                token_address=self.token_address)
            if channel.state == CHANNEL_STATE_OPENED
        ]

    @property
    def sum_deposits(self):
        """Shorthand for getting sum of all open channels deposited funds"""
        return sum(channel.contract_balance for channel in self.open_channels)

    @property
    def receiving_channels(self):
        """Shorthand for getting channels that had received any transfers in this token network.
        """
        return [
            channel for channel in self.open_channels
            if len(channel.received_transfers)
        ]

    @property
    def min_settle_blocks(self):
        """Returns the minimum necessary waiting time to settle all channels.
        """
        channels = self.receiving_channels
        timeouts = [0]
        current_block = self.raiden.get_block_number()
        for channel in channels:
            if channel.state == CHANNEL_STATE_CLOSED:
                since_closed = current_block - channel.external_state._closed_block
            elif channel.state == CHANNEL_STATE_OPENED:
                # it will at least take one more block to call close
                since_closed = -1
            else:
                since_closed = 0
            timeouts.append(channel.settle_timeout - since_closed)

        return max(timeouts)

    @property
    def leaving_state(self):
        return (self.token_address
                in self.raiden.message_handler.blocked_tokens
                or self.initial_channel_target < 1)
示例#3
0
def test_channel_lifecycle(blockchain_type, raiden_network, token_addresses,
                           deposit):
    if blockchain_type == 'tester':
        pytest.skip(
            'there is not support ATM for retrieving events from tester')

    node1, node2 = raiden_network
    token_address = token_addresses[0]

    api1 = RaidenAPI(node1.raiden)
    api2 = RaidenAPI(node2.raiden)

    # nodes don't have a channel, so they are not healthchecking
    assert api1.get_node_network_state(api2.address) == NODE_NETWORK_UNKNOWN
    assert api2.get_node_network_state(api1.address) == NODE_NETWORK_UNKNOWN
    assert api1.get_channel_list(token_address, api2.address) == []

    # this is a synchronous api
    api1.open(token_address, api2.address)
    channels = api1.get_channel_list(token_address, api2.address)
    assert len(channels) == 1
    channel12 = channels[0]

    event_list1 = api1.get_channel_events(
        channel12.channel_address,
        channel12.external_state.opened_block,
    )
    assert event_list1 == []

    # the channel has no deposit yet
    assert channel12.state == CHANNEL_STATE_OPENED

    api1.deposit(token_address, api2.address, deposit)

    assert channel12.state == CHANNEL_STATE_OPENED
    assert channel12.balance == deposit
    assert channel12.contract_balance == deposit
    assert api1.get_channel_list(token_address, api2.address) == [channel12]

    # there is a channel open, they must be healthchecking each other
    assert api1.get_node_network_state(api2.address) == NODE_NETWORK_REACHABLE
    assert api2.get_node_network_state(api1.address) == NODE_NETWORK_REACHABLE

    event_list2 = api1.get_channel_events(
        channel12.channel_address,
        channel12.external_state.opened_block,
    )
    assert any((event['_event_type'] == 'ChannelNewBalance'
                and event['participant'] == hexlify(api1.address))
               for event in event_list2)

    with pytest.raises(InvalidState):
        api1.settle(token_address, api2.address)

    api1.close(token_address, api2.address)
    node1.raiden.poll_blockchain_events()

    event_list3 = api1.get_channel_events(
        channel12.channel_address,
        channel12.external_state.opened_block,
    )
    assert len(event_list3) > len(event_list2)
    assert any((event['_event_type'] == 'ChannelClosed'
                and event['closing_address'] == hexlify(api1.address))
               for event in event_list3)
    assert channel12.state == CHANNEL_STATE_CLOSED

    settlement_block = (
        channel12.external_state.closed_block + channel12.settle_timeout +
        5  # arbitrary number of additional blocks, used to wait for the settle() call
    )
    wait_until_block(node1.raiden.chain, settlement_block)

    node1.raiden.poll_blockchain_events()
    assert channel12.state == CHANNEL_STATE_SETTLED
def run(
        privatekey,
        registry_contract_address,
        discovery_contract_address,
        listen_address,
        logging,
        logfile,
        scenario,
        stage_prefix,
        results_filename):  # pylint: disable=unused-argument

    # TODO: only enabled logging on "initiators"
    slogging.configure(logging, log_file=logfile)

    (listen_host, listen_port) = split_endpoint(listen_address)

    config = App.DEFAULT_CONFIG.copy()
    config['host'] = listen_host
    config['port'] = listen_port
    config['privatekey_hex'] = privatekey

    privatekey_bin = decode_hex(privatekey)

    rpc_client = JSONRPCClient(
        '127.0.0.1',
        8545,
        privatekey_bin,
    )

    blockchain_service = BlockChainService(
        privatekey_bin,
        rpc_client,
        GAS_LIMIT,
        GAS_PRICE,
    )

    discovery = ContractDiscovery(
        blockchain_service,
        decode_hex(discovery_contract_address)
    )

    registry = blockchain_service.registry(
        registry_contract_address
    )

    app = App(
        config,
        blockchain_service,
        registry,
        discovery,
    )

    app.discovery.register(
        app.raiden.address,
        listen_host,
        listen_port,
    )

    app.raiden.register_registry(app.raiden.default_registry.address)

    if scenario:
        script = json.load(scenario)

        tools = ConsoleTools(
            app.raiden,
            app.discovery,
            app.config['settle_timeout'],
            app.config['reveal_timeout'],
        )

        transfers_by_peer = {}

        tokens = script['tokens']
        token_address = None
        peer = None
        our_node = hexlify(app.raiden.address)
        log.warning("our address is {}".format(our_node))
        for token in tokens:
            # skip tokens that we're not part of
            nodes = token['channels']
            if our_node not in nodes:
                continue

            partner_nodes = [
                node
                for node in nodes
                if node != our_node
            ]

            # allow for prefunded tokens
            if 'token_address' in token:
                token_address = token['token_address']
            else:
                token_address = tools.create_token()

            transfers_with_amount = token['transfers_with_amount']

            # FIXME: in order to do bidirectional channels, only one side
            # (i.e. only token['channels'][0]) should
            # open; others should join by calling
            # raiden.api.deposit, AFTER the channel came alive!

            # NOTE: leaving unidirectional for now because it most
            #       probably will get to higher throughput

            log.warning("Waiting for all nodes to come online")

            api = RaidenAPI(app.raiden)

            for node in partner_nodes:
                api.start_health_check_for(node)

            while True:
                all_reachable = all(
                    api.get_node_network_state(node) == NODE_NETWORK_REACHABLE
                    for node in partner_nodes
                )

                if all_reachable:
                    break

                gevent.sleep(5)

            log.warning("All nodes are online")

            if our_node != nodes[-1]:
                our_index = nodes.index(our_node)
                peer = nodes[our_index + 1]

                tools.register_token(token_address)
                amount = transfers_with_amount[nodes[-1]]

                while True:
                    try:
                        app.discovery.get(peer.decode('hex'))
                        break
                    except KeyError:
                        log.warning("Error: peer {} not found in discovery".format(peer))
                        time.sleep(random.randrange(30))

                while True:
                    try:
                        log.warning("Opening channel with {} for {}".format(peer, token_address))
                        api.open(token_address, peer)
                        break
                    except KeyError:
                        log.warning("Error: could not open channel with {}".format(peer))
                        time.sleep(random.randrange(30))

                while True:
                    try:
                        log.warning("Funding channel with {} for {}".format(peer, token_address))
                        api.deposit(token_address, peer, amount)
                        break
                    except Exception:
                        log.warning("Error: could not deposit {} for {}".format(amount, peer))
                        time.sleep(random.randrange(30))

                if our_index == 0:
                    last_node = nodes[-1]
                    transfers_by_peer[last_node] = int(amount)
            else:
                peer = nodes[-2]

        if stage_prefix is not None:
            open('{}.stage1'.format(stage_prefix), 'a').close()
            log.warning("Done with initialization, waiting to continue...")
            event = gevent.event.Event()
            gevent.signal(signal.SIGUSR2, event.set)
            event.wait()

        transfer_results = {'total_time': 0, 'timestamps': []}

        def transfer(token_address, amount_per_transfer, total_transfers, peer, is_async):
            def transfer_():
                log.warning("Making {} transfers to {}".format(total_transfers, peer))
                initial_time = time.time()
                times = [0] * total_transfers
                for index in range(total_transfers):
                    RaidenAPI(app.raiden).transfer(
                        token_address.decode('hex'),
                        amount_per_transfer,
                        peer,
                    )
                    times[index] = time.time()

                transfer_results['total_time'] = time.time() - initial_time
                transfer_results['timestamps'] = times

                log.warning("Making {} transfers took {}".format(
                    total_transfers, transfer_results['total_time']))
                log.warning("Times: {}".format(times))

            if is_async:
                return gevent.spawn(transfer_)
            else:
                transfer_()

        # If sending to multiple targets, do it asynchronously, otherwise
        # keep it simple and just send to the single target on my thread.
        if len(transfers_by_peer) > 1:
            greenlets = []
            for peer_, amount in transfers_by_peer.items():
                greenlet = transfer(token_address, 1, amount, peer_, True)
                if greenlet is not None:
                    greenlets.append(greenlet)

            gevent.joinall(greenlets)

        elif len(transfers_by_peer) == 1:
            for peer_, amount in transfers_by_peer.items():
                transfer(token_address, 1, amount, peer_, False)

        log.warning("Waiting for termination")

        open('{}.stage2'.format(stage_prefix), 'a').close()
        log.warning("Waiting for transfers to finish, will write results...")
        event = gevent.event.Event()
        gevent.signal(signal.SIGUSR2, event.set)
        event.wait()

        results = tools.channel_stats_for(token_address, peer)
        if transfer_results['total_time'] != 0:
            results['total_time'] = transfer_results['total_time']
        if len(transfer_results['timestamps']) > 0:
            results['timestamps'] = transfer_results['timestamps']
        results['channel'] = repr(results['channel'])  # FIXME

        log.warning("Results: {}".format(results))

        with open(results_filename, 'w') as fp:
            json.dump(results, fp, indent=2)

        open('{}.stage3'.format(stage_prefix), 'a').close()
        event = gevent.event.Event()
        gevent.signal(signal.SIGQUIT, event.set)
        gevent.signal(signal.SIGTERM, event.set)
        gevent.signal(signal.SIGINT, event.set)
        event.wait()

    else:
        log.warning("No scenario file supplied, doing nothing!")

        open('{}.stage2'.format(stage_prefix), 'a').close()
        event = gevent.event.Event()
        gevent.signal(signal.SIGQUIT, event.set)
        gevent.signal(signal.SIGTERM, event.set)
        gevent.signal(signal.SIGINT, event.set)
        event.wait()

    app.stop()
示例#5
0
def run(privatekey, registry_contract_address, discovery_contract_address,
        listen_address, logging, logfile, scenario, stage_prefix,
        results_filename):  # pylint: disable=unused-argument

    # TODO: only enabled logging on "initiators"
    slogging.configure(logging, log_file=logfile)

    (listen_host, listen_port) = split_endpoint(listen_address)

    config = App.DEFAULT_CONFIG.copy()
    config['host'] = listen_host
    config['port'] = listen_port
    config['privatekey_hex'] = privatekey

    privatekey_bin = decode_hex(privatekey)

    rpc_client = JSONRPCClient(
        '127.0.0.1',
        8545,
        privatekey_bin,
    )

    blockchain_service = BlockChainService(
        privatekey_bin,
        rpc_client,
        GAS_PRICE,
    )

    discovery = ContractDiscovery(blockchain_service,
                                  decode_hex(discovery_contract_address))

    registry = blockchain_service.registry(registry_contract_address)

    app = App(
        config,
        blockchain_service,
        registry,
        discovery,
    )

    app.discovery.register(
        app.raiden.address,
        listen_host,
        listen_port,
    )

    app.raiden.register_registry(app.raiden.default_registry.address)

    if scenario:
        script = json.load(scenario)

        tools = ConsoleTools(
            app.raiden,
            app.discovery,
            app.config['settle_timeout'],
            app.config['reveal_timeout'],
        )

        transfers_by_peer = {}

        tokens = script['tokens']
        token_address = None
        peer = None
        our_node = hexlify(app.raiden.address)
        log.warning("our address is {}".format(our_node))
        for token in tokens:
            # skip tokens that we're not part of
            nodes = token['channels']
            if our_node not in nodes:
                continue

            partner_nodes = [node for node in nodes if node != our_node]

            # allow for prefunded tokens
            if 'token_address' in token:
                token_address = token['token_address']
            else:
                token_address = tools.create_token()

            transfers_with_amount = token['transfers_with_amount']

            # FIXME: in order to do bidirectional channels, only one side
            # (i.e. only token['channels'][0]) should
            # open; others should join by calling
            # raiden.api.deposit, AFTER the channel came alive!

            # NOTE: leaving unidirectional for now because it most
            #       probably will get to higher throughput

            log.warning("Waiting for all nodes to come online")

            api = RaidenAPI(app.raiden)

            for node in partner_nodes:
                api.start_health_check_for(node)

            while True:
                all_reachable = all(
                    api.get_node_network_state(node) == NODE_NETWORK_REACHABLE
                    for node in partner_nodes)

                if all_reachable:
                    break

                gevent.sleep(5)

            log.warning("All nodes are online")

            if our_node != nodes[-1]:
                our_index = nodes.index(our_node)
                peer = nodes[our_index + 1]

                tools.register_token(token_address)
                amount = transfers_with_amount[nodes[-1]]

                while True:
                    try:
                        app.discovery.get(peer.decode('hex'))
                        break
                    except KeyError:
                        log.warning(
                            "Error: peer {} not found in discovery".format(
                                peer))
                        time.sleep(random.randrange(30))

                while True:
                    try:
                        log.warning("Opening channel with {} for {}".format(
                            peer, token_address))
                        api.open(token_address, peer)
                        break
                    except KeyError:
                        log.warning(
                            "Error: could not open channel with {}".format(
                                peer))
                        time.sleep(random.randrange(30))

                while True:
                    try:
                        log.warning("Funding channel with {} for {}".format(
                            peer, token_address))
                        api.deposit(token_address, peer, amount)
                        break
                    except Exception:
                        log.warning(
                            "Error: could not deposit {} for {}".format(
                                amount, peer))
                        time.sleep(random.randrange(30))

                if our_index == 0:
                    last_node = nodes[-1]
                    transfers_by_peer[last_node] = int(amount)
            else:
                peer = nodes[-2]

        if stage_prefix is not None:
            open('{}.stage1'.format(stage_prefix), 'a').close()
            log.warning("Done with initialization, waiting to continue...")
            event = gevent.event.Event()
            gevent.signal(signal.SIGUSR2, event.set)
            event.wait()

        transfer_results = {'total_time': 0, 'timestamps': []}

        def transfer(token_address, amount_per_transfer, total_transfers, peer,
                     is_async):
            def transfer_():
                log.warning("Making {} transfers to {}".format(
                    total_transfers, peer))
                initial_time = time.time()
                times = [0] * total_transfers
                for index in range(total_transfers):
                    RaidenAPI(app.raiden).transfer(
                        token_address.decode('hex'),
                        amount_per_transfer,
                        peer,
                    )
                    times[index] = time.time()

                transfer_results['total_time'] = time.time() - initial_time
                transfer_results['timestamps'] = times

                log.warning("Making {} transfers took {}".format(
                    total_transfers, transfer_results['total_time']))
                log.warning("Times: {}".format(times))

            if is_async:
                return gevent.spawn(transfer_)
            else:
                transfer_()

        # If sending to multiple targets, do it asynchronously, otherwise
        # keep it simple and just send to the single target on my thread.
        if len(transfers_by_peer) > 1:
            greenlets = []
            for peer_, amount in transfers_by_peer.items():
                greenlet = transfer(token_address, 1, amount, peer_, True)
                if greenlet is not None:
                    greenlets.append(greenlet)

            gevent.joinall(greenlets)

        elif len(transfers_by_peer) == 1:
            for peer_, amount in transfers_by_peer.items():
                transfer(token_address, 1, amount, peer_, False)

        log.warning("Waiting for termination")

        open('{}.stage2'.format(stage_prefix), 'a').close()
        log.warning("Waiting for transfers to finish, will write results...")
        event = gevent.event.Event()
        gevent.signal(signal.SIGUSR2, event.set)
        event.wait()

        results = tools.channel_stats_for(token_address, peer)
        if transfer_results['total_time'] != 0:
            results['total_time'] = transfer_results['total_time']
        if len(transfer_results['timestamps']) > 0:
            results['timestamps'] = transfer_results['timestamps']
        results['channel'] = repr(results['channel'])  # FIXME

        log.warning("Results: {}".format(results))

        with open(results_filename, 'w') as fp:
            json.dump(results, fp, indent=2)

        open('{}.stage3'.format(stage_prefix), 'a').close()
        event = gevent.event.Event()
        gevent.signal(signal.SIGQUIT, event.set)
        gevent.signal(signal.SIGTERM, event.set)
        gevent.signal(signal.SIGINT, event.set)
        event.wait()

    else:
        log.warning("No scenario file supplied, doing nothing!")

        open('{}.stage2'.format(stage_prefix), 'a').close()
        event = gevent.event.Event()
        gevent.signal(signal.SIGQUIT, event.set)
        gevent.signal(signal.SIGTERM, event.set)
        gevent.signal(signal.SIGINT, event.set)
        event.wait()

    app.stop()
示例#6
0
class ConnectionManager(object):
    """The ConnectionManager provides a high level abstraction for connecting to a
    Token network.
    Note:
        It is initialized with 0 funds; a connection to the token network
        will be only established _after_ calling `connect(funds)`
    """
    # XXX Hack: for bootstrapping the first node on a network opens a channel
    # with this address to become visible.
    BOOTSTRAP_ADDR_HEX = '2' * 40
    BOOTSTRAP_ADDR = BOOTSTRAP_ADDR_HEX.decode('hex')

    def __init__(
        self,
        raiden,
        token_address,
        channelgraph,
    ):
        self.lock = Semaphore()
        self.raiden = raiden
        self.api = RaidenAPI(raiden)
        self.channelgraph = channelgraph
        self.token_address = token_address
        self.funds = 0
        self.initial_channel_target = 0
        self.joinable_funds_target = 0

    def connect(self,
                funds,
                initial_channel_target=3,
                joinable_funds_target=.4):
        """Connect to the network.
        Use this to establish a connection with the token network.

        Subsequent calls to `connect` are allowed, but will only affect the spendable
        funds and the connection strategy parameters for the future. `connect` will not
        close any channels.

        Note: the ConnectionManager does not discriminate manually opened channels from
        automatically opened ones. If the user manually opened channels, those deposit
        amounts will affect the funding per channel and the number of new channels opened.

        Args:
            funds (int): the amount of tokens spendable for this
            ConnectionManager.
            initial_channel_target (int): number of channels to open immediately
            joinable_funds_target (float): amount of funds not initially assigned
        """
        if funds <= 0:
            raise ValueError('connecting needs a positive value for `funds`')

        self.initial_channel_target = initial_channel_target
        self.joinable_funds_target = joinable_funds_target

        open_channels = self.open_channels
        # there are already channels open
        if len(open_channels):
            log.debug(
                'connect() called on an already joined token network',
                token_address=pex(self.token_address),
                open_channels=len(open_channels),
                sum_deposits=sum(channel.deposit for channel in open_channels),
                funds=funds,
            )

        if len(self.channelgraph.graph.nodes()) == 0:
            with self.lock:
                log.debug('bootstrapping token network.')
                # make ourselves visible
                self.api.open(self.token_address,
                              ConnectionManager.BOOTSTRAP_ADDR)

        with self.lock:
            self.funds = funds
            funding = self.initial_funding_per_partner
            # this could be a subsequent call, or some channels already open
            new_partner_count = max(
                0, self.initial_channel_target - len(self.open_channels))
            for partner in self.find_new_partners(new_partner_count):
                self.api.open(
                    self.token_address,
                    partner,
                )
                self.api.deposit(self.token_address, partner, funding)

    def leave(self, wait_for_settle=True, timeout=30):
        """
        Leave the token network.
        This implies closing all open channels and optionally waiting for
        settlement.
        Args:
            wait_for_settle (bool): block until successful settlement?
            timeout (float): maximum time to wait
        """
        with self.lock:
            self.initial_channel_target = 0
            open_channels = self.open_channels
            channel_specs = [(self.token_address, c.partner_address)
                             for c in open_channels]
            for channel in channel_specs:
                try:
                    self.api.close(*channel),
                except RuntimeError:
                    # if the error wasn't that the channel was already closed: raise
                    if channel[1] in [
                            c.partner_address for c in self.open_channels
                    ]:
                        raise

            # wait for events to propagate
            gevent.sleep(self.raiden.alarm.wait_time)

            if wait_for_settle:
                try:
                    with gevent.timeout.Timeout(timeout):
                        while any(c.state != CHANNEL_STATE_SETTLED
                                  for c in open_channels):
                            # wait for events to propagate
                            gevent.sleep(self.raiden.alarm.wait_time)

                except gevent.timeout.Timeout:
                    log.debug('timeout while waiting for settlement',
                              unsettled=sum(
                                  1 for channel in open_channels
                                  if channel.state != CHANNEL_STATE_SETTLED),
                              settled=sum(
                                  1 for channel in open_channels
                                  if channel.state == CHANNEL_STATE_SETTLED))

    def join_channel(self, partner_address, partner_deposit):
        """Will be called, when we were selected as channel partner by another
        node. It will fund the channel with up to the partner's deposit, but
        not more than remaining funds or the initial funding per channel.

        If the connection manager has no funds, this is a noop.
        """
        # not initialized
        if self.funds <= 0:
            return
        # in leaving state
        if self.initial_channel_target < 1:
            return
        with self.lock:
            remaining = self.funds_remaining
            initial = self.initial_funding_per_partner
            joining_funds = min(partner_deposit, remaining, initial)
            if joining_funds <= 0:
                return

            self.api.deposit(self.token_address, partner_address,
                             joining_funds)
            log.debug('joined a channel!',
                      funds=joining_funds,
                      me=pex(self.raiden.address),
                      partner=pex(partner_address))

    def retry_connect(self):
        """Will be called when new channels in the token network are detected.
        If the minimum number of channels was not yet established, it will try
        to open new channels.

        If the connection manager has no funds, this is a noop.
        """
        # not initialized
        if self.funds <= 0:
            return
        # in leaving state
        if self.initial_channel_target == 0:
            return
        with self.lock:
            if self.funds_remaining <= 0:
                return
            if len(self.open_channels) >= self.initial_channel_target:
                return
            for partner in self.find_new_partners(self.initial_channel_target -
                                                  len(self.open_channels)):
                try:
                    self.api.open(self.token_address, partner)
                    self.api.deposit(self.token_address, partner,
                                     self.initial_funding_per_partner)
                # this can fail because of a race condition, where the channel partner opens first
                except Exception as e:
                    log.error('could not open a channel', exc_info=e)

    def find_new_partners(self, number):
        """Search the token network for potential channel partners.
        Args:
            number (int): number of partners to return
        """
        known = set(c.partner_address for c in self.open_channels)
        known = known.union({self.__class__.BOOTSTRAP_ADDR})
        known = known.union({self.raiden.address})
        available = set(self.channelgraph.graph.nodes()) - known

        available = self._select_best_partners(available)
        log.debug('found {} partners'.format(len(available)))
        return available[:number]

    def _select_best_partners(self, partners):
        # FIXME: use a proper selection strategy
        # https://github.com/raiden-network/raiden/issues/576
        return list(partners)

    @property
    def initial_funding_per_partner(self):
        """The calculated funding per partner depending on configuration and
        overall funding of the ConnectionManager.
        """
        if self.initial_channel_target:
            return int(self.funds * (1 - self.joinable_funds_target) /
                       self.initial_channel_target)
        else:
            return 0

    @property
    def wants_more_channels(self):
        """True, if funds available and the `initial_channel_target` was not yet
        reached.
        """
        return (self.funds_remaining > 0
                and len(self.open_channels) < self.initial_channel_target)

    @property
    def funds_remaining(self):
        """The remaining funds after subtracting the already deposited amounts.
        """
        if self.funds > 0:
            remaining = self.funds - sum(channel.deposit
                                         for channel in self.open_channels)
            assert isinstance(remaining, int)
            return remaining
        return 0

    @property
    def open_channels(self):
        """Shorthand for getting our open channels in this token network.
        """
        return [
            channel for channel in self.api.get_channel_list(
                token_address=self.token_address)
            if channel.state == CHANNEL_STATE_OPENED
        ]
class ConsoleTools:
    def __init__(self, raiden_service, discovery, settle_timeout, reveal_timeout):
        self._chain = raiden_service.chain
        self._raiden = raiden_service
        self._api = RaidenAPI(raiden_service)
        self._discovery = discovery
        self.settle_timeout = settle_timeout
        self.reveal_timeout = reveal_timeout
        self.deposit = self._api.deposit

    def create_token(
            self,
            initial_alloc=10 ** 6,
            name='raidentester',
            symbol='RDT',
            decimals=2,
            timeout=60,
            gasprice=GAS_PRICE,
            auto_register=True):
        """ Create a proxy for a new HumanStandardToken (ERC20), that is
        initialized with Args(below).
        Per default it will be registered with 'raiden'.

        Args:
            initial_alloc (int): amount of initial tokens.
            name (str): human readable token name.
            symbol (str): token shorthand symbol.
            decimals (int): decimal places.
            timeout (int): timeout in seconds for creation.
            gasprice (int): gasprice for the creation transaction.
            auto_register (boolean): if True(default), automatically register
                the token with raiden.

        Returns:
            token_address_hex: the hex encoded address of the new token/token.
        """
        contract_path = get_contract_path('HumanStandardToken.sol')
        # Deploy a new ERC20 token
        token_proxy = self._chain.client.deploy_solidity_contract(
            self._raiden.address, 'HumanStandardToken',
            compile_file(contract_path),
            dict(),
            (initial_alloc, name, decimals, symbol),
            contract_path=contract_path,
            gasprice=gasprice,
            timeout=timeout)
        token_address_hex = hexlify(token_proxy.contract_address)
        if auto_register:
            self.register_token(token_address_hex)
        print("Successfully created {}the token '{}'.".format(
            'and registered ' if auto_register else ' ',
            name
        ))
        return token_address_hex

    def register_token(self, token_address_hex):
        """ Register a token with the raiden token manager.

        Args:
            token_address_hex (string): a hex encoded token address.

        Returns:
            channel_manager: the channel_manager contract_proxy.
        """
        # Add the ERC20 token to the raiden registry
        token_address = safe_address_decode(token_address_hex)
        self._raiden.default_registry.add_token(token_address)

        # Obtain the channel manager for the token
        channel_manager = self._raiden.default_registry.manager_by_token(token_address)

        # Register the channel manager with the raiden registry
        self._raiden.register_channel_manager(channel_manager.address)
        return channel_manager

    def open_channel_with_funding(
            self,
            token_address_hex,
            peer_address_hex,
            amount,
            settle_timeout=None,
            reveal_timeout=None):
        """ Convenience method to open a channel.

        Args:
            token_address_hex (str): hex encoded address of the token for the channel.
            peer_address_hex (str): hex encoded address of the channel peer.
            amount (int): amount of initial funding of the channel.
            settle_timeout (int): amount of blocks for the settle time (if None use app defaults).
            reveal_timeout (int): amount of blocks for the reveal time (if None use app defaults).

        Return:
            netting_channel: the (newly opened) netting channel object.
        """
        # Check, if peer is discoverable
        peer_address = safe_address_decode(peer_address_hex)
        token_address = safe_address_decode(token_address_hex)
        try:
            self._discovery.get(peer_address)
        except KeyError:
            print('Error: peer {} not found in discovery'.format(peer_address_hex))
            return

        self._api.open(
            token_address,
            peer_address,
            settle_timeout=settle_timeout,
            reveal_timeout=reveal_timeout,
        )

        return self._api.deposit(token_address, peer_address, amount)

    def channel_stats_for(self, token_address_hex, peer_address_hex, pretty=False):
        """ Collect information about sent and received transfers
        between yourself and your peer for the given token.

        Args:
            token_address_hex (string): hex encoded address of the token
            peer_address_hex (string): hex encoded address of the peer
            pretty (boolean): if True, print a json representation instead of returning a dict

        Returns:
            stats (dict): collected stats for the channel or None if pretty

        """
        peer_address = safe_address_decode(peer_address_hex)
        token_address = safe_address_decode(token_address_hex)

        # Get the token
        token = self._chain.token(token_address)

        # Obtain the token manager
        graph = self._raiden.token_to_channelgraph[token_address]
        assert graph

        # Get the channel
        channel = graph.partneraddress_to_channel[peer_address]
        assert channel

        # Collect data
        stats = dict(
            transfers=dict(
                received=[t.transferred_amount for t in channel.received_transfers],
                sent=[t.transferred_amount for t in channel.sent_transfers],
            ),
            channel=(channel
                     if not pretty
                     else hexlify(channel.external_state.netting_channel.address)),
            lifecycle=dict(
                opened_at=channel.external_state.opened_block or 'not yet',
                can_transfer=channel.can_transfer,
                closed_at=channel.external_state.closed_block or 'not yet',
                settled_at=channel.external_state.settled_block or 'not yet',
            ),
            funding=channel.external_state.netting_channel.detail(),
            token=dict(
                our_balance=token.balance_of(self._raiden.address),
                partner_balance=token.balance_of(peer_address),
                name=token.proxy.name(),
                symbol=token.proxy.symbol(),
            ),
        )
        stats['funding']['our_address'] = hexlify(stats['funding']['our_address'])
        stats['funding']['partner_address'] = hexlify(stats['funding']['partner_address'])
        if not pretty:
            return stats
        else:
            print(json.dumps(stats, indent=2, sort_keys=True))

    def show_events_for(self, token_address_hex, peer_address_hex):
        """ Find all EVM-EventLogs for a channel.

        Args:
            token_address_hex (string): hex encoded address of the token
            peer_address_hex (string): hex encoded address of the peer

        Returns:
            events (list)
        """
        token_address = safe_address_decode(token_address_hex)
        peer_address = safe_address_decode(peer_address_hex)

        graph = self._raiden.token_to_channelgraph[token_address]
        assert graph

        channel = graph.partneraddress_to_channel[peer_address]
        netcontract_address = channel.external_state.netting_channel.address
        assert netcontract_address

        netting_channel = self._chain.netting_channel(netcontract_address)
        return events.netting_channel_events(self._chain.client, netting_channel)

    def wait_for_contract(self, contract_address_hex, timeout=None):
        """ Wait until a contract is mined

        Args:
            contract_address_hex (string): hex encoded address of the contract
            timeout (int): time to wait for the contract to get mined

        Returns:
            True if the contract got mined, false otherwise
        """
        contract_address = safe_address_decode(contract_address_hex)
        start_time = time.time()
        result = self._raiden.chain.client.eth_getCode(contract_address)

        current_time = time.time()
        while len(result) == 0:
            if timeout and start_time + timeout > current_time:
                return False

            result = self._raiden.chain.client.eth_getCode(contract_address)
            gevent.sleep(0.5)

            current_time = time.time()

        return len(result) > 0