def main( private_key: str, state_db: str, web3: Web3, contracts: Dict[str, Contract], start_block: BlockNumber, rdn_per_eth: float, expire_within: Timestamp, ) -> None: pfs_address = private_key_to_address(private_key) chain_id = ChainID(web3.eth.chain_id) database = PFSDatabase(filename=state_db, chain_id=chain_id, pfs_address=pfs_address, sync_start_block=start_block) claim_cost_rdn = calc_claim_cost_rdn(web3, rdn_per_eth) time_now = get_posix_utc_time_now() ious = list( get_claimable_ious( database, claimable_until_after=time_now, claimable_until_before=Timestamp(time_now + expire_within), claim_cost_rdn=claim_cost_rdn, )) print(f"Found {len(ious)} claimable IOUs") _, failures = claim_ious(ious, claim_cost_rdn, contracts[CONTRACT_ONE_TO_N], web3, database) if failures: sys.exit(1)
def test_channel_closed_event_handler_closes_existing_channel(context: Context): context = setup_state_with_open_channel(context) current_block = get_posix_utc_time_now() // 15 event = ReceiveChannelClosedEvent( token_network_address=DEFAULT_TOKEN_NETWORK_ADDRESS, channel_identifier=DEFAULT_CHANNEL_IDENTIFIER, closing_participant=DEFAULT_PARTICIPANT2, block_number=BlockNumber(current_block + 1), ) channel_closed_event_handler(event, context) assert context.database.channel_count() == 1 assert_channel_state(context, ChannelState.CLOSED) event = ReceiveChannelClosedEvent( token_network_address=DEFAULT_TOKEN_NETWORK_ADDRESS, channel_identifier=DEFAULT_CHANNEL_IDENTIFIER, closing_participant=DEFAULT_PARTICIPANT2, block_number=BlockNumber(current_block), ) channel_closed_event_handler(event, context) # ActionMonitoringTriggeredEvent has been triggered assert context.database.scheduled_event_count() == 1 assert context.database.channel_count() == 1 assert_channel_state(context, ChannelState.CLOSED)
def test_trigger_scheduled_events(monitoring_service: MonitoringService): monitoring_service.context.required_confirmations = 5 create_default_token_network(monitoring_service.context) triggered_event = ActionMonitoringTriggeredEvent( token_network_address=DEFAULT_TOKEN_NETWORK_ADDRESS, channel_identifier=make_channel_identifier(), non_closing_participant=make_address(), ) trigger_timestamp = Timestamp(get_posix_utc_time_now()) assert len( monitoring_service.database.get_scheduled_events( trigger_timestamp)) == 0 monitoring_service.context.database.upsert_scheduled_event( ScheduledEvent(trigger_timestamp=trigger_timestamp, event=triggered_event)) assert len( monitoring_service.database.get_scheduled_events( trigger_timestamp)) == 1 # Now run `_trigger_scheduled_events` and see if the event is removed monitoring_service._trigger_scheduled_events() # pylint: disable=protected-access assert len( monitoring_service.database.get_scheduled_events( trigger_timestamp)) == 0
def test_payment_with_new_iou_rejected( # pylint: disable=too-many-locals api_sut, api_url: str, addresses: List[Address], token_network_model: TokenNetwork, make_iou: Callable, ): """Regression test for https://github.com/raiden-network/raiden-services/issues/624""" initiator_address = to_checksum_address(addresses[0]) target_address = to_checksum_address(addresses[1]) url = api_url + "/v1/" + to_checksum_address(token_network_model.address) + "/paths" default_params = {"from": initiator_address, "to": target_address, "value": 5, "max_paths": 3} def request_path_with(status_code=400, **kwargs): params = default_params.copy() params.update(kwargs) response = requests.post(url, json=params) assert response.status_code == status_code, response.json() return response # test with payment api_sut.service_fee = 100 sender = PrivateKey(get_random_privkey()) iou = make_iou( sender, api_sut.pathfinding_service.address, one_to_n_address=api_sut.one_to_n_address, amount=100, claimable_until=Timestamp(get_posix_utc_time_now() + 1_234_567), ) first_iou_dict = iou.Schema().dump(iou) second_iou = make_iou( sender, api_sut.pathfinding_service.address, one_to_n_address=api_sut.one_to_n_address, amount=200, claimable_until=Timestamp(get_posix_utc_time_now() + 1_234_568), ) second_iou_dict = second_iou.Schema().dump(second_iou) response = request_path_with(status_code=200, iou=first_iou_dict) assert response.status_code == 200 response = request_path_with(iou=second_iou_dict) assert response.json()["error_code"] == exceptions.UseThisIOU.error_code
def test_action_monitoring_rescheduling_when_user_lacks_funds(context: Context): reward_amount = TokenAmount(10) context = setup_state_with_closed_channel(context) context.database.upsert_monitor_request( create_signed_monitor_request(nonce=Nonce(6), reward_amount=reward_amount) ) event = ActionMonitoringTriggeredEvent( token_network_address=DEFAULT_TOKEN_NETWORK_ADDRESS, channel_identifier=DEFAULT_CHANNEL_IDENTIFIER, non_closing_participant=DEFAULT_PARTICIPANT2, ) scheduled_events_before = context.database.get_scheduled_events( max_trigger_timestamp=Timestamp(get_posix_utc_time_now()) ) # Try to call monitor when the user has insufficient funds with patch("monitoring_service.handlers.get_pessimistic_udc_balance", Mock(return_value=0)): action_monitoring_triggered_event_handler(event, context) assert not context.monitoring_service_contract.functions.monitor.called # Now the event must have been rescheduled # TODO: check that the event is rescheduled to trigger at the right block scheduled_events_after = context.database.get_scheduled_events( max_trigger_timestamp=Timestamp(get_posix_utc_time_now()) ) new_events = set(scheduled_events_after) - set(scheduled_events_before) assert len(new_events) == 1 assert new_events.pop().event == event # With sufficient funds it must succeed with patch( "monitoring_service.handlers.get_pessimistic_udc_balance", Mock(return_value=reward_amount * UDC_SECURITY_MARGIN_FACTOR_MS), ): action_monitoring_triggered_event_handler(event, context) assert context.monitoring_service_contract.functions.monitor.called
def test_channel_closed_event_handler_trigger_action_monitor_event_without_monitor_request( context: Context, ): context = setup_state_with_open_channel(context) current_block_number = get_posix_utc_time_now() // 15 event = ReceiveChannelClosedEvent( token_network_address=DEFAULT_TOKEN_NETWORK_ADDRESS, channel_identifier=DEFAULT_CHANNEL_IDENTIFIER, closing_participant=DEFAULT_PARTICIPANT2, block_number=BlockNumber(current_block_number + 1), ) channel_closed_event_handler(event, context) assert context.database.scheduled_event_count() == 1
def process_payment( # pylint: disable=too-many-branches iou: Optional[IOU], pathfinding_service: PathfindingService, service_fee: TokenAmount, one_to_n_address: Address, ) -> None: if service_fee == 0: if iou is not None: log.debug( "Discarding IOU, service fee is 0", sender=to_checksum_address(iou.sender), total_amount=iou.amount, claimable_until=iou.claimable_until, ) else: log.debug("No IOU and service fee is 0") return if iou is None: raise exceptions.MissingIOU log.debug( "Checking IOU", sender=to_checksum_address(iou.sender), total_amount=iou.amount, claimable_until=iou.claimable_until, ) # Basic IOU validity checks if not is_same_address(iou.receiver, pathfinding_service.address): raise exceptions.WrongIOURecipient(expected=pathfinding_service.address) if iou.chain_id != pathfinding_service.chain_id: raise exceptions.UnsupportedChainID(expected=pathfinding_service.chain_id) if iou.one_to_n_address != one_to_n_address: raise exceptions.WrongOneToNAddress(expected=one_to_n_address, got=iou.one_to_n_address) if not iou.is_signature_valid(): raise exceptions.InvalidSignature # Compare with known IOU latest_block = pathfinding_service.blockchain_state.latest_committed_block active_iou = pathfinding_service.database.get_iou(sender=iou.sender, claimed=False) if active_iou: if active_iou.claimable_until != iou.claimable_until: raise exceptions.UseThisIOU(iou=active_iou.Schema().dump(active_iou)) expected_amount = active_iou.amount + service_fee else: claimed_iou = pathfinding_service.database.get_iou( sender=iou.sender, claimable_until=iou.claimable_until, claimed=True ) if claimed_iou: raise exceptions.IOUAlreadyClaimed min_expiry = get_posix_utc_time_now() + MIN_IOU_EXPIRY if iou.claimable_until < min_expiry: raise exceptions.IOUExpiredTooEarly( min_expiry=min_expiry, claimable_until_received=iou.claimable_until ) expected_amount = service_fee if iou.amount < expected_amount: raise exceptions.InsufficientServicePayment( expected_amount=expected_amount, actual_amount=iou.amount ) # Check client's deposit in UserDeposit contract udc = pathfinding_service.user_deposit_contract udc_balance = get_pessimistic_udc_balance( udc=udc, address=iou.sender, from_block=BlockNumber(latest_block - pathfinding_service.required_confirmations), to_block=latest_block, ) required_deposit = round(expected_amount * UDC_SECURITY_MARGIN_FACTOR_PFS) if udc_balance < required_deposit: raise exceptions.DepositTooLow( required_deposit=required_deposit, seen_deposit=udc_balance, block_number=latest_block ) log.info( "Received service fee", sender=iou.sender, expected_amount=expected_amount, total_amount=iou.amount, added_amount=expected_amount - service_fee, ) # Save latest IOU iou.claimed = False pathfinding_service.database.upsert_iou(iou)
def action_monitoring_triggered_event_handler(event: Event, context: Context) -> None: assert isinstance(event, ActionMonitoringTriggeredEvent) log.info("Triggering channel monitoring") monitor_request = context.database.get_monitor_request( token_network_address=event.token_network_address, channel_id=event.channel_identifier, non_closing_signer=event.non_closing_participant, ) if monitor_request is None: log.error( "MonitorRequest cannot be found", token_network_address=event.token_network_address, channel_id=event.channel_identifier, ) metrics.get_metrics_for_label(metrics.ERRORS_LOGGED, metrics.ErrorCategory.STATE).inc() return channel = context.database.get_channel( token_network_address=monitor_request.token_network_address, channel_id=monitor_request.channel_identifier, ) if channel is None: log.error("Channel cannot be found", monitor_request=monitor_request) metrics.get_metrics_for_label(metrics.ERRORS_LOGGED, metrics.ErrorCategory.STATE).inc() return if not _is_mr_valid(monitor_request, channel): log.error( "MonitorRequest lost its validity", monitor_request=monitor_request, channel=channel, ) metrics.get_metrics_for_label(metrics.ERRORS_LOGGED, metrics.ErrorCategory.PROTOCOL).inc() return last_onchain_nonce = 0 if channel.update_status: last_onchain_nonce = channel.update_status.nonce if monitor_request.nonce <= last_onchain_nonce: log.info( "Another MS submitted the last known channel state", monitor_request=monitor_request, ) return user_deposit = get_pessimistic_udc_balance( udc=context.user_deposit_contract, address=monitor_request.non_closing_signer, from_block=context.latest_confirmed_block, to_block=context.get_latest_unconfirmed_block(), ) if monitor_request.reward_amount < context.min_reward: log.info( "Monitor request not executed due to insufficient reward amount", monitor_request=monitor_request, min_reward=context.min_reward, ) return if user_deposit < monitor_request.reward_amount * UDC_SECURITY_MARGIN_FACTOR_MS: log.debug( "User deposit is insufficient -> try monitoring again later", monitor_request=monitor_request, min_reward=context.min_reward, ) context.database.upsert_scheduled_event( ScheduledEvent(trigger_timestamp=get_posix_utc_time_now(), event=event)) return assert (channel.monitor_tx_hash is None ), "This MS already monitored this channel. Should be impossible." try: # Attackers might be able to construct MRs that make this fail. # Since we execute a gas estimation before doing the `transact`, # the gas estimation will fail before any gas is used. # If we stop doing a gas estimation, a `call` has to be done before # the `transact` to prevent attackers from wasting the MS's gas. tx_hash = TransactionHash( bytes( context.monitoring_service_contract.functions.monitor( monitor_request.signer, monitor_request.non_closing_signer, monitor_request.balance_hash, monitor_request.nonce, monitor_request.additional_hash, monitor_request.closing_signature, monitor_request.non_closing_signature, monitor_request.reward_amount, monitor_request.token_network_address, monitor_request.reward_proof_signature, ).transact({"from": context.ms_state.address}))) except Exception as exc: # pylint: disable=broad-except error_message = exc.args[0] if len(exc.args) > 0 else "" if "not allowed to monitor" in error_message: raise TransactionTooEarlyException first_allowed = _first_allowed_timestamp_to_monitor( event.token_network_address, channel, context) failed_at = context.web3.eth.block_number log.error( "Sending tx failed", exc_info=True, err=exc, first_allowed=first_allowed, failed_at=failed_at, ) metrics.get_metrics_for_label(metrics.ERRORS_LOGGED, metrics.ErrorCategory.BLOCKCHAIN).inc() return log.info( "Sent transaction calling `monitor` for channel", token_network_address=channel.token_network_address, channel_identifier=channel.identifier, transaction_hash=encode_hex(tx_hash), ) assert tx_hash is not None with context.database.conn: # Add tx hash to list of waiting transactions context.database.add_waiting_transaction(tx_hash) channel.monitor_tx_hash = tx_hash context.database.upsert_channel(channel)
def channel_closed_event_handler(event: Event, context: Context) -> None: assert isinstance(event, ReceiveChannelClosedEvent) channel = context.database.get_channel(event.token_network_address, event.channel_identifier) if channel is None: log.error( "Channel not in database", token_network_address=event.token_network_address, identifier=event.channel_identifier, ) metrics.get_metrics_for_label(metrics.ERRORS_LOGGED, metrics.ErrorCategory.STATE).inc() return # Check if the settle timeout is already over. # This is important when starting up the MS. timestamp_of_closing_block = Timestamp( context.web3.eth.get_block( event.block_number).timestamp # type: ignore ) settle_timeout = context.database.get_token_network_settle_timeout( event.token_network_address) settleable_after = Timestamp(timestamp_of_closing_block + settle_timeout) update_balance_proof_period_is_over = settleable_after < get_posix_utc_time_now( ) if not update_balance_proof_period_is_over: # Trigger the monitoring action event handler, this will check if a # valid MR is available. # This enables the client to send a late MR # also see https://github.com/raiden-network/raiden-services/issues/29 if channel.participant1 == event.closing_participant: non_closing_participant = channel.participant2 else: non_closing_participant = channel.participant1 # Transactions go into the next mined block, so we could trigger one block # before the `monitor` call is allowed to succeed to include it in the # first possible block. # Unfortunately, parity does the gas estimation on the current block # instead of the next one, so we have to wait for the first allowed # block to be finished to send the transaction successfully on parity. trigger_timestamp = _first_allowed_timestamp_to_monitor( event.token_network_address, channel, context) triggered_event = ActionMonitoringTriggeredEvent( token_network_address=channel.token_network_address, channel_identifier=channel.identifier, non_closing_participant=non_closing_participant, ) log.info( "Channel closed, triggering monitoring check", token_network_address=event.token_network_address, identifier=channel.identifier, scheduled_event=triggered_event, trigger_timestamp=trigger_timestamp, ) # Add scheduled event if it not exists yet. If the event is already # scheduled (e.g. after a restart) the DB takes care that it is only # stored once. context.database.upsert_scheduled_event( ScheduledEvent(trigger_timestamp=trigger_timestamp, event=triggered_event)) else: log.warning( "Update balance proof period is in the past, skipping", token_network_address=event.token_network_address, identifier=channel.identifier, settleable_after=settleable_after, latest_committed_block=context.latest_committed_block, latest_confirmed_block=context.latest_confirmed_block, ) channel.state = ChannelState.CLOSED channel.closing_block = event.block_number channel.closing_participant = event.closing_participant context.database.upsert_channel(channel)