Как с помощью smart-contract мы построили систему отбора лучших технологических проектов в ИТ МТС? И в какие «ловушки» мы попали, но смогли выбраться, доказав в итоге, что вести распределенный реестр на мобильных устройствах возможно!



Для чего понадобилась система на основе blockchain?


Начнем с самого начала. В МТС есть давняя традиция – выбирать лучший технологический проект, сделанный за год, и награждать его команду. Команда получает призы, уважение и славу. За долгие годы победителями становились различные проекты: от высоконагруженных телеком-систем до систем искусственного интеллекта.

Выборы лучшего проекта всегда происходили в несколько этапов:

  • Команды подают заявки
  • Проходит голосование уважаемых технических экспертов
  • После экспертов проекты отбирают руководители
  • По завершению всех этапов большой начальник выбирает лучший проект.

Мы решили, что данная схема недостаточно прозрачна для участников и подумали: почему бы не дать возможность выбирать лучший технологический проект абсолютно всем экспертам в компании? ?

Если мы реализуем такую возможность прямо в телефоне, то будем видеть текущий рейтинг проектов и кто за кого голосует — это обеспечит полную прозрачность процесса.

Мы прочитали несколько статей про blockchain, и идея построения системы распределенного реестра прочно осела у нас в головах. А что если и нам применить тут smart-contract?

Нас привлекли следующие свойства:

  • открытость – нет единого сервера, где можно манипулировать информацией;
  • информация, помещенная в распределенный реестр, остается там навсегда;
  • информацию нельзя подделать (ну… практически)

Не blockchain




Сам blockchain применять для таких выборов не стоит. Но что, если взять протоколы для построения консенсуса в распределенных системах и применить их к построению консенсуса в человеческих отношениях?

С учетом того, что нам придется работать в открытой сети, нужно защититься от невизантийских атак и от подмены информации на устройствах пользователей.

Какие альтернативы


Самый известный протокол – это PAXOS. В нем отсутствует явный лидер, и все изменения проходят через двухфазный commit. Вначале каждого изменения происходит Propose. Если он прошел успешно, то посылается Accept (принятие изменений).?

Особенностью алгоритма можно назвать то, что он использует глобальные таймеры для определения, какой запрос сформирован раньше и то, что узел, вносящий изменения, должен общаться со всеми узлами сети. Подробнее об алгоритме — здесь.

Алгоритм много где применяется, например, в СУБД Cassandra. Данный протокол нам не понравился с точки зрения сложности его реализации под задачу. Но второй вариант нам подошел – это RAFT. Фактически это эволюция протокола PAXOS с явным лидером.

Упрощенно протокол можно описать так:
·         строится сеть устройств, которые знают друг о друге;
·         устройства выбирают среди себя лидера, принимающего «важные решения» (например, о добавлении записи в реестр или изменении состава сети);
·         устройство-лидер отвечает за распределение информации по всей сети так, чтобы везде она была идентична;
·         как только лидер перестает справляться со своими обязанностями – выбирают нового лидера.

Прочитать про протокол можно тут.

Наша реализация


Что же мы делаем и зачем миру еще один велосипед? ?

Практически все пользователи нашего реестра имеют мобильные устройства, которые почти всегда включены и почти всегда в состоянии онлайн, и собственно используют их тоже почти всегда, поэтому мы решили запустить алгоритм распределительного реестра на мобильных устройствах, а не на серверной инфраструктуре, как у других известных реализаций.

Давайте посмотрим, в какие «ловушки» мы попали, но смогли выбраться…

Ловушка номер 1: «Мой адрес не дом и не улица»


«Неожиданно» выяснилось, что для построения P2P-сети, реализующей распределенный реестр с применением протокола RAFT для репликации данных, каждое устройство может общаться друг с другом, а значит является одновременно и клиентом, и сервером. Поэтому нам потребуется публичный «белый» IP-адрес для каждого мобильного телефона (его может и не быть).

Количество реальных IPv4 сильно ограничено, поэтому операторы связи применяют технологию NAT (Network Address Translation) в режиме PAT (Port Address Translation), транслируя несколько IP-адресов внутренней сети (которые и раздаются абонентам) в один внешний публичный IP-адрес. Таким образом, исключается возможность принимать входящие соединения из Интернет.

Хорошая новость – IPv6 много! ?

У нас поддержка IPv6 включена в базовый пакет. Также все современные телефоны поддерживают IPv6, и оператор назначает абоненту публичный «белый» v6-адрес. Наш выбор — IPv6.

Ловушка номер 2: Все пошли спать


В отличие от серверов, мобильные телефоны все-таки иногда выключают. Мы обнаружили это при тестировании первого прототипа. Кроме того, Ipv6-адрес, выдаваемый оператором, публичный, но не статичный, каждая новая сессия связи — новый адрес. Адрес мобильного устройства может измениться в любой момент. И если в сети не останется ни одного телефона с известным нам адресом – она просто перестанет существовать (не к чему подключаться, чтобы ее «нарастить»). Поэтому нам пришлось в какой-то мере нарушить свое правило «никаких серверов». Мы сделали в облаке один специальный узел со статичным известным адресом. Его задача – помнить/актуализировать состав сети и не выключаться. То есть это обычный узел, просто с него никто не голосует, и, обратившись к нему, всегда можно получить актуальный список адресов всех участников сети.

Ловушка номер 3: Идентификация и аутентификация ?


Надо было как-то решить задачу, чтобы голосовать за проекты могли не все, а те, кому это необходимо. Первая идея была такая: вести базу номеров телефонов экспертов. Но от нее отказались, поскольку не хотели давать приложению права на доступ к этой информации.
В итоге сделали все просто: каждый голос, каждую запись в реестре решили снабжать цифровой подписью на модных эллиптических кривых, которая и будет определять подлинность записи. В корпоративной сети разместили WEB-сервис, который по доменной авторизации определял эксперта и генерировал для него уникальный QR-код с открытым и закрытым ключом шифрования (само собой, на клиентской стороне). Эксперт сканировал код из приложения и подключался. После чего на его телефон «накатывалась» текущая актуальная версия реестра и появлялась возможность проголосовать.

Ловушка номер 4: Не Android единым ? ?


В ходе тестирования почти «внезапно» выяснилось, что некоторые пользователи являются владельцами небезызвестных моделей мобильных устройств, работающих под управлением операционной системы iOS. И стало понятно — программное обеспечение нашего реестра должно выполняться на разных платформах. Мы посмотрели в сторону языка программирования Kotlin, который не только «модный, стильный, молодежный», но еще и мультиплатформенный.

Концепт мультиплатформенности в Kotlin подразумевает, что есть часть кода сommon и platform-specific, но поскольку ресурсы нашей команды ограничены, мы поставили перед собой одиозную задачу — использовать единую версию исходного кода под все платформы! Само собой, исполняемый модуль должен быть native под каждую платформу. Кotlin на это оказался способен.

Сказано — сделано! Мы имеем один единственный SourceSet с исходным кодом, из которого собираем бинарники под все платформы (!), используя «фитчу» dependsOn. Круто? Очень круто!

Ловушка номер 5: Мобильный трафик не бесплатный ?


Как же нам наиболее эффективно реализовать взаимодействие между узлами сети так, чтобы не потратить весь трафик абонента и не разрядить батарею мобильного устройства? Мы предполагаем, что сеть может состоять из 1000 и более устройств! Самый очевидный вариант — использовать UDP вместо TCP, например, в процедуре выбора «лидера» или при отправке Heartbeat-ов без данных. UDP более экономный, так как использует простую модель передачи данных, без «рукопожатий» и подтверждений. Отлично! Что еще? Конечно, асинхронный ввод-вывод!

Внимательно читаем документацию по Kotlin Native.

For all Unix or Windows based targets (including Android and iPhone) we provide the posix platform lib. It contains bindings to platform's implementation of POSIX standard.

Затем так же внимательно читаем документацию стандарта POSIX и находим удивительную функцию, позволяющую обрабатывать события сокетов в неблокирующем режиме! Окунувшись с головой в удивительный мир корутин, сокетов и C Interop, мы смогли реализовать очень эффективный транспорт. Супер!

А в каком же виде пересылать данные? ??

Конечно же CBOR!

Компактный бинарный формат данных, который, по счастливой случайности, реализован в мультиплатформенной бибилиотеке kotlinx.serialization. Просто потрясно!

Ловушка номер 6: Сериализация ?


На этот раз действительно неожиданно выяснилось, что kotlinx.serialization нет под androidNative (под androidJvm, конечно же, есть). Уважаемые коллеги из JetBrains подтвердили, что на данный момент они не собирают библиотеку под androidNative, и до выпуска Kotlin 1.4 в роадмап места под эту задачу уже нет. ?

Что же делать? Если гора не идет к Магомету, Магомет идет к горе!

Мы сами собрали kotlinx.serialization под все платформы, в том числе под androidNative! Самое удивительное — все заработало! ?

Ловушка номер 7: Где хранить журнал? ?

?
Очевидно, в embedded key-value хранилище, но в каком? Мы выбрали lmdbx за компактность кода, скорость, мультиплатформенность, отсутствие WAL-файла. Эта библиотека разрабатывается ребятами из Positive Techlologies и берет начало из легендарной библиотеки LMDB от одного из авторов OpenLDAP Howard Chu. А та, в свою очередь, уходит корнями в реализацию B+tree от Martin Hedenfalk. Кстати, «из коробки» библиотека не собиралась под androidNative. Мы бережно собрали все ошибки, а авторы оперативно предоставили фиксы — за что им отдельная благодарность!

Ловушка номер 8: C Interop ?


Собрать все это вместе оказалась весьма нетривиальной задачей. Кроме lmdbx и сокетов из posix мы интегрировали библиотеки для формирования\валидации цифровой подписи на эллиптических кривых и вычисления SHA256 с помощью удивительного механизма C Interop. Если простыми словами – из native приложения на Kotlin можно вызвать функцию C-библиотеки, в том числе с указателями на указатели и прочей магией, выглядит это все правда немного странно.

Вот, например, вызов getaddrinfo с целью получения sockaddr.



Как тебе такое Илон Маск?

Линковка С-библиотеки в исполняемый файл Kotlin Native – это отдельный квест, который нам тоже удалось пройти, но не без «костылей». Мы динамически формируем def файл непосредственно в билд-скрипте грэдла, чтобы указывать правильный путь до библиотек относительно корневого каталога проекта и затем подставляем его (def файла) дескриптор в cinterops секцию. В самом же def файле определяется только абсолютный путь, который мало того что может иметь разный формат, если сборка выполняется под разными OS, но собственно на локальных машинах разработчиков, конечно, тоже может отличаться, что очевидно приводит к ошибке во время линковки.

Про выборы


Основные выборы мы проводили в течение суток. В тестировании с помощью нашей сети приняли участие чуть больше 20 экспертов. Оценивался 21 проект по 5 категориям, то есть всего в реестр было добавлено более 100 записей с голосами за проекты.

Вывод


В результате выполнения этого небольшого исследовательского проекта нам удалось доказать, что вести распределенный реестр на мобильных устройствах – возможно! Это открывает множество возможностей по использованию данной технологии на IoT-устройствах для организации вычислений по принципу Edge-computing. Впереди нас еще ждут испытания на нагрузку, уязвимость к атакам и на отказоустойчивость. Но мы верим, что у нас все получится!

Авторы статьи: архитекторы и разработчики R&D Центра МТС Дмитрий Дзюба, Алексей Василенко и Семен Неврев