class NotifyingQueue(Event): def __init__(self, maxsize=None, items=()): super().__init__() self._queue = Queue(maxsize, items) def put(self, item): """ Add new item to the queue. """ self._queue.put(item) self.set() def get(self, block=True, timeout=None): """ Removes and returns an item from the queue. """ value = self._queue.get(block, timeout) if self._queue.empty(): self.clear() return value def peek(self, block=True, timeout=None): return self._queue.peek(block, timeout) def __len__(self): return len(self._queue) def copy(self): """ Copies the current queue items. """ copy = self._queue.copy() result = list() while not copy.empty(): result.append(copy.get_nowait()) return result
class NotifyingQueue(Event, Generic[T]): """This is not the same as a JoinableQueue. Here, instead of waiting for all the work to be processed, the wait is for work to be available. """ def __init__(self, maxsize: int = None, items: Iterable[T] = ()) -> None: super().__init__() self.queue = Queue(maxsize, items) if items: self.set() def put(self, item: T) -> None: """ Add new item to the queue. """ self.queue.put(item) self.set() def get(self, block: bool = True, timeout: float = None) -> T: """ Removes and returns an item from the queue. """ value = self.queue.get(block, timeout) if self.queue.empty(): self.clear() return value def peek(self, block: bool = True, timeout: float = None) -> T: return self.queue.peek(block, timeout) def __len__(self) -> int: return len(self.queue) def copy(self) -> List[T]: """ Copies the current queue items. """ copy = self.queue.copy() result = list() while not copy.empty(): result.append(copy.get_nowait()) return result def __repr__(self) -> str: return f"NotifyingQueue(id={id(self)}, num_items={len(self.queue)})"
class EchoNode: def __init__(self, api, token_address): assert isinstance(api, RaidenAPI) self.ready = Event() self.api = api self.token_address = token_address existing_channels = self.api.get_channel_list( api.raiden.default_registry.address, self.token_address, ) open_channels = [ channel_state for channel_state in existing_channels if channel.get_status(channel_state) == CHANNEL_STATE_OPENED ] if len(open_channels) == 0: token = self.api.raiden.chain.token(self.token_address) if not token.balance_of(self.api.raiden.address) > 0: raise ValueError( 'not enough funds for echo node %s for token %s' % ( pex(self.api.raiden.address), pex(self.token_address), )) self.api.token_network_connect( self.api.raiden.default_registry.address, self.token_address, token.balance_of(self.api.raiden.address), initial_channel_target=10, joinable_funds_target=.5, ) self.last_poll_offset = 0 self.received_transfers = Queue() self.stop_signal = None # used to signal REMOVE_CALLBACK and stop echo_workers self.greenlets = list() self.lock = BoundedSemaphore() self.seen_transfers = deque(list(), TRANSFER_MEMORY) self.num_handled_transfers = 0 self.lottery_pool = Queue() # register ourselves with the raiden alarm task self.api.raiden.alarm.register_callback(self.echo_node_alarm_callback) self.echo_worker_greenlet = gevent.spawn(self.echo_worker) log.info('Echo node started') def echo_node_alarm_callback(self, block_number): """ This can be registered with the raiden AlarmTask. If `EchoNode.stop()` is called, it will give the return signal to be removed from the AlarmTask callbacks. """ if not self.ready.is_set(): self.ready.set() log.debug('echo_node callback', block_number=block_number) if self.stop_signal is not None: return REMOVE_CALLBACK else: self.greenlets.append(gevent.spawn(self.poll_all_received_events)) return True def poll_all_received_events(self): """ This will be triggered once for each `echo_node_alarm_callback`. It polls all channels for `EventPaymentReceivedSuccess` events, adds all new events to the `self.received_transfers` queue and respawns `self.echo_node_worker`, if it died. """ locked = False try: with Timeout(10): locked = self.lock.acquire(blocking=False) if not locked: return else: received_transfers = self.api.get_raiden_events_payment_history( token_address=self.token_address, offset=self.last_poll_offset, ) # received transfer is a tuple of (block_number, event) received_transfers = [ event for event in received_transfers if type(event) == EventPaymentReceivedSuccess ] for event in received_transfers: transfer = copy.deepcopy(event) self.received_transfers.put(transfer) # set last_poll_block after events are enqueued (timeout safe) if received_transfers: self.last_poll_offset += len(received_transfers) if not self.echo_worker_greenlet.started: log.debug( 'restarting echo_worker_greenlet', dead=self.echo_worker_greenlet.dead, successful=self.echo_worker_greenlet.successful(), exception=self.echo_worker_greenlet.exception, ) self.echo_worker_greenlet = gevent.spawn( self.echo_worker) except Timeout: log.info('timeout while polling for events') finally: if locked: self.lock.release() def echo_worker(self): """ The `echo_worker` works through the `self.received_transfers` queue and spawns `self.on_transfer` greenlets for all not-yet-seen transfers. """ log.debug('echo worker', qsize=self.received_transfers.qsize()) while self.stop_signal is None: if self.received_transfers.qsize() > 0: transfer = self.received_transfers.get() if transfer in self.seen_transfers: log.debug( 'duplicate transfer ignored', initiator=pex(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, ) else: self.seen_transfers.append(transfer) self.greenlets.append( gevent.spawn(self.on_transfer, transfer)) else: gevent.sleep(.5) def on_transfer(self, transfer): """ This handles the echo logic, as described in https://github.com/raiden-network/raiden/issues/651: - for transfers with an amount that satisfies `amount % 3 == 0`, it sends a transfer with an amount of `amount - 1` back to the initiator - for transfers with a "lucky number" amount `amount == 7` it does not send anything back immediately -- after having received "lucky number transfers" from 7 different addresses it sends a transfer with `amount = 49` to one randomly chosen one (from the 7 lucky addresses) - consecutive entries to the lucky lottery will receive the current pool size as the `echo_amount` - for all other transfers it sends a transfer with the same `amount` back to the initiator """ echo_amount = 0 if transfer.amount % 3 == 0: log.info( 'ECHO amount - 1', initiator=pex(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, ) echo_amount = transfer.amount - 1 elif transfer.amount == 7: log.info( 'ECHO lucky number draw', initiator=pex(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, poolsize=self.lottery_pool.qsize(), ) # obtain a local copy of the pool pool = self.lottery_pool.copy() tickets = [pool.get() for _ in range(pool.qsize())] assert pool.empty() del pool if any(ticket.initiator == transfer.initiator for ticket in tickets): assert transfer not in tickets log.debug( 'duplicate lottery entry', initiator=pex(transfer.initiator), identifier=transfer.identifier, poolsize=len(tickets), ) # signal the poolsize to the participant echo_amount = len(tickets) # payout elif len(tickets) == 6: log.info('payout!') # reset the pool assert self.lottery_pool.qsize() == 6 self.lottery_pool = Queue() # add new participant tickets.append(transfer) # choose the winner transfer = random.choice(tickets) echo_amount = 49 else: self.lottery_pool.put(transfer) else: log.debug( 'echo transfer received', initiator=pex(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, ) echo_amount = transfer.amount if echo_amount: log.debug( 'sending echo transfer', target=pex(transfer.initiator), amount=echo_amount, orig_identifier=transfer.identifier, echo_identifier=transfer.identifier + echo_amount, token_address=pex(self.token_address), num_handled_transfers=self.num_handled_transfers + 1, ) self.api.transfer( self.api.raiden.default_registry.address, self.token_address, echo_amount, transfer.initiator, identifier=transfer.identifier + echo_amount, ) self.num_handled_transfers += 1 def stop(self): self.stop_signal = True self.greenlets.append(self.echo_worker_greenlet) gevent.joinall(self.greenlets, raise_error=True)
class EchoNode: def __init__(self, api, token_address): assert isinstance(api, RaidenAPI) self.ready = Event() self.api = api self.token_address = token_address existing_channels = self.api.get_channel_list( api.raiden.default_registry.address, self.token_address, ) open_channels = [ channel_state for channel_state in existing_channels if channel.get_status(channel_state) == CHANNEL_STATE_OPENED ] if len(open_channels) == 0: token = self.api.raiden.chain.token(self.token_address) if not token.balance_of(self.api.raiden.address) > 0: raise ValueError('not enough funds for echo node %s for token %s' % ( pex(self.api.raiden.address), pex(self.token_address), )) self.api.token_network_connect( self.api.raiden.default_registry.address, self.token_address, token.balance_of(self.api.raiden.address), initial_channel_target=10, joinable_funds_target=.5, ) self.last_poll_offset = 0 self.received_transfers = Queue() self.stop_signal = None # used to signal REMOVE_CALLBACK and stop echo_workers self.greenlets = list() self.lock = BoundedSemaphore() self.seen_transfers = deque(list(), TRANSFER_MEMORY) self.num_handled_transfers = 0 self.lottery_pool = Queue() # register ourselves with the raiden alarm task self.api.raiden.alarm.register_callback(self.echo_node_alarm_callback) self.echo_worker_greenlet = gevent.spawn(self.echo_worker) log.info('Echo node started') def echo_node_alarm_callback(self, block_number): """ This can be registered with the raiden AlarmTask. If `EchoNode.stop()` is called, it will give the return signal to be removed from the AlarmTask callbacks. """ if not self.ready.is_set(): self.ready.set() log.debug('echo_node callback', block_number=block_number) if self.stop_signal is not None: return REMOVE_CALLBACK else: self.greenlets.append(gevent.spawn(self.poll_all_received_events)) return True def poll_all_received_events(self): """ This will be triggered once for each `echo_node_alarm_callback`. It polls all channels for `EventPaymentReceivedSuccess` events, adds all new events to the `self.received_transfers` queue and respawns `self.echo_node_worker`, if it died. """ locked = False try: with Timeout(10): locked = self.lock.acquire(blocking=False) if not locked: return else: received_transfers = self.api.get_raiden_events_payment_history( token_address=self.token_address, offset=self.last_poll_offset, ) # received transfer is a tuple of (block_number, event) received_transfers = [ event for event in received_transfers if type(event) == EventPaymentReceivedSuccess ] for event in received_transfers: transfer = copy.deepcopy(event) self.received_transfers.put(transfer) # set last_poll_block after events are enqueued (timeout safe) if received_transfers: self.last_poll_offset += len(received_transfers) if not self.echo_worker_greenlet.started: log.debug( 'restarting echo_worker_greenlet', dead=self.echo_worker_greenlet.dead, successful=self.echo_worker_greenlet.successful(), exception=self.echo_worker_greenlet.exception, ) self.echo_worker_greenlet = gevent.spawn(self.echo_worker) except Timeout: log.info('timeout while polling for events') finally: if locked: self.lock.release() def echo_worker(self): """ The `echo_worker` works through the `self.received_transfers` queue and spawns `self.on_transfer` greenlets for all not-yet-seen transfers. """ log.debug('echo worker', qsize=self.received_transfers.qsize()) while self.stop_signal is None: if self.received_transfers.qsize() > 0: transfer = self.received_transfers.get() if transfer in self.seen_transfers: log.debug( 'duplicate transfer ignored', initiator=pex(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, ) else: self.seen_transfers.append(transfer) self.greenlets.append(gevent.spawn(self.on_transfer, transfer)) else: gevent.sleep(.5) def on_transfer(self, transfer): """ This handles the echo logic, as described in https://github.com/raiden-network/raiden/issues/651: - for transfers with an amount that satisfies `amount % 3 == 0`, it sends a transfer with an amount of `amount - 1` back to the initiator - for transfers with a "lucky number" amount `amount == 7` it does not send anything back immediately -- after having received "lucky number transfers" from 7 different addresses it sends a transfer with `amount = 49` to one randomly chosen one (from the 7 lucky addresses) - consecutive entries to the lucky lottery will receive the current pool size as the `echo_amount` - for all other transfers it sends a transfer with the same `amount` back to the initiator """ echo_amount = 0 if transfer.amount % 3 == 0: log.info( 'ECHO amount - 1', initiator=pex(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, ) echo_amount = transfer.amount - 1 elif transfer.amount == 7: log.info( 'ECHO lucky number draw', initiator=pex(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, poolsize=self.lottery_pool.qsize(), ) # obtain a local copy of the pool pool = self.lottery_pool.copy() tickets = [pool.get() for _ in range(pool.qsize())] assert pool.empty() del pool if any(ticket.initiator == transfer.initiator for ticket in tickets): assert transfer not in tickets log.debug( 'duplicate lottery entry', initiator=pex(transfer.initiator), identifier=transfer.identifier, poolsize=len(tickets), ) # signal the poolsize to the participant echo_amount = len(tickets) # payout elif len(tickets) == 6: log.info('payout!') # reset the pool assert self.lottery_pool.qsize() == 6 self.lottery_pool = Queue() # add new participant tickets.append(transfer) # choose the winner transfer = random.choice(tickets) echo_amount = 49 else: self.lottery_pool.put(transfer) else: log.debug( 'echo transfer received', initiator=pex(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, ) echo_amount = transfer.amount if echo_amount: log.debug( 'sending echo transfer', target=pex(transfer.initiator), amount=echo_amount, orig_identifier=transfer.identifier, echo_identifier=transfer.identifier + echo_amount, token_address=pex(self.token_address), num_handled_transfers=self.num_handled_transfers + 1, ) self.api.transfer( self.api.raiden.default_registry.address, self.token_address, echo_amount, transfer.initiator, identifier=transfer.identifier + echo_amount, ) self.num_handled_transfers += 1 def stop(self): self.stop_signal = True self.greenlets.append(self.echo_worker_greenlet) gevent.joinall(self.greenlets, raise_error=True)
class EchoNode: # pragma: no unittest def __init__(self, api: RaidenAPI, token_address: TokenAddress) -> None: assert isinstance(api, RaidenAPI) self.ready = Event() self.api = api self.token_address = token_address existing_channels = self.api.get_channel_list( api.raiden.default_registry.address, self.token_address ) open_channels = [ channel_state for channel_state in existing_channels if channel.get_status(channel_state) == ChannelState.STATE_OPENED ] if len(open_channels) == 0: token_proxy = self.api.raiden.proxy_manager.token(self.token_address) if not token_proxy.balance_of(self.api.raiden.address) > 0: raise ValueError( f"Not enough funds for echo node " f"{to_checksum_address(self.api.raiden.address)} for token " f"{to_checksum_address(self.token_address)}" ) # Using the balance of the node as funds funds = TokenAmount(token_proxy.balance_of(self.api.raiden.address)) self.api.token_network_connect( registry_address=self.api.raiden.default_registry.address, token_address=self.token_address, funds=funds, initial_channel_target=10, joinable_funds_target=0.5, ) self.num_seen_events = 0 self.received_transfers: Queue[EventPaymentReceivedSuccess] = Queue() # This is used to signal REMOVE_CALLBACK and stop echo_workers self.stop_signal: Optional[bool] = None self.greenlets: Set[Greenlet] = set() self.lock = BoundedSemaphore() self.seen_transfers: Deque[EventPaymentReceivedSuccess] = deque(list(), TRANSFER_MEMORY) self.num_handled_transfers = 0 self.lottery_pool = Queue() # register ourselves with the raiden alarm task self.api.raiden.alarm.register_callback(self.echo_node_alarm_callback) self.echo_worker_greenlet = gevent.spawn(self.echo_worker) log.info("Echo node started") def echo_node_alarm_callback(self, block: Dict[str, Any]) -> Union[object, bool]: """ This can be registered with the raiden AlarmTask. If `EchoNode.stop()` is called, it will give the return signal to be removed from the AlarmTask callbacks. """ if not self.ready.is_set(): self.ready.set() log.debug( "echo_node callback", node=to_checksum_address(self.api.address), block_number=block["number"], ) if self.stop_signal is not None: return REMOVE_CALLBACK else: self.greenlets.add(gevent.spawn(self.poll_all_received_events)) return True def poll_all_received_events(self) -> None: """ This will be triggered once for each `echo_node_alarm_callback`. It polls all channels for `EventPaymentReceivedSuccess` events, adds all new events to the `self.received_transfers` queue and respawns `self.echo_worker`, if it died. """ locked = False try: with Timeout(10): locked = self.lock.acquire(blocking=False) if not locked: return else: received_transfers: List[Event] = self.api.get_raiden_events_payment_history( token_address=self.token_address, offset=self.num_seen_events ) received_transfers = [ event for event in received_transfers if type(event) == EventPaymentReceivedSuccess ] for event in received_transfers: transfer = copy.deepcopy(event) self.received_transfers.put(transfer) # set last_poll_block after events are enqueued (timeout safe) if received_transfers: self.num_seen_events += len(received_transfers) if not bool(self.echo_worker_greenlet): log.debug( "Restarting echo_worker_greenlet", node=to_checksum_address(self.api.address), dead=self.echo_worker_greenlet.dead, successful=self.echo_worker_greenlet.successful(), exception=self.echo_worker_greenlet.exception, ) self.echo_worker_greenlet = gevent.spawn(self.echo_worker) except Timeout: log.info("Timeout while polling for events") finally: if locked: self.lock.release() def echo_worker(self) -> None: """ The `echo_worker` works through the `self.received_transfers` queue and spawns `self.on_transfer` greenlets for all not-yet-seen transfers. """ log.debug("echo worker", qsize=self.received_transfers.qsize()) while self.stop_signal is None: if self.received_transfers.qsize() > 0: transfer = self.received_transfers.get() if transfer in self.seen_transfers: log.debug( "Duplicate transfer ignored", node=to_checksum_address(self.api.address), initiator=to_checksum_address(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, ) else: self.seen_transfers.append(transfer) self.greenlets.add(gevent.spawn(self.on_transfer, transfer)) else: gevent.sleep(0.5) def on_transfer(self, transfer: EventPaymentReceivedSuccess) -> None: """ This handles the echo logic, as described in https://github.com/raiden-network/raiden/issues/651: - for transfers with an amount that satisfies `amount % 3 == 0`, it sends a transfer with an amount of `amount - 1` back to the initiator - for transfers with a "lucky number" amount `amount == 7` it does not send anything back immediately -- after having received "lucky number transfers" from 7 different addresses it sends a transfer with `amount = 49` to one randomly chosen one (from the 7 lucky addresses) - consecutive entries to the lucky lottery will receive the current pool size as the `echo_amount` - for all other transfers it sends a transfer with the same `amount` back to the initiator """ echo_amount = PaymentAmount(0) if transfer.amount % 3 == 0: log.info( "Received amount divisible by three", node=to_checksum_address(self.api.address), initiator=to_checksum_address(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, ) echo_amount = PaymentAmount(transfer.amount - 1) elif transfer.amount == 7: log.info( "Received lottery entry", node=to_checksum_address(self.api.address), initiator=to_checksum_address(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, poolsize=self.lottery_pool.qsize(), ) # obtain a local copy of the pool pool = self.lottery_pool.copy() tickets = [pool.get() for _ in range(pool.qsize())] assert pool.empty() del pool if any(ticket.initiator == transfer.initiator for ticket in tickets): assert transfer not in tickets log.debug( "Duplicate lottery entry", node=to_checksum_address(self.api.address), initiator=to_checksum_address(transfer.initiator), identifier=transfer.identifier, poolsize=len(tickets), ) # signal the poolsize to the participant echo_amount = PaymentAmount(len(tickets)) # payout elif len(tickets) == 6: log.info("Payout!") # reset the pool assert self.lottery_pool.qsize() == 6 self.lottery_pool = Queue() # add the new participant tickets.append(transfer) # choose the winner transfer = random.choice(tickets) echo_amount = PaymentAmount(49) else: self.lottery_pool.put(transfer) else: log.debug( "Received transfer", node=to_checksum_address(self.api.address), initiator=to_checksum_address(transfer.initiator), amount=transfer.amount, identifier=transfer.identifier, ) echo_amount = PaymentAmount(transfer.amount) if echo_amount: echo_identifier = PaymentID(transfer.identifier + echo_amount) log.debug( "Sending echo transfer", node=to_checksum_address(self.api.address), target=to_checksum_address(transfer.initiator), amount=echo_amount, original_identifier=transfer.identifier, echo_identifier=echo_identifier, token_address=to_checksum_address(self.token_address), num_handled_transfers=self.num_handled_transfers + 1, ) self.api.transfer( registry_address=self.api.raiden.default_registry.address, token_address=self.token_address, amount=echo_amount, target=TargetAddress(transfer.initiator), identifier=echo_identifier, ) self.num_handled_transfers += 1 def stop(self) -> None: self.stop_signal = True self.greenlets.add(self.echo_worker_greenlet) gevent.joinall(self.greenlets, raise_error=True)