Архитектура
Инженерный спутник технического описания — как эталонный узел воплощает замысел и какие решения за этим стоят. Пояснительно, не нормативно.
Протокол — это файлы параметров и эталонные векторы, плюс именованные правила везде, где они определены; спецификация сетевого протокола несёт требования слоя пиров. Эта страница объясняет ту поверхность и фиксирует решения, стоящие за ней. Там, где она расходится с нормативной поверхностью, побеждает нормативная поверхность, а этот текст исправляется. Полный инженерный спутник — docs/ARCHITECTURE.md.
Два предиката, два движка#
Жизнь сертификата от начала до конца:
wallet network every full node
+------------+ gossip +--------------+ +---------------------+
| build cert | ---------> | cert topic | ------------> | STATELESS PIPELINE |
| (reads, | | (TLS gossip) | | V1..V8, batch sigs, |
| writes, | +--------------+ | native re-exec |
| sigs, seq, | | -> mark VALID |
| deposit) | +----------+----------+
+------------+ | mempool
v
miner (any node) +---------------------+
+-------------------+ block | FOLD (sequential) |
| assemble ordered | --------> | F-rules per cert: |
| hash list + bodies| gossip | APPLY / SKIP / DROP |
| + RandomX solve | | -> new state |
+-------------------+ +---------------------+
Действительность — без состояния и параллельна: она вычисляется один раз на сертификат на узел, до блоков и независимо от них. Применимость — с состоянием и последовательна: она вычисляется внутри свёртки на зафиксированной позиции сертификата. Майнер не выполняет исполнения; он упорядочивает хеши, действительность которых он уже видел проверенной, и решает доказательство работы. Свёртка — это плотный цикл по рабочему множеству в памяти — сравнить, прибавить, записать.
Инженерные принципы#
| Принцип | |
|---|---|
| P1 | Свёртка священна. Функция перехода состояния живёт в одном чистом пакете без ввода-вывода, без часов, без горутин, без обхода отображений и без плавающей точки. Это единственный код, ошибки которого нельзя исправить задним числом. Всё остальное в узле — заменяемая обвязка. |
| P2 | Детерминизм важнее производительности. Любая оптимизация, рискующая недетерминизмом, в консенсусном коде отвергается. Производительности место в конвейере без состояния, где она безопасна. |
| P3 | Никаких административных ключей, никакого привилегированного RPC. Не существует пути в коде, которым какой-либо ключ мог бы приостановить, обновить, выпустить или реорганизовать. Чего нет в правилах свёртки, того не существует. |
| P4 | Малая поверхность консенсуса. core/ не импортирует ничего вне стандартной библиотеки. Остальной узел может пользоваться библиотеками экосистемы; ядро — нет. |
| P5 | Сначала спецификация. Эталонные векторы и есть протокол. Код на Go — это одна из эталонных реализаций; независимая реализация, проходящая векторы, — равноправный участник, а не форк. Именно это позволяет сопровождающему в итоге быть никем в частности. |
| P6 | Воспроизводимость начиная с v0.1. Закреплённый набор инструментов Go, -trimpath, закреплённые зависимости. Доверие переносится с бинарника на код, а это единственное доверие, которое может предложить анонимный автор. |
| P7 | Один бинарник, открываемый высотой. Машина, залоговые операции и регистрация секвенсоров компилируются в каждый выпуск и отклоняются ниже своей высоты активации — Validate требует h1_vm ≥ h1_bond и требует, чтобы h1_vm приходилась на границу эпохи. Эра наступает потому, что цепочка дошла до некоторого числа, и никогда потому, что операторов попросили обновиться. |
P3 — тот принцип, на который скептический читатель должен давить сильнее всего, а казна — то место, где давить: генезис-блок не содержит ни ключа, ни какого-либо пути расходования вовсе, так что нет и привилегии, которую можно было бы держать, делегировать или украсть. Кворум 3 из 5, который вводит эра Era 2, закрепляется будущим хардфорком — тем же социальным механизмом, что и любое другое изменение консенсуса, подлежащим тому же отказу и способным сдвинуть ровно одну ячейку даже тогда. Кворум, который появляется, только если сеть согласится его вписать, и который затем держит одну ячейку, — это правило расходования. Административный ключ — это ключ, существующий до чьего-либо согласия и дотягивающийся до всего.
Криптографические примитивы#
| Роль | Выбор | Почему |
|---|---|---|
| Подписи | Ed25519 | Пакетная проверка (пандус для GPU), отсутствие ковкости, крошечные ключи. Строгие правила, зафиксированные в генезис-блоке: канонические кодировки обязательны для открытого ключа и R, открытый ключ свободен от кручения и не малого порядка, а проверка ведётся без кофактора. |
| Хеширование | BLAKE3 | Идентификаторы сертификатов и блоков, адреса, корень состояния. Достаточно быстр, чтобы хешировать на скорости линии ретрансляции; дружелюбен к параллелизму для корня состояния эпохи. |
| Доказательство работы | RandomX | Оптимизирован под CPU. Единственный cgo в дереве, за тегом сборки, и отсутствующий в сборке без него. pow_engine входит в корень консенсуса, поэтому бинарник с неверным движком отказывается запускаться, а не принимает неверное доказательство. |
| Разделение доменов | обязательно | Каждый хеш — это blake3(tag ‖ payload). Подписи подписывают идентификатор цепочки и корень консенсуса, что убивает повтор и между сетями, и между двумя воплощениями одной сети. |
Отклонение кручения — это то, что делает пакетный путь безопасным. Ключ смешанного порядка не является ключом малого порядка, поэтому до него не дотягивается никакой чёрный список, и это ровно то место, где пакетный проверяющий с кофактором и одиночный проверяющий без кофактора расходятся. Когда ключ и R лежат в подгруппе простого порядка, эти два доказуемо эквивалентны — так что пакетный проверяющий может быть с кофактором, при условии что он применяет те же правила кодировки и кручения до пакетирования. Это обязательство — цена такого выбора, а пакетного проверяющего пока не существует.
Каноническая кодировка и идентификаторы#
Все консенсусные объекты — это контейнеры SSZ: единая каноническая байтовая кодировка, никакого порядка отображений, никакой неоднозначности необязательных полей, фиксированные смещения для дешёвого частичного разбора и нативная мерклизация.
cert_id = blake3("zcd/certid/v1" || ssz(certificate with an empty signature list))
cert_exemplar = blake3("zcd/cert/v1" || ssz(certificate))
block_id = blake3("zcd/block/v1" || ssz(header))
Первые два — это разные дайджесты, отвечающие на разные вопросы, и реализация, использующая один там, где место другому, денежно сломана. Идентификатор отвечает на вопрос оплачено ли уже это разрешение; хеш экземпляра отвечает на вопрос доказывают ли это данные байты. Эти два никогда не делят ключ.
Подписи лежат вне прообраза идентификатора, потому что подпись — это рандомизированная демонстрация: подписывающий выбирает одноразовое число, каждое такое число даёт очередную действительную и совершенно каноническую подпись над тем же телом, и ни один проверяющий не может определить, какое было использовано. Будь они внутри, у одного разрешения было бы неограниченно много идентификаторов, каждый оплачиваемый, каждый производимый любым одним из требуемых подписантов за счёт полномочий остальных.
Ставка вне идентификатора была бы ставкой, которую любой в пути мог бы переписать — вздуть, чтобы сжечь баланс подписавшего через базовую комиссию, или обнулить, чтобы не пустить сертификат ни в один блок. То, сколько сертификат платит, — часть того, что разрешил его подписавший, поэтому это хешируется и подписывается. Позднее изменение кодировки, «выносящее комиссию из подписываемого тела ради гибкости ретрансляции», выглядело бы как оптимизация и было бы вектором кражи.
Отсюда следует правило: разбирайте, не проверяйте дважды. Декодирование обеспечивает каноническую форму, поэтому декодированный объект структурно действительный по построению, и механизмы правил никогда не перепроверяют форму.
Модель ячейки#
Ячейка — это значение в слоте. Значения ячеек — беззнаковые 256-битные целые, хранимые с обратным порядком байтов в 32 байтах. Отсутствующие ячейки читаются как ноль — ноль — это отсутствие, и это требование консенсуса, а не удобство реализации: оно делает корень состояния функцией состояния, а не истории, его породившей.
Отдельно протокол ведёт реестр израсходованных адресов: постоянное консенсусное множество одноразовых адресов, чьи полномочия на подпись сожжены.
Addr = version || blake3("zcd/addr/v1" || version || payload)[:31]
| Версия | Вид | Разрешение на списание |
|---|---|---|
0x01 | одноразовый пользовательский | Подпись владельца; любой списывающий сертификат обязан также нести явный MARK_SPENT. После его применения любое чтение и любая запись под этим адресом навсегда завершаются отказом. |
0x02 | постоянный пользовательский | Подпись владельца; многоразовый навсегда. Никогда не может попасть в реестр израсходованных. |
0x03 | актив | Управляется неизменяемыми ячейками полномочий актива. |
0x00 | протокольный | Только для свёртки: маяк эпохи, ячейки базовой комиссии, кольцо созревания coinbase, ячейка казны. |
0x04 | зарезервировано — ячейка со скрытым значением (эра Era S) | Недостижимо, пока идёт эра Era 0. |
0x04 резервируется сейчас, а не выделяется потом, потому что скрытое
значение должно отличаться от обычного по своему адресу: обязательство Педерсена
и 256-битный баланс оба занимают 32 байта, так что охраняемая дельта, наведённая по ошибке или злому умыслу на
слот с обязательством, заставила бы свёртку прибавить целое к кодировке точки на кривой — арифметика,
проходящая все проверки и оставляющая ячейку, которую никто никогда не сможет потратить. Эта таблица заморожена в генезис-блоке,
поэтому байт застолблён здесь и оставлен недостижимым.
Запись в реестре никогда не уплотняется. Значения в ячейках под израсходованным адресом могут быть удалены после горизонта отката, но запись, фиксирующая адрес как израсходованный, — это то, что не даёт его воскресить. Это консенсусное состояние, только дополняемое, около 33 байт на адрес, навсегда — честно признанная открытая проблема протокола, структурно общая с любой схемой множества нуллификаторов.
Действительность без состояния и закон тарификации#
V-правила выполняются для каждого сертификата, параллельно, до приёма в мемпул и во время проверки блока, и не требуют состояния вовсе: каноническая форма и идентификатор цепочки; проверка каждой подписи над корнем подписания; выводимость разрешения из одного лишь сертификата; равенство заявленных чтений тому, что выводит программа; и сверка адресата возврата с тем, что сжигает сам сертификат.
Закон тарификации в системе умещается в одно предложение, и держаться стоит именно за него:
Одна подпись, не более одного счёта, и никогда на позиции, которой её подписант не мог избежать.
Эта спецификация добавляет к словарю технического описания один термин: отбрасывание, неоплачиваемое несобытие. Сертификат, дошедший до применения с уже израсходованным депозитом, отбрасывается — не оплачивается, не отмечается как виденный, свободно подаётся заново против свежего депозита — так что честные пользователи ничего не теряют из-за гонок на собственной депозитной ячейке.
Структура репозитория#
zycord/
spec/ parameter sets, golden vectors, library images <- THE PROTOCOL
core/ consensus-critical; standard library only (P4)
types/ crypto/ ssz/ u256/ state/ validity/ fold/ params/ genesis/
cevm/ the certificate-adapted EVM; vendored interpreter, pure Go
stdlib/ the pre-deployed library, its addresses and code hashes
pow/randomx/ the mainnet engine: vendored C++, cgo, behind a build tag
node/ storage/ chain/ verify/ mempool/ miner/ p2p/ sync/ rpc/ stratum/
wallet/ key management, certificate builders (reference; not consensus)
contracts/ the reference contracts, in Solidity
sim/ simulator, fuzz harnesses, differential refold, chaos soak
cmd/ zycordd, zcd
desktop/ the wallet in a native window — a separate Go module
docs/ architecture, protocol, operating guide, whitepaper
Стрелки зависимостей направлены только внутрь — node → core,
wallet → core, и никогда наоборот — и ничто внутри core/
не дотягивается за пределы core/ и стандартной библиотеки. Одно исключение, названное и обеспеченное:
core/pow/randomx — единственный cgo в дереве, и он компилируется только под тегом
сборки, поэтому любая сборка без тега по-прежнему обходится одной стандартной библиотекой и не требует набора инструментов C. CI
выполняет проверку, которая это обеспечивает, потому что проверка сторонних зависимостей ищет пути модулей, а у cgo их
нет — из-за чего правило не обеспечивалось никем, пока эту проверку не добавили.
Как это тестируется#
- Эталонные векторы. У каждого правила свёртки, блока и действительности есть положительные и отрицательные случаи в виде
(pre-state, block) → (post-state | invalid, outcomes, fees). Этот набор — контракт совместимости для независимых реализаций. - Набор на злоупотребления. Повторное включение применённых и пропущенных сертификатов, включение истёкших, включение с заниженной ставкой, зависимые цепочки при перетасовке предлагающим, циклы сжигания и возврата, штормы сторонних зачислений, гонки на границе лимита эмиссии.
- Тесты на свойствах. Детерминизм свёртки при перестановке порядка предлагающим, коммутативность дельт, устойчивость к ABA и сохранение — включая депозиты в пути, кольцо созревания и ячейку казны, поскольку проверка сохранения, пропускающая казну, сообщает о создании стоимости в каждом блоке.
- Дифференциальные. Намеренно наивная вторая реализация свёртки, написанная ради очевидности, а не скорости, фаззится против настоящей. Расхождение блокирует релиз.
- Состязательное моделирование. Штормы пропусков, гонки на опустошение депозита, майнеры, набивающие блок под отбрасывание, пытка реорганизациями, манипуляции метками времени, облегчённые сценарии затмения ретрансляции. Конфигурации сценариев зафиксированы в репозитории; запуски воспроизводимы по семени.
- Параллелизм, намеренно. У каждого компонента, которого касается более одной горутины, есть тест, который сам параллелен, и в той форме, в какой это делает настоящий процесс.
-raceна однопоточном наборе ничего не измеряет и сообщает об успехе. - Хаос-прогон. Настоящие процессы узлов на настоящих сокетах за прокси, вносящим задержку, джиттер, потери и разделения, с узлами, убиваемыми
SIGKILLслучайным образом. Именно эта поверхность нашла гонку данных, которую весь набор с-raceупустил.
Генезис-блок — это артефакт, а не церемония#
zcd genesis выдаёт генезис-блок — пустое состояние, инициализированные ячейки маяка,
пустой реестр израсходованных, никаких распределений какого бы то ни было рода — и его идентификатор. Объявленный
запуск обязуется, за недели вперёд, к тегу кода, хешу параметров, идентификатору генезис-блока и
времени запуска. Все четыре кто угодно может пересобрать за миллисекунды, из исходного кода, на любой машине.
Больше доверять нечему.