Блокчейн и все, что с ним связано, нынче у всех на слуху, и на сегодняшний день вряд ли можно найти издание, которое обошло эту тему своим вниманием. Мы тоже не оставались в стороне и в свое время подробно разобрались, что собой представляет блокчейн, зачем он нужен и что можно с ним делать. Сегодня мы попробуем рассмотреть данную тему с практической стороны и напишем простейший локальный блокчейн. Сразу оговорюсь, что до полноценного блокчейн-проекта ему далеко, однако с его помощью вполне можно получить общее представление о том, как работает эта технология изнутри.
Вас также может заинтересовать: Лучшие ICO-компании
- Общая структура нашего блокчейна
- Функция подсчета хеш-суммы
- Файл block.h
- Файл block.cpp
- Конструктор для инициализации очередного блока
- Создаем genesis-блок
- Майним очередной блок
- Считаем хеш очередного блока
- Записываем в блок значение хеша предыдущего блока
- Читаем значение хеша блока из поля hash
- Файл blockchain.h
- Файл blockchain.cpp
- Добавляем genesis-блок
- Добавляем очередной блок
- Заключение
Общая структура нашего блокчейна
Итак, наш блокчейн (как и положено) будет представлять собой цепочку из блоков, каждый из которых включает в себя следующее:
- номер блока [index];
- метка времени [timestamp];
- содержание транзакции [transaction];
- значение так называемого доказательства работы [proof] (о том, что это такое, чуть ниже);
- значение хеш-суммы предыдущего блока [previous hash];
- значение хеш-суммы текущего блока [hash].
В содержание транзакции мы включим отправителя денежных средств [sender], имя получателя этих средств [recipient] и количество переданных денежных средств [amount]. Для простоты в блок будем включать сведения только об одной транзакции.
Общая структура блока, таким образом, будет выглядеть вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
block = { 'index': 2, 'timestamp': 1527444458, 'transactions': [ { 'sender': "Petrov", 'recipient': "Ivanov", 'amount': 15418, } ], 'proof': 4376, 'previous hash': "000bdf8cd989eb26be64a07b80a8cdcaf27476d8473efbde66c9dd857b94ab9", 'hash' : "00e86d5fce9d492c8fac40762fa3f4eee9a4ae4a17ee834be69aa05dff1309cc" } |
Помимо очередных текущих блоков, блокчейн должен включать в себя начальный (или первый) блок, с которого, собственно говоря, и начинается вся цепочка блоков. Этот блок называется genesis-блоком, и он, в отличие от текущих блоков, содержит в себе только номер (который всегда равен нулю), метку времени, какое-либо рандомное значение вместо хеш-суммы предыдущего блока (поскольку genesis-блок первый и предыдущего блока у него просто-напросто нет) и собственное значение хеш-суммы:
1 2 3 4 5 6 |
genesisblock = { 'index': 0, 'timestamp': 1527443257, 'random hash': "f2fcc3da79c77883a11d5904e53b684ded8d6bb4b5bc73370dfe7942c1cd7ebf", 'hash' : "3fe4364375ef31545fa13aa94ec10abdfdead26307027cf290573a249a209a62" } |
В целом наш блокчейн будет выглядеть следующим образом.
Функция подсчета хеш-суммы
Поскольку для подсчета хешей мы собрались использовать алгоритм «Стрибог», нам необходима соответствующая функция. Ее мы напишем, используя код из указанной статьи. Качаем его отсюда и подключаем в наш проект нужные файлы следующей строчкой:
1 |
# include "gost_3411_2012/gost_3411_2012_calc.h" |
Саму функцию объявим так:
1 |
std::string get_hash_stribog(std::string str) |
Далее объявляем структуру для хранения результатов подсчета хешей и выделяем для нее память:
1 2 3 4 |
... TGOSTHashContext *CTX; CTX = (TGOSTHashContext*)(malloc(sizeof(TGOSTHashContext))); ... |
Поскольку в теле блока значения хеш-сумм представлены в виде строк, то функция должна получить на вход содержимое блока в виде строки. Для этого напишем следующее:
1 2 3 4 5 6 7 8 |
... // Преобразуем строку входных данных к виду const char const char *c = str.c_str(); // Формируем буфер для входных данных и копируем в него входные данные uint8_t *in_buffer; in_buffer = (uint8_t *)malloc(str.size()); memcpy(in_buffer, c, str.size()); ... |
Далее считаем хеш:
1 2 3 4 5 |
... GOSTHashInit(CTX, HASH_SIZE); GOSTHashUpdate(CTX, in_buffer, str.size()); GOSTHashFinal(CTX); ... |
Поскольку выход функции тоже должен быть в виде строки, а рассчитанное значение хеша представлено в виде байтового массива, нам необходимо сделать соответствующее преобразование. Сделаем это следующим образом ( HASH_SIZE — длина хеш-суммы, 512 или 256 бит, выберем 256):
1 2 3 4 5 6 7 8 9 10 11 |
... // Формируем буфер для выходных данных char out_buffer[2 * (HASH_SIZE / 8)]; // Пишем в буфер значение хеш-суммы for (int i = 0; i < HASH_SIZE / 8; i++) sprintf(out_buffer + i * 2, "%02x", CTX->hash[i]); // Не забываем освободить выделенную память free(CTX); free(in_buffer); // Возвращаем строку с хеш-суммой return std::string((const char *)out_buffer); |
Функция по расчету хешей у нас готова, можно писать дальше.
Файл block.h
В этом файле опишем класс CBlock, в который войдет все, что нам нужно для создания блока (как очередного, так и genesis-блока). Однако прежде чем описывать сам класс, определим структуру, которая будет описывать транзакцию. Как мы уже решили, транзакция будет включать в себя три поля — отправитель, получатель и сумма транзакции:
1 2 3 4 5 |
struct STransaction { std::string sender; std::string recipient; uintmax_t amount; }; |
Теперь можно приступить к описанию нашего класса CBlock. В него входит public-секция, включающая два конструктора (тот, который без параметров, служит для инициализации genesis-блока, а тот, который с параметрами, — для инициализации очередных блоков); метод, создающий genesis-блок; метод, с помощью которого будет майниться очередной блок; метод, записывающий значение хеша предыдущего блока в нужное место текущего блока; метод получения значения хеша блока из соответствующего поля и private-секция со всеми необходимыми полями (номер блока, имя блока, метка времени и так далее) и одним методом подсчета хеш-суммы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class CBlock { public: CBlock(); CBlock(uint32_t index_in, const std::string &in_name_block, const std::string &in_sender, const std::string &in_recipient, uintmax_t in_amount); void create_genesis_block(); void mine_block(uint32_t diff); void set_previous_hash(std::string in_previous_hash); std::string get_hash(); private: uintmax_t index; // Номер блока std::string name_block; // Имя блока time_t time_stamp; // Метка времени STransaction transaction; // Транзакция uintmax_t proof; // Доказательство выполнения работы std::string previous_hash; // Хеш предыдущего блока std::string hash; // Хеш текущего блока std::string calc_hash() const; // Метод подсчета хеша }; |
Теперь можно написать реализацию всех указанных методов. Все это мы поместим в файл block.cpp.
Файл block.cpp
В начале не забудем написать
1 |
# include "block.h" |
Конструктор для инициализации genesis-блока
Как было сказано выше, конструктор без параметров нужен для инициализации genesis-блока. Поле, содержащее метку времени, инициализируется значением текущего времени, поле с именем блока — строкой «Genesis block», все остальное по нулям:
1 2 3 |
CBlock::CBlock() : index(0), name_block("Genesis block"), transaction {"None", "None", 0}, proof(0){ time_stamp = time(nullptr); } |
Конструктор для инициализации очередного блока
Здесь мы инициализируем все поля нужными значениями (очередной номер блока, имя блока, например «Current block», значения полей транзакции и так далее):
1 2 3 |
CBlock::CBlock(uint32_t index_in, const std::string &in_name_block, const std::string &in_sender, const std::string &in_recipient, uintmax_t in_amount) : index(index_in), name_block(in_name_block), transaction {in_sender, in_recipient, in_amount}, proof(0) { time_stamp = time(nullptr); } |
Создаем genesis-блок
Основное, что нужно сделать, — это сгенерировать рандомное значение хеша для записи его в место, предназначенное для хеша предыдущего блока.
1 2 3 4 5 6 7 8 9 10 11 |
void CBlock::create_genesis_block(){ // Инициализируем ГСЧ текущим временем srand(time(nullptr)); // Заносим в строку случайное число std::stringstream ss; ss << rand(); // В нужное поле заносим хеш, посчитанный из рандомной строки previous_hash = get_hash_stribog(ss.str()); // Считаем хеш genesis-блока hash = calc_hash(); } |
Майним очередной блок
Основная цель майнинга — сделать так, чтобы формирование очередного блока было некоторым образом затратным (в общем случае требовало времени и значительных вычислительных ресурсов). Для этого реализуем алгоритм доказательства работы (Proof of work) по аналогии с большинством известных криптовалют. Суть этого алгоритма заключается в переборе подсчитанных хеш-сумм блока, пока не выполнится определенное условие. Для того чтобы значения хеш-суммы блока при каждой итерации подсчета были разными, нужно каждую итерацию каким-то образом менять что-либо в содержимом блока. Для этого мы в описании класса определили поле proof, значение которого каждую итерацию будем увеличивать на единицу. Условием, по которому будем прекращать итерационный подсчет хеш-сумм, будет наличие в начале получившейся в результате очередной итерации хеш-суммы определенного количества нулей. В общем случае чем большим количеством нулей в начале хеша мы зададимся, тем большее количество итераций необходимо проделать, чтобы получился искомый хеш. Количество нулей в начале хеша определяется уровнем сложности diff.
Исходя из всего этого, метод, реализующий майнинг блока, напишем следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 |
void CBlock::mine_block(uint32_t diff){ // Делаем строку из нулей размером в diff char zero_ch[diff]; memset(zero_ch, '0', diff); zero_ch[diff] = '\0'; std::string zero_str(zero_ch); // Делаем итерационный подсчет хеша, пока не получим нужное количество нулей в начале do { hash = calc_hash(); proof++; } while (hash.substr(0, diff) != zero_str); } |
Считаем хеш очередного блока
В предыдущем методе использовался метод calc_hash(), с помощью которого и осуществляется подсчет хеш-суммы блока. Тут все просто — содержимое блока засовываем в одну строку и с помощью функции get_hash_stribog(), которую мы описали в самом начале, получаем искомое значение хеша от получившейся строки:
1 2 3 4 5 |
std::string CBlock::calc_hash() const { std::stringstream ss; ss << index << time_stamp << transaction.sender << transaction.recipient << transaction.amount << proof << previous_hash; return get_hash_stribog(ss.str()); } |
Записываем в блок значение хеша предыдущего блока
Здесь все просто, обычный сеттер:
1 2 3 |
void CBlock::set_previous_hash(std::string in_previous_hash){ previous_hash = in_previous_hash; } |
Читаем значение хеша блока из поля hash
Здесь также все очень просто:
1 2 3 |
std::string CBlock::get_hash(){ return hash; } |
Итак, класс CBlok, из которого в дальнейшем мы будем делать блоки, мы описали и реализовали, теперь нужно эти блоки складывать в цепочку. Для этого напишем класс CBlockchain.
Файл blockchain.h
В начале подключим описание класса CBlock:
1 |
# include "block.h" |
В public-секцию класса запишем:
- конструктор с входным параметром in_diff, задающим сложность майнинга блоков;
- метод, добавляющий в блокчейн genesis-блок;
- метод, добавляющий в блокчейн очередной блок.
В private-секцию определим:
- поле, в котором будем хранить значение сложности майнинга;
- поле, в котором будем хранить цепочку наших блоков, представляющее динамический массив типа vector (чтобы это сделать, не забудь подключить соответствующую библиотеку vector.h);
- метод возвращающий предыдущий блок из цепочки, для того чтобы вытащить из него хеш-сумму.class CBlockchain {
public:
CBlockchain(uint32_t in_diff);
void add_genesis_block(CBlock new_block);
void add_block(CBlock new_block);private:
uint32_t diff; // Сложность майнинга
std::vector chain; // Цепочка блоков
CBlock get_last_block() const;
};
Класс CBlockchain описан, можно писать его реализацию.
Вам может быть интересно: Как запустить свое ICO
Файл blockchain.cpp
В первой строчке подключим только что написанный blockchain.h:
1 |
# include "blockchain.h" |
Инициализируем блокчейн
Конструктор класса включает одну строку, в которой очищается массив для хранения блоков.
1 2 3 |
CBlockchain::CBlockchain(uint32_t in_diff) : diff(in_diff){ chain.clear(); } |
Добавляем genesis-блок
В метод для добавления genesis-блока запишем функцию его создания из класса CBlock, после чего созданный блок помещаем в начало массива для хранения блоков:
1 2 3 4 |
void CBlockchain::add_genesis_block(CBlock new_block){ new_block.create_genesis_block(); chain.emplace_back(new_block); } |
Добавляем очередной блок
В этом методе получаем хеш предыдущего блока, майним очередной блок (с учетом поля diff) и помещаем смайненный блок в массив:
1 2 3 4 5 6 7 8 |
void CBlockchain::add_block(CBlock new_block){ // Пишем в нужное место хеш-сумму предыдущего блока new_block.set_previous_hash(get_last_block().get_hash()); // Майним очередной блок new_block.mine_block(diff); // Помещаем его в цепочку блоков chain.push_back(new_block); } |
Здесь используется метод get_last_block(), возвращающий предыдущий блок, чтобы из него можно было прочитать значение его хеш-суммы. Написать его особых сложностей не представляет:
1 2 3 |
CBlock CBlockchain::get_last_block() const { return chain.back(); } |
Запустив получившийся блокчейн у себя на компьютере, я попробовал смайнить три блока с разными уровнями сложности майнинга. Для сложности, равной шести, три блока майнились немного меньше часа. Для семерки один блок майнился около полутора часов, дальше экспериментировать у меня терпения не хватило.
Диспетчер задач при этом показывал, что основные усилия процессора направлены исключительно на решение задачи Proof of work.
Рекомендуем к прочтению: Создание биткойн-майнера на Java
Заключение
Конечно, мы упростили и не учли очень многое: и распределенность самого блокчейна, и процедуры достижения консенсуса, и возможность включения большего количества транзакций в один блок, и подтверждение целостности этих транзакций с помощью деревьев Меркла, да и много чего еще… Но во-первых, в одну статью это все никак не уместить, а во-вторых, цель статьи все-таки немного другая — положить начало в не совсем легком, но весьма интересном деле освоения этого перспективного направления. Дальше качай White Paper Bitcoin (в том числе и на русском), исходники Bitcoin и Ethereum, читай, вникай, разбирайся.