Хранение

Mikhail Savenkov
6 min readMar 3, 2022

В этом разделе мы начнем с рассмотрения того, что мы храним (структуры данных и модели Iron Fish), а затем перейдем к тому, как мы храним их (используя LevelDB и IndexDB). Мы начнем с рассмотрения самых основных структур данных, которые представляют глобальное состояние Iron Fish: заметки и нуллификаторы.

Структуры и модели данных

Заметка

Заметка — это представление расходной формы платежа, например, векселя. Она очень похожа на UTXO в Биткойне. Публично на банкноту ссылаются только тогда, когда она создается как результат транзакции, и только в ее хэшированной форме. Содержимое самой купюры является приватным.

Содержимым открытого текста записки является:

(pk, d)(pk,d): ключ передачи и диверсификатор адреса получателя (например, владельца открытого ключа записки, который мы объясним в разделе “Счет”).
vv : значение открытого текста, которое хранится в записке
rcm: случайность записки, используемая для генерации хэша Педерсена для записки
memo: 32-байтовое поле для заметок

Нуллификатор

Нуллификатор является уникальным идентификатором купюры, но не связан с ней. Записка может быть потрачена только в том случае, если ее нуллификатор раскрыт в процессе транзакции. Как только нуллификатор раскрыт, он сохраняется в одной из двух глобальных структур данных Iron Fish, которые отслеживаются всеми узлами — Дерево Меркла банкнот и Дерево Меркла нуллификаторов (подробнее об этом чуть позже).

Это гарантирует, что купюра не может быть потрачена дважды (поскольку в этом случае пришлось бы дважды раскрывать один и тот же нуллификатор, а такие попытки будут отклонены). Нуллификатор уникален для своей заметки, поскольку он получен из частной информации о заметке — nk (ключ получения нуллификатора, подробнее об этом в разделе “Счет”), cm (обязательство заметки) и позиции (индекс в дереве Меркла).

Деревья Меркле

Как упоминалось выше, Iron Fish хранит как примечания, так и нуллификаторы в деревьях Меркла. Дерево Меркла — это аккумуляторная структура данных, то есть она используется для представления многих элементов одним маленьким идентификатором (хэшем).

Дерево заметок Меркле

Дерево заметок Меркла имеет фиксированный размер и глубину 32; оно используется для хранения всех создаваемых заметок. В отличие от других блокчейнов, где UTXO удаляется после того, как он потрачен, это дерево Меркла является структурой данных “только добавление”, где заметки добавляются в дерево последовательно. Хотя дерево Меркла глубиной 32 очень велико (то есть оно может хранить 4 294 967 296 банкнот!), оно конечное, что не является лучшим решением для постоянно растущей валюты. Когда дерево Меркла полностью заполняется, оно становится деревом только для чтения, позволяя тратить из него старые банкноты. Затем строится совершенно новое дерево Меркла.

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

Помните, что основная цель нашего “дерева Меркла” — хранить заметки, которые пользователи могут использовать позже, сохраняя при этом конфиденциальность. Для этого заметки в нашем дереве Меркла хранят зашифрованную заметку, а также другие вспомогательные поля. Вся информация, необходимая для расходования заметки, содержится здесь, поэтому владельцу заметки не нужно загружать конкретный блок или транзакцию, которая привела к появлению этой заметки.

Записки Мерке состоят из:

value_commitment — Pedersen Commitment значения ноты.
note_commitment — оконный Pedersen Commitment, который используется для хэширования всего содержимого заметки Merkle.
public_key — открытый ключ, который создается при создании заметки, с которой он связан. Он используется для того, чтобы получатель мог расшифровать заметку и использовать ее в будущем, используя технику обмена ключами Диффи-Хеллмана.
encrypted_note — зашифрованная заметка, которую получатель может расшифровать, используя вышеупомянутый public_key.
note_encryption_keys — зашифрованное поле, в котором хранится вся информация, необходимая отправителю для последующей расшифровки заметки (чтобы отправитель мог восстановить историю транзакций).

Дерево Меркле нуллификаторов

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

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

У нас не было бы всех этих примечаний и нуллификаторов, если бы они не были частью транзакций (более подробно мы рассматриваем транзакции, которые принимаются в общий блокчейн). Далее мы рассмотрим, как именно все эти компоненты образуют блоки, которые, в свою очередь, составляют блокчейн Iron Fish.

Блок и заголовок блока

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

Учитывая все это, как заголовок блока помогает подтвердить блок?

Заголовок блока

Заголовок блока состоит из следующего (некоторые из этих терминов, например, Output Description, мы рассмотрим далее в статье):

sequence — номер последовательности данного блока. Блоки в цепочке увеличиваются в порядке возрастания последовательности. Более одного блока могут иметь одинаковый порядковый номер (что указывает на развилку в цепочке), но за один раз выбирается только одна развилка.
previousBlockHash — хэш предыдущего блока в цепочке.
noteCommitment — обязательство перед деревом Меркла заметок после того, как в него будут добавлены все новые заметки от транзакций в этом блоке. Хранится в виде хэша и размера дерева на момент вычисления хэша.
nullifierCommitment — обязательство перед набором нуллификаторов после того, как в него были добавлены все траты в этом блоке. Хранится в виде хэша и размера набора на момент вычисления хэша.
target — хэш этого блока должен быть ниже этого целевого значения, чтобы блок был принят в цепочку.
randomness — nonce, используемый для вычисления хэша этого блока.
временная метка — временная метка unix по данным майнера, добывшего блок. Это значение следует принимать с долей соли, но майнеры захотят убедиться, что оно находится на достаточном расстоянии от временной метки предыдущего блока.
minersFee — единая (упрощенная) транзакция, представляющая плату за майнинг, состоящая только из одного выходного описания.

Обратите внимание, что хотя в заголовке блока отсутствует хэш блока, его можно вычислить с помощью алгоритма хэширования Iron Fish, учитывая все элементы заголовка блока.

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

  1. Предыдущий блок, на который ссылается данный блок, существует (с помощью поля previousBlockHash).
  2. Целью является та, с которой соглашается проверяющий узел (подробнее об этом позже, как рассчитывается цель и сложность).
  3. Когда все содержимое заголовка блока хэшируется, этот хэш оказывается численно меньше целевого — это в значительной степени достигается майнером за счет подстройки значения случайности.
  4. Временная метка для этого блока имеет смысл (ее временная метка больше, чем у предыдущего блока на 12 секунд, +/- 10 секунд в качестве буфера).
  5. Что все транзакции в блоке действительны (подробнее об этом в разделе “Транзакции”).
  6. Что вознаграждение minersFee, которым майнер вознаграждает себя за представление этого блока, является действительным, то есть это именно согласованное вознаграждение за блок плюс все комиссии за транзакции в транзакциях (подробнее об этом в разделе “Майнинг”).
  7. And finally, that after all the transactions are added to the two global Merkle Trees of Notes and Nullifiers, the appropriate Merkle tree roots are updated and referenced in the BlockHeader correctly as noteCommitment for the Merkle root for the Notes tree, and nullifierCommitment for the Merkle root for the Nullifier tree.

Как Iron Fish хранит данные

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

Поскольку мы знали, что запустить полную реализацию Iron Fish в браузере будет сложнее, чем в терминальной среде NodeJS, мы в первую очередь сосредоточились на этом. Наиболее надежным выбором базы данных для приложений, которым нужна база данных в браузере, является IndexedDB. К сожалению, для NodeJS не существовало сопутствующей реализации IndexedDB, поэтому мы выбрали LevelDB для нашей реализации NodeJS.

Чтобы избежать необходимости жонглировать двумя отдельными реализациями хранения данных для двух разных баз данных, наша реализация Iron Fish имеет общий уровень абстракции для хранилищ данных и доступа к базе данных на основе LevelUp. Этот уровень абстракции заботится о конкретных реализациях базовой базы данных и предоставляет общий уровень, который можно использовать как в браузере, так и в среде NodeJS, предлагая простой API, не зависящий от хранилища данных.

API уровня хранения

Проще говоря, слой хранения представляет собой API для базовых хранилищ данных — он может создавать хранилища на основе схем и работать с ними с помощью всех обычных операций хранения ключевых значений, таких как GET, PUT, DEL и HAS. Для полного обзора конкретной реализации нашего уровня хранения, пожалуйста, обратитесь к README уровня хранения в нашем репозитории Github.

В следующей статье мы рассмотрим вопрос Сетевого хранения в протоколе IronFish.

Следите за обновлениями!

--

--