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

Цепь, майнинг и SPV

middle-senior~35 мин

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

Цепь, майнинг и SPV

Мы уже майнили в своём узле (урок 15) и выбирали самую тяжёлую ветку (урок 19). Bitcoin делает то же самое, но с деталями, которые стоит увидеть: компактная запись сложности, ретаргет раз в две недели, пулы майнеров и, главное, приём, позволяющий телефону проверять платежи, не скачивая сотни гигабайт цепи.

Идея

Всё, что защищает Bitcoin от переписывания истории, укладывается в 80 байт. Это заголовок блока, единственное, что перемалывает майнер. Тело блока (транзакции) в PoW не участвует напрямую, оно висит на заголовке через один хеш, корень Меркла. Найти заголовок с хешем ниже цели дорого, проверить мгновенно, ровно та асимметрия из урока 15.

/// Заголовок блока Bitcoin: ровно 80 байт, всё, что участвует в PoW.
pub struct BlockHeader {
    pub version: i32,
    pub prev_block: [u8; 32],  // хеш предыдущего заголовка, это и есть «цепь»
    pub merkle_root: [u8; 32], // корень дерева всех транзакций блока
    pub time: u32,             // unix-время
    pub bits: u32,             // цель сложности в компактной записи
    pub nonce: u32,            // то, что перебирает майнер
}

/// Хеш блока это двойной SHA-256 его 80-байтового заголовка.
pub fn block_hash(header_bytes: &[u8; 80]) -> [u8; 32] {
    sha256(&sha256(header_bytes))
}

Обрати внимание на prev_block: каждый заголовок держит хеш предыдущего. Меняешь древний блок, меняется его хеш, а значит, prev_block следующего, и так до вершины. Переписать блок значит перемолоть PoW всех блоков над ним. Это то же связывание хешем, что мы делали в уроке 14.

Тело блока: транзакции и witness

За 80 байтами заголовка идёт тело: компактный счётчик числа транзакций и сами транзакции подряд, первой всегда coinbase. Никакого оглавления внутри блока нет, узел читает транзакции одну за другой и строит из их txid дерево Меркла, корень которого обязан совпасть с merkle_root заголовка.

С SegWit (урок 21) у тела появился второй слой. Witness-данные (подписи segwit-входов) физически передаются внутри тех же транзакций, в расширенном формате сериализации, но в txid не входят, а значит, и в дерево Меркла из заголовка не попадают. Чтобы майнер не мог подменить подписи, у блока есть второе дерево: wtxid всех транзакций сворачиваются в свой корень, и этот корень (witness commitment) записывается отдельным выходом в coinbase-транзакцию. Так witness всё равно прибит к заголовку, только через coinbase и merkle_root, а не напрямую.

Старые узлы про второй слой не знают: им блок передаётся без witness, и они видят валидные по старым правилам транзакции. Именно поэтому SegWit удалось развернуть мягко, без раскола сети.

Сложность: компактная цель

В своём узле сложность была «сколько ведущих нулевых бит». Bitcoin хранит полноценную 256-битную цель (target): хеш заголовка, прочитанный как число, обязан быть не больше неё. Именно не больше: Bitcoin Core отклоняет только хеш строго больше цели, точное равенство валидно. Чем ниже цель, тем реже подходящий хеш, тем выше сложность.

Хранить 32 байта в каждом заголовке расточительно, поэтому цель пакуется в 4 байта nBits: байт показателя степени плюс три байта мантиссы, как усечённый float для целых.

/// Разворачивает компактную запись nBits в 256-битную цель.
/// Формат: старший байт это показатель, младшие три это мантисса.
/// target = mantissa * 256^(exponent - 3).
///
/// Упрощение для урока: края не обработаны. exponent больше 32 уронит
/// вычитание `32 + j - exponent` в underflow usize (паника), а знаковый
/// бит мантиссы 0x00800000 (цель со знаком минус) мы не проверяем.
/// Реальный код Bitcoin Core оба случая проверяет и отвергает.
pub fn target_from_bits(bits: u32) -> [u8; 32] {
    let exponent = (bits >> 24) as usize;
    let mantissa = bits & 0x00ff_ffff;
    let mut target = [0u8; 32];
    // мантисса это три младших байта; кладём их big-endian так, чтобы её
    // младший байт встал на значимость (exponent - 3).
    let mantissa_bytes = mantissa.to_be_bytes(); // [0, m2, m1, m0]
    for (j, byte) in mantissa_bytes[1..].iter().enumerate() {
        let index = 32 + j - exponent; // big-endian позиция байта в 256-битном числе
        if index < 32 {
            target[index] = *byte;
        }
    }
    target
}

/// Блок валиден по PoW, если его хеш как число не больше цели:
/// Bitcoin Core отклоняет только хеш строго больше цели, равенство проходит.
pub fn meets_target(block_hash: &[u8; 32], target: &[u8; 32]) -> bool {
    // сравниваем как 256-битные big-endian числа
    block_hash.as_slice() <= target.as_slice()
}

Ретаргет: сеть держит темп

Bitcoin целится в один блок раз в 10 минут. Но суммарная мощность майнеров гуляет в разы год к году, поэтому цель периодически пересчитывается. Правило простое: раз в 2016 блоков (примерно две недели) сеть смотрит, за сколько реально намайнились эти 2016 блоков, и подстраивает цель, чтобы вернуть темп к десяти минутам.

const TARGET_TIMESPAN: u64 = 14 * 24 * 60 * 60; // две недели в секундах

/// Новая сложность через каждые 2016 блоков.
/// Если предыдущее окно намайнилось быстро, сложность растёт (цель падает),
/// если медленно, падает. Шаг ограничен коридором в 4 раза в любую сторону,
/// чтобы одним окном нельзя было обрушить или взвинтить сложность.
pub fn retarget(old_target: u64, actual_timespan: u64) -> u64 {
    let clamped = actual_timespan.clamp(TARGET_TIMESPAN / 4, TARGET_TIMESPAN * 4);
    // new = old * actual / timespan; считаем в u128, чтобы не переполниться
    (u128::from(old_target) * u128::from(clamped) / u128::from(TARGET_TIMESPAN)) as u64
}

Тот же коридор ограничения мы закладывали в свой ретаргет в уроке 15. Логика идентична, Bitcoin просто выбрал окно в 2016 блоков и цель в 10 минут.

Две оговорки. Наш retarget считает на u64 ради наглядности, настоящая цель это 256-битное число, и Bitcoin Core делает ту же арифметику над большими числами. И вкусная деталь: в расчёте окна живёт знаменитый off-by-one. Сеть берёт время между первым и последним блоками окна из 2016 блоков, а между ними 2015 интервалов, не 2016. Ошибка сидит в коде с 2009 года, и чинить её нельзя по той же причине, что баг OP_CHECKMULTISIG из прошлого урока: любое исправление меняет правила консенсуса, а на это должна согласиться вся сеть.

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

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

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