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

За послед­ние два меся­ца, про­шед­шие с момен­та дистан­ци­он­но­го элек­трон­но­го голо­со­ва­ния (ДЭГ) на выбо­рах 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.