Раздел 30 · Web3 на Rust

Bitcoin: ключи, адреса и транзакции

senior~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Bitcoin: ключи, адреса и транзакции

Свой блокчейн мы уже собрали: аккаунты, балансы, Ed25519, PoW. Теперь посмотрим на первый и самый живучий блокчейн и специально будем сравнивать. Bitcoin почти в каждом решении пошёл иначе, чем мы, и почти всегда не случайно. Понять почему это лучший способ понять и свой узел, и сам Bitcoin.

Идея

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

Это UTXO-модель. Мы её сознательно не взяли (урок 13 объясняет почему аккаунты проще для учебного узла), но именно UTXO стоит в основе Bitcoin, и у неё есть свои сильные стороны: параллельная проверка, приватность через новые адреса, отсутствие глобального nonce.

Деньги считаются в сатоши: 1 BTC это ровно 100 000 000 сатоши. Целые числа, никаких дробей и никакого float (почему float тут был бы катастрофой, мы разбирали в уроке 20 раздела Rust).

Ключи: снова кривая, но другая

Bitcoin подписывает транзакции на кривой secp256k1, не на ed25519, который мы взяли для своего узла. Математику кривых мы прошли в уроке 04: закрытый ключ это скаляр d, открытый это точка P = d·G, а восстановить d из P мешает задача дискретного логарифма.

Рождается закрытый ключ тривиально: это случайный скаляр от 1 до n минус 1, где n это порядок группы точек, простое число около 2^256. Никакой структуры, просто 32 байта хорошей энтропии, и почти любые 32 байта годятся: диапазон настолько огромен, что случайно промахнуться мимо него нельзя. Для переноса между кошельками ключ записывают в формате WIF: Base58Check от байта версии 0x80, самого ключа и флага сжатия. Та же упаковка с контрольной суммой, что у адресов чуть ниже.

Разница в упаковке. Открытый ключ secp256k1 это точка (x, y), но y восстанавливается из x с точностью до знака, поэтому его хранят в сжатом виде: один байт чётности плюс координата x, всего 33 байта. Несжатый вид (байт 0x04 плюс x плюс y, 65 байт) сейчас почти не используют.

/// Открытый ключ secp256k1 в сжатом виде: 33 байта.
/// Первый байт 0x02 если y чётный, 0x03 если нечётный, дальше x (32 байта).
pub struct CompressedPublicKey([u8; 33]);

impl CompressedPublicKey {
    pub fn prefix(&self) -> u8 {
        self.0[0]
    }

    pub fn is_y_even(&self) -> bool {
        self.0[0] == 0x02
    }
}

Подпись у Bitcoin это ECDSA, а не EdDSA. В уроке 05 мы видели ловушку ECDSA: повтор nonce выдаёт закрытый ключ, поэтому подписи делают детерминированными по RFC 6979. Там же жила малигибельность: из подписи (r, s) тривиально делается вторая валидная (r, n - s), и это меняло txid. Bitcoin прикрыл дыру политикой low-s: начиная с Bitcoin Core 0.11.1 узлы не ретранслируют транзакции с подписью в высокой форме. Важно, что это standardness-политика узлов, а не правило консенсуса: софтфорк BIP-146, который сделал бы low-s обязательным для всех, так и не активировали. По-настоящему проблему изменчивого txid закрыл SegWit, к нему вернёмся в уроке 21.

Адрес это отпечаток ключа, но обёрнутый

У своего узла адрес это первые 20 байт хеша ключа (урок 18). Идея Bitcoin ровно та же, только хеш двойной и результат аккуратно упакован, чтобы человек не ошибся при копировании.

Ядро адреса это HASH160: RIPEMD160(SHA256(pubkey)), ровно 20 байт. Дальше эти 20 байт заворачиваются в один из форматов адреса.

/// Ядро большинства адресов: 20 байт от двойного хеша ключа.
/// SHA-256 даёт стойкость, RIPEMD-160 ужимает до 20 байт.
pub fn hash160(public_key: &[u8]) -> [u8; 20] {
    let sha = sha256(public_key);
    ripemd160(&sha) // 20 байт
}

Форматов адреса три поколения, и по началу строки видно, какой перед тобой:

  • P2PKH, начинается с 1. Base58Check от версии 0x00 плюс HASH160. Платёж «тому, кто докажет владение ключом с этим хешем».
  • P2SH, начинается с 3. Base58Check от версии 0x05 плюс хеш скрипта. Платёж «тому, кто предъявит скрипт с этим хешем и данные к нему».
  • Bech32, начинается с bc1. Это SegWit-формат: bc1q... для witness v0, bc1p... для Taproot. Контрольная сумма ловит опечатки лучше Base58 и весь адрес в нижнем регистре.

Общее у всех трёх это встроенная контрольная сумма: адрес с опечаткой почти наверняка не пройдёт проверку, и монеты не уйдут в никуда. У нашего узла такой защиты не было, адрес был просто 20 голых байт.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.