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

Выбор базы данных

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

Какая база данных нам нужна? Собственно, любой из них. В оригинальной статье о биткойнах ничего не говорится об использовании определенной базы данных, поэтому разработчик решает, какую базу данных использовать. Bitcoin Core, которое изначально было опубликовано Сатоши Накамото и которое в настоящее время является эталонной реализацией Биткойна, использует LevelDB (хотя он был представлен клиенту только в 2012 году). И мы будем использовать…

BoltDB

Потому что:

  1. Это просто и минималистично.
  2. Он реализован в Go.
  3. Запускать сервер не требуется.
  4. Это позволяет построить желаемую структуру данных.

Из README on Github BoltDB:

Bolt - это чистое хранилище ключей и значений Go, вдохновленное проектом LMDB Говарда Чу. Цель проекта - предоставить простую, быструю и надежную базу данных для проектов, которым не требуется полноценный сервер базы данных, такой как Postgres или MySQL.

Поскольку Bolt предназначен для использования в качестве такой низкоуровневой функциональности, простота является ключевым моментом. API будет небольшим и ориентирован только на получение значений и установку значений. Вот и все.

Звучит идеально для наших нужд! Давайте потратим минутку на его рассмотрение.

BoltDB - это хранилище «ключ-значение», что означает, что здесь нет таблиц, как в СУБД SQL (MySQL, PostgreSQL и т. Д.), Ни строк, ни столбцов. Вместо этого данные хранятся в виде пар ключ-значение (как на картах Golang). Пары ключ-значение хранятся в сегментах, которые предназначены для группировки похожих пар (это похоже на таблицы в СУБД). Таким образом, чтобы получить значение, вам нужно знать ведро и ключ.

Одна из важных особенностей BoltDB заключается в том, что здесь нет типов данных: ключи и значения представляют собой байтовые массивы. Поскольку мы будем хранить в нем структуры Go (в частности, Block), нам нужно будет их сериализовать, то есть реализовать механизм преобразования структуры Go в байтовый массив и восстановления ее обратно из байтового массива. Мы будем использовать для этого encoding / gob, но также можно использовать JSON, XML, Protocol Buffers и т. Д. Мы используем encoding/gob, потому что он простой и является частью стандартной библиотеки Go.

Структура базы данных

Прежде чем приступить к реализации логики сохраняемости, нам сначала нужно решить, как мы будем хранить данные в БД. И для этого мы обратимся к тому, как это делает Bitcoin Core.

Проще говоря, Bitcoin Core использует два «ведра» для хранения данных:

  1. blocks хранит метаданные, описывающие все блоки в цепочке.
  2. chainstate хранит состояние цепочки, которое представляет собой все неизрасходованные на данный момент выходы транзакций и некоторые метаданные.

Также блоки хранятся в виде отдельных файлов на диске. Это сделано для повышения производительности: чтение одного блока не требует загрузки всех (или некоторых) из них в память. Мы этого не реализуем.

В blocks пары key -> value:

  1. 'b' + 32-byte block hash -> block index record
  2. 'f' + 4-byte file number -> file information record
  3. 'l' -> 4-byte file number: the last block file number used
  4. 'R' -> 1-byte boolean: whether we're in the process of reindexing
  5. 'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
  6. 't' + 32-byte transaction hash -> transaction index record

В chainstate пары key -> value:

  1. 'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
  2. 'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs

(Подробное объяснение можно найти здесь)

Поскольку у нас еще нет транзакций, у нас будет только blocks сегмент. Кроме того, как было сказано выше, мы будем хранить всю БД как один файл, не сохраняя блоки в отдельных файлах. Так что ничего, связанного с номерами файлов, нам не понадобится. Итак, мы будем использовать key -> value пару:

  1. 32-byte block-hash -> Block structure (serialized)
  2. 'l' -> the hash of the last block in a chain

Это все, что нам нужно знать, чтобы приступить к реализации механизма сохраняемости.

Сериализация

Как было сказано ранее, в BoltDB значения могут быть только типа []byte, и мы хотим хранить Block структур в БД. Мы будем использовать encoding / gob для сериализации структур.

Реализуем Serialize метод Block (обработка ошибок для краткости опущена):

Все просто: сначала мы объявляем буфер, в котором будут храниться сериализованные данные; затем мы инициализируем кодировщик gob и кодируем блок; результат возвращается в виде байтового массива.

Затем нам нужна функция десериализации, которая получит массив байтов в качестве входных данных и вернет Block. Это будет не метод, а независимая функция:

И это все, что касается сериализации!

Упорство

Начнем с функции NewBlockchain. В настоящее время он создает новый экземпляр Blockchain и добавляет к нему генезис-блок. Мы хотим, чтобы он:

  1. Откройте файл БД.
  2. Проверьте, хранится ли в нем блокчейн.
  3. Если есть блокчейн: а) создайте новый экземпляр Blockchain; б) установить конец экземпляра Blockchain на последний хэш блока, хранящийся в БД.
  4. Если нет существующей цепочки блоков: а) создайте блок генезиса; б) хранить в БД; c) сохранить хэш исходного блока как хеш последнего блока; г) создать новый экземпляр Blockchain с его концом, указывающим на генезисный блок.

В коде это выглядит так:

Давайте рассмотрим это по частям.

Это стандартный способ открытия файла BoltDB. Обратите внимание, что он не вернет ошибку, если такого файла нет.

В BoltDB операции с базой данных выполняются внутри транзакции. И есть два типа транзакций: только чтение и чтение-запись. Здесь мы открываем транзакцию чтения-записи (db.Update(...)), потому что мы ожидаем поместить генезис-блок в БД.

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

Также обратите внимание на новый способ создания Blockchain:

Мы больше не храним в нем все блоки, вместо этого сохраняется только кончик цепочки. Кроме того, мы сохраняем соединение с БД, потому что хотим открыть его один раз и оставить открытым во время работы программы. Таким образом, структура Blockchain теперь выглядит так:

Следующее, что мы хотим обновить, - это метод AddBlock: теперь добавить блоки в цепочку не так просто, как добавить элемент в массив. С этого момента блоки будем хранить в БД:

Давайте рассмотрим это по частям:

Это другой тип транзакций BoltDB (только для чтения). Здесь мы получаем последний хэш блока из БД, чтобы использовать его для добычи нового хэша блока.

После добычи нового блока мы сохраняем его сериализованное представление в БД и обновляем ключ l, в котором теперь хранится хэш нового блока.

Выполнено! Это было несложно, правда?

Проверка блокчейна

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

BoltDB позволяет перебирать все ключи в корзине, но ключи хранятся в отсортированном по байтам порядке, и мы хотим, чтобы блоки печатались в том порядке, в котором они находятся в цепочке блоков. Кроме того, поскольку мы не хотим загружать все блоки в память (наша БД цепочки блоков может быть огромной! .. или давайте просто притворимся, что может), мы будем читать их один за другим. Для этого нам понадобится итератор блокчейна:

Итератор будет создаваться каждый раз, когда мы хотим перебрать блоки в цепочке блоков, и он будет хранить хэш блока текущей итерации и соединение с БД. Из-за последнего итератор логически присоединяется к цепочке блоков (это Blockchain экземпляр, который хранит соединение с БД) и, таким образом, создается в Blockchain методе:

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

BlockchainIterator будет делать только одно: он вернет следующий блок из цепочки блоков.

Это все, что касается БД!

CLI

До сих пор наша реализация не предоставляла никакого интерфейса для взаимодействия с программой: мы просто выполнили NewBlockchain, bc.AddBlock в функции main. Пора это исправить! Нам нужны эти команды:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain

Все операции, связанные с командной строкой, будут обрабатываться структурой CLI:

Его «точкой входа» является функция Run:

Мы используем стандартный пакет flag для анализа аргументов командной строки.

Сначала мы создаем две подкоманды, addblock и printchain, затем добавляем флаг -data к первой. printchain не будет никаких флагов.

Затем мы проверяем команду, предоставленную пользователем, и анализируем связанную подкоманду flag.

Затем мы проверяем, какие из подкоманд были проанализированы, и запускаем связанные функции.

Эта вещь очень похожа на ту, что у нас была раньше. Единственная разница в том, что теперь мы используем BlockchainIterator для перебора блоков в цепочке блоков.

Также не забудьте соответствующим образом изменить функцию main:

Обратите внимание, что новый Blockchain создается независимо от аргументов командной строки.

Вот и все! Проверим, все ли работает как положено:

$ blockchain_go printchain
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block"
000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true
$ blockchain_go addblock -data "Send 1 BTC to Ivan"
Mining the block containing "Send 1 BTC to Ivan"
000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Success!
$ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee"
Mining the block containing "Pay 0.31337 BTC for a coffee"
000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
Success!
$ blockchain_go printchain
Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Data: Pay 0.31337 BTC for a coffee
Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
PoW: true
Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Data: Send 1 BTC to Ivan
Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
PoW: true
Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true

(звук открывающейся пивной банки)

Заключение

В следующий раз мы реализуем адреса, кошельки и (возможно) транзакции. Так что следите за обновлениями!

Ссылки

  1. Полные исходные коды
  2. Хранилище данных ядра биткойнов
  3. Болтдб
  4. Кодирование / gob
  5. "флаг"

Первоначально опубликовано на jeiwan.cc.