ZYCORD docs
Русский
ZycordДокументацияАрхитектура

Архитектура

Инженерный спутник технического описания — как эталонный узел воплощает замысел и какие решения за этим стоят. Пояснительно, не нормативно.

Этот документ не является нормативным

Протокол — это файлы параметров и эталонные векторы, плюс именованные правила везде, где они определены; спецификация сетевого протокола несёт требования слоя пиров. Эта страница объясняет ту поверхность и фиксирует решения, стоящие за ней. Там, где она расходится с нормативной поверхностью, побеждает нормативная поверхность, а этот текст исправляется. Полный инженерный спутник — docs/ARCHITECTURE.md.

Два предиката, два движка#

Жизнь сертификата от начала до конца:

 wallet or sequencer         network                        every full node
+-------------+   gossip   +--------------+               +---------------------+
| build cert  | ---------> | cert topic   | ------------> | STATELESS PIPELINE  |
| (reads,     |            | (TLS gossip) |               | V-rules, strict     |
| writes,     |            +--------------+               | Ed25519, program    |
| sigs, fee,  |                                           | re-execution        |
| deposit)    |                                           |  -> mark VALID      |
+-------------+                                           +----------+----------+
                                                                     | mempool
                                                                     v
                        miner (any node)                  +---------------------+
                      +----------------------+   block    | FOLD (sequential)   |
                      | order the hash list, | ---------> | per certificate:    |
                      | append forced certs, |   gossip   | APPLIED / SKIPPED / |
                      | RandomX solve        |            | FORCED_FAILED /     |
                      +----------------------+            | DROPPED -> state    |
                                                          +---------------------+

Действительность не зависит от состояния и параллельна: она проверяется один раз на сертификат на узле, до блоков и независимо от них. Применимость зависит от состояния и последовательна: она определяется внутри свёртки на зафиксированной позиции сертификата. Майнер не исполняет ничего из того, что ему дают: он упорядочивает хеши, которые уже видел проверенными, и решает доказательство работы, или, после перехода на доказательство стейка в Era 2, подписывает слот, для которого его выбрал жребий. Единственное, что он исполняет, — это принудительный путь, начиная с Era 1: он может исполнить открытые интенты над состоянием в конце своего блока и добавить результаты, не более 16 на блок, которые затем каждый узел проверяет, как любой другой сертификат. Свёртка — это плотный цикл над рабочим множеством в памяти — сравнить, сложить, записать — и не исполняет вообще ничего.

Инженерные принципы#

Принцип
P1Свёртка священна. Функция перехода состояния живёт в одном чистом пакете без ввода-вывода, без часов, без горутин, без обхода отображений и без плавающей точки. Это единственный код, ошибки которого нельзя исправить задним числом. Всё остальное в узле — заменяемая обвязка.
P2Детерминизм важнее производительности. Любая оптимизация, рискующая недетерминизмом, в консенсусном коде отвергается. Производительности место в конвейере без состояния, где она безопасна.
P3Никаких административных ключей, никакого привилегированного RPC. Не существует пути в коде, которым какой-либо ключ мог бы приостановить, обновить, выпустить или реорганизовать. Чего нет в правилах свёртки, того не существует.
P4Малая поверхность консенсуса. core/ не импортирует ничего вне стандартной библиотеки. Остальной узел может пользоваться библиотеками экосистемы; ядро — нет.
P5Сначала спецификация. Эталонные векторы и есть протокол. Код на Go — это одна из эталонных реализаций; независимая реализация, проходящая векторы, — равноправный участник, а не форк. Именно это позволяет сопровождающему в итоге быть никем в частности.
P6Воспроизводимость начиная с v0.1. Закреплённый набор инструментов Go, -trimpath, закреплённые зависимости. Доверие переносится с бинарника на код, а это единственное доверие, которое может предложить анонимный автор.
P7Один бинарник, закрытый высотой. Машина, механизм секвенсоров и принудительный путь скомпилированы в каждый релиз и отвергаются ниже своей высоты активации, h1_vm (80 640 в mainnet), которая, как требует Validate, должна приходиться на границу эпохи. Era 1 наступает потому, что цепочка достигла числа, а не потому, что операторов попросили обновиться. Более поздних эр в бинарнике нет: каждая выходит отдельным релизом, через жёсткий форк.

P3 — то свойство, на которое скептически настроенному читателю стоит давить сильнее всего, и давить лучше всего на казну: генезис не содержит ни ключа, ни пути расходования вообще, поэтому нет никакой привилегии, которую можно держать, делегировать или украсть. Казну нельзя потратить до высоты 259 200, и какое бы правило её ни открыло, оно придёт как изменение консенсуса — тот же социальный механизм, что и для любого другого, подверженный тому же отказу и способный даже тогда сдвинуть ровно одну ячейку. Доля основателя — это зачисление, а не ключ: 3% каждой субсидии на один адрес, зафиксированный в файле параметров, до высоты 3 153 600, которую Era 2 читает как эпоху 1 095. Этот адрес хранит монеты и больше ни до чего не дотягивается. Ключ администратора — это ключ, который существует до того, как кто-либо дал согласие, и дотягивается до всего.

Криптографические примитивы#

РольВыборПочему
Подписи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, ячейка казны, счётчики секвенсоров.
0x05контрактНикто по ключу: ни один ключ не хешируется в такой адрес. Решают код контракта и правила свёртки.
0x04не определёнЯчеек под ним не существует; сертификат, называющий такой адрес, недействителен.

Параллелизм берётся из типизированного доступа. Чтение объявляет, как оно ограничивает свою ячейку — EXACT или одно из охранных условий GUARD_GE, GUARD_LE, GUARD_EQ — а запись объявляет, что она делает: SET, DELTA_ADD, DELTA_SUB или MARK_SPENT. Только EXACT питает вычисление, и только EXACT устаревает, когда ячейка меняется; охранное условие ничего не возвращает, а дельта ничего не читает, поэтому повторное исполнение остаётся чистой функцией от точных чтений, а любое число охраняемых списаний и зачислений в один слот коммутирует. Одна дисциплина на слот на сертификат: установка и дельта никогда не делят слот. Запись нуля удаляет ячейку, поэтому слот, в который никогда не писали, и слот, в который записали ноль, — это один и тот же слот.

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

Запись в реестре никогда не уплотняется. Значения в ячейках под израсходованным адресом могут быть удалены после горизонта отката, но запись, фиксирующая адрес как израсходованный, — это то, что не даёт его воскресить. Это консенсусное состояние, только дополняемое, около 33 байт на адрес, навсегда — честно признанная открытая проблема протокола, структурно общая с любой схемой множества нуллификаторов.

Действительность без состояния и закон тарификации#

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

Закон тарификации в системе умещается в одно предложение, и держаться стоит именно за него:

Одна подпись, не более одного счёта, и никогда на позиции, которой её подписант не мог избежать.

Свёртка упорядочивает блок в три класса и не оставляет производителю блока никакой свободы: сначала fill, в порядке конвейеров секвенсоров, к которым они принадлежат; затем все остальные сертификаты по (deposit address, Seq, id); последними — принудительные сертификаты, по идентификатору. Затем у каждого сертификата ровно один из четырёх исходов, и эталонные векторы их называют:

ИсходКогдаКто платит
APPLIEDКаждое заявленное чтение выполняется.Вносящий депозит, по фактической стоимости: базовая комиссия сжигается, приоритетная достаётся производителю блока.
SKIPPED_STALE, SKIPPED_OVERFLOWЧтение больше не выполняется, или дельта вывела бы значение за пределы [0, 2²⁵⁶). Ничего не записывается.Вносящий депозит: фиксированная плата за пропуск, сжигается. Для fill — секвенсор — никогда не пользователь.
FORCED_FAILEDПринудительный сертификат, вызов которого откатился или нарушил свои границы.Его плательщики; больше ничего не записывается.
DROPPEDДепозит не может быть зарезервирован: ячейка не пополнена или израсходована.Никто. Не помечается как виденный, поэтому может быть подан повторно с пополненным депозитом.

Применённые, пропущенные и принудительно-неудавшиеся сертификаты помечаются как виденные, а включение виденного, просроченного или недоплаченного сертификата делает блок недействительным, так что ни один производитель не может повторно включить или задержать чужой сертификат, чтобы собрать его депозит. Неудачи, за которые некого взыскать, — fill, чей счётчик конвейера нарушает порядок или чей счётчик fence сдвинулся, принудительный сертификат, чьи чтения не выполняются, — вместо этого делают блок недействительным: производитель, построивший блок из состояния, которым он располагает, — это та сторона, которая могла об этом знать.

Рубежи эр#

Высоты активации находятся в spec/params.json. h1_vm = 80 640 открывает Era 1: cEVM и предварительно развёрнутую библиотеку, записи контрактов с ключом секвенсора, администратором и счётчиком fence, интенты и fill, каталог и принудительный путь. Собственные константы эры — 8-блочное окно интента, 16 принудительных сертификатов на блок по 4 000 единиц деривации каждый, время жизни записи каталога в 259 200 блоков после TTL объявившего её сертификата — скомпилированы, а не передаются файлом параметров, потому что добавление параметра сдвинуло бы идентификатор генезис-блока каждой работающей сети; блок рубежа записывает их в протокольные ячейки, так что с этого момента корень состояния обязывается к ним.

h1_bond равна h1_vm в mainnet и в публичном testnet, поэтому операции внесения залога там никогда не открываются: в Era 1 внесения стейка в залог нет. Дерево по-прежнему содержит правила поручительства, аренды и принудительной очереди v1.0, и они действительны только в сети разработки, в окне из 256 блоков выше её h1_vm и ниже её собственного рубежа v1, которое существует для того, чтобы у векторов, фиксирующих этот механизм, оставалась сеть, в которой он действителен. h2_pos = 1 576 800 зарезервирована для доказательства стейка, заморожена в генезисе и не читается ни одним правилом. Era 2, доказательство стейка, выходит отдельным релизом через хардфорк, а её константы скомпилированы, как у Era 1: она открывает внесение залога на эпохе 182 по высоте, начинает свой разгон, когда активный стейк удерживает 10% предложения в течение 30 эпох, или не позднее эпохи 273, и переходит на 5-секундные слоты, когда разгон завершается. С момента перехода она читает каждую константу генезиса, которая на самом деле является длительностью, в той единице, которую та измеряет, поэтому файл параметров не меняется.

Структура репозитория#

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