Наблюдение за электронным голосованием: как это было

За послед­ние два меся­ца, про­шед­шие с момен­та дистан­ци­он­но­го элек­трон­но­го голо­со­ва­ния (ДЭГ) на выбо­рах 17–19 сен­тяб­ря 2021, было напи­са­но мно­го ста­тей и ком­мен­та­ри­ев о том, как оце­ни­вать его резуль­тат. При этом одним из самых рас­про­стра­нён­ных тези­сов был тезис о яко­бы непро­зрач­но­сти ДЭГ и пол­ной невоз­мож­но­сти наблю­де­ния за ним.

Партия пря­мой демо­кра­тии и при­гла­ша­е­мые нами неза­ви­си­мые экс­пер­ты зани­ма­ют­ся наблю­де­ни­ем за ДЭГ уже не пер­вый год — и мы вынуж­де­ны кате­го­ри­че­ски с этим тези­сом не согла­сить­ся, с одним лишь уточ­не­ни­ем: наблю­де­ние за ДЭГ тре­бу­ет серьёз­ной тех­ни­че­ской под­го­тов­ки. Невозможно про­сто прий­ти на уча­сток или сесть за ком­пью­тер у себя дома утром пер­во­го дня голо­со­ва­ния и мето­дом «при­сталь­но­го взгля­да» быст­ро всё оце­нить — тре­бу­ют­ся спе­ци­фи­че­ские зна­ния, спе­ци­фи­че­ские инстру­мен­ты и серьёз­ная пред­ва­ри­тель­ная подготовка.

Впрочем, даже в этом невер­но про­ти­во­по­став­лять ДЭГ обыч­но­му, «бумаж­но­му» голо­со­ва­нию – в послед­нем так­же весь­ма серьёз­ные уси­лия уде­ля­ют­ся под­го­тов­ке наблю­да­те­лей, хоть и в дру­гих вопро­сах: в зна­нии про­це­дур, зако­но­да­тель­ства, воз­мож­ных нару­ше­ний и мер про­ти­во­дей­ствия им, и про­чее, и про­чее. Просто зашед­ший с ули­цы на УИК чело­век точ­но так же в кон­тек­сте наблю­де­ния ока­жет­ся прак­ти­че­ски бес­по­ле­зен — мак­си­мум, что он смо­жет заме­тить, это самый гру­бые, мож­но ска­зать, наг­лые нарушения.

Техническая под­го­тов­ка наблю­де­ния за ДЭГ — лишь пер­вый, но крайне важ­ный этап рабо­ты. И, напри­мер, имен­но за пол­ное, тоталь­ное отсут­ствие такой под­го­тов­ки мы пори­ца­ли рабо­тав­ших на мос­ков­ских выбо­рах наблю­да­те­лей от КПРФ и неза­ви­си­мых депу­та­тов. При том, что ход ДЭГ в Москве дей­стви­тель­но вызы­ва­ет ряд вопро­сов, со сто­ро­ны пыта­ю­щих­ся оспо­рить резуль­та­ты голо­со­ва­ния мы видим фак­ти­че­ское отсут­ствие наблю­де­ния в ходе голо­со­ва­ния и бес­си­стем­ное набра­сы­ва­ние кучи пре­тен­зий, от реаль­ных до вооб­ра­жа­е­мых или про­дик­то­ван­ных поли­ти­че­ской конъ­юнк­ту­рой, вме­сто выстра­и­ва­ния после­до­ва­тель­ной так­ти­ки и вдум­чи­во­го ана­ли­за резуль­та­тов ДЭГ.

В минув­шие выбо­ры мы плот­но рабо­та­ли с феде­раль­ной систе­мой ДЭГ – она исполь­зо­ва­лась в Ярославской, Нижегородской, Курской, Мурманской и Ростовской обла­стях, а так­же Севастополе — и нам хоте­лось бы верить, что имен­но гра­мот­но орга­ни­зо­ван­ное наблю­де­ние, к кото­ро­му мы гото­ви­лись в тече­ние несколь­ких меся­цев, поз­во­ли­ло избе­жать не то что скан­да­лов, а даже недо­по­ни­ма­ния меж­ду ТИК ДЭГ, ЦИК России, раз­ра­бот­чи­ка­ми систе­мы (а это ПАО «Ростелеком» и ком­па­ния Waves Enterprise) и пред­ста­ви­те­ля­ми пар­тий и кандидатов.

Чтобы пока­зать, как было орга­ни­зо­ва­но наблю­де­ние, сего­дня мы пуб­ли­ку­ем текст Алексея Щербакова, раз­ра­бот­чи­ка блокчейн-систем и неза­ви­си­мо­го экс­пер­та в наблю­де­нии за ДЭГ (кам­па­ния в МГД 2019 года, голо­со­ва­ния по Конституции и ЕДГ 2020 года). В ходе голо­со­ва­ния Алексей рабо­тал в Ситуационном цен­тре по наблю­де­нию за выбо­ра­ми Общественной пала­ты РФ в соста­ве груп­пы экс­перт­но­го наблю­де­ния за ДЭГ.

Это – лишь пер­вая часть боль­шой рабо­ты: в зада­чу Алексея вхо­ди­ло надёж­ное полу­че­ние дан­ных о тран­зак­ци­ях в блок­чейне феде­раль­ной систе­мы ДЭГ и их пер­вич­ный ана­лиз на пред­мет целост­но­сти дан­ных, ста­биль­но­сти рабо­ты систе­мы ДЭГ в ходе все­го голо­со­ва­ния и отсут­ствия подо­зри­тель­ных ано­ма­лий в дина­ми­ке полу­че­ния и обра­бот­ки дан­ных голосования.

В даль­ней­шем мы пла­ни­ру­ем про­дол­жить рабо­ту как над ана­ли­зом дан­ных, полу­чен­ных в ходе элек­трон­но­го голо­со­ва­ния, так и над общей оцен­кой при­ме­не­ния ДЭГ на выбо­рах осе­ни 2021 года.

А сей­час — сло­во Алексею.


Четверг, 16 сен­тяб­ря: пустая ещё ком­на­та пресс-конференций на чет­вёр­том эта­же Общественной пала­ты РФ.
Проверяем вывод видео с наше­го ком­пью­те­ра на боль­шой экран, где кар­тин­ку смо­жет уви­деть пресса.

В 2019 году я раз­би­рал­ся в дистан­ци­он­ном элек­трон­ном голо­со­ва­нии на выбо­рах в Московскую город­скую думу, в 2020 у нас было голо­со­ва­ние по Конституции, так­же про­хо­див­шее на мос­ков­ской плат­фор­ме ДЭГ – где само голо­со­ва­ние про­шло хоро­шо, но слу­чи­лись наклад­ки в сты­ков­ке ДЭГ с клас­си­че­ски­ми участ­ка­ми: база дан­ных с номе­ра­ми пас­пор­тов изби­ра­те­лей, про­го­ло­со­вав­ших на ДЭГ, была пере­да­на на эти участ­ки и в резуль­та­те попа­ла в сеть.

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


Как это было

Ещё в июле на 26 засе­да­нии ЦИК России член Общественной пала­ты РФ Александр Малькевич сооб­щил, что ОП РФ пла­ни­ру­ет сфор­ми­ро­вать груп­пу тех­ни­че­ско­го наблю­де­ния за под­го­тов­кой и ходом дистан­ци­он­но­го элек­трон­но­го голо­со­ва­ния. Так как голо­со­ва­ние на выбо­рах в Москве нахо­дит­ся под кон­тро­лем Общественной пала­ты Москвы, эта груп­па долж­на была зани­мать­ся феде­раль­ной систе­мой ДЭГ.

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

Помимо ГТН, кото­рая рабо­та­ла совер­шен­но само­сто­я­тель­но, у Общественной пала­ты был и соб­ствен­ный про­ект, озву­чен­ный Максимом Григорьевым – он назы­вал­ся «циф­ро­вой сейф-пакет» и заклю­чал­ся в демон­стра­ции воз­мож­но­сти неза­ви­си­мо­го наблю­де­ния за целост­но­стью и неиз­мен­но­стью дан­ных о ходе голо­со­ва­ния в блок­чейне. В его рам­ках на чет­вёр­том эта­же ОП РФ, в зале пресс-конференций, дол­жен был сто­ять ком­пью­тер, под­клю­чён­ный к ноде блок­чей­на и в реаль­ном вре­ме­ни пишу­щий все про­хо­дя­щие через неё тран­зак­ции на внеш­ние дис­ки. Каждый день на вечер­ней пресс-конференции ком­плект дис­ков дол­жен был сни­мать­ся и уби­рать­ся в сейф – а по окон­ча­нии голо­со­ва­ния, то есть уже утром 20 сен­тяб­ря, дис­ки долж­ны были быть извле­че­ны из сей­фа, а сохра­нён­ные на них тран­зак­ции сли­че­ны с опуб­ли­ко­ван­ны­ми в офи­ци­аль­ном блок­чейне. Этот про­ект дол­жен был пока­зать, что одна­жды попав­шее в блок­чейн дей­стви­тель­но нику­да не про­па­да­ет – любые стёр­тые, изме­нён­ные или добав­лен­ные уже после окон­ча­ния голо­со­ва­ния тран­зак­ции на свер­ке сра­зу ста­ли бы видны.

Так полу­чи­лось, что софт для «циф­ро­во­го сейф-пакета» писал я (вме­сте с про­грам­ми­стом Партии пря­мой демо­кра­тии Алексеем Зайцевым, кото­рый делал к нему гра­фи­че­ский интер­фейс) – и все свер­ки дан­ных так­же делал я. Это, поми­мо про­че­го, дало мне доступ к пол­но­му дам­пу всех тран­зак­ций блок­чей­на, полу­чен­но­му в реаль­ном вре­ме­ни и напря­мую через его ноду. Работал этот софт на нашем же ком­пью­те­ре, толь­ко канал под­клю­че­ния к ноде обес­пе­чи­ва­ла Общественная пала­та – а точ­нее, «Ростелеком».

Написание соф­та «сейф-пакета» заня­ло при­мер­но две неде­ли – 1 сен­тяб­ря мы ско­опе­ри­ро­ва­лись с раз­ра­бот­чи­ка­ми из Waves, полу­чи­ли от них API блок­чей­на, при­ме­ры кода, тесто­вый доступ к ноде блок­чей­на и при­ня­лись за работу.

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

Разработчики блок­чей­на феде­раль­ной систе­мы ДЭГ (Waves Enterprise) в гсо­тях у груп­пы тех­ни­че­ско­го наблю­де­ния в Общественной пала­те РФ 17 сен­тяб­ря 2021.

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

Четыре экзем­пля­ра ути­ли­ты наблю­де­ния запу­сти­лись, под­клю­чи­лись к шар­дам блок­чей­на феде­раль­ной систе­мы ДЭГ и ждут тран­зак­ций. Голосование ещё не началось.

Кроме это­го, всем чле­нам ГТН в Общественной пала­те был доступ интер­фейс наблю­да­те­ля за блок­чей­ном и сайт со ста­ти­сти­кой голо­со­ва­ния и выгруз­ка­ми из блок­чей­на — тот же stat.vybory.gov.ru, что и все­му осталь­но­му миру, но с выде­лен­ным под­клю­че­ни­ем по защи­щён­но­му каналу.

Интерфейс наблю­да­те­ля феде­раль­ной систе­мы ДЭГ с тран­зак­ци­я­ми блокчейна

Замечу, что интер­фейс наблю­да­те­ля не надо путать с нодой блок­чей­на – эта пута­ни­ца посто­ян­но вид­на в отчё­тах мос­ков­ских наблюдателей:

  • интер­фейс наблю­да­те­ля – это веб-интерфейс, в кото­ром мож­но про­смат­ри­вать на экране тран­зак­ции блок­чей­на в реаль­ном вре­ме­ни, груп­пи­ро­вать их, филь­тро­вать, и так далее;
  • нода блок­чей­на (нода наблю­да­те­ля) — это сер­вер, у кото­ро­го нет ника­ко­го веб-интерфейса, толь­ко воз­мож­ность напи­сать соб­ствен­ный софт, кото­рый будет отправ­лять к нему запро­сы и полу­чать отве­ты (по сути, интер­фейс наблю­да­те­ля при­ме­ром тако­го соф­та и является).

В тер­ри­то­ри­аль­ной изби­ра­тель­ной комис­сии ДЭГ (ТИК ДЭГ) в зда­нии ЦИК был ещё один спо­соб рабо­ты с голо­со­ва­ни­я­ми – интер­фейс чле­на ТИК ДЭГ, это такой «пульт управ­ле­ния голо­со­ва­ни­ем». Для ГТН в Общественной пала­те он недо­сту­пен, так как одним из его эле­мен­тов явля­ют­ся спис­ки изби­ра­те­лей – а их может смот­реть толь­ко член ТИК ДЭГ. В ТИК ДЭГ на рабо­чих местах при этом был и интер­фейс наблю­да­те­ля — в резуль­та­те, напри­мер, Виктор Толстогузов, кото­рый и вхо­дил в ГТН, и был чле­ном ТИК ДЭГ от КПРФ, в Общественную пала­ту не поехал, удо­вле­тво­рив­шись интер­фей­сом наблю­да­те­ля в ТИК ДЭГ.

Члены ТИК ДЭГ за сво­и­ми рабо­чи­ми места­ми.
На бли­жай­шем мони­то­ре вид­но тот же интер­фейс наблю­да­те­ля, что и в Общественной пала­те, но в менее кра­си­вых инте­рье­рах поме­ще­ний ЦИК России 🙂

Чтобы пред­став­лять себе доступ­ный раз­ным участ­ни­кам про­цес­са инстру­мен­та­рий чуть наглядее:

ТИК ДЭГ
(Большой Черкасский пер., 9)
Общественная Палата РФ
(Миусская пл., 7 стр. 1)
Все жела­ю­щие
(stat.vybory.gov.ru)
Статистика о ходе выборовЕстьЕстьЕсть
Выгрузки блок­чей­на в CSVЕстьЕстьЕсть
Цифровой сейф-пакетНетЕстьНет
Интерфейс наблю­да­те­ляЕстьЕстьНет
Интерфейс чле­на ТИКЕстьНетНет
Нода наблю­да­те­ляНетЕстьНет
Инструментарий тех­ни­че­ско­го наблю­де­ния в феде­раль­ной систе­ме ДЭГ
Рабочие места груп­пы тех­ни­че­ско­го наблю­де­ния в Общественной пала­те РФ.
Ноутбуки HP предо­став­ле­ны «Ростелекомом» и под­клю­че­ны по защи­щён­но­му кана­лу к сер­ве­рам феде­раль­ной систе­мы ДЭГ.

Одновременно для пуб­лич­но доступ­но­го сай­та stat.vybory.gov.ru было реше­но напи­сать про­грам­му, кото­рая будет ска­чи­вать отту­да поча­со­вые выгруз­ки тран­зак­ций блок­чей­на – слож­ность заклю­ча­лась в том, что они были раз­би­ты по изби­ра­тель­ным окру­гам, то есть, там лежа­ло гро­мад­ное коли­че­ство фай­лов, «про­щёл­кать» кото­рые мыш­кой и ска­чать вруч­ную, если вас инте­ре­су­ют все дан­ные, а не толь­ко один округ, про­сто нере­аль­но. Интерес они пред­став­ля­ют по двум при­чи­нам – во-первых, их может ска­чать любой жела­ю­щий, ника­ких допус­ков не надо, во-вторых, Waves пока­зы­ва­ли ути­ли­ты про­вер­ки целост­но­сти и кор­рект­но­сти бюл­ле­те­ней, то есть в общем тоже часть инфра­струк­ту­ры тех­ни­че­ско­го наблю­де­ния, рабо­та­ю­щие имен­но с этим фор­ма­том фай­лов. Ну и нако­нец, было инте­рес­но в рам­ках наблю­де­ния про­кон­тро­ли­ро­вать, что выгруз­ка дела­ет­ся чест­но – то есть, сов­па­да­ет с нашим дам­пом циф­ро­во­го сейф-пакета.

Финальный вари­ант этой про­грам­мы я выло­жил в суб­бо­ту, 16 сен­тяб­ря, для всех жела­ю­щих. Это кон­соль­ная ути­ли­та, напи­сан­ная на .NET Core. Публичный пор­тал наблю­де­ния устро­ен таким обра­зом, что пар­се­ром HTML-страниц най­ти и загру­зить выгруз­ки было невоз­мож­но, поэто­му я «дер­гал» напря­мую API его бэка, «под­смот­рев», куда дела­ет обра­ще­ния бра­у­зер во вре­мя пере­хо­дов меж­ду стра­ни­ца­ми. Затем я столк­нул­ся с тем, что систе­ма без­опас­но­сти не даёт делать к это­му сай­ту черес­чур мно­го запро­сов. В резуль­та­те, про­кон­суль­ти­ро­вав­шись с раз­ра­бот­чи­ка­ми из «Ростелекома» и подо­брав вре­мя задерж­ки меж­ду запро­са­ми, мне уда­лось побо­роть и её. Если вы ска­чи­ва­ли фай­лы – то там долж­но было полу­чить­ся 43 199 фай­ла внут­ри 1691 папок, сум­мар­но почти 9 ГБ. Каждая пап­ка – это один изби­ра­тель­ный округ.

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

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

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

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

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

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

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

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

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

  1. Исторические дан­ные до теку­ще­го (для блок­чей­на) момен­та – AppendedBlockHistory
  2. Данные о накоп­ле­нии тран­зак­ций – MicroBlockAppended
  3. Данные о добав­ле­нии накоп­лен­ных тран­зак­ций в блок – BlockAppended
  4. Данные по отка­ту тран­зак­ций (в голо­со­ва­нии не долж­ны были исполь­зо­вать­ся и не исполь­зо­ва­лись, но я их тоже доба­вил – а вдруг)

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

  1. Если момент вре­ме­ни нахо­дил­ся в про­шлом для блок­чей­на, то начи­на­ли отсы­лать­ся собы­тия AppendedBlockHistory, пока вре­мя не дой­дет до теку­ще­го момента
  2. В теку­щий момент вре­ме­ни при­хо­ди­ли собы­тия по накоп­ле­нию тран­зак­ций MicroBlockAppended, а через какое-то вре­мя собы­тие добав­ле­ния бло­ка BlockAppended
  3. И так про­ис­хо­ди­ло до момен­та отключения

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

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

«Цифровой сейф-пакет» в Общественной пала­те РФ в про­цес­се рабо­ты (интер­фейс был напи­сан в основ­ном ради прессы 🙂

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

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

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

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

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

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

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

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

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

  1. Загрузка фай­ла цели­ком в память
  2. Распаковка CSV-файла и чте­ние инфор­ма­ции в объекты
  3. Подготовка груп­пы объ­ек­тов к пакет­но­му импор­ту в БД
  4. Импорт груп­пы объ­ек­тов в БД

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

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

  1. Вначале чита­ем инфор­ма­цию из блоков
  2. Читаем файл шар­да с его БД цели­ком в память через про­еци­ру­е­мые в память файлы
  3. Считываем тран­зак­ции из фор­ма­та protobuff в объ­ек­ты и выде­ля­ем необ­хо­ди­мые поля для БД, допол­ни­тель­но кон­вер­ти­ру­ем в тран­зак­ции в JSON
  4. Группируем объ­ект для пакет­но­го импор­та в БД
  5. Импортируем груп­пу объектов

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

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

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

Принудительно ука­зав в каче­стве вре­мен­ной пап­ки пап­ку на внеш­нем SSD, эту про­бле­му я решил.

Теперь, когда у нас есть уже при­год­ные для ана­ли­за базы, попро­бу­ем най­ти сле­ду­ю­щую тран­зак­цию из блок­чей­на в фай­лах выгруз­ки. Данное пред­став­ле­ние полу­че­но из Protobuf с помо­щью стан­дарт­но­го меха­низ­ма сери­а­ли­за­ции сооб­ще­ния в JSON:

{
    "version": 2,
    "executedContractTransaction": {
        "id": "oUI7hiIudzJ5tHwRv9mtKSo9I/T96V2uQNVBzaxPPO0=",
        "senderPublicKey": "YokI3qTb3tVwzFp0Mel8kDkxC9dG1JcPVIm2W261MB2ihglzCCO0P5liyPMJUcOsmY5yk16Zqc9dumSrffpiWg==",
        "tx": {
            "version": 3,
            "createContractTransaction": {
                "id": "mdE3Qgl7+le9XweM5V++gZYfPX+KQE2H5wyfYh5jbxU=",
                "senderPublicKey": "hCWRuHtQC9QlE2XQuHD2BByM66taGJIQhmDvtxNtKu0aZnT/mmbPQWoXesFKy2L6WAqkFRsTb4pvzL5eBKv44g==",
                "image": "voting/voting-contract:v1.5.0",
                "imageHash": "48de795e67a538bbc53da37d622ee69490e0572a43af3818483f302e00cb423d",
                "contractName": "voting-contract",
                "params": [
                    {
                        "key": "pollId",
                        "stringValue": "c6e9ed2a-a972-4701-a67f-321d29523f1d"
                    },
                    {
                        "key": "type",
                        "stringValue": "blind"
                    },
                    {
                        "key": "blindSigModulo",
                        "binaryValue": "yiJKYoVwHEnh0k+lDBl27NUo6JQurRloDedQI4zEIdQL7/gveClkgWZF1jDMNEX3+MgdMHwuxLR1QMiv576MC04a5F0acHgTSb5DRaPnjMaW3OhqFwVpv7I7YmPefIGSl/pHKfFXiI32qGRIlfbOritwNgs5E5iwiEA3DGB7jEOCpKA3kU1EnuYgmtDaqMWTeev+qyZiO3Qia0PzkH8hd4yKtNkFN0tT81FALZo36CX5mdAcoTycJ0ss1iHbNTw+rYiu3v87WaGsBFOiUsZIoIVgwhmipZpStdgQkrkJDyhc2koqq8H8WeA1AmbINjSxTzAnqa847ne4rfo+gMXCM4lDJPCktw4bnmXw6fSBmXSwgLilsy+WKwGmK96NZEHbXfIS3Zq+C5vK8eVmXzTUnYN68G81tVC8PqF213Vh3pYLabEpcwS0f1VDWbzB7+GWRirLN6rrd5mHVoVUZLUn8mOxbhS9W39XKAOfFkrIqqrajOgsTNlphkBwNtAbLcE3sHrFI9TlpP4M1pJPOtM3G8vm+lRpXzHWm2yC5+RAqqjUIvJR9GYESAYcL6jd/988cVxX8RiJs4p/4r1xvNOeA8EABZgCRNpVxDHuqvMHFw03A6Cvi+Mk2fAsWxXV6PQnn0UTzIsywLE/W6ATaIsiFLhAHTg2SY80+VHc1hj5R8E="
                    },
                    {
                        "key": "blindSigExponent",
                        "binaryValue": "AQAB"
                    },
                    {
                        "key": "bulletinHash",
                        "stringValue": "6VZ3SiX5B2U67xJCDaNYDUsjHnLfmU79B8GJHs3HgRAS"
                    },
                    {
                        "key": "dimension",
                        "stringValue": "[[1,3,5]]"
                    },
                    {
                        "key": "votersListRegistrator",
                        "stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
                    },
                    {
                        "key": "blindSigIssueRegistrator",
                        "stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
                    },
                    {
                        "key": "issueBallotsRegistrator",
                        "stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
                    },
                    {
                        "key": "jwtTokenRegistrator",
                        "stringValue": "MIIDVDCCAjygAwIBAgIEU4b7XDANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MDUyOTA5MTgyMFoXDTI0MDUyNjA5MTgyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIOP1eIjY2KwuIAUjOzpQ22Bzaqcn1GJKEaGfhhnE1P1XeO7K0y5YRQA7U3AuBmp4E2Padc6ZtQxju54VUW+iFClrePY8EQKbx9pP76ODZaLum8KmXnoNQVSWURgR+VLZ2eZOYEd6isp7W/kaRt7AFn2UInB+sn6FmwUusqXydBCexjcWngJ+WpI0mDMBceJkVRtqWKPi8to43eV5W1oapzJXurETr0eMu0mrnaltgYO7BP/Ga2BiUQXYDJ+XfNzOrsThIaeMqfEz9/jZ1wMSJHiuCDGgTMM38Pzho20vGv1DJzdRDE+G5F25NM/2P+YLiNh9TK64LCELOlUKK3/Sc8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAI3yE1yF+ldMYM9ZBIeSB0LC2BsfTS7pX8Vl0gFATljnsOzcXPITdjf3pJmyi7B+AMKW6A4JqrMKRBr92FHw7CJccqZ6O5MWjO0ca7oDHXNin+WeyrzNZajkoLXR7Ah1RzGtsFnF/tKGL9ecPfIZG7G6rpt3SknrAcB1rmK+0auDphnvvECkCLx/MzPCbTHdqJC9no7d/IbxYIg57HCv2tQsTJJtRT7TmmQUB0BQf+Hmk7v6dLXaqufB0dx7BTqkKhRJvSXKRyX1LopAB9VHiP8R8EKv/QYoOBlw1EVvrzMaOb6wc7ElkCwdYl6oGSb3CTlSuhcOLsf6gkZGiCeWu3A=="
                    },
                    {
                        "key": "ballotReceivedCert",
                        "stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
                    },
                    {
                        "key": "servers",
                        "stringValue": "[\"3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK\"]"
                    }
                ],
                "timestamp": "1631813108420",
                "proofs": [
                    "mGCRyCm7Uytv3xr2+t9qJFZsJql0ZqA4KHlbEsJ7LuF+mJBTrRFFzmpEDrbWAKcQbRnCZDAHm86LlhKdfXl6Eg=="
                ]
            }
        },
        "results": [
            {
                "key": "VOTING_BASE",
                "stringValue": "{\"pollId\":\"c6e9ed2a-a972-4701-a67f-321d29523f1d\",\"bulletinHash\":\"6VZ3SiX5B2U67xJCDaNYDUsjHnLfmU79B8GJHs3HgRAS\",\"dimension\":[[1,3,5]],\"blindSigModulo\":\"ca224a6285701c49e1d24fa50c1976ecd528e8942ead19680de750238cc421d40beff82f782964816645d630cc3445f7f8c81d307c2ec4b47540c8afe7be8c0b4e1ae45d1a70781349be4345a3e78cc696dce86a170569bfb23b6263de7c819297fa4729f157888df6a8644895f6ceae2b70360b391398b08840370c607b8c4382a4a037914d449ee6209ad0daa8c59379ebfeab26623b74226b43f3907f21778c8ab4d905374b53f351402d9a37e825f999d01ca13c9c274b2cd621db353c3ead88aedeff3b59a1ac0453a252c648a08560c219a2a59a52b5d81092b9090f285cda4a2aabc1fc59e0350266c83634b14f3027a9af38ee77b8adfa3e80c5c233894324f0a4b70e1b9e65f0e9f4819974b080b8a5b32f962b01a62bde8d6441db5df212dd9abe0b9bcaf1e5665f34d49d837af06f35b550bc3ea176d77561de960b69b1297304b47f554359bcc1efe196462acb37aaeb77998756855464b527f263b16e14bd5b7f5728039f164ac8aaaada8ce82c4cd96986407036d01b2dc137b07ac523d4e5a4fe0cd6924f3ad3371bcbe6fa54695f31d69b6c82e7e440aaa8d422f251f4660448061c2fa8ddffdf3c715c57f11889b38a7fe2bd71bcd39e03c10005980244da55c431eeaaf307170d3703a0af8be324d9f02c5b15d5e8f4279f4513cc8b32c0b13f5ba013688b2214b8401d3836498f34f951dcd618f947c1\",\"blindSigExponent\":\"10001\",\"status\":\"Active\",\"isRevoteBlocked\":true}"
            },
            {
                "key": "SERVERS",
                "stringValue": "[\"3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK\"]"
            },
            {
                "key": "VOTERS_LIST_REGISTRATOR",
                "stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
            },
            {
                "key": "ISSUE_BALLOTS_REGISTRATOR",
                "stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
            },
            {
                "key": "BLINDSIG_ISSUE_REGISTRATOR",
                "stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
            },
            {
                "key": "JWTTOKEN_REGISTRATOR",
                "stringValue": "MIIDVDCCAjygAwIBAgIEU4b7XDANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MDUyOTA5MTgyMFoXDTI0MDUyNjA5MTgyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIOP1eIjY2KwuIAUjOzpQ22Bzaqcn1GJKEaGfhhnE1P1XeO7K0y5YRQA7U3AuBmp4E2Padc6ZtQxju54VUW+iFClrePY8EQKbx9pP76ODZaLum8KmXnoNQVSWURgR+VLZ2eZOYEd6isp7W/kaRt7AFn2UInB+sn6FmwUusqXydBCexjcWngJ+WpI0mDMBceJkVRtqWKPi8to43eV5W1oapzJXurETr0eMu0mrnaltgYO7BP/Ga2BiUQXYDJ+XfNzOrsThIaeMqfEz9/jZ1wMSJHiuCDGgTMM38Pzho20vGv1DJzdRDE+G5F25NM/2P+YLiNh9TK64LCELOlUKK3/Sc8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAI3yE1yF+ldMYM9ZBIeSB0LC2BsfTS7pX8Vl0gFATljnsOzcXPITdjf3pJmyi7B+AMKW6A4JqrMKRBr92FHw7CJccqZ6O5MWjO0ca7oDHXNin+WeyrzNZajkoLXR7Ah1RzGtsFnF/tKGL9ecPfIZG7G6rpt3SknrAcB1rmK+0auDphnvvECkCLx/MzPCbTHdqJC9no7d/IbxYIg57HCv2tQsTJJtRT7TmmQUB0BQf+Hmk7v6dLXaqufB0dx7BTqkKhRJvSXKRyX1LopAB9VHiP8R8EKv/QYoOBlw1EVvrzMaOb6wc7ElkCwdYl6oGSb3CTlSuhcOLsf6gkZGiCeWu3A=="
            },
            {
                "key": "BALLOT_RECEIVED_CERT",
                "stringValue": "38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"
            }
        ],
        "resultsHash": "/9vjjH4zD9n7Lf8g2rkI5UZhHLnH7gT54xS90+yjenA=",
        "timestamp": "1631813108821",
        "proofs": [
            "Tx/HPuGAFJLFGxOIlQp3bdXvsu3JBAJw62t7DQ8JMGzt80A4Gc/HQsag3H1CZKl5tk4lmyzm/eO3PFfnEK4z8g=="
        ]
    }
}

Обратите вни­ма­ние, что в наших дан­ных два timestamp – 1631813108420 и 1631813108821 – кото­рые опи­сы­ва­ют два момен­та вре­ме­ни 2021-09-16 17:25:08 (по Гринвичу) с раз­ни­цей око­ло 400 миллисекунд.

Чтобы най­ти эту тран­зак­цию в иссле­до­ва­тель­ской БД votings.db3 и полу­чить файл, где она нахо­дит­ся, нуж­но взять ПЕРВЫЙ timestamp и исполь­зо­вать сле­ду­ю­щий SQL-код:

SELECT F.filename
FROM transaction_in_file AS T
INNER JOIN voting_file AS F
ON T.file_id=F.id
INNER JOIN voting AS V
ON F.voting_id=V.id
where T.timestamp=1631813108420

Получается, что нуж­ный файл – это BMSQjwfeFpJRz8eKJgmxvAZcv2bGiuuYqvLYHfAZ8wZ6_2021-09–16_2000-2100.zip. Транзакция с вре­мен­ной мет­кой 2021-09-16 20:25:08 по Москве дей­стви­тель­но попа­да­ет в этот диапазон!

Искомая тран­зак­ция в csv-файле:

BMSQjwfeFpJRz8eKJgmxvAZcv2bGiuuYqvLYHfAZ8wZ6;103;43hTRMyqfip9f5E3RA3TrtuMJMvkziaUrp8iQemW6YanUpVVkkNRujEC6JabdjSgMjfBsSDTMEmPGWKKxpJsLeyw;3;1631813108420;3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK;0;;[{"key":"pollId","stringValue":"c6e9ed2a-a972-4701-a67f-321d29523f1d"},{"key":"type","stringValue":"blind"},{"key":"blindSigModulo","binaryValue":"yiJKYoVwHEnh0k+lDBl27NUo6JQurRloDedQI4zEIdQL7/gveClkgWZF1jDMNEX3+MgdMHwuxLR1QMiv576MC04a5F0acHgTSb5DRaPnjMaW3OhqFwVpv7I7YmPefIGSl/pHKfFXiI32qGRIlfbOritwNgs5E5iwiEA3DGB7jEOCpKA3kU1EnuYgmtDaqMWTeev+qyZiO3Qia0PzkH8hd4yKtNkFN0tT81FALZo36CX5mdAcoTycJ0ss1iHbNTw+rYiu3v87WaGsBFOiUsZIoIVgwhmipZpStdgQkrkJDyhc2koqq8H8WeA1AmbINjSxTzAnqa847ne4rfo+gMXCM4lDJPCktw4bnmXw6fSBmXSwgLilsy+WKwGmK96NZEHbXfIS3Zq+C5vK8eVmXzTUnYN68G81tVC8PqF213Vh3pYLabEpcwS0f1VDWbzB7+GWRirLN6rrd5mHVoVUZLUn8mOxbhS9W39XKAOfFkrIqqrajOgsTNlphkBwNtAbLcE3sHrFI9TlpP4M1pJPOtM3G8vm+lRpXzHWm2yC5+RAqqjUIvJR9GYESAYcL6jd/988cVxX8RiJs4p/4r1xvNOeA8EABZgCRNpVxDHuqvMHFw03A6Cvi+Mk2fAsWxXV6PQnn0UTzIsywLE/W6ATaIsiFLhAHTg2SY80+VHc1hj5R8E="},{"key":"blindSigExponent","binaryValue":"AQAB"},{"key":"bulletinHash","stringValue":"6VZ3SiX5B2U67xJCDaNYDUsjHnLfmU79B8GJHs3HgRAS"},{"key":"dimension","stringValue":"[[1,3,5]]"},{"key":"votersListRegistrator","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"blindSigIssueRegistrator","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"issueBallotsRegistrator","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"jwtTokenRegistrator","stringValue":"MIIDVDCCAjygAwIBAgIEU4b7XDANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MDUyOTA5MTgyMFoXDTI0MDUyNjA5MTgyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIOP1eIjY2KwuIAUjOzpQ22Bzaqcn1GJKEaGfhhnE1P1XeO7K0y5YRQA7U3AuBmp4E2Padc6ZtQxju54VUW+iFClrePY8EQKbx9pP76ODZaLum8KmXnoNQVSWURgR+VLZ2eZOYEd6isp7W/kaRt7AFn2UInB+sn6FmwUusqXydBCexjcWngJ+WpI0mDMBceJkVRtqWKPi8to43eV5W1oapzJXurETr0eMu0mrnaltgYO7BP/Ga2BiUQXYDJ+XfNzOrsThIaeMqfEz9/jZ1wMSJHiuCDGgTMM38Pzho20vGv1DJzdRDE+G5F25NM/2P+YLiNh9TK64LCELOlUKK3/Sc8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAI3yE1yF+ldMYM9ZBIeSB0LC2BsfTS7pX8Vl0gFATljnsOzcXPITdjf3pJmyi7B+AMKW6A4JqrMKRBr92FHw7CJccqZ6O5MWjO0ca7oDHXNin+WeyrzNZajkoLXR7Ah1RzGtsFnF/tKGL9ecPfIZG7G6rpt3SknrAcB1rmK+0auDphnvvECkCLx/MzPCbTHdqJC9no7d/IbxYIg57HCv2tQsTJJtRT7TmmQUB0BQf+Hmk7v6dLXaqufB0dx7BTqkKhRJvSXKRyX1LopAB9VHiP8R8EKv/QYoOBlw1EVvrzMaOb6wc7ElkCwdYl6oGSb3CTlSuhcOLsf6gkZGiCeWu3A=="},{"key":"ballotReceivedCert","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"servers","stringValue":"[\"3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK\"]"}];[{"key":"VOTING_BASE","stringValue":"{\"pollId\":\"c6e9ed2a-a972-4701-a67f-321d29523f1d\",\"bulletinHash\":\"6VZ3SiX5B2U67xJCDaNYDUsjHnLfmU79B8GJHs3HgRAS\",\"dimension\":[[1,3,5]],\"blindSigModulo\":\"ca224a6285701c49e1d24fa50c1976ecd528e8942ead19680de750238cc421d40beff82f782964816645d630cc3445f7f8c81d307c2ec4b47540c8afe7be8c0b4e1ae45d1a70781349be4345a3e78cc696dce86a170569bfb23b6263de7c819297fa4729f157888df6a8644895f6ceae2b70360b391398b08840370c607b8c4382a4a037914d449ee6209ad0daa8c59379ebfeab26623b74226b43f3907f21778c8ab4d905374b53f351402d9a37e825f999d01ca13c9c274b2cd621db353c3ead88aedeff3b59a1ac0453a252c648a08560c219a2a59a52b5d81092b9090f285cda4a2aabc1fc59e0350266c83634b14f3027a9af38ee77b8adfa3e80c5c233894324f0a4b70e1b9e65f0e9f4819974b080b8a5b32f962b01a62bde8d6441db5df212dd9abe0b9bcaf1e5665f34d49d837af06f35b550bc3ea176d77561de960b69b1297304b47f554359bcc1efe196462acb37aaeb77998756855464b527f263b16e14bd5b7f5728039f164ac8aaaada8ce82c4cd96986407036d01b2dc137b07ac523d4e5a4fe0cd6924f3ad3371bcbe6fa54695f31d69b6c82e7e440aaa8d422f251f4660448061c2fa8ddffdf3c715c57f11889b38a7fe2bd71bcd39e03c10005980244da55c431eeaaf307170d3703a0af8be324d9f02c5b15d5e8f4279f4513cc8b32c0b13f5ba013688b2214b8401d3836498f34f951dcd618f947c1\",\"blindSigExponent\":\"10001\",\"status\":\"Active\",\"isRevoteBlocked\":true}"},{"key":"SERVERS","stringValue":"[\"3eEp6svZ9aJguXzoMZNVeybQESrWTxc793ULp4fxsteAyfGMszT4UkV7t2zWZkTwkftAw1UxS4BdcyzodUbMMCTK\"]"},{"key":"VOTERS_LIST_REGISTRATOR","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"ISSUE_BALLOTS_REGISTRATOR","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"BLINDSIG_ISSUE_REGISTRATOR","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"},{"key":"JWTTOKEN_REGISTRATOR","stringValue":"MIIDVDCCAjygAwIBAgIEU4b7XDANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MDUyOTA5MTgyMFoXDTI0MDUyNjA5MTgyMFowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIOP1eIjY2KwuIAUjOzpQ22Bzaqcn1GJKEaGfhhnE1P1XeO7K0y5YRQA7U3AuBmp4E2Padc6ZtQxju54VUW+iFClrePY8EQKbx9pP76ODZaLum8KmXnoNQVSWURgR+VLZ2eZOYEd6isp7W/kaRt7AFn2UInB+sn6FmwUusqXydBCexjcWngJ+WpI0mDMBceJkVRtqWKPi8to43eV5W1oapzJXurETr0eMu0mrnaltgYO7BP/Ga2BiUQXYDJ+XfNzOrsThIaeMqfEz9/jZ1wMSJHiuCDGgTMM38Pzho20vGv1DJzdRDE+G5F25NM/2P+YLiNh9TK64LCELOlUKK3/Sc8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAI3yE1yF+ldMYM9ZBIeSB0LC2BsfTS7pX8Vl0gFATljnsOzcXPITdjf3pJmyi7B+AMKW6A4JqrMKRBr92FHw7CJccqZ6O5MWjO0ca7oDHXNin+WeyrzNZajkoLXR7Ah1RzGtsFnF/tKGL9ecPfIZG7G6rpt3SknrAcB1rmK+0auDphnvvECkCLx/MzPCbTHdqJC9no7d/IbxYIg57HCv2tQsTJJtRT7TmmQUB0BQf+Hmk7v6dLXaqufB0dx7BTqkKhRJvSXKRyX1LopAB9VHiP8R8EKv/QYoOBlw1EVvrzMaOb6wc7ElkCwdYl6oGSb3CTlSuhcOLsf6gkZGiCeWu3A=="},{"key":"BALLOT_RECEIVED_CERT","stringValue":"38yYuGfM5NzpG3P7bPB145ZGvnQJTAPDVKZM2ho7gnytrbeBto9K6cGGx6YDweEaBxMbQXZZCoFYuf4WGUB7iUrL"}];{"image":"voting/voting-contract:v1.5.0","imageHash":"48de795e67a538bbc53da37d622ee69490e0572a43af3818483f302e00cb423d","contractName":"voting-contract"};1

Важное заме­ча­ние. Когда бинар­ные дан­ные пред­став­ля­ют­ся в виде тек­ста – это мож­но сде­лать несколь­ки­ми спо­со­ба­ми. Для про­грам­ми­ста есть два зна­ко­мых фор­ма­та пред­став­ле­ния – hex и base64. Блокчейн доба­вил еще один фор­мат – base58. По уди­ви­тель­но­му сов­па­де­нию у нас тут исполь­зу­ют­ся все три. Так в выгруз­ке stat адре­са блок­чей­на пред­став­ля­ет­ся в виде base58, а крип­то­гра­фи­че­ские кон­стан­ты вро­де моду­ля и экс­по­нен­ты сле­пой под­пи­си в hex. Когда мы про­сим Protobuf сери­а­ли­зо­вать в тран­зак­цию в JSON (сам Protobuff име­ет бинар­ный сери­а­ли­за­тор) то все бай­то­вые после­до­ва­тель­но­сти сохра­ня­ют­ся в base64. Для того что­бы поиг­рать­ся в репо­зи­то­рии с исход­ным кодом есть про­ект EncodingConverter, кото­рый дает воз­мож­ность пере­во­дить один фор­мат в другой.

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

  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

Отсюда мы видим, что иско­мая тран­зак­ция нахо­ди­лась в чет­вёр­том шар­де блок­чей­на, а её пол­ное представление:

{
    "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. Транзакции, запи­сан­ные в ходе наблю­де­ния, иден­тич­ны тран­зак­ци­ям в фай­лах выгруз­ки на пор­та­ле пуб­лич­но­го наблюдения
  2. Транзакции, ска­чан­ные в поне­дель­ник после под­ве­де­ния ито­гов голо­со­ва­ния, иден­тич­ны тран­зак­ци­ям выгруз­ки на пор­та­ле пуб­лич­но­го наблюдения

Нам для это­го потре­бу­ет­ся три базы. Одна база с фай­ла­ми пор­та­ла пуб­лич­но­го наблб­ле­ния 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 addMainKey1691
Executed CallContractTransaction addVotersList4775
Executed CallContractTransaction blindSigIssue1553575
Executed CallContractTransaction commissionDecryption1672
Executed CallContractTransaction decryption1672
Executed CallContractTransaction finishVoting1691
Executed CallContractTransaction removeFromVotersList48
Executed CallContractTransaction results1691
Executed CallContractTransaction startVoting1691
Executed CallContractTransaction vote1537676
Executed CreateContractTransaction voting-contract1691

Финальный дамп, загру­жен­ный утром поне­дель­ни­каblockchain_dump_final.zip

ТипКоличество
Executed CallContractTransaction addMainKey1691
Executed CallContractTransaction addVotersList4775
Executed CallContractTransaction blindSigIssue1553575
Executed CallContractTransaction commissionDecryption1672
Executed CallContractTransaction decryption1672
Executed CallContractTransaction finishVoting1691
Executed CallContractTransaction removeFromVotersList48
Executed CallContractTransaction results1691
Executed CallContractTransaction startVoting1691
Executed CallContractTransaction vote1537676
Executed CreateContractTransaction voting-contract1691

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

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

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

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

Запустив срав­не­ние, мож­но сме­ло идти смот­реть фильм – про­цесс будет небыстрым.

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

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

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

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

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

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

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

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

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

Зависимость номера блока от времени без учета 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 – иссле­до­ва­тель­ская база, постро­ен­ная из дан­ных на утро понедельника

Выводы

К тех­ни­че­ско­му наблю­де­нию нуж­но гото­вить­ся. Просто для того, что­бы собрать дан­ные о тран­зак­ци­ях в блок­чейне дву­мя неза­ви­си­мы­ми спо­со­ба­ми и све­рить их, исклю­чив про­стей­шие вари­ан­ты фаль­си­фи­ка­ции (вброс, уда­ле­ние или изме­не­ние бюл­ле­те­ней 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

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

Directed by Pixel Imperfect Studio. Produced by Git Force Programming LLC.