Изучите внутреннее устройство криптовалют, изучая основы программирования на Rust.

Добро пожаловать в первый пост в этом сопутствующем руководстве к серии видео Blockchain In Rust! Мы рассмотрим разработку простого проекта блокчейна, похожего на криптовалюту. Эти сообщения будут концептуальными по своей природе, тогда как видео, как правило, ориентированы на код. Это сопутствующее руководство, перефразирующее концепции, обсуждаемые в видеороликах.

Тем не менее, давайте приступим.

«Блокчейн»?

Это модное техническое словечко, которое волшебным образом делает компании более ценными.

Это поедание Уолл-стрит.

В нем могут быть салат, апельсины, электричество, кошки и, конечно же, деньги.

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

Блоки

На данный момент они содержат такую ​​информацию:

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

Постой, что за хэш?

В отличие от того, что вы, возможно, ели на завтрак, хеш является результатом ряда вычислений (алгоритм хеширования), выполненных с некоторыми данными, создание идентификатора или отпечатка пальца для этих данных. Очень важно, чтобы выполнение хеш-алгоритма для одних и тех же данных всегда давало один и тот же результирующий хеш. Есть много разных алгоритмов хеширования, но интересующие нас будут иметь следующие свойства:

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

Вот пример того, как выглядят хэши для одного и того же фрагмента данных (байтовая строка "GeekLaunch") для разных алгоритмов хеширования:

MD5("GeekLaunch") = "e76485e55ba4c16aac30bd446b73d96e"
SHA1("GeekLaunch") = "c333e84f729c67d6b591e056e1b51e0077a9c030
SHA256("GeekLaunch") = 
"a17d5669f2148e2982baab7c0b4c7d81100c7cf52c45a8d7deb429aeba156ea6"

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

Первые два алгоритма, MD5 и SHA1, больше не считаются безопасными, но остаются неидеально популярными. Пожалуйста, не используйте их в новых проектах. SHA256 по-прежнему считается безопасным, и, поскольку Биткойн его использует, мы тоже.

Блоки хеширования

На данный момент мы определили шесть полей в нашей структуре блока: индекс, полезная нагрузка, временная метка, одноразовый номер, хэш предыдущего блока, хеш. Чтобы вычислить хэш данного блока, мы собираемся использовать байты, составляющие первые пять из этих полей, а затем сохранить хеш этих байтов в последнем поле. (Мы узнаем, почему хеширование важно в первую очередь в посте о майнинге.) Проще говоря, алгоритм хеширования блока:

  1. Сожмите все байты в один большой кусок.
  2. Подайте кусок в хеш-функцию SHA256.

Оказывается, все! Это создаст уникальный идентификатор хэша для каждого отдельного блока.

Интересные фрагменты кода

В этой серии статей мы используем язык программирования Rust, но не волнуйтесь - вам не нужно знать Rust, чтобы следовать за ним! Вы можете посмотреть полный код этого поста + видео здесь.

Мы собираемся определить структуру для представления блоков:

type BlockHash = Vec<u8>; // vector of bytes
pub struct Block {
    pub index: u32,
    pub timestamp: u128,
    pub hash: BlockHash,
    pub prev_block_hash: BlockHash,
    pub nonce: u64,
    pub payload: String,
}

На английском языке это гласит: «Данные типа Block будут содержать шесть полей: 32-битное целое число без знака, называемое index, 128-битное целое число без знака, называемое timestamp, тип BlockHash, называемое hash (где BlockHash - это вектор байтов), другое Тип BlockHash называется prev_block_hash, 64-битное целое число без знака называется nonce, а строка называется payload ». Сейчас полезная нагрузка - это просто строка, но позже это будет список транзакций.

Позже мы будем хешировать другие вещи, поэтому давайте определим черту под названием Hashable:

pub trait Hashable {
    fn bytes (&self) -> Vec<u8>; // function returns vector of bytes
                            // ^ this semicolon = no function body
    fn hash (&self) -> Vec<u8> {
        crypto_hash::digest(
            crypto_hash::Algorithm::SHA256,
            &self.bytes(),
        )
    }
}

Это позволяет нам предоставлять hash (&self) -> Vec<u8> функцию для разных структур. Все, что нужно сделать структуре, - это предоставить функцию bytes (&self) -> Vec<u8>. Возвращаясь к нашему двухэтапному алгоритму, bytes (&self) -> Vec<u8> сжимает байты вместе, а hash (&self) -> Vec<u8> передает их в SHA256. Вызов crypto_hash::digest(...) вычисляет, как и следовало ожидать, хэш байтов, предоставленных вызовом &self.bytes(), с использованием алгоритма SHA256.

Теперь мы можем настроить хеширование для любых структур, которые захотим! Все, что нам нужно сделать, это реализовать Hashable и предоставить bytes (&self) -> Vec<u8> тело функции.

Сделаем это для нашей структуры Block. Чтобы реализовать трейт, мы используем ключевое слово impl, поэтому оно выглядит примерно так:

impl Hashable for Block {
    fn bytes (&self) -> Vec<u8> {
        let mut bytes = vec![]; // empty, mutable vector
        bytes.extend(&u32_bytes(&self.index));
        bytes.extend(&u128_bytes(&self.timestamp));
        bytes.extend(&self.prev_block_hash);
        bytes.extend(&u64_bytes(&self.nonce));
        bytes.extend(self.payload.as_bytes());
        bytes // implicit return. Same as `return bytes;`
    }
}

Первая строка сообщает компилятору Rust, что то, что заключено между первым набором фигурных скобок, предоставит Block все необходимое для реализации трейта Hashable. Помните, что структуры, реализующие Hashable, должны предоставлять только bytes (&self) -> Vec<u8> функцию, так что это именно то, что мы делаем.

К счастью, текст функции bytes (&self) -> Vec<u8> здесь нетрудно прочитать. Мы просто создаем вектор с именем bytes и extend добавляем к нему байтовые представления каждого из пяти полей, которые мы обсуждали выше.

(Примечание: функции uX_bytes(...) определены здесь.)

Теперь, чтобы закончить это красиво и чисто, все, что нам нужно сделать, чтобы получить хеш-значение для блока, - это вызвать его hash (&self) -> Vec<u8> функцию!

let block = Block { ... };
let h = block.hash();
println!("{:?}", &h); // e.g. [34, 187, ..., 21]

Заключение

Теперь, когда у нас за плечами хэширование, давайте посмотрим, что мы можем сделать с майнингом!

Вы можете полностью найти код этого выпуска на GitHub.

Удачного кодирования!