示例#1
0
def test_get_appointment_in_responder(api, client, appointment):
    # Mock the appointment in the Responder
    tracker_data = {
        "locator": appointment.locator,
        "dispute_txid": get_random_value_hex(32),
        "penalty_txid": get_random_value_hex(32),
        "penalty_rawtx": get_random_value_hex(250),
        "user_id": get_random_value_hex(16),
    }
    tx_tracker = TransactionTracker.from_dict(tracker_data)

    uuid = hash_160("{}{}".format(appointment.locator, user_id))
    api.watcher.db_manager.create_triggered_appointment_flag(uuid)
    api.watcher.responder.db_manager.store_responder_tracker(uuid, tx_tracker.to_dict())

    # Request back the data
    message = "get appointment {}".format(appointment.locator)
    signature = Cryptographer.sign(message.encode("utf-8"), user_sk)
    data = {"locator": appointment.locator, "signature": signature}

    # Next we can request it
    r = client.post(get_appointment_endpoint, json=data)
    assert r.status_code == HTTP_OK

    # Check that the appointment is on the Responder
    assert r.json.get("status") == "dispute_responded"

    # Check the the sent appointment matches the received one
    assert tx_tracker.locator == r.json.get("locator")
    assert tx_tracker.dispute_txid == r.json.get("appointment").get("dispute_txid")
    assert tx_tracker.penalty_txid == r.json.get("appointment").get("penalty_txid")
    assert tx_tracker.penalty_rawtx == r.json.get("appointment").get("penalty_rawtx")
示例#2
0
def test_get_appointment(watcher, generate_dummy_appointment,
                         generate_dummy_tracker, monkeypatch):
    # Get appointment should return back data as long a the user does have the requested appointment
    locator = get_random_value_hex(32)
    signature = get_random_value_hex(71)
    uuid = hash_160("{}{}".format(locator, user_id))

    # Mock the user being registered
    monkeypatch.setattr(watcher.gatekeeper, "authenticate_user",
                        lambda x, y: user_id)
    monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired",
                        lambda x: (False, 1))

    # The appointment can either be in the Watcher of the Responder, mock the former case
    appointment = generate_dummy_appointment()
    monkeypatch.setitem(watcher.appointments, uuid, appointment)
    monkeypatch.setattr(watcher.db_manager, "load_watcher_appointment",
                        lambda x: appointment.to_dict())

    # Request and check
    appointment_data, status = watcher.get_appointment(locator, signature)
    assert appointment_data == appointment.to_dict()
    assert status == AppointmentStatus.BEING_WATCHED

    # Do the same for the appointment being in the Responder
    monkeypatch.delitem(watcher.appointments, uuid)
    tracker = generate_dummy_tracker()
    monkeypatch.setattr(watcher.responder, "has_tracker", lambda x: True)
    monkeypatch.setattr(watcher.db_manager, "load_responder_tracker",
                        lambda x: tracker.to_dict())

    # Request and check
    tracker_data, status = watcher.get_appointment(locator, signature)
    assert tracker_data == tracker.to_dict()
    assert status == AppointmentStatus.DISPUTE_RESPONDED
示例#3
0
def test_get_appointment_in_responder(internal_api, client,
                                      generate_dummy_tracker, monkeypatch):
    tx_tracker = generate_dummy_tracker()

    # Mock the appointment in the Responder
    uuid = hash_160("{}{}".format(tx_tracker.locator, user_id))
    internal_api.watcher.responder.trackers[uuid] = tx_tracker.get_summary()
    internal_api.watcher.responder.db_manager.store_responder_tracker(
        uuid, tx_tracker.to_dict())

    # mock the gatekeeper (user won't be registered if the previous tests weren't ran)
    monkeypatch.setattr(internal_api.watcher.gatekeeper, "authenticate_user",
                        mock_authenticate_user)

    # Request back the data
    message = "get appointment {}".format(tx_tracker.locator)
    signature = Cryptographer.sign(message.encode("utf-8"), user_sk)
    data = {"locator": tx_tracker.locator, "signature": signature}

    # Next we can request it
    r = client.post(get_appointment_endpoint, json=data)
    assert r.status_code == HTTP_OK

    # Check that the appointment is on the Responder
    assert r.json.get("status") == AppointmentStatus.DISPUTE_RESPONDED

    # Check the the sent appointment matches the received one
    assert tx_tracker.locator == r.json.get("locator")
    assert tx_tracker.dispute_txid == r.json.get("appointment").get(
        "dispute_txid")
    assert tx_tracker.penalty_txid == r.json.get("appointment").get(
        "penalty_txid")
    assert tx_tracker.penalty_rawtx == r.json.get("appointment").get(
        "penalty_rawtx")
示例#4
0
def test_get_appointment_in_watcher(internal_api, client, appointment,
                                    monkeypatch):
    # Mock the appointment in the Watcher
    uuid = hash_160("{}{}".format(appointment.locator, user_id))
    extended_appointment_summary = {
        "locator": appointment.locator,
        "user_id": user_id
    }
    internal_api.watcher.appointments[uuid] = extended_appointment_summary
    internal_api.watcher.db_manager.store_watcher_appointment(
        uuid, appointment.to_dict())

    # mock the gatekeeper (user won't be registered if the previous tests weren't ran)
    monkeypatch.setattr(internal_api.watcher.gatekeeper, "authenticate_user",
                        mock_authenticate_user)

    # Next we can request it
    message = "get appointment {}".format(appointment.locator)
    signature = Cryptographer.sign(message.encode("utf-8"), user_sk)
    data = {"locator": appointment.locator, "signature": signature}
    r = client.post(get_appointment_endpoint, json=data)
    assert r.status_code == HTTP_OK

    # Check that the appointment is on the Watcher
    assert r.json.get("status") == AppointmentStatus.BEING_WATCHED

    # Cast the extended appointment (used by the tower) to a regular appointment (used by the user)
    appointment = Appointment.from_dict(appointment.to_dict())

    # Check the the sent appointment matches the received one
    assert r.json.get("locator") == appointment.locator
    assert appointment.to_dict() == r.json.get("appointment")
示例#5
0
def add_appointment(client, appointment_data, user_id):
    r = client.post(add_appointment_endpoint, json=appointment_data)

    if r.status_code == HTTP_OK:
        locator = appointment_data.get("appointment").get("locator")
        uuid = hash_160("{}{}".format(locator, user_id))
        appointments[uuid] = appointment_data["appointment"]

    return r
def test_add_appointment_already_triggered(internal_api, stub,
                                           generate_dummy_appointment):
    # If the appointment has already been trigger we should get ALREADY_EXISTS
    stub.register(RegisterRequest(user_id=user_id))

    appointment, _ = generate_dummy_appointment()
    appointment_uuid = hash_160("{}{}".format(appointment.locator, user_id))
    # Adding the uuid to the Responder trackers so the Watcher thinks it is in there. The data does not actually matters
    internal_api.watcher.responder.trackers[appointment_uuid] = {}
    appointment_signature = Cryptographer.sign(appointment.serialize(),
                                               user_sk)

    e = send_wrong_appointment(stub, appointment, appointment_signature)

    assert e.value.code() == grpc.StatusCode.ALREADY_EXISTS
    assert "The provided appointment has already been triggered" in e.value.details(
    )
示例#7
0
def test_get_appointment_in_watcher(api, client, appointment):
    # Mock the appointment in the Watcher
    uuid = hash_160("{}{}".format(appointment.locator, user_id))
    api.watcher.db_manager.store_watcher_appointment(uuid, appointment.to_dict())

    # Next we can request it
    message = "get appointment {}".format(appointment.locator)
    signature = Cryptographer.sign(message.encode("utf-8"), user_sk)
    data = {"locator": appointment.locator, "signature": signature}
    r = client.post(get_appointment_endpoint, json=data)
    assert r.status_code == HTTP_OK

    # Check that the appointment is on the Watcher
    assert r.json.get("status") == "being_watched"

    # Check the the sent appointment matches the received one
    appointment_dict = appointment.to_dict()
    appointment_dict.pop("user_id")
    assert r.json.get("locator") == appointment.locator
    assert appointment.to_dict() == r.json.get("appointment")
示例#8
0
    def get_appointment(self, locator, user_signature):
        """
        Gets information about an appointment.

        The appointment can either be in the watcher, the responder, or not found.

        Args:
            locator (:obj:`str`): a 16-byte hex-encoded value used by the tower to detect channel breaches.
            user_signature (:obj:`str`): the signature of the request by the user.

        Returns:
            :obj:`tuple`: A tuple containing the appointment data and the status, either
                ``AppointmentStatus.BEING_WATCHED`` or ``AppointmentStatus.DISPUTE_RESPONDED``

        Raises:
            :obj:`AppointmentNotFound`: if the appointment is not found in the tower.
            :obj:`SubscriptionExpired`: If the user subscription has expired.
        """

        message = "get appointment {}".format(locator).encode("utf-8")
        user_id = self.gatekeeper.authenticate_user(message, user_signature)
        has_expired, expiry = self.gatekeeper.has_subscription_expired(user_id)
        if has_expired:
            raise SubscriptionExpired(
                f"Your subscription expired at block {expiry}")

        uuid = hash_160("{}{}".format(locator, user_id))

        with self.rw_lock.gen_rlock():
            if uuid in self.appointments:
                appointment_data = self.db_manager.load_watcher_appointment(
                    uuid)
                status = AppointmentStatus.BEING_WATCHED
            elif self.responder.has_tracker(uuid):
                appointment_data = self.db_manager.load_responder_tracker(uuid)
                status = AppointmentStatus.DISPUTE_RESPONDED
            else:
                raise AppointmentNotFound("Cannot find {}".format(locator))

            return appointment_data, status
示例#9
0
    def add_appointment(self, appointment, user_signature):
        """
        Adds a new appointment to the ``appointments`` dictionary if ``max_appointments`` has not been reached.

        ``add_appointment`` is the entry point of the :obj:`Watcher`. Upon receiving a new appointment it will start
        monitoring the blockchain (``do_watch``) until ``appointments`` is empty.

        Once a breach is seen on the blockchain, the :obj:`Watcher` will decrypt the corresponding ``encrypted_blob``
        and pass the information to the :obj:`Responder <teos.responder.Responder>`.

        The tower may store multiple appointments with the same ``locator`` to avoid DoS attacks based on data
        rewriting. `locators`` should be derived from the ``dispute_txid``, but that task is performed by the user, and
        the tower has no way of verifying whether or not they have been properly derived. Therefore, appointments are
        identified by ``uuid`` and stored in ``appointments`` and ``locator_uuid_map``.

        Args:
            appointment (:obj:`Appointment <common.appointment.Appointment>`): the appointment to be added to the
                :obj:`Watcher`.
            user_signature (:obj:`str`): the user's appointment signature (hex-encoded).

        Returns:
            :obj:`dict`: The tower response as a dict, containing: ``locator``, ``signature``, ``available_slots`` and
            ``subscription_expiry``.

        Raises:
            :obj:`AppointmentLimitReached`: If the tower cannot hold more appointments (cap reached).
            :obj:`AuthenticationFailure`: If the user cannot be authenticated.
            :obj:`NotEnoughSlots`: If the user does not have enough available slots, so the appointment is rejected.
            :obj:`SubscriptionExpired`: If the user subscription has expired.
        """

        with self.rw_lock.gen_wlock():
            if len(self.appointments) >= self.max_appointments:
                message = "Maximum appointments reached, appointment rejected"
                self.logger.info(message, locator=appointment.locator)
                raise AppointmentLimitReached(message)

            user_id = self.gatekeeper.authenticate_user(
                appointment.serialize(), user_signature)
            has_subscription_expired, expiry = self.gatekeeper.has_subscription_expired(
                user_id)
            if has_subscription_expired:
                raise SubscriptionExpired(
                    f"Your subscription expired at block {expiry}")

            start_block = self.block_processor.get_block(
                self.last_known_block).get("height")
            extended_appointment = ExtendedAppointment(
                appointment.locator,
                appointment.encrypted_blob,
                appointment.to_self_delay,
                user_id,
                user_signature,
                start_block,
            )

            # The uuids are generated as the RIPEMD160(locator||user_pubkey).
            # If an appointment is requested by the user the uuid can be recomputed and queried straightaway (no maps).
            uuid = hash_160("{}{}".format(extended_appointment.locator,
                                          user_id))

            # If this is a copy of an appointment we've already reacted to, the new appointment is rejected.
            if self.responder.has_tracker(uuid):
                message = "Appointment already in Responder"
                self.logger.info(message)
                raise AppointmentAlreadyTriggered(message)

            # Add the appointment to the Gatekeeper
            available_slots = self.gatekeeper.add_update_appointment(
                user_id, uuid, extended_appointment)

            # Appointments that were triggered in blocks held in the cache
            dispute_txid = self.locator_cache.get_txid(
                extended_appointment.locator)
            if dispute_txid:
                try:
                    penalty_txid, penalty_rawtx = self.check_breach(
                        uuid, extended_appointment, dispute_txid)
                    receipt = self.responder.handle_breach(
                        uuid,
                        extended_appointment.locator,
                        dispute_txid,
                        penalty_txid,
                        penalty_rawtx,
                        user_id,
                        self.last_known_block,
                    )

                    # At this point the appointment is accepted but data is only kept if it goes through the Responder.
                    # Otherwise it is dropped.
                    if receipt.delivered:
                        self.db_manager.store_watcher_appointment(
                            uuid, extended_appointment.to_dict())
                        self.db_manager.create_append_locator_map(
                            extended_appointment.locator, uuid)
                        self.db_manager.create_triggered_appointment_flag(uuid)

                except (EncryptionError, InvalidTransactionFormat):
                    # If data inside the encrypted blob is invalid, the appointment is accepted but the data is dropped.
                    # (same as with data that bounces in the Responder). This reduces the appointment slot count so it
                    # could be used to discourage user misbehaviour.
                    pass

            # Regular appointments that have not been triggered (or, at least, not recently)
            else:
                self.appointments[uuid] = extended_appointment.get_summary()

                if extended_appointment.locator in self.locator_uuid_map:
                    # If the uuid is already in the map it means this is an update.
                    if uuid not in self.locator_uuid_map[
                            extended_appointment.locator]:
                        self.locator_uuid_map[
                            extended_appointment.locator].append(uuid)
                else:
                    # Otherwise two users have sent an appointment with the same locator, so we need to store both.
                    self.locator_uuid_map[extended_appointment.locator] = [
                        uuid
                    ]

                self.db_manager.store_watcher_appointment(
                    uuid, extended_appointment.to_dict())
                self.db_manager.create_append_locator_map(
                    extended_appointment.locator, uuid)

            try:
                signature = Cryptographer.sign(
                    receipts.create_appointment_receipt(
                        user_signature, start_block), self.signing_key)

            except (InvalidParameter, SignatureError):
                # This should never happen since data is sanitized, just in case to avoid a crash
                self.logger.error("Data couldn't be signed",
                                  appointment=extended_appointment.to_dict())
                signature = None

            self.logger.info("New appointment accepted",
                             locator=extended_appointment.locator)

            return {
                "locator":
                extended_appointment.locator,
                "start_block":
                extended_appointment.start_block,
                "signature":
                signature,
                "available_slots":
                available_slots,
                "subscription_expiry":
                self.gatekeeper.registered_users[user_id].subscription_expiry,
            }
示例#10
0
    def get_appointment(self):
        """
        Gives information about a given appointment state in the Watchtower.

        The information is requested by ``locator``.

        Returns:
            :obj:`str`: A json formatted dictionary containing information about the requested appointment.

            Returns not found if the user does not have the requested appointment or the locator is invalid.

            A ``status`` flag is added to the data provided by either the :obj:`Watcher <teos.watcher.Watcher>` or the
            :obj:`Responder <teos.responder.Responder>` that signals the status of the appointment.

            - Appointments hold by the :obj:`Watcher <teos.watcher.Watcher>` are flagged as ``being_watched``.
            - Appointments hold by the :obj:`Responder <teos.responder.Responder>` are flagged as ``dispute_triggered``.
            - Unknown appointments are flagged as ``not_found``.
        """

        # Getting the real IP if the server is behind a reverse proxy
        remote_addr = get_remote_addr()

        # Check that data type and content are correct. Abort otherwise.
        try:
            request_data = get_request_data_json(request)

        except InvalidParameter as e:
            logger.info("Received invalid get_appointment request",
                        from_addr="{}".format(remote_addr))
            return jsonify({
                "error": str(e),
                "error_code": errors.INVALID_REQUEST_FORMAT
            }), HTTP_BAD_REQUEST

        locator = request_data.get("locator")

        try:
            self.inspector.check_locator(locator)
            logger.info("Received get_appointment request",
                        from_addr="{}".format(remote_addr),
                        locator=locator)

            message = "get appointment {}".format(locator).encode()
            signature = request_data.get("signature")
            user_id = self.watcher.gatekeeper.authenticate_user(
                message, signature)

            triggered_appointments = self.watcher.db_manager.load_all_triggered_flags(
            )
            uuid = hash_160("{}{}".format(locator, user_id))

            # If the appointment has been triggered, it should be in the locator (default else just in case).
            if uuid in triggered_appointments:
                appointment_data = self.watcher.db_manager.load_responder_tracker(
                    uuid)
                if appointment_data:
                    rcode = HTTP_OK
                    # Remove user_id field from appointment data since it is an internal field
                    appointment_data.pop("user_id")
                    response = {
                        "locator": locator,
                        "status": "dispute_responded",
                        "appointment": appointment_data
                    }
                else:
                    rcode = HTTP_NOT_FOUND
                    response = {"locator": locator, "status": "not_found"}

            # Otherwise it should be either in the watcher, or not in the system.
            else:
                appointment_data = self.watcher.db_manager.load_watcher_appointment(
                    uuid)
                if appointment_data:
                    rcode = HTTP_OK
                    # Remove user_id field from appointment data since it is an internal field
                    appointment_data.pop("user_id")
                    response = {
                        "locator": locator,
                        "status": "being_watched",
                        "appointment": appointment_data
                    }
                else:
                    rcode = HTTP_NOT_FOUND
                    response = {"locator": locator, "status": "not_found"}

        except (InspectionFailed, AuthenticationFailure):
            rcode = HTTP_NOT_FOUND
            response = {"locator": locator, "status": "not_found"}

        return jsonify(response), rcode
示例#11
0
def test_do_watch(watcher, generate_dummy_appointment_w_trigger, monkeypatch):
    # do_watch creates a thread in charge of watching for breaches. It also triggers data deletion when necessary, based
    # in the block height of the received blocks.
    # Test the following:
    # - Adding transactions to the Watcher and trigger them
    # - Check triggered appointments are removed from the Watcher
    # - Outdate appointments and check they are also removed

    # Mock the user being registered
    expiry = 100
    user_info = UserInfo(MAX_APPOINTMENTS, expiry)
    monkeypatch.setattr(watcher.gatekeeper, "authenticate_user",
                        lambda x, y: user_id)
    monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired",
                        lambda x: (False, expiry))
    monkeypatch.setattr(watcher.gatekeeper, "get_user_info",
                        lambda x: user_info)

    # Mock the interactions with the Gatekeeper
    monkeypatch.setattr(watcher.gatekeeper, "get_outdated_appointments",
                        lambda x: [])
    monkeypatch.setattr(watcher.responder, "handle_breach", mock_receipt_true)

    # Add the appointments to the tower. We add them instead of mocking to avoid having to mock all the data structures
    # plus database
    commitment_txids = []
    triggered_valid = []
    for i in range(APPOINTMENTS):
        appointment, commitment_txid = generate_dummy_appointment_w_trigger()
        watcher.add_appointment(appointment, appointment.user_signature)
        commitment_txids.append(commitment_txid)

        uuid = hash_160("{}{}".format(appointment.locator, user_id))
        if i < 2:
            triggered_valid.append(uuid)

    # Start the watching thread
    do_watch_thread = Thread(target=watcher.do_watch, daemon=True)
    do_watch_thread.start()

    # Mock a new block with the two first triggers in it
    block_id = get_random_value_hex(32)
    block = {"tx": commitment_txids[:2], "height": 1}
    monkeypatch.setattr(watcher.block_processor, "get_block",
                        lambda x, blocking: block)
    watcher.block_queue.put(block_id)
    time.sleep(0.2)

    # After generating a block, the appointment count should have been reduced by 2 (two breaches)
    assert len(watcher.appointments) == APPOINTMENTS - 2

    # This two first should have gone to the Responder, we can check the trigger flags to validate
    assert set(
        watcher.db_manager.load_all_triggered_flags()) == set(triggered_valid)

    # Mock two more transactions being triggered, this time with invalid data
    monkeypatch.setattr(watcher.responder, "handle_breach", mock_receipt_false)
    block_id = get_random_value_hex(32)
    block = {"tx": commitment_txids[2:4], "height": 2}
    monkeypatch.setattr(watcher.block_processor, "get_block",
                        lambda x, blocking: block)
    watcher.block_queue.put(block_id)
    time.sleep(0.2)

    # Two more appointments should be gone but none of them should have gone trough the Responder
    assert len(watcher.appointments) == APPOINTMENTS - 4
    # Check the triggers are the same as before
    assert set(
        watcher.db_manager.load_all_triggered_flags()) == set(triggered_valid)

    # The rest of appointments will timeout after the subscription timesout
    monkeypatch.setattr(watcher.gatekeeper, "get_outdated_appointments",
                        lambda x: list(watcher.appointments.keys()))
    mock_generate_blocks(1, {}, watcher.block_queue)
    assert len(watcher.appointments) == 0