Цепь, майнинг и 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-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.