Beispiel #1
0
def test_from_dict(ext_appointment_data):
    # The appointment should be build if we don't miss any field
    ext_appointment = ExtendedAppointment.from_dict(ext_appointment_data)
    assert isinstance(ext_appointment, ExtendedAppointment)

    # Otherwise it should fail
    for key in ext_appointment_data.keys():
        prev_val = ext_appointment_data[key]
        ext_appointment_data[key] = None

        with pytest.raises(ValueError, match="Wrong appointment data"):
            ExtendedAppointment.from_dict(ext_appointment_data)
            ext_appointment_data[key] = prev_val
Beispiel #2
0
    def build_appointments(appointments_data):
        """
        Builds an appointments dictionary (``uuid:ExtendedAppointment``) and a locator_uuid_map (``locator:uuid``)
        given a dictionary of appointments from the database.

        Args:
            appointments_data (:obj:`dict`): a dictionary of dictionaries representing all the
                :obj:`Watcher <teos.watcher.Watcher>` appointments stored in the database. The structure is as follows:

                    ``{uuid: {locator: str, ...}, uuid: {locator:...}}``

        Returns:
            :obj:`tuple`: A tuple with two dictionaries. ``appointments`` containing the appointment information in
            :obj:`ExtendedAppointment <teos.extended_appointment.ExtendedAppointment>` objects and ``locator_uuid_map``
            containing a map of appointment (``uuid:locator``).
        """

        appointments = {}
        locator_uuid_map = {}

        for uuid, data in appointments_data.items():
            appointment = ExtendedAppointment.from_dict(data)
            appointments[uuid] = appointment.get_summary()

            if appointment.locator in locator_uuid_map:
                locator_uuid_map[appointment.locator].append(uuid)

            else:
                locator_uuid_map[appointment.locator] = [uuid]

        return appointments, locator_uuid_map
Beispiel #3
0
def test_add_appointment_in_cache_invalid_transaction(api, client):
    api.watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=0)

    # We need to create the appointment manually
    dispute_tx = create_dummy_transaction()
    dispute_txid = dispute_tx.tx_id.hex()
    penalty_tx = create_dummy_transaction(dispute_txid)

    locator = compute_locator(dispute_txid)
    dummy_appointment_data = {"tx": penalty_tx.hex(), "tx_id": dispute_txid, "to_self_delay": 20}
    encrypted_blob = Cryptographer.encrypt(dummy_appointment_data.get("tx")[::-1], dummy_appointment_data.get("tx_id"))

    appointment_data = {
        "locator": locator,
        "to_self_delay": dummy_appointment_data.get("to_self_delay"),
        "encrypted_blob": encrypted_blob,
        "user_id": get_random_value_hex(16),
    }

    appointment = ExtendedAppointment.from_dict(appointment_data)
    api.watcher.locator_cache.cache[appointment.locator] = dispute_tx.tx_id.hex()
    appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk)

    # Add the data to the cache
    api.watcher.locator_cache.cache[dispute_txid] = appointment.locator

    # The appointment should be accepted
    r = add_appointment(client, {"appointment": appointment.to_dict(), "signature": appointment_signature}, user_id)
    assert (
        r.status_code == HTTP_OK
        and r.json.get("available_slots") == 0
        and r.json.get("start_block") == api.watcher.last_known_block
    )
Beispiel #4
0
    def filter_breaches(self, breaches):
        """
        Filters the valid from the invalid channel breaches.

        The :obj:`Watcher` cannot know if an ``encrypted_blob`` contains a valid transaction until a breach is seen.
        Blobs that contain arbitrary data are dropped and not sent to the :obj:`Responder <teos.responder.Responder>`.

        Args:
            breaches (:obj:`dict`): a dictionary containing channel breaches (``locator:txid``).

        Returns:
            :obj:`tuple`: A dictionary and a list. The former contains the valid breaches, while the latter contain the
            invalid ones.

            The valid breaches dictionary has the following structure:

            ``{locator, dispute_txid, penalty_txid, penalty_rawtx}``
        """

        valid_breaches = {}
        invalid_breaches = []

        # A cache of the already decrypted blobs so replicate decryption can be avoided
        decrypted_blobs = {}

        for locator, dispute_txid in breaches.items():
            for uuid in self.locator_uuid_map[locator]:
                appointment = ExtendedAppointment.from_dict(
                    self.db_manager.load_watcher_appointment(uuid))

                if appointment.encrypted_blob in decrypted_blobs:
                    penalty_txid, penalty_rawtx = decrypted_blobs[
                        appointment.encrypted_blob]
                    valid_breaches[uuid] = {
                        "locator": appointment.locator,
                        "dispute_txid": dispute_txid,
                        "penalty_txid": penalty_txid,
                        "penalty_rawtx": penalty_rawtx,
                    }

                else:
                    try:
                        penalty_txid, penalty_rawtx = self.check_breach(
                            uuid, appointment, dispute_txid)
                        valid_breaches[uuid] = {
                            "locator": appointment.locator,
                            "dispute_txid": dispute_txid,
                            "penalty_txid": penalty_txid,
                            "penalty_rawtx": penalty_rawtx,
                        }
                        decrypted_blobs[appointment.encrypted_blob] = (
                            penalty_txid, penalty_rawtx)

                    except (EncryptionError, InvalidTransactionFormat):
                        invalid_breaches.append(uuid)

        return valid_breaches, invalid_breaches
Beispiel #5
0
    def _generate_dummy_appointment():
        appointment_data = {
            "locator": get_random_value_hex(16),
            "to_self_delay": 20,
            "encrypted_blob": get_random_value_hex(150),
            "user_id": get_random_value_hex(16),
            "user_signature": get_random_value_hex(50),
            "start_block": 200,
        }

        return ExtendedAppointment.from_dict(appointment_data)
def test_init_appointment(appointment_data):
    # The appointment has no checks whatsoever, since the inspector is the one taking care or that, and the only one
    # creating appointments.
    appointment = ExtendedAppointment(
        appointment_data["locator"],
        appointment_data["to_self_delay"],
        appointment_data["encrypted_blob"],
        appointment_data["user_id"],
    )

    assert (appointment_data["locator"] == appointment.locator
            and appointment_data["to_self_delay"] == appointment.to_self_delay
            and appointment_data["encrypted_blob"]
            == appointment.encrypted_blob
            and appointment_data["user_id"] == appointment.user_id)
Beispiel #7
0
    def _generate_dummy_appointment():
        commitment_txid = get_random_value_hex(32)
        penalty_tx = get_random_value_hex(150)

        appointment_data = {
            "locator": compute_locator(commitment_txid),
            "to_self_delay": 20,
            "encrypted_blob": Cryptographer.encrypt(penalty_tx,
                                                    commitment_txid),
            "user_id": get_random_value_hex(16),
            "user_signature": get_random_value_hex(50),
            "start_block": 200,
        }

        return ExtendedAppointment.from_dict(appointment_data), commitment_txid
Beispiel #8
0
def generate_dummy_appointment():
    dispute_tx = create_dummy_transaction()
    dispute_txid = dispute_tx.tx_id.hex()
    penalty_tx = create_dummy_transaction(dispute_txid)

    locator = compute_locator(dispute_txid)
    dummy_appointment_data = {
        "tx": penalty_tx.hex(),
        "tx_id": dispute_txid,
        "to_self_delay": 20
    }
    encrypted_blob = Cryptographer.encrypt(dummy_appointment_data.get("tx"),
                                           dummy_appointment_data.get("tx_id"))

    appointment_data = {
        "locator": locator,
        "to_self_delay": dummy_appointment_data.get("to_self_delay"),
        "encrypted_blob": encrypted_blob,
        "user_id": get_random_value_hex(16),
    }

    return ExtendedAppointment.from_dict(appointment_data), dispute_tx.hex()
def test_add_appointment_in_cache_invalid_blob(watcher):
    # Generate an appointment with an invalid transaction and add the dispute txid to the cache
    user_sk, user_pk = generate_keypair()
    user_id = Cryptographer.get_compressed_pk(user_pk)
    watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=10)

    # We need to create the appointment manually
    dispute_tx = create_dummy_transaction()
    dispute_txid = dispute_tx.tx_id.hex()
    penalty_tx = create_dummy_transaction(dispute_txid)

    locator = compute_locator(dispute_txid)
    dummy_appointment_data = {"tx": penalty_tx.hex(), "tx_id": dispute_txid, "to_self_delay": 20}
    encrypted_blob = Cryptographer.encrypt(dummy_appointment_data.get("tx")[::-1], dummy_appointment_data.get("tx_id"))

    appointment_data = {
        "locator": locator,
        "to_self_delay": dummy_appointment_data.get("to_self_delay"),
        "encrypted_blob": encrypted_blob,
        "user_id": get_random_value_hex(16),
    }

    appointment = ExtendedAppointment.from_dict(appointment_data)
    watcher.locator_cache.cache[appointment.locator] = dispute_tx.tx_id.hex()

    # Try to add the appointment
    response = watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk))

    # The appointment is accepted but dropped (same as an invalid appointment that gets triggered)
    assert (
        response
        and response.get("locator") == appointment.locator
        and Cryptographer.get_compressed_pk(watcher.signing_key.public_key)
        == Cryptographer.get_compressed_pk(Cryptographer.recover_pk(appointment.serialize(), response.get("signature")))
    )

    assert not watcher.locator_uuid_map.get(appointment.locator)
    assert appointment.locator not in [tracker.get("locator") for tracker in watcher.responder.trackers.values()]
    def inspect(self, appointment_data):
        """
        Inspects whether the data provided by the user is correct.

        Args:
            appointment_data (:obj:`dict`): a dictionary containing the appointment data.

        Returns:
            :obj:`Extended <teos.extended_appointment.ExtendedAppointment>`: An appointment initialized with
            the provided data.

        Raises:
           :obj:`InspectionFailed`: if any of the fields is wrong.
        """

        if appointment_data is None:
            raise InspectionFailed(errors.APPOINTMENT_EMPTY_FIELD,
                                   "empty appointment received")
        elif not isinstance(appointment_data, dict):
            raise InspectionFailed(errors.APPOINTMENT_WRONG_FIELD,
                                   "wrong appointment format")

        block_height = self.block_processor.get_block_count()
        if block_height is None:
            raise InspectionFailed(errors.UNKNOWN_JSON_RPC_EXCEPTION,
                                   "unexpected error occurred")

        self.check_locator(appointment_data.get("locator"))
        self.check_to_self_delay(appointment_data.get("to_self_delay"))
        self.check_blob(appointment_data.get("encrypted_blob"))

        # Set user_id to None since we still don't know it, it'll be set by the API after querying the gatekeeper
        return ExtendedAppointment(
            appointment_data.get("locator"),
            appointment_data.get("to_self_delay"),
            appointment_data.get("encrypted_blob"),
            user_id=None,
        )
Beispiel #11
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,
            }
Beispiel #12
0
def test_get_summary(ext_appointment_data):
    assert ExtendedAppointment.from_dict(
        ext_appointment_data).get_summary() == {
            "locator": ext_appointment_data["locator"],
            "user_id": ext_appointment_data["user_id"],
        }