Наблюдение за электронным голосованием: как это было
За последние два месяца, прошедшие с момента дистанционного электронного голосования (ДЭГ) на выборах 17–19 сентября 2021, было написано много статей и комментариев о том, как оценивать его результат. При этом одним из самых распространённых тезисов был тезис о якобы непрозрачности ДЭГ и полной невозможности наблюдения за ним.
Партия прямой демократии и приглашаемые нами независимые эксперты занимаются наблюдением за ДЭГ уже не первый год — и мы вынуждены категорически с этим тезисом не согласиться, с одним лишь уточнением: наблюдение за ДЭГ требует серьёзной технической подготовки. Невозможно просто прийти на участок или сесть за компьютер у себя дома утром первого дня голосования и методом «пристального взгляда» быстро всё оценить — требуются специфические знания, специфические инструменты и серьёзная предварительная подготовка.
Впрочем, даже в этом неверно противопоставлять ДЭГ обычному, «бумажному» голосованию — в последнем также весьма серьёзные усилия уделяются подготовке наблюдателей, хоть и в других вопросах: в знании процедур, законодательства, возможных нарушений и мер противодействия им, и прочее, и прочее. Просто зашедший с улицы на УИК человек точно так же в контексте наблюдения окажется практически бесполезен — максимум, что он сможет заметить, это самый грубые, можно сказать, наглые нарушения.
Техническая подготовка наблюдения за ДЭГ — лишь первый, но крайне важный этап работы. И, например, именно за полное, тотальное отсутствие такой подготовки мы порицали работавших на московских выборах наблюдателей от КПРФ и независимых депутатов. При том, что ход ДЭГ в Москве действительно вызывает ряд вопросов, со стороны пытающихся оспорить результаты голосования мы видим фактическое отсутствие наблюдения в ходе голосования и бессистемное набрасывание кучи претензий, от реальных до воображаемых или продиктованных политической конъюнктурой, вместо выстраивания последовательной тактики и вдумчивого анализа результатов ДЭГ.
В минувшие выборы мы плотно работали с федеральной системой ДЭГ — она использовалась в Ярославской, Нижегородской, Курской, Мурманской и Ростовской областях, а также Севастополе — и нам хотелось бы верить, что именно грамотно организованное наблюдение, к которому мы готовились в течение нескольких месяцев, позволило избежать не то что скандалов, а даже недопонимания между ТИК ДЭГ, ЦИК России, разработчиками системы (а это ПАО «Ростелеком» и компания Waves Enterprise) и представителями партий и кандидатов.
Чтобы показать, как было организовано наблюдение, сегодня мы публикуем текст Алексея Щербакова, разработчика блокчейн-систем и независимого эксперта в наблюдении за ДЭГ (кампания в МГД 2019 года, голосования по Конституции и ЕДГ 2020 года). В ходе голосования Алексей работал в Ситуационном центре по наблюдению за выборами Общественной палаты РФ в составе группы экспертного наблюдения за ДЭГ.
Это — лишь первая часть большой работы: в задачу Алексея входило надёжное получение данных о транзакциях в блокчейне федеральной системы ДЭГ и их первичный анализ на предмет целостности данных, стабильности работы системы ДЭГ в ходе всего голосования и отсутствия подозрительных аномалий в динамике получения и обработки данных голосования.
В дальнейшем мы планируем продолжить работу как над анализом данных, полученных в ходе электронного голосования, так и над общей оценкой применения ДЭГ на выборах осени 2021 года.
А сейчас — слово Алексею.
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/bdb/dec/168/bdbdec168103877606b38725431c0c74.jpg)
Проверяем вывод видео с нашего компьютера на большой экран, где картинку сможет увидеть пресса.
В 2019 году я разбирался в дистанционном электронном голосовании на выборах в Московскую городскую думу, в 2020 у нас было голосование по Конституции, также проходившее на московской платформе ДЭГ — где само голосование прошло хорошо, но случились накладки в стыковке ДЭГ с классическими участками: база данных с номерами паспортов избирателей, проголосовавших на ДЭГ, была передана на эти участки и в результате попала в сеть.
В этом же году мне, как наблюдателю, удалось получить доступ непосредственно к нодам наблюдения всех четырёх шардов блокчейна, использовавшегося в федеральной платформе ДЭГ, получить полный дамп транзакций блокчейна голосования и начать его исследование.
Как это было
Ещё в июле на 26 заседании ЦИК России член Общественной палаты РФ Александр Малькевич сообщил, что ОП РФ планирует сформировать группу технического наблюдения за подготовкой и ходом дистанционного электронного голосования. Так как голосование на выборах в Москве находится под контролем Общественной палаты Москвы, эта группа должна была заниматься федеральной системой ДЭГ.
К сентябрю эта группа была сформирована под большим и длинным труднозапоминающимся названием, которое никто никогда нигде не мог произнести и запомнить: «Команда технических экспертов рабочей группы по общественному контролю за ДЭГ и внедрению информационных технологий в избирательный процесс при Координационном совете ОП РФ по общественному контролю за голосованием». Запомнить это название было решительно невозможно ни одному человеку из тех, с кем я разговаривал, и поэтому мы сами себя продолжали называть группой технического наблюдения (ГТН).
Помимо ГТН, которая работала совершенно самостоятельно, у Общественной палаты был и собственный проект, озвученный Максимом Григорьевым — он назывался «цифровой сейф-пакет» и заключался в демонстрации возможности независимого наблюдения за целостностью и неизменностью данных о ходе голосования в блокчейне. В его рамках на четвёртом этаже ОП РФ, в зале пресс-конференций, должен был стоять компьютер, подключённый к ноде блокчейна и в реальном времени пишущий все проходящие через неё транзакции на внешние диски. Каждый день на вечерней пресс-конференции комплект дисков должен был сниматься и убираться в сейф — а по окончании голосования, то есть уже утром 20 сентября, диски должны были быть извлечены из сейфа, а сохранённые на них транзакции сличены с опубликованными в официальном блокчейне. Этот проект должен был показать, что однажды попавшее в блокчейн действительно никуда не пропадает — любые стёртые, изменённые или добавленные уже после окончания голосования транзакции на сверке сразу стали бы видны.
Так получилось, что софт для «цифрового сейф-пакета» писал я (вместе с программистом Партии прямой демократии Алексеем Зайцевым, который делал к нему графический интерфейс) — и все сверки данных также делал я. Это, помимо прочего, дало мне доступ к полному дампу всех транзакций блокчейна, полученному в реальном времени и напрямую через его ноду. Работал этот софт на нашем же компьютере, только канал подключения к ноде обеспечивала Общественная палата — а точнее, «Ростелеком».
Написание софта «сейф-пакета» заняло примерно две недели — 1 сентября мы скооперировались с разработчиками из Waves, получили от них API блокчейна, примеры кода, тестовый доступ к ноде блокчейна и принялись за работу.
В четверг 16 сентября (за день до выборов) я был в Общественной Палате, где мне уже должен был быть настроен прямой доступ к блокчейну. Конечно, как это всегда бывает, когда я туда приехал — доступ еще не был настроен, но в течении 2–3 часов, созваниваясь с инженером «Ростелекома» и переписываясь в Telegram с ребятами из Waves, я его таки получил. Очень важно было прийти именно в четверг, потому что, насколько я понял, никакие изменения в доступах в пятницу — после начала голосования — уже были бы невозможны. Я запустил написанную мной программу для наблюдения в количестве четырёх экземпляров (по одной на каждый шард блокчейна), проверил, что всё работает, и с чувством выполненного долга ушел из Общественной Палаты.
![](https://evoting.digitaldem.ru/wp-content/uploads/sites/2/2021/10/image-2-1024x766.png)
Кстати, Waves в итоге — уже в ходе голосования — заглянули к нам и несколько часов рассказывали экспертам из группы технического наблюдения про внутренее устройство системы и даже показывали технические метрики по блокчену, Kafka и другим компонентам, не скрывая особо ничего, за что им отдельное спасибо.
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/fd2/7e8/19b/fd27e819b6edce2941c58cb657b8ff92.jpg)
Кроме этого, всем членам ГТН в Общественной палате был доступ интерфейс наблюдателя за блокчейном и сайт со статистикой голосования и выгрузками из блокчейна — тот же stat.vybory.gov.ru, что и всему остальному миру, но с выделенным подключением по защищённому каналу.
![](https://evoting.digitaldem.ru/wp-content/uploads/sites/2/2021/10/image-1024x766.png)
Замечу, что интерфейс наблюдателя не надо путать с нодой блокчейна — эта путаница постоянно видна в отчётах московских наблюдателей:
- интерфейс наблюдателя — это веб-интерфейс, в котором можно просматривать на экране транзакции блокчейна в реальном времени, группировать их, фильтровать, и так далее;
- нода блокчейна (нода наблюдателя) — это сервер, у которого нет никакого веб-интерфейса, только возможность написать собственный софт, который будет отправлять к нему запросы и получать ответы (по сути, интерфейс наблюдателя примером такого софта и является).
В территориальной избирательной комиссии ДЭГ (ТИК ДЭГ) в здании ЦИК был ещё один способ работы с голосованиями — интерфейс члена ТИК ДЭГ, это такой «пульт управления голосованием». Для ГТН в Общественной палате он недоступен, так как одним из его элементов являются списки избирателей — а их может смотреть только член ТИК ДЭГ. В ТИК ДЭГ на рабочих местах при этом был и интерфейс наблюдателя — в результате, например, Виктор Толстогузов, который и входил в ГТН, и был членом ТИК ДЭГ от КПРФ, в Общественную палату не поехал, удовлетворившись интерфейсом наблюдателя в ТИК ДЭГ.
![](https://evoting.digitaldem.ru/wp-content/uploads/sites/2/2021/10/image-3-1024x766.png)
На ближайшем мониторе видно тот же интерфейс наблюдателя, что и в Общественной палате, но в менее красивых интерьерах помещений ЦИК России 🙂
Чтобы представлять себе доступный разным участникам процесса инструментарий чуть наглядее:
ТИК ДЭГ (Большой Черкасский пер., 9) | Общественная Палата РФ (Миусская пл., 7 стр. 1) | Все желающие (stat.vybory.gov.ru) | |
Статистика о ходе выборов | Есть | Есть | Есть |
Выгрузки блокчейна в CSV | Есть | Есть | Есть |
Цифровой сейф-пакет | Нет | Есть | Нет |
Интерфейс наблюдателя | Есть | Есть | Нет |
Интерфейс члена ТИК | Есть | Нет | Нет |
Нода наблюдателя | Нет | Есть | Нет |
![](https://evoting.digitaldem.ru/wp-content/uploads/sites/2/2021/10/image-1-1024x766.png)
Ноутбуки HP предоставлены «Ростелекомом» и подключены по защищённому каналу к серверам федеральной системы ДЭГ.
Одновременно для публично доступного сайта stat.vybory.gov.ru было решено написать программу, которая будет скачивать оттуда почасовые выгрузки транзакций блокчейна — сложность заключалась в том, что они были разбиты по избирательным округам, то есть, там лежало громадное количество файлов, «прощёлкать» которые мышкой и скачать вручную, если вас интересуют все данные, а не только один округ, просто нереально. Интерес они представляют по двум причинам — во-первых, их может скачать любой желающий, никаких допусков не надо, во-вторых, Waves показывали утилиты проверки целостности и корректности бюллетеней, то есть в общем тоже часть инфраструктуры технического наблюдения, работающие именно с этим форматом файлов. Ну и наконец, было интересно в рамках наблюдения проконтролировать, что выгрузка делается честно — то есть, совпадает с нашим дампом цифрового сейф-пакета.
Финальный вариант этой программы я выложил в субботу, 16 сентября, для всех желающих. Это консольная утилита, написанная на .NET Core. Публичный портал наблюдения устроен таким образом, что парсером HTML-страниц найти и загрузить выгрузки было невозможно, поэтому я «дергал» напрямую API его бэка, «подсмотрев», куда делает обращения браузер во время переходов между страницами. Затем я столкнулся с тем, что система безопасности не даёт делать к этому сайту чересчур много запросов. В результате, проконсультировавшись с разработчиками из «Ростелекома» и подобрав время задержки между запросами, мне удалось побороть и её. Если вы скачивали файлы — то там должно было получиться 43 199 файла внутри 1691 папок, суммарно почти 9 ГБ. Каждая папка — это один избирательный округ.
Как вы помните, на протяжении всего голосования специально написанная мной программа «сейф-пакета» заботливо сохраняла информацию о блоках и транзакциях из блокчейна. После завершения голосования и проверки, что количество транзакций в дампах совпадает с количеством транзакций в блокчейне (это уже было в понедельник утром), я взял полученный дамп к себе домой на изучение.
![Вечер воскресения - подводятся результаты Вечер воскресения - подводятся результаты](https://habrastorage.org/r/w1560/getpro/habr/upload_files/b6c/475/63c/b6c47563cf56a2bfee574301e4900ec7.jpg)
В итоге к концу голосования у меня были следующие дампы:
- Выгрузка с портала публичного наблюдения 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 — эта БД служила оперативным хранилищем для обработки событий. Важно сказать, что при доступе к блокчейну через ноду наблюдения мы получаем больше информации, чем в файлах с портала наблюдения. Например, здесь у нас есть информация о блоках, в то время как в экспорте на портале — только транзакции. Это даст нам возможность построить некоторые метрики, недоступные напрямую из публичной выгрузки.
![](https://evoting.digitaldem.ru/wp-content/uploads/sites/2/2021/10/image-4-1024x766.png)
За время тестирования (две недели до голосования) было установлено, что нагрузка на программу достаточно маленькая, получение данных прекрасно работает в однозадачном режиме (всё выполняется в рамках одной 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 при больших запросах создает временные файлы на системном диске, на котором у меня было свободно всего гигабайт пять. Это поведение, конечно же, описано в документации, но вечером в понедельник про такое сложно вспомнить.
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/c7b/109/a4b/c7b109a4bb4a723799bfa0a879e80e52.jpg)
Принудительно указав в качестве временной папки папку на внешнем 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 для части сообщения в объект, но поскольку эти объекты представляют собой по сути обычный словарь, то для них просто сделаем свою собственную логика сравнения.
Запустив сравнение, можно смело идти смотреть фильм — процесс будет небыстрым.
![Проверка соответствия транзакций Проверка соответствия транзакций](https://habrastorage.org/r/w1560/getpro/habr/upload_files/0eb/437/bfd/0eb437bfde8afa5ecd71908a5562fad3.png)
Это нужно повторить дважды для каждого из дампов.
Полный код сравнения находится в проекте DatabaseComparer в исходном коде.
Метрики и графики
Традиционно для систем голосования на базе блокчейна я строю графики зависимости номера блока от времени его добавления (block number/block timestamp), а также производного от него времени вычисления блока от номера блока. Эти графики дают представление о стабильности работы блокчейна — и поводы для размышлений. График зависимости номера блока от времени в случае стабильно работающего частного блокчейна должен быть практически прямой наклонной линией; график зависимости времени вычисления блока от номера блока — практически прямой горизонтальной линией. Это объясняется тем, что для частных блокчейнов, использующихся в голосованиях в России (с 2019 года я таких наблюдал три: Parity, Exonum и теперь Waves) консенсус настроен так, что события формирования блоков разделяют практически равные интервалы времени. Этот простой факт, например, позволил в 2019 году увидеть технические проблемы с голосованием в МГД 2019 года.
Поскольку у нас был прямой доступ к нодам наблюдения, помимо информации о транзакциях у нас есть ещё информация о блоках.
![Зависимость номера блока от времени Зависимость номера блока от времени](https://habrastorage.org/r/w1560/getpro/habr/upload_files/d9a/07c/133/d9a07c133d6b327ac793e0d5f4641749.png)
Самое первое, что мы видим: метки времени genesis-блока аж 2021-09-03. Это где-то две недели до голосования. Наверно, в этот момент как раз сформировали рабочие образы системы и подготовили её к дальнейшему развертыванию. Смахнул слезу, когда вспомнил про первый блок биткоина.
Исключим первый блок и выведем график без него:
![Зависимость номера блока от времени без учета genesis-блока Зависимость номера блока от времени без учета genesis-блока](https://habrastorage.org/r/w1560/getpro/habr/upload_files/cf8/ccb/874/cf8ccb8748038ddf2fe3ab704753c3cb.png)
Выглядит хорошо. График ровный, для пущей уверенности построим график зависимости времени вычисления блока от его номера.
Остальные шарды выглядят аналогично.
Как видно из графиков, среднее время вычисления блока — около 8 секунд.
Теперь построим графики распределения транзакций:
![Распределение транзакций в блоках Распределение транзакций в блоках](https://habrastorage.org/r/w1560/getpro/habr/upload_files/c19/94d/7b0/c1994d7b01f5ba57c9c92cfed6985c5a.png)
![Распределение транзакций в блоках с привязкой ко времени Распределение транзакций в блоках с привязкой ко времени](https://habrastorage.org/r/w1560/getpro/habr/upload_files/d7c/0ef/464/d7c0ef464796dcfdd865ed5b502bc9c9.png)
![Распределение транзакций по типам для первого шарда Распределение транзакций по типам для первого шарда](https://habrastorage.org/r/w1560/getpro/habr/upload_files/842/14e/f13/84214ef13935fe2c15760c2c414e9543.png)
![Распределение транзакций по типам для первого шарда с привязкой по времени Распределение транзакций по типам для первого шарда с привязкой по времени](https://habrastorage.org/r/w1560/getpro/habr/upload_files/255/0de/281/2550de281277fbd92ee0a180e9b29fb4.png)
Тут мы видим три этапа работы системы:
- Подготовительный — в четверг в 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