Майнинг: proof-of-work и сложность
Блоки связаны хешем, но слепить блок пока ничего не стоит, а значит, и переписать историю тоже. Добавим цену. Proof-of-work делает создание блока дорогим, а проверку мгновенной, и сеть сама держит темп блоков постоянным, как бы ни менялась суммарная мощность.
Идея
Мы уже видели механику PoW в уроке 11, теперь встроим её в свой узел. Чтобы блок стоил работы, его хеш обязан уложиться в порог. У нас порог это «хеш заголовка начинается не меньше чем с bits нулевых бит». Найти такой nonce можно только перебором (хеш ведёт себя случайно), а проверить мгновенно.
Эта асимметрия и защищает историю. Из прошлого урока: переписать глубокий блок значит пересчитать заголовки всех блоков сверху. Теперь каждый такой пересчёт требует заново перемолоть PoW, обгоняя честную сеть. Вот откуда берётся практическая неизменяемость.
Проверка работы
Сложность у нас это число требуемых ведущих нулевых бит хеша. Считать их умеет сам тип Hash:
impl Hash {
/// Сколько ведущих нулевых бит в хеше. На этом стоит проверка работы:
/// чем больше нулей требует сеть, тем выше сложность.
pub fn leading_zero_bits(&self) -> u32 {
let mut zeros = 0u32;
for &byte in &self.0 {
if byte == 0 {
zeros += 8;
} else {
zeros += byte.leading_zeros();
break;
}
}
zeros
}
}
/// Заголовок удовлетворяет своей сложности? Блок носит порог, под который замайнен.
pub fn header_meets_target(header: &BlockHeader) -> bool {
header.hash().leading_zero_bits() >= header.bits
}
Майнинг это честный перебор: крутим nonce, пока хеш не уложится в порог.
/// Майнит заголовок: крутит nonce, пока хеш не уложится в порог. Возвращает
/// число попыток. На учебной сложности это секунды.
pub fn mine(header: &mut BlockHeader) -> u64 {
let mut attempts = 0u64;
loop {
attempts += 1;
if header_meets_target(header) {
return attempts;
}
header.nonce = header.nonce.wrapping_add(1);
}
}
Покрути ползунок сложности и посмотри, как растёт число попыток, а проверка остаётся мгновенной. Это и есть асимметрия PoW вживую.
Накопленная работа
Тонкий момент, который понадобится при выборе цепи в 30-web3/19 · Консенсус и выбор цепи. Когда сравниваешь две ветки, сравнивать надо не их длину (число блоков), а накопленную работу. При смене сложности эти величины расходятся: десять лёгких блоков могут стоить меньше, чем три тяжёлых.
Работа одного блока это ожидаемое число попыток хеша, то есть 2^bits. Её суммируют по всей цепи.
/// Работа одного блока: ожидаемое число попыток хеша, то есть 2^bits.
/// Её суммируют по цепи, чтобы сравнивать ветки по накопленной работе,
/// а не по длине. u128, чтобы сумма не переполнилась.
pub fn work(bits: u32) -> u128 {
1u128 << bits
}
Запомни это: «самая длинная цепь» это бытовое упрощение, а настоящее правило это «самая тяжёлая по работе».
Подстройка сложности
Если бы сложность была фиксированной, рост хешрейта сети ускорял бы блоки без предела: они рождались бы быстрее, чем успевают разойтись по сети, и вместе с ними росла бы доля осиротевших блоков. Поэтому сеть подстраивает сложность сама, без координатора, по часам блоков. Раз в RETARGET_INTERVAL блоков она смотрит, сколько реально заняли последние блоки, и двигает порог: шли быстро, сложность растёт; медленно, падает.
pub const RETARGET_INTERVAL: u64 = 16; // у Bitcoin 2016
pub const TARGET_BLOCK_TIME: u64 = 10; // целевой интервал, секунды
/// Нижняя и верхняя границы сложности. Снизу, чтобы genesis майнился мгновенно,
/// сверху, чтобы учебный CPU не завис на одном блоке.
pub const MIN_BITS: u32 = 8;
pub const MAX_BITS: u32 = 32;
/// Новая сложность по фактическому времени последнего окна. Сдвиг равен
/// log2(ожидаемое / фактическое), зажатому в коридор от -2 до +2 (то есть
/// отношение времён зажато в 0.25..4, ровно как у Bitcoin). Затем bits
/// клампится в рабочие границы.
pub fn retarget(old_bits: u32, actual_timespan: u64) -> u32 {
let expected = (RETARGET_INTERVAL * TARGET_BLOCK_TIME).max(1) as f64;
let actual = (actual_timespan.max(1)) as f64;
// Блоки шли быстрее (actual < expected) -> ratio > 1 -> сложность растёт.
let shift = (expected / actual).log2();
let shift = shift.clamp(-2.0, 2.0).round() as i32;
let new_bits = old_bits as i32 + shift;
new_bits.clamp(MIN_BITS as i32, MAX_BITS as i32) as u32
}
Коридор 0.25..4 это защита из реального Bitcoin: один скачок мощности не должен выбросить сложность в крайность, за один период она не меняется больше чем вчетверо в любую сторону. У нас это и вовсе «не больше двух бит за период», потому что сложность у нас целочисленная (каждый бит вдвое дороже предыдущего).
Так средний интервал между блоками держится у цели десятилетиями, пока суммарная мощность растёт в триллионы раз.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.