Наблюдение за Дистанционным Электронным Голосованием 2021 года (система Waves-РосТелеКом, федеральная)

Привет Хабр, в 2019 году я разбирался в дистанционном электронном голосовании в МГД, в 2020 у нас было голосование по Конституции, где само голосование прошло хорошо, но номера паспортов неудачно попали в сеть. Ну а в этом году мне, как наблюдателю, удалось получить доступ непосредственно к нодам наблюдения всех 4-ех шардов блокчейна, использовавшегося в федеральном голосовании, получить полный дамп транзакций блокчейна голосования и начать его исследование. Как это было и что из этого вышло — под катом


Как это было

Была сформирована группа с большим и длинным труднозапоминающимся названием, которое никто никогда нигде не мог произнести и запомнить: «Команда технических экспертов Рабочей группы по общественному контролю за ДЭГ и внедрению информационных технологий в избирательный процесс при Координационном совете ОП РФ по общественному контролю за голосованием». Запомнить это название было решительно невозможно ни одному человеку из тех с которыми я разговаривал и поэтому мы сами себя называли группой технического наблюдения.

В четверг (за день до выборов) я был в Общественной Палате, где мне уже должен был быть настроен доступ ко всему. Конечно, как это всегда бывает, когда я туда приехал там доступ еще не был настроен, но в течении 2-3 часов созваниваясь с инженером РосТелеКома и переписываясь в Telegram с ребятами из Waves в итоге после всей цепочки одобрений я таки получил долгожданный доступ. Очень важно было прийти именно в четверг, потому что, насколько я понял, никакие изменения в доступах в пятницу после начала голосования уже были бы невозможны. Я запустил написанную мной программу для наблюдения в количестве 4-ех экземпляров (по одной на каждый шард блокчейна), проверил что всё работает и с чувством выполненного долга ушел из Общественной Палаты. Кстати, Waves в итоге заглянули к нам и рассказывали про внутренее устройство системы и даже показывали технические метрики по блокчену, Kafka и другим компонентам, не скрывая особо ничего, за что им отдельный респект

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

Рабочие места для группы технического наблюдения
Рабочие места для группы технического наблюдения

Для самого сайта было решено написать программу которая скачает все эти файлы, финальный вариант которой я выложил в субботу для всех желающих. Это консольная утилита написанная на .NET Core. Сайт stat устроен таким образом, что парсером html страниц его стащить было невозможно, поэтому я «дергал» напрямую API его бэка, «подсмотрев» куда делает обращения браузер во время переходов между страницами. Затем я столкнулся с тем, что система безопасности не даёт делать к этому сайту черезчур много запросов. В результате подобрав время задержки между запросами мне удалось побороть и её. Если вы скачивали файлы — то там должно было получиться 43 199 файла внутри 1691 папок — суммарно почти 9 Гб. Каждая папка — это один избирательный округ.

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

Вечер воскресения - подводятся результаты
Вечер воскресения — подводятся результаты

В итоге к концу голосования у меня были следующие дампы:

  1. Выгрузка с stat-сайта, сделанная с домашнего ноутбука

  2. Запись данных в реальном времени на вечер воскресения и на утро понедельника (по транзакциям они не отличались, потому что после завершения голосования новых транзакций не было, но тем не менее ноды наблюдения были активны всю ночь с воскресения на понедельник)

  3. Выгрузка всего блокчейна на утро понедельника

Предварительная работа

Естественно для того чтобы всё это проделать потребовалась предварительная работа.

Как было сказано выше интерфейс наблюдателя и нода наблюдения — это не одно и то же. Интерфейс наблюдателя — это веб-приложение которое выводит информацию об транзакциях в блокчейне, но оно может получать эту информацию не напрямую из блокчейна, а через промежуточные системы. В то время как нода наблюдения, это компонент самого блокчейна. Она не имеет никакого графического интерфейса и всё общение с ней происходит через API. И для того чтобы наблюдать за блокчейном требуется написать программу, которая будет непрерывно скачивать транзакции с ноды наблюдения и сохранять к себе.

Нода наблюдения для системы Waves предоставляет несколько типов API для взаимодействия с ней. Для того чтобы не сильно нагружать систему был выбран механизм получения событий по подписке через gRPC Streaming.

Нода наблюдения, может присылать следующие события:

  1. Исторические данные до текущего (для блокчейна) момента — AppendedBlockHistory

  2. Данные о накоплению транзакций — MicroBlockAppended

  3. Данные об добавлении накопленных транзакций в блок — BlockAppended

  4. Данные по откату транзакций (в голосовании не должны были использоваться и не использовались, но их тоже добавил — а вдруг)

При подписывании на события нужно выбрать с какого момента необходимо получить данные (генезис,конкретный блок или текущий момент), после чего нода наблюдателя действовала по следующему алгоритму:

  1. Если момент времени находился в прошлом для блокчейна, то начинали отсылаться события AppendedBlockHistory, пока не время не дойдет до текущего момента

  2. В текущий момент времени приходили события по накоплению транзакций MicroBlockAppended, а через какое-то время событие добавления блока BlockAppended

  3. И так происходило до момента отключения

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

Затратив определенное время (где-то две пары выходных на написание кода, тестирование в фоновом режиме и отладка в оставшееся свободное время), была написана программа, которая следила за блокчейном через ноду наблюдателя, и записывала все транзакции в реальном времени. Посколько шардов было 4, то за каждым шардом блокчейна следил отдельный экземпляр программы, потому что так было банально проще. Все транзакции писались в отдельный файл transaction_output.bin, отдельно в sqlite БД сохранялось текущее состояние блокчейна: блоки и транзакции c полями необходимыми для быстрого поиска. При этом полная транзакция записывалась в transaction_output.bin, а в эта БД служила оперативным хранилищем для обработки событий. Важно сказать, что при доступе к блокчейну через ноду наблюдения мы получаем больше информации, чем в файлах с stat-сайта. Например здесь у нас есть информация о блоках, в то время как в экспорте только транзакции. Это даст нам возможность построить некоторые метрики, недоступные напрямую из выгрузки со stat-сайта.

За время тестирования (две недели до голосования) было установлено что нагрузка на программу достаточно маленькая и получение данных прекрасно работает в однозадачном режиме (всё выполняется в рамках одной Task внутри HostedService и не потребовались никакие чудеса распараллеливания). Единственная оптимизация которая  реально оказалась нужна, это объединение в batch-и событий типа AppendedBlockHistory в зависимости от количества транзакций в них. Это связано с тем, что при голосования бывает большое число блоков с небольшим количеством транзакций и когда мы в финале выкачиваем весь блокчейн для сравнения с данными реального времени, нам важно обработать как можно больше таких событий за одну транзакцию БД. Ребята из Waves любезно предоставили возможность подключения к тестовому серверу, чтобы я мог погонять свою программу и отработать разные сценарии её использования.

Исследование данных

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

Всего я использовал две структуры БД.

Первая база — это расширенная версия БД, использовавшаяся для скачивания файлов транзакций с сайта stat — votings.db3 (в ней сами транзакции были импортированы из файлов внутрь БД и были созданы поля для быстрого поиска)

Вторая база — это БД в которую я импортировал данные из дампа четырёх шардов блокчейна — research.db3

Первая база формируется на основе скачанных файлов и БД метаданных по ним. Она расширяет уже имеющуюся структуру БД написанной мной программы для скачивания файлов с свйта stat, таблицей transaction_in_file в которой размещаются данные из csv-файлов, при этом добавляется отношение 1-ко-многим для таблицы voting_file

Вторая база формируется на основе 4-ех пар файлов (blockchain.db3,transaction_output.bin) из дампа с программ наблюдения. Данные по блокам по сравнению с blockchain.db3 расширяются номером шарда. А для транзакций дополнительно выносятся метки времени, идентификаторы и подпись. Сама транзакций сохраняется в двух форматах: бинарном protobuf (как есть, raw) и json, чтобы всем кто захочет после меня с этим разобраться было легче увидеть общую структуру транзакции в удобном текстовом json-виде.

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

  1. Загрузка файла целиком в память

  2. Распаковывание csv-файла и чтение информации в объекты

  3. Подготовка группы объектов к batch импорту в БД

  4. Импорт группы объектов в БД

Конвеер построен стандартными средствами .NET на Dataflow. Шаги 1 и 4 выполняются всегда в однопоточном режиме, а шаг 2 — в параллельном. Посколько на моем компьютере 8 логических процессоров и эта операция не содержит операций ввода вывода, а только операции с объектами в памяти, то сверху устанавливается ограничение в 8 максимально исполняющихся задач для этого шага. Дополнительно в шагах устанавливается ограничение в виде 1000 импортируемых файлов в группе. Также накладывается ограничение в виде 5000 одновременно загруженных файлов в память и 5000 одновременно обрабатываемых файлов для того чтобы не занимать слишком много памяти

Загрузку данных шардов делаем немного по-другому.

  1. Вначале читаем информацию из блоков

  2. Читаем файл шарда с его БД целиком в память через проецируемые в память файлы

  3. Считываем транзакции из формата protobuff в объекты и выделяем необходимые поля для БД, дополнительно конвертируем в транзакции в json

  4. Группируем объект для batch импорта в БД

  5. Импортируем группу объектов

Здесь шаг 3 выполняется в многопоточном режиме.

Важное замечание, я старался потратить меньше своего времени поэтому весь код для импорта использует достаточно много оперативной памяти. Во всех случаях были выбраны варианты грузить файлы целиком в память и дальше уже обрабатывать в многопоточном режиме. На ноутбуке 32 Гб памяти, поэтому экономилось самое ценное — время программиста. Тем более что это единоразовый процесс.

Однако с первого раза создать БД не получилось. Оказалось что sqlite при больших запросах создает временные файлы на систеном диске, на котором у меня было свободно всего гигабайт 5. Это поведение конечно же описано в документации, но вечером в понедельник про такое сложно вспомнить.

Принудительно указав в качестве временной папки папку на внешенем 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-файле

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, который дает возможность переводить один формат в другой.

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

  1. BMSQjwfeFpJRz8eKJgmxvAZcv2bGiuuYqvLYHfAZ8wZ6 при переводе из base58 в base64 превращается в mdE3Qgl7+le9XweM5V++gZYfPX+KQE2H5wyfYh5jbxU= и это то что называется nested_tx_id в csv

  2. 103 — это код типа. 103 — создание, 104 — вызов. Исходный для 103 можно найти в папке creation, а 104 — в папке invocation исходного кода смарт-контрактов (см. VotingMessagesHandler.scala)

  3. 43hTRMyqfip9f5E3RA3TrtuMJMvkziaUrp8iQemW6YanUpVVkkNRujEC6JabdjSgMjfBsSDTMEmPGWKKxpJsLeyw это mGCRyCm7Uytv3xr2+t9qJFZsJql0ZqA4KHlbEsJ7LuF+mJBTrRFFzmpEDrbWAKcQbRnCZDAHm86LlhKdfXl6Eg== (signature — то что в блокчейне называется proof)

  4. 3 — это номер версии

  5. 1631813108420 — метка времени

  6. 3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK — hCWRuHtQC9QlE2XQuHD2BByM66taGJIQhmDvtxNtKu0aZnT/mmbPQWoXesFKy2L6WAqkFRsTb4pvzL5eBKv44g== это sender_public_key

  7. не используется

  8. не используется (7 и 8 судя по всему относятся к вознаграждению майнеров/комиссии и не нужны нам, да и в файлах они нигде не заполнены)

  9. это входные параметры

  10. это выходные параметры

  11. это дополнительные параметры, которые не вошли в вышеуказанные 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

Откуда мы видим что искомая транзакция находилась в 4-ом шарде и её полное представление:

Транзакция
{ "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 Гб размером, которые содержат данные о транзакциях которые нам теперь нужно сравнить.

Ведь всё это мы должные проделать для того, чтобы убедиться что данные в блокчейне не были никак подменены.

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

Сравнение исследовательских баз

На этом этапе нам необходимо проверить два факта:

  1. Транзакции записанные в ходе наблюдения идентичны транзакциям в файлах выгрузки на сайте stat

  2. Транзакции скачанные в понедельник с после завершения голосования с нод наблюдения идентичны транзакциям выгрузки на сайте stat

Нам для этого потребуется три базы. Одна база с файлами stat и две других полученные с нод наблюдения шардов блокчейна. Создаем последнюю по аналогии со второй имеем три БД по 18,33 и 33 Гб.

Рассмотрим дампы транзакций из блокчейна

Дамп полученный во время наблюдения

В нем всего 3107873 транзакции. Распределение по типам можно получить так:

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

Финальный дамп

Тип

Количество

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

Распределения аналогичные. Теперь нам нужно сравнить их с сайтом stat, но пока интересное наблюдение. В пятницу в 2021-09-17 15:20 происходила небольшая корректировка списка избирателей (removeFromVotersList). Такие события также видны в блокчейне, поэтому если вдруг кто-то умер, то факт удаления из списка избирателей также будет виден в дампе. В исходном коде смартконтрактов также есть операция по добавлению избирателей, но в голосовании она не использовалась

Сравнивать транзакции в дампах мы будем так:

Транзакции будут равными если у них совпадают временные метки, сигнатуры, внутренний id, входные и выходные параметры.

Для сравнения поделим всё время голосования на 200 временных отрезков и для каждого временного отрезка будем загружать все данные в память и в памяти искать соответствие транзакциям и сравнивать их.

Единственная сложность будет в сравнении входных и выходных параметров, т.к. они в дампе в формате protobuff, а в csv-файлах в json. C# реализация protobuf не позволяет напрямую загрузить json для части сообщения в объект, но поскольку, эти объекты представляют собой по сути обычный словарь — то для них просто сделаем кастомную логика сравнения.

Запустив сравнение можно смело идти смотреть фильм.

Проверка соответствия транзакций
Проверка соответствия транзакций

Это нужно повторить дважды для каждого из дампов.

Полный код сравнения находится в проекте DatabaseComparer в исходном коде

Метрики и графики

Традиционно для систем голосования на базе блокчейна я строю графики зависимости номера блока от времени его добавления (block number/block timestamp), а также производного от него графика времени вычисления блока от номера блока. Эти графики дают представление о стабильности работы блокчейна и дают поводы для размышлений. График зависимости номера блока от времени в случае стабильно работающего private блокчейна должен быть практически прямой наклоненной линией. А график зависимости времени вычисления блока от номера блока должен быть практически прямой горизонтальной линией. Это объясняется тем, что для приватных блокчейнов использующихся в голосованиях в России (с 2019 года я таких наблюдал три: Parity, Exonum и теперь Waves) консенсус настроен таким образом что в среднем каждый новый блок формируется таким образом что события формирования блоков разделяют практически равные интервалы времени. Этот простой факт, например, позволил в 2019 году увидеть проблемы с голосованием в МГД 2019 года.

Поскольку у нас был прямой доступ к нодам наблюдения, помимо информации о транзакциях у нас есть ещё информация о блоках.

Зависимость номера блока от времени
Зависимость номера блока от времени

Самое первое что мы видим: метки времени genesis-блока аж 2021-09-03. Это где-то две недели до голосования. Наверно, в этот момент как раз сформировали рабочие образы системы и подготовили её к дальнейшему развертыванию. Смахнул слезу, когда вспомнил про первый блок биткоина.

Исключим первый блок и выведем график без него

Зависимость номера блока от времени без учета genesis-блока
Зависимость номера блока от времени без учета genesis-блока

Выглядит хорошо. График ровный, для пущей уверенности построим график зависимости времени вычисления блока от его номера.

Время вычисления блока для первого шарда
Время вычисления блока для первого шарда
Остальные шарды

Как видно из графиков среднее время вычисления блока — около 8 секунд.

Теперь построим графики распределения транзакций

Распределение транзакций в блоках
Распределение транзакций в блоках
Распределение транзакций в блоках с привязкой ко времени
Распределение транзакций в блоках с привязкой ко времени
Распределение транзакций по типам для первого шарда
Распределение транзакций по типам для первого шарда
Распределение транзакций по типам для первого шарда с привязкой по времени
Распределение транзакций по типам для первого шарда с привязкой по времени

Тут мы видим три этапа работы системы

  1. Подготовительный — в четверг в 21:00

  2. Само голосование — с 8 утра пятницы до 20 вечера воскресения

  3. Подведение итогов — до 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 — исследовательская база построенная из данных на утро понедельника

Выводы

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

Обработка всех данных с написанием кода необходимого для этого, потребовала гораздо больше времени

Количество данных (а исходные данные + исследовательские БД в сумме дали почти 122 Гб) уже таково, что в дальнейшем использовать sqlite будет уже нецелесообразно.

Независимо собранные данные совпали с выгрузкой на сайте

В блокчейне отражаются операции удаления умерших избирателей.

С технической точки зрения проблем в работе системы видно не было

Ссылки

Репозиторий с кодом — 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

Читайте так же:

  • Роскомнадзор снова угрожает штрафом ВКонтактеРоскомнадзор снова угрожает штрафом ВКонтакте Роскомнадзор в очередной раз привлекает ВКонтакте к административной ответственности за неудаление запрещенной информации.  Как пояснили в соцсети, среди материалов дел фигурируют YouTube-ролики — контент, который размещен не на серверах ВКонтакте, а встроен с внешних ресурсов. […]
  • Как мы преуспели на международном конкурсе по выращиванию цифрового салатаКак мы преуспели на международном конкурсе по выращиванию цифрового салата Настоящее всё больше походит на то, что некогда представлялось фантастикой. Меня зовут Павел Дудукин, руководитель Data Science-направления в Центре развития финансовых технологий (ЦРФТ) Россельхозбанка, и в этой статье расскажу, как мы вышли в финал международного конкурса Autonomous […]
  • Microsoft сообщила об активизации «российских хакеров» из NobeliumMicrosoft сообщила об активизации «российских хакеров» из Nobelium Microsoft опубликовала отчёт о деятельности хакерской группировки Nobelium, которую американские спецслужбы связывают с Правительством РФ. Предположительно с 1 июля по 19 октября этого года она атаковала по меньшей мере 609 клиентов компании не менее 22 868 раз. Это больше, чем у всех […]
  • QA митап SuperJobQA митап SuperJob 27 октября в 18.30 состоится онлайн-митап SuperJob по тестированию. В программе — интересные доклады, классные спикеры из Badoo, Skillbox, Почтатех и SuperJob, а также розыгрыш мерча за самые интересные вопросы. Митап будет полезен QA-руководителям, QA-инженерам, проджект-менеджерам и […]