Наблюдение за электронным голосованием: как это было
За последние два месяца, прошедшие с момента дистанционного электронного голосования (ДЭГ) на выборах 17–19 сентября 2021, было написано много статей и комментариев о том, как оценивать его результат. При этом одним из самых распространённых тезисов был тезис о якобы непрозрачности ДЭГ и полной невозможности наблюдения за ним.
Партия прямой демократии и приглашаемые нами независимые эксперты занимаются наблюдением за ДЭГ уже не первый год — и мы вынуждены категорически с этим тезисом не согласиться, с одним лишь уточнением: наблюдение за ДЭГ требует серьёзной технической подготовки. Невозможно просто прийти на участок или сесть за компьютер у себя дома утром первого дня голосования и методом «пристального взгляда» быстро всё оценить — требуются специфические знания, специфические инструменты и серьёзная предварительная подготовка.
Впрочем, даже в этом неверно противопоставлять ДЭГ обычному, «бумажному» голосованию — в последнем также весьма серьёзные усилия уделяются подготовке наблюдателей, хоть и в других вопросах: в знании процедур, законодательства, возможных нарушений и мер противодействия им, и прочее, и прочее. Просто зашедший с улицы на УИК человек точно так же в контексте наблюдения окажется практически бесполезен — максимум, что он сможет заметить, это самый грубые, можно сказать, наглые нарушения.
Техническая подготовка наблюдения за ДЭГ — лишь первый, но крайне важный этап работы. И, например, именно за полное, тотальное отсутствие такой подготовки мы порицали работавших на московских выборах наблюдателей от КПРФ и независимых депутатов. При том, что ход ДЭГ в Москве действительно вызывает ряд вопросов, со стороны пытающихся оспорить результаты голосования мы видим фактическое отсутствие наблюдения в ходе голосования и бессистемное набрасывание кучи претензий, от реальных до воображаемых или продиктованных политической конъюнктурой, вместо выстраивания последовательной тактики и вдумчивого анализа результатов ДЭГ.
В минувшие выборы мы плотно работали с федеральной системой ДЭГ — она использовалась в Ярославской, Нижегородской, Курской, Мурманской и Ростовской областях, а также Севастополе — и нам хотелось бы верить, что именно грамотно организованное наблюдение, к которому мы готовились в течение нескольких месяцев, позволило избежать не то что скандалов, а даже недопонимания между ТИК ДЭГ, ЦИК России, разработчиками системы (а это ПАО «Ростелеком» и компания Waves Enterprise) и представителями партий и кандидатов.
Чтобы показать, как было организовано наблюдение, сегодня мы публикуем текст Алексея Щербакова, разработчика блокчейн-систем и независимого эксперта в наблюдении за ДЭГ (кампания в МГД 2019 года, голосования по Конституции и ЕДГ 2020 года). В ходе голосования Алексей работал в Ситуационном центре по наблюдению за выборами Общественной палаты РФ в составе группы экспертного наблюдения за ДЭГ.
Это — лишь первая часть большой работы: в задачу Алексея входило надёжное получение данных о транзакциях в блокчейне федеральной системы ДЭГ и их первичный анализ на предмет целостности данных, стабильности работы системы ДЭГ в ходе всего голосования и отсутствия подозрительных аномалий в динамике получения и обработки данных голосования.
В дальнейшем мы планируем продолжить работу как над анализом данных, полученных в ходе электронного голосования, так и над общей оценкой применения ДЭГ на выборах осени 2021 года.
А сейчас — слово Алексею.
В 2019 году я разбирался в дистанционном электронном голосовании на выборах в Московскую городскую думу, в 2020 у нас было голосование по Конституции, также проходившее на московской платформе ДЭГ — где само голосование прошло хорошо, но случились накладки в стыковке ДЭГ с классическими участками: база данных с номерами паспортов избирателей, проголосовавших на ДЭГ, была передана на эти участки и в результате попала в сеть.
В этом же году мне, как наблюдателю, удалось получить доступ непосредственно к нодам наблюдения всех четырёх шардов блокчейна, использовавшегося в федеральной платформе ДЭГ, получить полный дамп транзакций блокчейна голосования и начать его исследование.
Как это было
Ещё в июле на 26 заседании ЦИК России член Общественной палаты РФ Александр Малькевич сообщил, что ОП РФ планирует сформировать группу технического наблюдения за подготовкой и ходом дистанционного электронного голосования. Так как голосование на выборах в Москве находится под контролем Общественной палаты Москвы, эта группа должна была заниматься федеральной системой ДЭГ.
К сентябрю эта группа была сформирована под большим и длинным труднозапоминающимся названием, которое никто никогда нигде не мог произнести и запомнить: «Команда технических экспертов рабочей группы по общественному контролю за ДЭГ и внедрению информационных технологий в избирательный процесс при Координационном совете ОП РФ по общественному контролю за голосованием». Запомнить это название было решительно невозможно ни одному человеку из тех, с кем я разговаривал, и поэтому мы сами себя продолжали называть группой технического наблюдения (ГТН).
Помимо ГТН, которая работала совершенно самостоятельно, у Общественной палаты был и собственный проект, озвученный Максимом Григорьевым — он назывался «цифровой сейф-пакет» и заключался в демонстрации возможности независимого наблюдения за целостностью и неизменностью данных о ходе голосования в блокчейне. В его рамках на четвёртом этаже ОП РФ, в зале пресс-конференций, должен был стоять компьютер, подключённый к ноде блокчейна и в реальном времени пишущий все проходящие через неё транзакции на внешние диски. Каждый день на вечерней пресс-конференции комплект дисков должен был сниматься и убираться в сейф — а по окончании голосования, то есть уже утром 20 сентября, диски должны были быть извлечены из сейфа, а сохранённые на них транзакции сличены с опубликованными в официальном блокчейне. Этот проект должен был показать, что однажды попавшее в блокчейн действительно никуда не пропадает — любые стёртые, изменённые или добавленные уже после окончания голосования транзакции на сверке сразу стали бы видны.
Так получилось, что софт для «цифрового сейф-пакета» писал я (вместе с программистом Партии прямой демократии Алексеем Зайцевым, который делал к нему графический интерфейс) — и все сверки данных также делал я. Это, помимо прочего, дало мне доступ к полному дампу всех транзакций блокчейна, полученному в реальном времени и напрямую через его ноду. Работал этот софт на нашем же компьютере, только канал подключения к ноде обеспечивала Общественная палата — а точнее, «Ростелеком».
Написание софта «сейф-пакета» заняло примерно две недели — 1 сентября мы скооперировались с разработчиками из Waves, получили от них API блокчейна, примеры кода, тестовый доступ к ноде блокчейна и принялись за работу.
В четверг 16 сентября (за день до выборов) я был в Общественной Палате, где мне уже должен был быть настроен прямой доступ к блокчейну. Конечно, как это всегда бывает, когда я туда приехал — доступ еще не был настроен, но в течении 2–3 часов, созваниваясь с инженером «Ростелекома» и переписываясь в Telegram с ребятами из Waves, я его таки получил. Очень важно было прийти именно в четверг, потому что, насколько я понял, никакие изменения в доступах в пятницу — после начала голосования — уже были бы невозможны. Я запустил написанную мной программу для наблюдения в количестве четырёх экземпляров (по одной на каждый шард блокчейна), проверил, что всё работает, и с чувством выполненного долга ушел из Общественной Палаты.
Кстати, Waves в итоге — уже в ходе голосования — заглянули к нам и несколько часов рассказывали экспертам из группы технического наблюдения про внутренее устройство системы и даже показывали технические метрики по блокчену, Kafka и другим компонентам, не скрывая особо ничего, за что им отдельное спасибо.
Кроме этого, всем членам ГТН в Общественной палате был доступ интерфейс наблюдателя за блокчейном и сайт со статистикой голосования и выгрузками из блокчейна — тот же stat.vybory.gov.ru, что и всему остальному миру, но с выделенным подключением по защищённому каналу.
Замечу, что интерфейс наблюдателя не надо путать с нодой блокчейна — эта путаница постоянно видна в отчётах московских наблюдателей:
- интерфейс наблюдателя — это веб-интерфейс, в котором можно просматривать на экране транзакции блокчейна в реальном времени, группировать их, фильтровать, и так далее;
- нода блокчейна (нода наблюдателя) — это сервер, у которого нет никакого веб-интерфейса, только возможность написать собственный софт, который будет отправлять к нему запросы и получать ответы (по сути, интерфейс наблюдателя примером такого софта и является).
В территориальной избирательной комиссии ДЭГ (ТИК ДЭГ) в здании ЦИК был ещё один способ работы с голосованиями — интерфейс члена ТИК ДЭГ, это такой «пульт управления голосованием». Для ГТН в Общественной палате он недоступен, так как одним из его элементов являются списки избирателей — а их может смотреть только член ТИК ДЭГ. В ТИК ДЭГ на рабочих местах при этом был и интерфейс наблюдателя — в результате, например, Виктор Толстогузов, который и входил в ГТН, и был членом ТИК ДЭГ от КПРФ, в Общественную палату не поехал, удовлетворившись интерфейсом наблюдателя в ТИК ДЭГ.
Чтобы представлять себе доступный разным участникам процесса инструментарий чуть наглядее:
ТИК ДЭГ (Большой Черкасский пер., 9) | Общественная Палата РФ (Миусская пл., 7 стр. 1) | Все желающие (stat.vybory.gov.ru) | |
Статистика о ходе выборов | Есть | Есть | Есть |
Выгрузки блокчейна в CSV | Есть | Есть | Есть |
Цифровой сейф-пакет | Нет | Есть | Нет |
Интерфейс наблюдателя | Есть | Есть | Нет |
Интерфейс члена ТИК | Есть | Нет | Нет |
Нода наблюдателя | Нет | Есть | Нет |
Одновременно для публично доступного сайта stat.vybory.gov.ru было решено написать программу, которая будет скачивать оттуда почасовые выгрузки транзакций блокчейна — сложность заключалась в том, что они были разбиты по избирательным округам, то есть, там лежало громадное количество файлов, «прощёлкать» которые мышкой и скачать вручную, если вас интересуют все данные, а не только один округ, просто нереально. Интерес они представляют по двум причинам — во-первых, их может скачать любой желающий, никаких допусков не надо, во-вторых, Waves показывали утилиты проверки целостности и корректности бюллетеней, то есть в общем тоже часть инфраструктуры технического наблюдения, работающие именно с этим форматом файлов. Ну и наконец, было интересно в рамках наблюдения проконтролировать, что выгрузка делается честно — то есть, совпадает с нашим дампом цифрового сейф-пакета.
Финальный вариант этой программы я выложил в субботу, 16 сентября, для всех желающих. Это консольная утилита, написанная на .NET Core. Публичный портал наблюдения устроен таким образом, что парсером HTML-страниц найти и загрузить выгрузки было невозможно, поэтому я «дергал» напрямую API его бэка, «подсмотрев», куда делает обращения браузер во время переходов между страницами. Затем я столкнулся с тем, что система безопасности не даёт делать к этому сайту чересчур много запросов. В результате, проконсультировавшись с разработчиками из «Ростелекома» и подобрав время задержки между запросами, мне удалось побороть и её. Если вы скачивали файлы — то там должно было получиться 43 199 файла внутри 1691 папок, суммарно почти 9 ГБ. Каждая папка — это один избирательный округ.
Как вы помните, на протяжении всего голосования специально написанная мной программа «сейф-пакета» заботливо сохраняла информацию о блоках и транзакциях из блокчейна. После завершения голосования и проверки, что количество транзакций в дампах совпадает с количеством транзакций в блокчейне (это уже было в понедельник утром), я взял полученный дамп к себе домой на изучение.
В итоге к концу голосования у меня были следующие дампы:
- Выгрузка с портала публичного наблюдения stat.vybory.gov.ru, сделанная с домашнего ноутбука
- Запись данных в реальном времени на вечер воскресенья и на утро понедельника (по транзакциям они не отличались, потому что после завершения голосования новых транзакций не было, но тем не менее, ноды наблюдения были активны всю ночь с воскресенья на понедельник)
- Выгрузка всего блокчейна на утро понедельника
Предварительная работа
Естественно, чтобы всё это проделать, потребовалась объёмистая предварительная работа.
Как было сказано, выше интерфейс наблюдателя и нода наблюдения — это не одно и то же. Интерфейс наблюдателя — это веб-приложение, которое выводит информацию о транзакциях в блокчейне, но оно может получать эту информацию не напрямую из блокчейна, а через промежуточные системы. В то время как нода наблюдения — это компонент самого блокчейна. Она не имеет никакого графического интерфейса и всё общение с ней происходит через API. Для того, чтобы наблюдать за блокчейном напрямую, через ноду, требуется написать программу, которая будет непрерывно скачивать транзакции с ноды наблюдения и сохранять к себе.
Нода наблюдения для системы Waves предоставляет несколько типов API для взаимодействия с ней. Чтобы не сильно нагружать систему, был выбран механизм получения событий по подписке через gRPC Streaming.
Нода наблюдения может присылать следующие события:
- Исторические данные до текущего (для блокчейна) момента — AppendedBlockHistory
- Данные о накоплении транзакций — MicroBlockAppended
- Данные о добавлении накопленных транзакций в блок — BlockAppended
- Данные по откату транзакций (в голосовании не должны были использоваться и не использовались, но я их тоже добавил — а вдруг)
При регистрации подписки на события нужно выбрать, с какого момента необходимо получить данные (генезис, конкретный блок или текущий момент), после чего нода наблюдателя действовала по следующему алгоритму:
- Если момент времени находился в прошлом для блокчейна, то начинали отсылаться события AppendedBlockHistory, пока время не дойдет до текущего момента
- В текущий момент времени приходили события по накоплению транзакций MicroBlockAppended, а через какое-то время событие добавления блока BlockAppended
- И так происходило до момента отключения
При правильной реализации мы можем использовать одну и ту же программу как для наблюдения в реальном времени, так и для скачивания данных блокчейна уже после голосования для верификации post factum.
Затратив определенное время (где-то две пары выходных на написание кода, тестирование в фоновом режиме и отладка в оставшееся свободное время), я написал программу, которая следила за блокчейном через ноду наблюдателя, и записывала все транзакции в реальном времени — именно она и стала базой «цифрового сейф-пакета». Посколько шардов было 4, то за каждым шардом блокчейна следил отдельный экземпляр программы, потому что так было банально проще. Все транзакции писались в отдельный файл transaction_output.bin, отдельно в базе данных sqlite сохранялось текущее состояние блокчейна: блоки и транзакции c полями, необходимыми для быстрого поиска. При этом полная транзакция записывалась в transaction_output.bin — эта БД служила оперативным хранилищем для обработки событий. Важно сказать, что при доступе к блокчейну через ноду наблюдения мы получаем больше информации, чем в файлах с портала наблюдения. Например, здесь у нас есть информация о блоках, в то время как в экспорте на портале — только транзакции. Это даст нам возможность построить некоторые метрики, недоступные напрямую из публичной выгрузки.
За время тестирования (две недели до голосования) было установлено, что нагрузка на программу достаточно маленькая, получение данных прекрасно работает в однозадачном режиме (всё выполняется в рамках одной Task внутри HostedService, не потребовались никакие чудеса распараллеливания). Единственная оптимизация, которая реально оказалась нужна — это объединение в batch‑и событий типа AppendedBlockHistory в зависимости от количества транзакций в них. Это связано с тем, что при голосованиях бывает большое число блоков с небольшим количеством транзакций, и когда мы в финале выкачиваем весь блокчейн для сравнения с данными реального времени, нам важно обработать как можно больше таких событий за одну транзакцию БД. Ребята из Waves любезно предоставили возможность подключения к тестовому серверу, чтобы я мог погонять свою программу и отработать разные сценарии её использования.
Исследование данных
Выспавшись за неделю после окончания голосования, я приступил к анализу собранных данных. Всю информацию я собрал в БД sqlite. Во-первых, чтобы иметь по сути один файл со всей структурированной информацией, во-вторых, чтобы другие люди получили возможность удобно всё это использовать, т.к. формат сохранения транзакций был изначально выбран таким, чтобы было удобно транзакции сохранять, а не осуществлять поиск по ним.
Всего я использовал две структуры БД.
Первая база — это расширенная версия БД, использовавшаяся для скачивания файлов транзакций с публичного портала — votings.db3 (в ней сами транзакции были импортированы из файлов внутрь БД и были созданы поля для быстрого поиска).
Вторая база — это БД, в которую я импортировал данные из дампа четырёх шардов блокчейна — research.db3.
Первая база формируется на основе скачанных файлов и БД метаданных по ним. Она расширяет уже имеющуюся структуру БД написанной мной программы для скачивания файлов с публичного портала таблицей transaction_in_file, в которой размещаются данные из CSV-файлов, при этом добавляется отношение 1‑ко-многим для таблицы voting_file
Вторая база формируется на основе четырёх пар файлов (blockchain.db3, transaction_output.bin) из дампа с программ наблюдения. Данные по блокам по сравнению с blockchain.db3 расширяются номером шарда, а для транзакций дополнительно выносятся метки времени, идентификаторы и подпись. Сами транзакции сохраняются в двух форматах: бинарном protobuf (как есть, raw) и JSON, чтобы всем, кто захочет после меня с этим разобраться, было легче увидеть общую структуру транзакции в удобном текстовом JSON-виде.
Загрузка файлов в том формате, что представлен на сайте — это далеко не самый быстрый процесс, и чтобы с одной стороны получить целевое решение, которое напрямую работает с этими файлами без распаковки на диск, а с другой — сильно ускорить процесс, я построил конвеер обработки данных. У этого конвеера есть четыре шага:
- Загрузка файла целиком в память
- Распаковка CSV-файла и чтение информации в объекты
- Подготовка группы объектов к пакетному импорту в БД
- Импорт группы объектов в БД
Конвеер построен стандартными средствами .NET на Dataflow. Шаги 1 и 4 выполняются всегда в однопоточном режиме, а шаг 2 — в параллельном. Посколько на моем компьютере 8 логических процессоров, а эта операция не содержит операций ввода вывода, а только операции с объектами в памяти, то сверху устанавливается ограничение в 8 максимально исполняющихся задач для этого шага. Дополнительно в шагах устанавливается ограничение в виде 1000 импортируемых файлов в группе. Также накладывается ограничение в виде 5000 одновременно загруженных файлов в память и 5000 одновременно обрабатываемых файлов, чтобы не занимать слишком много памяти
Загрузку данных шардов делаем немного по-другому:
- Вначале читаем информацию из блоков
- Читаем файл шарда с его БД целиком в память через проецируемые в память файлы
- Считываем транзакции из формата protobuff в объекты и выделяем необходимые поля для БД, дополнительно конвертируем в транзакции в JSON
- Группируем объект для пакетного импорта в БД
- Импортируем группу объектов
Здесь шаг 3 выполняется в многопоточном режиме.
Важное замечание: я старался потратить меньше своего времени, поэтому весь код для импорта использует достаточно много оперативной памяти. Во всех случаях была выбрана загрузка файлов целиком в память с дальнейшей обработкой в многопоточном режиме. На моём ноутбуке 32 ГБ памяти, поэтому экономилось самое ценное — время программиста.
Однако с первого раза создать БД не получилось. Оказалось что sqlite при больших запросах создает временные файлы на системном диске, на котором у меня было свободно всего гигабайт пять. Это поведение, конечно же, описано в документации, но вечером в понедельник про такое сложно вспомнить.
Принудительно указав в качестве временной папки папку на внешнем SSD, эту проблему я решил.
Теперь, когда у нас есть уже пригодные для анализа базы, попробуем найти следующую транзакцию из блокчейна в файлах выгрузки. Данное представление получено из Protobuf с помощью стандартного механизма сериализации сообщения в JSON:
{
"version": 2,
"executedContractTransaction": {
"id": "oUI7hiIudzJ5tHwRv9mtKSo9I/T96V2uQNVBzaxPPO0=",
"senderPublicKey": "YokI3qTb3tVwzFp0Mel8kDkxC9dG1JcPVIm2W261MB2ihglzCCO0P5liyPMJUcOsmY5yk16Zqc9dumSrffpiWg==",
"tx": {
"version": 3,
"createContractTransaction": {
"id": "mdE3Qgl7+le9XweM5V++gZYfPX+KQE2H5wyfYh5jbxU=",
"senderPublicKey": "hCWRuHtQC9QlE2XQuHD2BByM66taGJIQhmDvtxNtKu0aZnT/mmbPQWoXesFKy2L6WAqkFRsTb4pvzL5eBKv44g==",
"image": "voting/voting-contract:v1.5.0",
"imageHash": "48de795e67a538bbc53da37d622ee69490e0572a43af3818483f302e00cb423d",
"contractName": "voting-contract",
"params": [
{
"key": "pollId",
"stringValue": "c6e9ed2a-a972-4701-a67f-321d29523f1d"
},
{
"key": "type",
"stringValue": "blind"
},
{
"key": "blindSigModulo",
"binaryValue": "yiJKYoVwHEnh0k+lDBl27NUo6JQurRloDedQI4zEIdQL7/gveClkgWZF1jDMNEX3+MgdMHwuxLR1QMiv576MC04a5F0acHgTSb5DRaPnjMaW3OhqFwVpv7I7YmPefIGSl/pHKfFXiI32qGRIlfbOritwNgs5E5iwiEA3DGB7jEOCpKA3kU1EnuYgmtDaqMWTeev+qyZiO3Qia0PzkH8hd4yKtNkFN0tT81FALZo36CX5mdAcoTycJ0ss1iHbNTw+rYiu3v87WaGsBFOiUsZIoIVgwhmipZpStdgQkrkJDyhc2koqq8H8WeA1AmbINjSxTzAnqa847ne4rfo+gMXCM4lDJPCktw4bnmXw6fSBmXSwgLilsy+WKwGmK96NZEHbXfIS3Zq+C5vK8eVmXzTUnYN68G81tVC8PqF213Vh3pYLabEpcwS0f1VDWbzB7+GWRirLN6rrd5mHVoVUZLUn8mOxbhS9W39XKAOfFkrIqqrajOgsTNlphkBwNtAbLcE3sHrFI9TlpP4M1pJPOtM3G8vm+lRpXzHWm2yC5+RAqqjUIvJR9GYESAYcL6jd/988cVxX8RiJs4p/4r1xvNOeA8EABZgCRNpVxDHuqvMHFw03A6Cvi+Mk2fAsWxXV6PQnn0UTzIsywLE/W6ATaIsiFLhAHTg2SY80+VHc1hj5R8E="
},
{
"key": "blindSigExponent",
"binaryValue": "AQAB"
},
{
"key": "bulletinHash",
"stringValue": "6VZ3SiX5B2U67xJCDaNYDUsjHnLfmU79B8GJHs3HgRAS"
},
{
"key": "dimension",
"stringValue": "[[1,3,5]]"
},
{
"key": "votersListRegistrator",
"stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
},
{
"key": "blindSigIssueRegistrator",
"stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
},
{
"key": "issueBallotsRegistrator",
"stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
},
{
"key": "jwtTokenRegistrator",
"stringValue": "MIIDVDCCAjygAwIBAgIEU4b7XDANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MDUyOTA5MTgyMFoXDTI0MDUyNjA5MTgyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIOP1eIjY2KwuIAUjOzpQ22Bzaqcn1GJKEaGfhhnE1P1XeO7K0y5YRQA7U3AuBmp4E2Padc6ZtQxju54VUW+iFClrePY8EQKbx9pP76ODZaLum8KmXnoNQVSWURgR+VLZ2eZOYEd6isp7W/kaRt7AFn2UInB+sn6FmwUusqXydBCexjcWngJ+WpI0mDMBceJkVRtqWKPi8to43eV5W1oapzJXurETr0eMu0mrnaltgYO7BP/Ga2BiUQXYDJ+XfNzOrsThIaeMqfEz9/jZ1wMSJHiuCDGgTMM38Pzho20vGv1DJzdRDE+G5F25NM/2P+YLiNh9TK64LCELOlUKK3/Sc8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAI3yE1yF+ldMYM9ZBIeSB0LC2BsfTS7pX8Vl0gFATljnsOzcXPITdjf3pJmyi7B+AMKW6A4JqrMKRBr92FHw7CJccqZ6O5MWjO0ca7oDHXNin+WeyrzNZajkoLXR7Ah1RzGtsFnF/tKGL9ecPfIZG7G6rpt3SknrAcB1rmK+0auDphnvvECkCLx/MzPCbTHdqJC9no7d/IbxYIg57HCv2tQsTJJtRT7TmmQUB0BQf+Hmk7v6dLXaqufB0dx7BTqkKhRJvSXKRyX1LopAB9VHiP8R8EKv/QYoOBlw1EVvrzMaOb6wc7ElkCwdYl6oGSb3CTlSuhcOLsf6gkZGiCeWu3A=="
},
{
"key": "ballotReceivedCert",
"stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
},
{
"key": "servers",
"stringValue": "[\"3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK\"]"
}
],
"timestamp": "1631813108420",
"proofs": [
"mGCRyCm7Uytv3xr2+t9qJFZsJql0ZqA4KHlbEsJ7LuF+mJBTrRFFzmpEDrbWAKcQbRnCZDAHm86LlhKdfXl6Eg=="
]
}
},
"results": [
{
"key": "VOTING_BASE",
"stringValue": "{\"pollId\":\"c6e9ed2a-a972-4701-a67f-321d29523f1d\",\"bulletinHash\":\"6VZ3SiX5B2U67xJCDaNYDUsjHnLfmU79B8GJHs3HgRAS\",\"dimension\":[[1,3,5]],\"blindSigModulo\":\"ca224a6285701c49e1d24fa50c1976ecd528e8942ead19680de750238cc421d40beff82f782964816645d630cc3445f7f8c81d307c2ec4b47540c8afe7be8c0b4e1ae45d1a70781349be4345a3e78cc696dce86a170569bfb23b6263de7c819297fa4729f157888df6a8644895f6ceae2b70360b391398b08840370c607b8c4382a4a037914d449ee6209ad0daa8c59379ebfeab26623b74226b43f3907f21778c8ab4d905374b53f351402d9a37e825f999d01ca13c9c274b2cd621db353c3ead88aedeff3b59a1ac0453a252c648a08560c219a2a59a52b5d81092b9090f285cda4a2aabc1fc59e0350266c83634b14f3027a9af38ee77b8adfa3e80c5c233894324f0a4b70e1b9e65f0e9f4819974b080b8a5b32f962b01a62bde8d6441db5df212dd9abe0b9bcaf1e5665f34d49d837af06f35b550bc3ea176d77561de960b69b1297304b47f554359bcc1efe196462acb37aaeb77998756855464b527f263b16e14bd5b7f5728039f164ac8aaaada8ce82c4cd96986407036d01b2dc137b07ac523d4e5a4fe0cd6924f3ad3371bcbe6fa54695f31d69b6c82e7e440aaa8d422f251f4660448061c2fa8ddffdf3c715c57f11889b38a7fe2bd71bcd39e03c10005980244da55c431eeaaf307170d3703a0af8be324d9f02c5b15d5e8f4279f4513cc8b32c0b13f5ba013688b2214b8401d3836498f34f951dcd618f947c1\",\"blindSigExponent\":\"10001\",\"status\":\"Active\",\"isRevoteBlocked\":true}"
},
{
"key": "SERVERS",
"stringValue": "[\"3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK\"]"
},
{
"key": "VOTERS_LIST_REGISTRATOR",
"stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
},
{
"key": "ISSUE_BALLOTS_REGISTRATOR",
"stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
},
{
"key": "BLINDSIG_ISSUE_REGISTRATOR",
"stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
},
{
"key": "JWTTOKEN_REGISTRATOR",
"stringValue": "MIIDVDCCAjygAwIBAgIEU4b7XDANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MDUyOTA5MTgyMFoXDTI0MDUyNjA5MTgyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIOP1eIjY2KwuIAUjOzpQ22Bzaqcn1GJKEaGfhhnE1P1XeO7K0y5YRQA7U3AuBmp4E2Padc6ZtQxju54VUW+iFClrePY8EQKbx9pP76ODZaLum8KmXnoNQVSWURgR+VLZ2eZOYEd6isp7W/kaRt7AFn2UInB+sn6FmwUusqXydBCexjcWngJ+WpI0mDMBceJkVRtqWKPi8to43eV5W1oapzJXurETr0eMu0mrnaltgYO7BP/Ga2BiUQXYDJ+XfNzOrsThIaeMqfEz9/jZ1wMSJHiuCDGgTMM38Pzho20vGv1DJzdRDE+G5F25NM/2P+YLiNh9TK64LCELOlUKK3/Sc8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAI3yE1yF+ldMYM9ZBIeSB0LC2BsfTS7pX8Vl0gFATljnsOzcXPITdjf3pJmyi7B+AMKW6A4JqrMKRBr92FHw7CJccqZ6O5MWjO0ca7oDHXNin+WeyrzNZajkoLXR7Ah1RzGtsFnF/tKGL9ecPfIZG7G6rpt3SknrAcB1rmK+0auDphnvvECkCLx/MzPCbTHdqJC9no7d/IbxYIg57HCv2tQsTJJtRT7TmmQUB0BQf+Hmk7v6dLXaqufB0dx7BTqkKhRJvSXKRyX1LopAB9VHiP8R8EKv/QYoOBlw1EVvrzMaOb6wc7ElkCwdYl6oGSb3CTlSuhcOLsf6gkZGiCeWu3A=="
},
{
"key": "BALLOT_RECEIVED_CERT",
"stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
}
],
"resultsHash": "/9vjjH4zD9n7Lf8g2rkI5UZhHLnH7gT54xS90+yjenA=",
"timestamp": "1631813108821",
"proofs": [
"Tx/HPuGAFJLFGxOIlQp3bdXvsu3JBAJw62t7DQ8JMGzt80A4Gc/HQsag3H1CZKl5tk4lmyzm/eO3PFfnEK4z8g=="
]
}
}
Обратите внимание, что в наших данных два timestamp — 1631813108420 и 1631813108821 — которые описывают два момента времени 2021-09-16 17:25:08 (по Гринвичу) с разницей около 400 миллисекунд.
Чтобы найти эту транзакцию в исследовательской БД votings.db3 и получить файл, где она находится, нужно взять ПЕРВЫЙ timestamp и использовать следующий SQL-код:
SELECT F.filename
FROM transaction_in_file AS T
INNER JOIN voting_file AS F
ON T.file_id=F.id
INNER JOIN voting AS V
ON F.voting_id=V.id
where T.timestamp=1631813108420
Получается, что нужный файл — это BMSQjwfeFpJRz8eKJgmxvAZcv2bGiuuYqvLYHfAZ8wZ6_2021-09–16_2000-2100.zip. Транзакция с временной меткой 2021-09-16 20:25:08 по Москве действительно попадает в этот диапазон!
Искомая транзакция в csv-файле:
BMSQjwfeFpJRz8eKJgmxvAZcv2bGiuuYqvLYHfAZ8wZ6;103;43hTRMyqfip9f5E3RA3TrtuMJMvkziaUrp8iQemW6YanUpVVkkNRujEC6JabdjSgMjfBsSDTMEmPGWKKxpJsLeyw;3;1631813108420;3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK;0;;[{"key":"pollId","stringValue":"c6e9ed2a-a972-4701-a67f-321d29523f1d"},{"key":"type","stringValue":"blind"},{"key":"blindSigModulo","binaryValue":"yiJKYoVwHEnh0k+lDBl27NUo6JQurRloDedQI4zEIdQL7/gveClkgWZF1jDMNEX3+MgdMHwuxLR1QMiv576MC04a5F0acHgTSb5DRaPnjMaW3OhqFwVpv7I7YmPefIGSl/pHKfFXiI32qGRIlfbOritwNgs5E5iwiEA3DGB7jEOCpKA3kU1EnuYgmtDaqMWTeev+qyZiO3Qia0PzkH8hd4yKtNkFN0tT81FALZo36CX5mdAcoTycJ0ss1iHbNTw+rYiu3v87WaGsBFOiUsZIoIVgwhmipZpStdgQkrkJDyhc2koqq8H8WeA1AmbINjSxTzAnqa847ne4rfo+gMXCM4lDJPCktw4bnmXw6fSBmXSwgLilsy+WKwGmK96NZEHbXfIS3Zq+C5vK8eVmXzTUnYN68G81tVC8PqF213Vh3pYLabEpcwS0f1VDWbzB7+GWRirLN6rrd5mHVoVUZLUn8mOxbhS9W39XKAOfFkrIqqrajOgsTNlphkBwNtAbLcE3sHrFI9TlpP4M1pJPOtM3G8vm+lRpXzHWm2yC5+RAqqjUIvJR9GYESAYcL6jd/988cVxX8RiJs4p/4r1xvNOeA8EABZgCRNpVxDHuqvMHFw03A6Cvi+Mk2fAsWxXV6PQnn0UTzIsywLE/W6ATaIsiFLhAHTg2SY80+VHc1hj5R8E="},{"key":"blindSigExponent","binaryValue":"AQAB"},{"key":"bulletinHash","stringValue":"6VZ3SiX5B2U67xJCDaNYDUsjHnLfmU79B8GJHs3HgRAS"},{"key":"dimension","stringValue":"[[1,3,5]]"},{"key":"votersListRegistrator","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"blindSigIssueRegistrator","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"issueBallotsRegistrator","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"jwtTokenRegistrator","stringValue":"MIIDVDCCAjygAwIBAgIEU4b7XDANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MDUyOTA5MTgyMFoXDTI0MDUyNjA5MTgyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIOP1eIjY2KwuIAUjOzpQ22Bzaqcn1GJKEaGfhhnE1P1XeO7K0y5YRQA7U3AuBmp4E2Padc6ZtQxju54VUW+iFClrePY8EQKbx9pP76ODZaLum8KmXnoNQVSWURgR+VLZ2eZOYEd6isp7W/kaRt7AFn2UInB+sn6FmwUusqXydBCexjcWngJ+WpI0mDMBceJkVRtqWKPi8to43eV5W1oapzJXurETr0eMu0mrnaltgYO7BP/Ga2BiUQXYDJ+XfNzOrsThIaeMqfEz9/jZ1wMSJHiuCDGgTMM38Pzho20vGv1DJzdRDE+G5F25NM/2P+YLiNh9TK64LCELOlUKK3/Sc8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAI3yE1yF+ldMYM9ZBIeSB0LC2BsfTS7pX8Vl0gFATljnsOzcXPITdjf3pJmyi7B+AMKW6A4JqrMKRBr92FHw7CJccqZ6O5MWjO0ca7oDHXNin+WeyrzNZajkoLXR7Ah1RzGtsFnF/tKGL9ecPfIZG7G6rpt3SknrAcB1rmK+0auDphnvvECkCLx/MzPCbTHdqJC9no7d/IbxYIg57HCv2tQsTJJtRT7TmmQUB0BQf+Hmk7v6dLXaqufB0dx7BTqkKhRJvSXKRyX1LopAB9VHiP8R8EKv/QYoOBlw1EVvrzMaOb6wc7ElkCwdYl6oGSb3CTlSuhcOLsf6gkZGiCeWu3A=="},{"key":"ballotReceivedCert","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"servers","stringValue":"[\"3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK\"]"}];[{"key":"VOTING_BASE","stringValue":"{\"pollId\":\"c6e9ed2a-a972-4701-a67f-321d29523f1d\",\"bulletinHash\":\"6VZ3SiX5B2U67xJCDaNYDUsjHnLfmU79B8GJHs3HgRAS\",\"dimension\":[[1,3,5]],\"blindSigModulo\":\"ca224a6285701c49e1d24fa50c1976ecd528e8942ead19680de750238cc421d40beff82f782964816645d630cc3445f7f8c81d307c2ec4b47540c8afe7be8c0b4e1ae45d1a70781349be4345a3e78cc696dce86a170569bfb23b6263de7c819297fa4729f157888df6a8644895f6ceae2b70360b391398b08840370c607b8c4382a4a037914d449ee6209ad0daa8c59379ebfeab26623b74226b43f3907f21778c8ab4d905374b53f351402d9a37e825f999d01ca13c9c274b2cd621db353c3ead88aedeff3b59a1ac0453a252c648a08560c219a2a59a52b5d81092b9090f285cda4a2aabc1fc59e0350266c83634b14f3027a9af38ee77b8adfa3e80c5c233894324f0a4b70e1b9e65f0e9f4819974b080b8a5b32f962b01a62bde8d6441db5df212dd9abe0b9bcaf1e5665f34d49d837af06f35b550bc3ea176d77561de960b69b1297304b47f554359bcc1efe196462acb37aaeb77998756855464b527f263b16e14bd5b7f5728039f164ac8aaaada8ce82c4cd96986407036d01b2dc137b07ac523d4e5a4fe0cd6924f3ad3371bcbe6fa54695f31d69b6c82e7e440aaa8d422f251f4660448061c2fa8ddffdf3c715c57f11889b38a7fe2bd71bcd39e03c10005980244da55c431eeaaf307170d3703a0af8be324d9f02c5b15d5e8f4279f4513cc8b32c0b13f5ba013688b2214b8401d3836498f34f951dcd618f947c1\",\"blindSigExponent\":\"10001\",\"status\":\"Active\",\"isRevoteBlocked\":true}"},{"key":"SERVERS","stringValue":"[\"3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK\"]"},{"key":"VOTERS_LIST_REGISTRATOR","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"ISSUE_BALLOTS_REGISTRATOR","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"BLINDSIG_ISSUE_REGISTRATOR","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"JWTTOKEN_REGISTRATOR","stringValue":"MIIDVDCCAjygAwIBAgIEU4b7XDANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MDUyOTA5MTgyMFoXDTI0MDUyNjA5MTgyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIOP1eIjY2KwuIAUjOzpQ22Bzaqcn1GJKEaGfhhnE1P1XeO7K0y5YRQA7U3AuBmp4E2Padc6ZtQxju54VUW+iFClrePY8EQKbx9pP76ODZaLum8KmXnoNQVSWURgR+VLZ2eZOYEd6isp7W/kaRt7AFn2UInB+sn6FmwUusqXydBCexjcWngJ+WpI0mDMBceJkVRtqWKPi8to43eV5W1oapzJXurETr0eMu0mrnaltgYO7BP/Ga2BiUQXYDJ+XfNzOrsThIaeMqfEz9/jZ1wMSJHiuCDGgTMM38Pzho20vGv1DJzdRDE+G5F25NM/2P+YLiNh9TK64LCELOlUKK3/Sc8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAI3yE1yF+ldMYM9ZBIeSB0LC2BsfTS7pX8Vl0gFATljnsOzcXPITdjf3pJmyi7B+AMKW6A4JqrMKRBr92FHw7CJccqZ6O5MWjO0ca7oDHXNin+WeyrzNZajkoLXR7Ah1RzGtsFnF/tKGL9ecPfIZG7G6rpt3SknrAcB1rmK+0auDphnvvECkCLx/MzPCbTHdqJC9no7d/IbxYIg57HCv2tQsTJJtRT7TmmQUB0BQf+Hmk7v6dLXaqufB0dx7BTqkKhRJvSXKRyX1LopAB9VHiP8R8EKv/QYoOBlw1EVvrzMaOb6wc7ElkCwdYl6oGSb3CTlSuhcOLsf6gkZGiCeWu3A=="},{"key":"BALLOT_RECEIVED_CERT","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"}];{"image":"voting/voting-contract:v1.5.0","imageHash":"48de795e67a538bbc53da37d622ee69490e0572a43af3818483f302e00cb423d","contractName":"voting-contract"};1
Важное замечание. Когда бинарные данные представляются в виде текста — это можно сделать несколькими способами. Для программиста есть два знакомых формата представления — hex и base64. Блокчейн добавил еще один формат — base58. По удивительному совпадению у нас тут используются все три. Так в выгрузке stat адреса блокчейна представляется в виде base58, а криптографические константы вроде модуля и экспоненты слепой подписи в hex. Когда мы просим Protobuf сериализовать в транзакцию в JSON (сам Protobuff имеет бинарный сериализатор) то все байтовые последовательности сохраняются в base64. Для того чтобы поиграться в репозитории с исходным кодом есть проект EncodingConverter, который дает возможность переводить один формат в другой.
В выгрузке публичного портала отображаются не все поля транзакций, доступные в блокчейне, а только смысловая часть вызовов. Теперь расскажу, как соотносятся эти поля между собой
- BMSQjwfeFpJRz8eKJgmxvAZcv2bGiuuYqvLYHfAZ8wZ6 при переводе из base58 в base64 превращается в mdE3Qgl7+le9XweM5V++gZYfPX+KQE2H5wyfYh5jbxU= и это то что называется nested_tx_id в CSV
- 103 — это код типа. 103 — создание, 104 — вызов. Исходный для 103 можно найти в папке creation, а 104 — в папке invocation исходного кода смарт-контрактов (см. VotingMessagesHandler.scala)
- 43hTRMyqfip9f5E3RA3TrtuMJMvkziaUrp8iQemW6YanUpVVkkNRujEC6JabdjSgMjfBsSDTMEmPGWKKxpJsLeyw это mGCRyCm7Uytv3xr2+t9qJFZsJql0ZqA4KHlbEsJ7LuF+mJBTrRFFzmpEDrbWAKcQbRnCZDAHm86LlhKdfXl6Eg== (signature — то, что в блокчейне называется proof)
- 3 — это номер версии
- 1631813108420 — метка времени
- 3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK — hCWRuHtQC9QlE2XQuHD2BByM66taGJIQhmDvtxNtKu0aZnT/mmbPQWoXesFKy2L6WAqkFRsTb4pvzL5eBKv44g== это sender_public_key
- не используется
- не используется (7 и 8, судя по всему, относятся к вознаграждению майнеров/комиссии и не нужны нам, да и в файлах они нигде не заполнены)
- это входные параметры
- это выходные параметры
- это дополнительные параметры, которые не вошли в вышеуказанные 9 и 10 (тут, например, название docker-образа смарт-контракта).
Как видите, при ручной проверке операции переводы между представлениями байт нужно делать довольно часто — поэтому и была написана небольшая программа для проверки в процессе отладки кода.
Покажу еще обратный процесс — как по транзакции в файле найти её в выгрузке. Для этого выберем транзакцию с типом 104:
BmFxzLavaz2qhVpkfsXm4j8JqzhmUj5wxVGtvxLf15bi;104;4HnAPSLGwx2NWgMiv3NwEKnXWUExK37WKh2jszUhqUHQfRzjBr1tcMEnWfBmNbd12STKQASrJS6bpFV3XdPHd4MX;4;1631988672987;38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL;0;;[{"key":"operation","stringValue":"blindSigIssue"},{"key":"data","stringValue":"[{\"userId\":\"NH7VjMULkBhSEEQ+ctm5ARkjl7XTAKKRZ+jV9XAusS0=\", \"maskedSig\": \"aa93cfef25af25ea9fb6db0d1f073600f7d8f622d0b31b44b8fd1ec2921d15aa0ff2e237ef6f9ab76f2d73ae6c06acaca533aa28dde9996c034d5bc61135782c058f67fb2562f6b1b90f02c20a44d19549da2587e8d3e1bc649da39524835cbc76352480c0546ab0a90b11fb2a68326311cec6a92e9513918b8c215c81d5c50f152610e991d5532fda7fc9cc53fd322f2ae270669656da50af5cbef2647a5664b8c69c7d5e3dda1abb2dfc50040a05b80b6586ca6781337ccc1e6139e7269f75c0fd9287d3bf7c6703e64f9b6fd26ec95b05a484c82474280eafda8dec347f7ef7d0c4c90db03e46e38eeda89008dc77b43228916b4e54bfd1621142349b4ccec8d81cb6b56f0fe1a4ef4a24d3086039eb344b0bf7e4fa8aeaf8a90e4e47c77246e16c254a4a6b6571a49d496538dbe060b83d2c99730d00eedc62029bec9103c34ce015e77ab45141b0160464623a9ad2d2427aa796fae9d36648a1638b6bc1a40e2ce79ade6dcfee320de96bb4a44f3770aa206cc47ed85f28930e0b7ee2278c16f159ef0bcc138ba6a065299ec227d4bf6ed37b43313a22f3126fad06151a0dd686a01855b25ff9961b8712e883292d19a2341f7f7e0deda63502b0e934db7be130cedefb25709d8bab90e9052f7f2665cce91f3cde49767872d7414f4480f531abaaec02eb15405d472a8b8de3ad66281453ebed51712b7ccbfdfd91324e\"}]"}];[{"key":"BLINDSIG_BmFxzLavaz2qhVpkfsXm4j8JqzhmUj5wxVGtvxLf15bi","stringValue":"[{\"userId\":\"NH7VjMULkBhSEEQ+ctm5ARkjl7XTAKKRZ+jV9XAusS0=\",\"maskedSig\":\"aa93cfef25af25ea9fb6db0d1f073600f7d8f622d0b31b44b8fd1ec2921d15aa0ff2e237ef6f9ab76f2d73ae6c06acaca533aa28dde9996c034d5bc61135782c058f67fb2562f6b1b90f02c20a44d19549da2587e8d3e1bc649da39524835cbc76352480c0546ab0a90b11fb2a68326311cec6a92e9513918b8c215c81d5c50f152610e991d5532fda7fc9cc53fd322f2ae270669656da50af5cbef2647a5664b8c69c7d5e3dda1abb2dfc50040a05b80b6586ca6781337ccc1e6139e7269f75c0fd9287d3bf7c6703e64f9b6fd26ec95b05a484c82474280eafda8dec347f7ef7d0c4c90db03e46e38eeda89008dc77b43228916b4e54bfd1621142349b4ccec8d81cb6b56f0fe1a4ef4a24d3086039eb344b0bf7e4fa8aeaf8a90e4e47c77246e16c254a4a6b6571a49d496538dbe060b83d2c99730d00eedc62029bec9103c34ce015e77ab45141b0160464623a9ad2d2427aa796fae9d36648a1638b6bc1a40e2ce79ade6dcfee320de96bb4a44f3770aa206cc47ed85f28930e0b7ee2278c16f159ef0bcc138ba6a065299ec227d4bf6ed37b43313a22f3126fad06151a0dd686a01855b25ff9961b8712e883292d19a2341f7f7e0deda63502b0e934db7be130cedefb25709d8bab90e9052f7f2665cce91f3cde49767872d7414f4480f531abaaec02eb15405d472a8b8de3ad66281453ebed51712b7ccbfdfd91324e\"}]"}];{"contractVersion":1};1
Её timestamp — 1631988672987. В базе votings.db3 её можно найти так:
SELECT T.*, F.filename
FROM transaction_in_file AS T
INNER JOIN voting_file AS F
ON T.file_id=F.id
INNER JOIN voting AS V
ON F.voting_id=V.id
where T.timestamp=1631988672987
В базе research.db3 её можно найти так:
SELECT block.height,block.shard, tx.* FROM tx
INNER JOIN block
on tx.block_id=block.id
WHERE nested_timestamp=1631988672987
Отсюда мы видим, что искомая транзакция находилась в четвёртом шарде блокчейна, а её полное представление:
{
"version": 2,
"executedContractTransaction": {
"id": "XsCzX6RynOCB3hpidZueLiyswcd14+zqyVlWorz1m3U=",
"senderPublicKey": "4zNGEAmoqMMoRn090G4wtEW9UB9Tf+EXNa9uX6unxHryRM1Zt4ibbJCzQ/Flce2lW+/OuG3yizPr2pSiRD8ggA==",
"tx": {
"version": 4,
"callContractTransaction": {
"id": "n+tYoNubdg+jUZKN8Kap6EjGyUQDc7k3HzCEIeHDDO0=",
"senderPublicKey": "aulNNVDJ7mpgSXDDFelmxQxt/cTFMYKWUwuXnXZF0Oz5BMTY1pOBKiw3vaNrlP63tNLGh3tkIcgYh4CUlzR+QQ==",
"contractId": "ruahrCcUvR4yKs28nzYDTF/Pp+DyOGw1AdO05pKGNec=",
"params": [
{
"key": "operation",
"stringValue": "blindSigIssue"
},
{
"key": "data",
"stringValue": "[{\"userId\":\"NH7VjMULkBhSEEQ+ctm5ARkjl7XTAKKRZ+jV9XAusS0=\", \"maskedSig\": \"aa93cfef25af25ea9fb6db0d1f073600f7d8f622d0b31b44b8fd1ec2921d15aa0ff2e237ef6f9ab76f2d73ae6c06acaca533aa28dde9996c034d5bc61135782c058f67fb2562f6b1b90f02c20a44d19549da2587e8d3e1bc649da39524835cbc76352480c0546ab0a90b11fb2a68326311cec6a92e9513918b8c215c81d5c50f152610e991d5532fda7fc9cc53fd322f2ae270669656da50af5cbef2647a5664b8c69c7d5e3dda1abb2dfc50040a05b80b6586ca6781337ccc1e6139e7269f75c0fd9287d3bf7c6703e64f9b6fd26ec95b05a484c82474280eafda8dec347f7ef7d0c4c90db03e46e38eeda89008dc77b43228916b4e54bfd1621142349b4ccec8d81cb6b56f0fe1a4ef4a24d3086039eb344b0bf7e4fa8aeaf8a90e4e47c77246e16c254a4a6b6571a49d496538dbe060b83d2c99730d00eedc62029bec9103c34ce015e77ab45141b0160464623a9ad2d2427aa796fae9d36648a1638b6bc1a40e2ce79ade6dcfee320de96bb4a44f3770aa206cc47ed85f28930e0b7ee2278c16f159ef0bcc138ba6a065299ec227d4bf6ed37b43313a22f3126fad06151a0dd686a01855b25ff9961b8712e883292d19a2341f7f7e0deda63502b0e934db7be130cedefb25709d8bab90e9052f7f2665cce91f3cde49767872d7414f4480f531abaaec02eb15405d472a8b8de3ad66281453ebed51712b7ccbfdfd91324e\"}]"
}
],
"timestamp": "1631988672987",
"contractVersion": 1,
"proofs": [
"pIUwSrO53C5D+SQ9LMxztgb+3ihz0Kh3vk1WyH6+s2zBMKE1D6tttEJtqNYDy+MyIWUpdSliD/bHTSEDln4Mcg=="
]
}
},
"results": [
{
"key": "BLINDSIG_BmFxzLavaz2qhVpkfsXm4j8JqzhmUj5wxVGtvxLf15bi",
"stringValue": "[{\"userId\":\"NH7VjMULkBhSEEQ+ctm5ARkjl7XTAKKRZ+jV9XAusS0=\",\"maskedSig\":\"aa93cfef25af25ea9fb6db0d1f073600f7d8f622d0b31b44b8fd1ec2921d15aa0ff2e237ef6f9ab76f2d73ae6c06acaca533aa28dde9996c034d5bc61135782c058f67fb2562f6b1b90f02c20a44d19549da2587e8d3e1bc649da39524835cbc76352480c0546ab0a90b11fb2a68326311cec6a92e9513918b8c215c81d5c50f152610e991d5532fda7fc9cc53fd322f2ae270669656da50af5cbef2647a5664b8c69c7d5e3dda1abb2dfc50040a05b80b6586ca6781337ccc1e6139e7269f75c0fd9287d3bf7c6703e64f9b6fd26ec95b05a484c82474280eafda8dec347f7ef7d0c4c90db03e46e38eeda89008dc77b43228916b4e54bfd1621142349b4ccec8d81cb6b56f0fe1a4ef4a24d3086039eb344b0bf7e4fa8aeaf8a90e4e47c77246e16c254a4a6b6571a49d496538dbe060b83d2c99730d00eedc62029bec9103c34ce015e77ab45141b0160464623a9ad2d2427aa796fae9d36648a1638b6bc1a40e2ce79ade6dcfee320de96bb4a44f3770aa206cc47ed85f28930e0b7ee2278c16f159ef0bcc138ba6a065299ec227d4bf6ed37b43313a22f3126fad06151a0dd686a01855b25ff9961b8712e883292d19a2341f7f7e0deda63502b0e934db7be130cedefb25709d8bab90e9052f7f2665cce91f3cde49767872d7414f4480f531abaaec02eb15405d472a8b8de3ad66281453ebed51712b7ccbfdfd91324e\"}]"
}
],
"resultsHash": "n6kJJcT/5r3CVXYQfD9MDEYVY8btOB35y7Lh+ICH3AE=",
"timestamp": "1631988675474",
"proofs": [
"qYoRnNRMdQOXoznimpzYqpPCvBuPXZkj/UarFckIMhaVtvz9TG9X0vZQqzstd+tK4t9JlDI7Sg8auKCGzbHd5A=="
]
}
}
На этом подготовительную часть для анализа можно считать законченной. В результате имеем две БД по 18 и 33 ГБ размером, которые содержат данные о транзакциях, которые нам теперь нужно сравнить — чтобы убедиться, для начала, в целостности результатов голосования (бюллетени не удалялись, не изменялись, не добавлялись после окончания или до старта голосования, публичная выгрузка в точности соответствует результатам наблюдения через ноду блокчейна).
Если вы, голосуя, сохранили квиточек о получении вашего голоса системой, то вы можете его найти в любой из этих баз. При этом схема шифрования выбрана такой, что расшифровать индивидуальные бюллетени невозможно — поэтому, даже если вы знаете данные чьей-то чужой транзакции, вы всё равно не сможете посмотреть, за кого голосовал этот человек.
Сравнение исследовательских баз
На этом этапе нам необходимо проверить два факта:
- Транзакции, записанные в ходе наблюдения, идентичны транзакциям в файлах выгрузки на портале публичного наблюдения
- Транзакции, скачанные в понедельник после подведения итогов голосования, идентичны транзакциям выгрузки на портале публичного наблюдения
Нам для этого потребуется три базы. Одна база с файлами портала публичного наблбления stat.vybory.gov.ru и две другие, полученные с нод наблюдения шардов блокчейна (одна в реальном времени, вторая — уже утром понедельника, после подведения результатов).
Рассмотрим дампы транзакций из блокчейна.
Дамп, полученный во время наблюдения «цифровым сейф-пакетом» — blockchain_dump_3dayend.zip
В нем всего 3 107 873 транзакции. Распределение по типам можно получить так:
SELECT tx.operation_type,COUNT(*) FROM tx
GROUP BY tx.operation_type
ORDER BY tx.operation_type
Тип | Количество |
Executed CallContractTransaction addMainKey | 1691 |
Executed CallContractTransaction addVotersList | 4775 |
Executed CallContractTransaction blindSigIssue | 1553575 |
Executed CallContractTransaction commissionDecryption | 1672 |
Executed CallContractTransaction decryption | 1672 |
Executed CallContractTransaction finishVoting | 1691 |
Executed CallContractTransaction removeFromVotersList | 48 |
Executed CallContractTransaction results | 1691 |
Executed CallContractTransaction startVoting | 1691 |
Executed CallContractTransaction vote | 1537676 |
Executed CreateContractTransaction voting-contract | 1691 |
Финальный дамп, загруженный утром понедельника — blockchain_dump_final.zip
Тип | Количество |
Executed CallContractTransaction addMainKey | 1691 |
Executed CallContractTransaction addVotersList | 4775 |
Executed CallContractTransaction blindSigIssue | 1553575 |
Executed CallContractTransaction commissionDecryption | 1672 |
Executed CallContractTransaction decryption | 1672 |
Executed CallContractTransaction finishVoting | 1691 |
Executed CallContractTransaction removeFromVotersList | 48 |
Executed CallContractTransaction results | 1691 |
Executed CallContractTransaction startVoting | 1691 |
Executed CallContractTransaction vote | 1537676 |
Executed CreateContractTransaction voting-contract | 1691 |
Распределения аналогичные. Теперь нам нужно сравнить их с порталом публичного наблюдения, но пока — интересное замечание. В пятницу в 2021-09-17 15:20 происходила небольшая корректировка списка избирателей (removeFromVotersList). Такие события также видны в блокчейне, поэтому если вдруг кто-то умер или по иным причинам лишился «активного избирательного права», то факт удаления из списка избирателей также будет виден в дампе. В исходном коде смартконтрактов также есть операция по добавлению избирателей, но в голосовании она не использовалась
Сравнивать транзакции в дампах мы будем так: транзакции будут считаться равными, если у них совпадают временные метки, сигнатуры, внутренний id, входные и выходные параметры.
Для сравнения поделим всё время голосования на 200 временных отрезков и для каждого временного отрезка будем загружать все данные в память и в памяти искать соответствие транзакциям и сравнивать их.
Единственная сложность будет в сравнении входных и выходных параметров, т.к. они в дампе в формате protobuff, а в csv-файлах в JSON. C# реализация protobuf не позволяет напрямую загрузить JSON для части сообщения в объект, но поскольку эти объекты представляют собой по сути обычный словарь, то для них просто сделаем свою собственную логика сравнения.
Запустив сравнение, можно смело идти смотреть фильм — процесс будет небыстрым.
Это нужно повторить дважды для каждого из дампов.
Полный код сравнения находится в проекте DatabaseComparer в исходном коде.
Метрики и графики
Традиционно для систем голосования на базе блокчейна я строю графики зависимости номера блока от времени его добавления (block number/block timestamp), а также производного от него времени вычисления блока от номера блока. Эти графики дают представление о стабильности работы блокчейна — и поводы для размышлений. График зависимости номера блока от времени в случае стабильно работающего частного блокчейна должен быть практически прямой наклонной линией; график зависимости времени вычисления блока от номера блока — практически прямой горизонтальной линией. Это объясняется тем, что для частных блокчейнов, использующихся в голосованиях в России (с 2019 года я таких наблюдал три: Parity, Exonum и теперь Waves) консенсус настроен так, что события формирования блоков разделяют практически равные интервалы времени. Этот простой факт, например, позволил в 2019 году увидеть технические проблемы с голосованием в МГД 2019 года.
Поскольку у нас был прямой доступ к нодам наблюдения, помимо информации о транзакциях у нас есть ещё информация о блоках.
Самое первое, что мы видим: метки времени genesis-блока аж 2021-09-03. Это где-то две недели до голосования. Наверно, в этот момент как раз сформировали рабочие образы системы и подготовили её к дальнейшему развертыванию. Смахнул слезу, когда вспомнил про первый блок биткоина.
Исключим первый блок и выведем график без него:
Выглядит хорошо. График ровный, для пущей уверенности построим график зависимости времени вычисления блока от его номера.
Остальные шарды выглядят аналогично.
Как видно из графиков, среднее время вычисления блока — около 8 секунд.
Теперь построим графики распределения транзакций:
Тут мы видим три этапа работы системы:
- Подготовительный — в четверг в 21:00 (в ТИК ДЭГ загрузили в систему полученные из ГАС «Выборы» списки избирателей и голосований)
- Само голосование — с 8 утра пятницы до 20 вечера воскресения
- Подведение итогов — до 21:25 в воскресение
Какой-либо неожиданной активности за пределами этих понятных интервалов нет.
Описание проектов в репозитории
Analyzer — визуализатор данных
BlockchainVerifier — программа для переноса дампа блокчейна полученного с ноды наблюдателя в исследовательскую БД
DatabaseComparer — утилита для сверки исследовательских БД
EncodingConverter — программа для перевода последовательностей между форматами base64,base58 и hex
StatDownloadVerifier — программа для переноса скачанный файлов с сайта stat.vybory.gov.ru в исследовательскую БД
Voting2021.BlockchainWatcher.Console — тестовый вариант программы для загрузки данных
Voting2021.BlockchainWatcher.Web — программа для загрузки данных из одного шарда блокчейна с ноды наблюдателя
VotingFilesDownloader — программа для скачивания файлов транзакций с официального сайта https://stat.vybory.gov.ru/
Описание файлов данных
Votings.db3 — база, в которую загружены все транзакции из CSV-файлов
blockchain_dump_3dayend.7z — сырые данные, собранные с блокчейна на вечер воскресенья
blockchain_dump_3dayend.zip — исследовательская база, построенная из данных на вечер воскресенья
blockchain_dump_final.7z — сырые данные, собранные с блокчейна на утро понедельника
blockchain_dump_final.zip — исследовательская база, построенная из данных на утро понедельника
Выводы
К техническому наблюдению нужно готовиться. Просто для того, чтобы собрать данные о транзакциях в блокчейне двумя независимыми способами и сверить их, исключив простейшие варианты фальсификации (вброс, удаление или изменение бюллетеней post factum, например, уже при подсчёте голосов), потребовалось более месяца работы — две недели на написание, тестирование и отладку необходимых утилит и ещё больше на первичный анализ данных, призванный установить их целостность и отсутствие подозрительных аномалий.
Ссылки
Репозиторий с кодом — https://github.com/AlexeiScherbakov/Voting2021
voting.db3 — https://bdsm.ddem.ru/wl/?id=dnBxeMHTAViiuRzqXGTGy34nt9ega29L
blockchain_dump_3dayend.7z — https://bdsm.ddem.ru/wl/?id=ya5cC04NyHxoDeFh4aRbV9bRyVT9impv
blockchain_dump_3dayend.zip — https://bdsm.ddem.ru/wl/?id=uutaBLZiB9SbZXX6yIL1UgBITnSkAS6g
blockchain_dump_final.7z — https://bdsm.ddem.ru/wl/?id=Og22znJ6eDGWIFW5DizBp3C1pfRB5gt6
blockchain_dump_final.zip — https://bdsm.ddem.ru/wl/?id=vE0gRuJ55efL8ku3uvhW0U1P9qn749VY