Как устроен блокчейн: блоки, хеш-цепочка, дерево Меркла, распределённый реестр
Слово «блокчейн» обросло таким количеством маркетинга, что за ним почти не видно инженерии. А инженерия там простая и красивая: это реплицированный append-only журнал, целостность которого удостоверяется хешами, а порядок записей согласуется между взаимно недоверяющими узлами. Ни одной магии, и каждый кирпич старше биткоина на десятилетия: хеш-цепочка — Хабер и Сторнетта, 1991; деревья Меркла — 1979; репликация конечного автомата — Лэмпорт, 1978.
Здесь мы разберём заголовок блока Bitcoin по байтам и посчитаем его хеш кодом, который можно запустить; построим дерево Меркла и доказательство включения; посмотрим, как блок расходится по сети и что происходит, когда два блока находятся одновременно. Подписи и адреса — тема следующей статьи, консенсус — третьей. И сразу оговорка о жанре: мы разбираем технологию, а не рынок. Прогнозов курсов и советов, что покупать, здесь не будет — зато будет честный разговор о том, чего блокчейн принципиально не умеет.
Задача, которую решает блокчейн
Начнём с постановки. Есть множество участников, которые хотят вести общий журнал фактов (переводы, записи о владении, вызовы контрактов), при этом не доверяют друг другу и не готовы назначить администратора, решающего, что записано, — и вдобавок могут врать, уходить оффлайн, переприсылать старые сообщения и раскалываться на изолированные сегменты сети.
Уберите второе условие — и задача решается банальным Postgres с ролями и аудит-логом; это почти всегда лучший выбор. Блокчейн осмыслен ровно тогда, когда доверенного центра нет и его нельзя создать — политически, юридически или из-за конфликта интересов. Это единственный критерий применимости, остальное — производные.
Ядро проблемы формулируется как двойная трата: цифровой объект копируется бесплатно, поэтому один и тот же «рубль» можно отправить двум получателям, и оба увидят корректно подписанное сообщение. Подпись доказывает авторство, но не отвечает, какая транзакция была первой. Порядок нельзя вывести локально — о нём нужно договориться. Отсюда весь дизайн:
подпись и адрес"] B --> B1["Хеш-цепочка и дерево Меркла:
подмена становится заметной"] C --> C1["Консенсус PoW / PoS:
дорогое право дописать блок"] A1 --> R["Реплицированный конечный автомат:
одинаковый лог даёт одинаковое состояние"] B1 --> R C1 --> R
Первая ветка — статья «Криптография Web3», третья — «Механизмы консенсуса». Здесь мы закрываем вторую и разбираем, как из неё собирается сеть.
Идея, которую стоит зафиксировать сразу: блокчейн — не база данных, а протокол репликации детерминированного конечного автомата. Узлы обмениваются не состоянием («у Алисы 5 монет»), а упорядоченным логом переходов. Каждый применяет один и тот же лог одной и той же функцией перехода и обязан получить то же состояние. Если два узла получили разное — это либо расхождение логов, либо консенсусный баг реализации, самая страшная категория багов в отрасли.
Кирпич первый: криптографический хеш
Всё держится на функции H(x), которая берёт байты произвольной длины и выдаёт фиксированные 32. От неё требуется: детерминированность (одинаковый вход — одинаковый выход всегда и везде); лавинный эффект (смена одного бита меняет примерно половину битов выхода); стойкость к прообразу (по H(x) не восстановить x дешевле перебора); стойкость к коллизиям (не найти x != y с одинаковым хешем дешевле, чем за 2^128 операций для 256-битного выхода — парадокс дней рождения).
Bitcoin использует SHA256d(x) = SHA-256(SHA-256(x)); двойное хеширование — историческая перестраховка от атак удлинением сообщения на конструкцию Меркла — Дамгора. Ethereum использует keccak256 — Keccak с оригинальным паддингом, не совпадающий со стандартом SHA3-256 NIST. Разница в один байт паддинга, но хеши разные, и это регулярный источник багов у тех, кто берёт первую попавшуюся библиотеку «sha3».
Практический смысл: хеш — это имя данных. Не указатель на место, где они лежат, а имя, вычисляемое из самого содержимого. Отсюда контентная адресация, на которой целиком построены git и IPFS (см. «Децентрализованные хранилища»), и её главное следствие: имя нельзя сохранить, поменяв данные. Детали самих функций — в «Криптографии Web3».
Кирпич второй: блок
Блок состоит из заголовка и тела. Разделение не косметическое: заголовок мал и фиксирован, поэтому его можно скачать и проверить, не скачивая тело. На этом стоят все лёгкие клиенты.
Заголовок Bitcoin — ровно 80 байт, все поля little-endian:
| Смещение | Размер | Поле | Смысл |
|---|---|---|---|
| 0 | 4 | version |
версия правил и биты сигнализации софт-форков |
| 4 | 32 | hashPrevBlock |
хеш заголовка предыдущего блока |
| 36 | 32 | hashMerkleRoot |
корень дерева Меркла транзакций тела |
| 68 | 4 | time |
Unix-время, заявленное майнером |
| 72 | 4 | bits |
компактная запись целевого порога сложности |
| 76 | 4 | nonce |
счётчик, который перебирает майнер |
Два поля — «швы» всей конструкции: hashPrevBlock сшивает блоки в цепь, hashMerkleRoot сшивает заголовок с телом, коммитя к списку транзакций любого размера 32 байтами. Соберём заголовок генезис-блока руками и проверим, что получится всем известный хеш — код самодостаточен, запустите и сверьте:
import hashlib
import struct
def sha256d(b: bytes) -> bytes:
"""Двойной SHA-256 — базовая хеш-функция Bitcoin."""
return hashlib.sha256(hashlib.sha256(b).digest()).digest()
# Хеши внутри заголовка хранятся little-endian, а показываются пользователю big-endian.
# Отсюда развороты [::-1] — источник половины ошибок у новичков.
merkle_root_display = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
header = (
struct.pack("<I", 1) # version = 1
+ bytes(32) # hashPrevBlock: у генезиса его нет
+ bytes.fromhex(merkle_root_display)[::-1] # merkle root во внутреннем порядке
+ struct.pack("<I", 1231006505) # time: 2009-01-03 18:15:05 UTC
+ struct.pack("<I", 0x1D00FFFF) # bits: стартовая сложность
+ struct.pack("<I", 2083236893) # nonce, найденный майнером
)
assert len(header) == 80
block_hash = sha256d(header)[::-1] # разворачиваем для отображения
print(block_hash.hex())
# 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
# Проверим, что блок «намайнен»: хеш как число меньше целевого порога из bits
exponent, mantissa = 0x1D00FFFF >> 24, 0x1D00FFFF & 0xFFFFFF
target = mantissa * 256 ** (exponent - 3)
print(int.from_bytes(block_hash, "big") < target) # True
Ведущие нули не «встроены» в формат — они следствие неравенства hash < target. Майнинг и есть перебор nonce до его выполнения; механика — в статье про консенсус.
Заголовок Ethereum устроен богаче: он содержит не один корень, а три — stateRoot, transactionsRoot, receiptsRoot, — плюс parentHash, number, gasLimit, gasUsed, timestamp, logsBloom, baseFeePerGas (EIP-1559), withdrawalsRoot и другие поля. Зачем столько корней — ниже и подробно в «Ethereum и EVM».
Кирпич третий: хеш-цепочка
Каждый блок содержит хеш предыдущего, поэтому хеш блока N транзитивно зависит от всего содержимого вплоть до генезиса. Изменение одного байта в старом блоке меняет его хеш — и hashPrevBlock следующего блока перестаёт совпадать. Разрыв видит любой, кто идёт по цепочке.
prev = hash(98)
merkle = R99"] T99["tx-список"] end subgraph B100["Блок 100"] H100["header
prev = hash(99)
merkle = R100"] T100["tx-список"] end subgraph B101["Блок 101"] H101["header
prev = hash(100)
merkle = R101"] T101["tx-список"] end T99 -->|"дерево Меркла"| H99 T100 -->|"дерево Меркла"| H100 T101 -->|"дерево Меркла"| H101 H99 -->|"hash(99)"| H100 H100 -->|"hash(100)"| H101
Минимальная реализация, на которой это видно руками:
import hashlib, json
from dataclasses import dataclass, asdict
from typing import List
def sha256d(b: bytes) -> bytes:
return hashlib.sha256(hashlib.sha256(b).digest()).digest()
@dataclass
class Block:
height: int
prev_hash: str
merkle_root: str
timestamp: int
nonce: int = 0
def hash(self) -> str:
# Каноническая сериализация обязана быть детерминированной: sort_keys и без пробелов,
# иначе два честных узла получат разные байты и разные хеши одного блока.
raw = json.dumps(asdict(self), sort_keys=True, separators=(",", ":")).encode()
return sha256d(raw).hex()
def mine(blocks: List[Block], merkle_root: str, ts: int, difficulty_bits: int = 16) -> Block:
prev = blocks[-1].hash() if blocks else "00" * 32
block = Block(len(blocks), prev, merkle_root, ts)
target = 1 << (256 - difficulty_bits) # чем больше bits, тем дороже блок
while int(block.hash(), 16) >= target: # честный перебор nonce
block.nonce += 1
blocks.append(block)
return block
def validate(blocks: List[Block]) -> bool:
for i, block in enumerate(blocks):
expected = "00" * 32 if i == 0 else blocks[i - 1].hash()
if block.prev_hash != expected:
print(f"разрыв хеш-цепочки на высоте {i}")
return False
return True
chain: List[Block] = []
for i in range(4):
mine(chain, merkle_root=f"root{i}", ts=1_700_000_000 + i * 600)
print(validate(chain)) # True
chain[1].merkle_root = "подмена"
print(validate(chain)) # разрыв хеш-цепочки на высоте 2 → False
Разрыв обнаружился на высоте 2, хотя подменили блок 1: ломается связь у следующего блока. Это и есть механика «переписать историю можно только вместе со всем хвостом». Проверка стоит O(n) по числу блоков и O(1) по памяти при потоковом обходе; подделка блока на глубине k требует заново выполнить работу для k+1 блоков, обогнав при этом честную сеть, которая продолжает наращивать цепь.
Здесь важно не переоценить хеш-цепочку. Она даёт tamper-evidence — «подмена заметна», — но не даёт tamper-resistance. Контролируя все копии реестра, я пересчитаю цепочку с нуля, и она будет идеально консистентной. Неподделываемость возникает только из комбинации трёх вещей: цепочка хешей + дороговизна создания блока + независимые наблюдатели, у которых уже есть старая версия истории. Уберите любую — и «блокчейн» в приватной сети из трёх узлов одного владельца превращается в дорогой и медленный audit log.
Кирпич четвёртый: дерево Меркла
Заголовок мог бы коммитить к телу простым H(tx0 || tx1 || ... || txN) — целостность обеспечена. Но тогда для доказательства «tx5 лежит в этом блоке» пришлось бы передавать все транзакции. Дерево Меркла делает то же самое с логарифмическим доказательством: транзакции — листья, соседние узлы попарно хешируются, повторяем до единственного корня. При нечётном числе узлов на уровне Bitcoin дублирует последний (в других системах паддинг устроен иначе — и это важно, см. ниже).
Merkle proof (audit path) — список узлов-братьев на пути от листа к корню и сторона приклеивания каждого. Проверяющий берёт свою транзакцию, последовательно склеивает с братьями, хеширует — и должен получить корень из заголовка.
import hashlib
from typing import List, Tuple
Proof = List[Tuple[bytes, str]] # (хеш брата, с какой стороны он стоит)
def h(b: bytes) -> bytes:
return hashlib.sha256(hashlib.sha256(b).digest()).digest()
def build_levels(leaves: List[bytes]) -> List[List[bytes]]:
"""Строит уровни дерева снизу вверх. O(n) хеширований, O(n) памяти."""
if not leaves:
raise ValueError("дерево Меркла не определено для пустого списка")
levels = [list(leaves)]
while len(levels[-1]) > 1:
cur = levels[-1]
if len(cur) % 2 == 1:
cur = cur + [cur[-1]] # дублируем последний — правило Bitcoin
levels[-1] = cur
levels.append([h(cur[i] + cur[i + 1]) for i in range(0, len(cur), 2)])
return levels
def make_proof(levels: List[List[bytes]], index: int) -> Proof:
"""Путь доказательства для листа index: O(log n) элементов по 32 байта."""
proof: Proof = []
for level in levels[:-1]: # все уровни, кроме корня
sibling = index ^ 1 # брат — сосед по младшему биту индекса
side = "right" if index % 2 == 0 else "left"
proof.append((level[sibling], side))
index //= 2
return proof
def verify(leaf: bytes, proof: Proof, root: bytes) -> bool:
"""Проверка не требует ни блока, ни сети — только proof и доверенный корень."""
acc = leaf
for sibling, side in proof:
acc = h(acc + sibling) if side == "right" else h(sibling + acc)
return acc == root
leaves = [h(f"tx{i}".encode()) for i in range(7)]
levels = build_levels(leaves)
root = levels[-1][0]
proof = make_proof(levels, 5)
print(len(proof)) # 3 хеша на 7 транзакций
print(verify(leaves[5], proof, root)) # True
print(verify(h(b"tx-fake"), proof, root)) # False
| Операция | Время | Память / трафик |
|---|---|---|
| Построение дерева | O(n) хеширований |
O(n) |
| Генерация proof | O(log n) |
O(log n) |
| Проверка proof | O(log n) хеширований |
32 байта на уровень |
| Обновление одного листа | O(log n) |
— |
Для блока из 4096 транзакций доказательство — 12 хешей, 384 байта. Именно это делает возможными SPV-клиенты (раздел 8 white paper Сатоши): кошелёк качает только заголовки — 80 байт на блок, порядка 4 МБ за десять лет десятиминутных блоков — и запрашивает proof для интересующих транзакций.
Три инженерных предупреждения, каждое из которых кому-то стоило денег.
1. Дублирование нечётного узла не бесплатно. В Bitcoin оно породило CVE-2012-2459: списки [A, B, C] и [A, B, C, C] дают одинаковый merkle root. Атакующий брал валидный блок, дублировал в нём хвостовые транзакции и рассылал; узлы принимали блок с тем же хешем заголовка, обнаруживали дубли, помечали блок невалидным навсегда и потом отказывались принимать настоящий. Бесплатный DoS с расколом сети. Лечится явной проверкой на дубликаты идентификаторов транзакций.
2. Атака второго прообраза на «плоское» дерево. Если листья и внутренние узлы хешируются одинаково, внутренний узел можно выдать за лист: пара (H(0,1), H(2,3)) неотличима от 64-байтной «транзакции». В Bitcoin спасает лишь то, что валидная транзакция не бывает 64-байтной. Правильное решение — разделение доменов: RFC 6962 (Certificate Transparency) хеширует листья как H(0x00 || data), а узлы как H(0x01 || left || right). Проектируете своё дерево — делайте так.
3. Корень фиксирует порядок по позиции, а не «по смыслу». Дерево удостоверяет конкретную последовательность, поэтому любая нормализация (сортировка, дедупликация) обязана быть частью консенсусных правил — иначе два честных узла построят разные корни из одного набора данных.
Изменяемое состояние: почему в Ethereum дерево другое
Дерево Меркла отлично коммитит список. Но состояние Ethereum — словарь: адрес → (nonce, баланс, хранилище, код). Нужны операции, которых у простого дерева нет: доказать, что по ключу лежит значение; доказать, что по ключу ничего нет; обновлять тысячи ключей за блок, не перестраивая всё дерево. Ответ — Merkle Patricia Trie: префиксное дерево, где каждый узел адресуется своим keccak256 и лежит в key-value базе, ключ — keccak256(адрес), разбитый на полубайты, а узлы бывают трёх видов: лист, расширение (сжимает длинную цепочку одинаковых полубайтов) и ветвление на 16 детей.
Следствия. Состояние вложено: корень блока коммитит к аккаунтам, аккаунт — к своему хранилищу, так что один 32-байтный stateRoot удостоверяет каждый слот каждого контракта. Trie персистентен: изменённые узлы дописываются, старые остаются, поэтому по историческому stateRoot читается состояние на любом блоке — за это archive-ноды и платят терабайтами. И он даёт stateless-верификацию: eth_getProof возвращает доказательство, проверяемое клиентом, который знает только заголовок. Плата — амплификация чтений: одно обращение к аккаунту разворачивается в несколько случайных чтений из базы, отчего синхронизация узла Ethereum становится дисковой, а не сетевой задачей. Подробности — в «Ethereum и EVM».
Реестр становится распределённым
До сих пор речь шла о структуре данных на одной машине. Распределённой она становится, когда одинаковую копию независимо строят тысячи узлов:
поэтому каждый проверяет всё сам
Никто никому не верит на слово. Узел B принимает блок не потому, что его прислал уважаемый майнер: он пересчитывает хеш заголовка, пересчитывает merkle root из тела, исполняет каждую транзакцию и сверяет получившийся stateRoot с заявленным. Это дублирование работы — фундаментальная цена децентрализации и главная причина, почему L1 медленный. Не «плохо написан», а устроен так по смыслу.
Транзакция в mempool — это не запись в реестре. Её могут вытеснить по комиссии, заменить (RBF), она может протухнуть или проиграть конкуренту. Продукт, который показывает пользователю «платёж принят» по факту попадания в mempool, рано или поздно получит инцидент.
Пропускная способность упирается в распространение. Блок должен дойти до большинства узлов раньше следующего, иначе растёт доля осиротевших блоков. Отсюда оптимизации вроде compact blocks (BIP 152): вместо тела передаются короткие идентификаторы транзакций, которые получатель уже видел в mempool, и типичный блок сжимается до единиц килобайт.
Порядки величин: Bitcoin — блок раз в ~10 минут, предел веса ~4 млн единиц, единицы транзакций в секунду. Ethereum — слот 12 секунд, лимит газа блока порядка нескольких десятков миллионов, десятки транзакций в секунду. Это на два-три порядка меньше централизованной платёжной системы, и никакая «оптимизация базы» этого не меняет: платим за то, что каждый узел независимо перепроверяет всё. Обходные пути — вынос исполнения на второй уровень, см. «Масштабирование».
Форки и реорганизации: цепочка на самом деле дерево
Два узла могут одновременно создать валидный блок на одной высоте. Сеть временно расходится, и «последний блок» у разных узлов разный. Это не сбой, а штатный режим.
Правило выбора ветки (fork-choice rule) — часть протокола, а не «здравый смысл». В Bitcoin выигрывает цепочка с наибольшей суммарной работой (не длиннейшая — именно самая тяжёлая). В Ethereum на PoS работает LMD-GHOST поверх голосований валидаторов, а сверху — финальность Casper FFG, объявляющая блоки необратимыми примерно через две эпохи (около 13 минут). Разбор — в «Механизмах консенсуса».
Практическое следствие для всех, кто пишет код поверх блокчейна: реорганизация — обычное событие, и индексатор обязан её переживать. Транзакция из осиротевшего блока в реестре отсутствует. Записали в свою БД «заказ оплачен» после одного подтверждения — обработали платёж, которого нет. Отсюда правило k подтверждений, где k — не магия, а функция цены ошибки: для мелкого платежа хватает единиц, для крупного ждут десятки блоков или, в сетях с явной финальностью, статуса finalized.
Отдельно: транзакция, попавшая в блок и зафейлившаяся при исполнении (контракт сделал revert), всё равно остаётся в реестре и всё равно списывает комиссию. «Не выполнилась» и «не записана» — разные вещи, и путать их дорого.
Родственники: git, Certificate Transparency, аутентифицированные логи
Блокчейн — частный случай семейства аутентифицированных структур данных. Ближайший родственник лежит у каждого на диске:
| git | Блокчейн | |
|---|---|---|
| Объекты | контентная адресация по SHA-1/SHA-256 | контентная адресация по SHA-256/keccak256 |
| Связь | коммит ссылается на родителя | блок ссылается на предыдущий |
| Структура | Merkle DAG, родителей может быть несколько | цепь, родитель ровно один |
| Расхождения | человек делает merge | автоматический fork-choice rule |
| Кто решает истину | владелец репозитория | правило консенсуса, владельца нет |
Вся суть — в последней строке. Git прекрасно доказывает, что история не менялась незаметно, но не отвечает на вопрос «чья история настоящая» — это решает человек с правами на origin/main. Блокчейн убирает эту роль и заменяет её протоколом, платя дороговизной записи и медленным подтверждением. Если вам нужен только журнал, который нельзя незаметно переписать, но администратор допустим, — дешевле взять git-подобную схему или Certificate Transparency, реально работающий Merkle-лог всех выпущенных TLS-сертификатов.
Цена: сколько стоит вести реестр
Компромиссы честнее всего видны в цифрах хранения. Полная цепочка Bitcoin — сотни гигабайт с предсказуемо линейным ростом (блоки ограничены по весу). Полный узел Ethereum со snap-синхронизацией — порядка терабайта; архивный, хранящий состояние на каждом блоке, — единицы-десятки терабайт в зависимости от клиента. Pruning позволяет узлу проверить всю историю, а хранить только недавние блоки и текущее состояние: важно, что обрезанный узел историю проверил сам, он просто не может её раздавать. Отдельная головная боль Ethereum — state bloat: растёт не история, а состояние, потому что удаление данных экономически почти не поощряется (возврат газа за очистку слотов урезали как раз из-за побочных эффектов, EIP-3529).
Отсюда правило для проектировщика: on-chain хранят не данные, а обязательства о данных. Писать в блокчейн килобайты пользовательского контента — архитектурная ошибка: дорого, навсегда, публично и неудаляемо (привет, GDPR). Правильный паттерн — контент в контентно-адресуемое хранилище, хеш в цепочку; ровно об этом статья «Децентрализованные хранилища».
Чего блокчейн не умеет
Не проверяет правду о внешнем мире. Протокол гарантирует, что запись подписана нужным ключом и не переписана задним числом. Он не гарантирует, что записанное соответствует реальности: напишет оператор, что коробка погружена в фуру, а коробка пуста — в реестре навсегда останется заверенная ложь. Это проблема оракула, и внутри цепочки она не решается в принципе; см. «Безопасность контрактов».
Не даёт приватности. Адреса псевдонимны, а не анонимны, анализ графа транзакций — зрелая индустрия. Любая связка адреса с личностью (биржа с KYC, IP, повторное использование адреса) ретроспективно раскрывает всю историю.
Не прощает ошибок. Нет кнопки «отменить перевод» и нет «восстановить пароль». Потерянный seed — потерянные средства навсегда, и это массовое явление, а не редкость (см. «Кошельки и ключи»).
Не делает код правильным. Неизменяемость означает, что баг в контракте тоже неизменяем и лежит в открытом доступе вместе с деньгами, которые он охраняет. The DAO (2016) и десятки более поздних взломов — это не «блокчейн взломали», это обычные ошибки в обычном коде, у которых цена ошибки выше обычной.
Не защищает от мошенничества. Технология не отличает добросовестный проект от скама. «Децентрализация» и «неизменяемость» в маркетинговых материалах не являются свойствами продукта — их проверяют по коду и по тому, у кого лежат админские ключи контракта.
И самое частое: если у системы есть владелец, которому все и так доверяют, блокчейн ей не нужен. «Приватный блокчейн» из пяти узлов одной компании — это реплицированная БД с необычно плохой производительностью.
Типичные заблуждения
- «Данные в блокчейне зашифрованы» — нет, они открыты, но подписаны и захешированы; шифрование и хеширование — разные операции.
- «Блок содержит балансы» — в Bitcoin блок содержит транзакции, а балансы выводятся из набора непотраченных выходов (UTXO); в Ethereum балансы лежат в состоянии, а блок хранит лишь его корень и переходы.
- «Хеш блока хранится в блоке» — нет, он вычисляется из заголовка; в блоке лежит хеш предыдущего.
- «Транзакция не прошла, значит комиссии нет» — если она в блоке, комиссия списана.
- «Нужно скачать всю цепочку, чтобы что-то проверить» — заголовков и merkle proof достаточно для проверки включения.
- «Больше блоков в секунду — лучше» — уменьшение интервала повышает долю осиротевших блоков и снижает эффективную безопасность; это компромисс, а не свободный параметр.
Как посмотреть своими руками
Ничего написанного не нужно принимать на веру. Публичных RPC хватит для чтения; для серьёзной работы поднимают свой узел (см. «Архитектуру dApp»).
# Ethereum: заголовок блока по номеру (hex), без тела транзакций.
# В ответе видны parentHash, stateRoot, transactionsRoot, receiptsRoot — те самые корни.
curl -s -X POST https://ethereum-rpc.publicnode.com \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["0x1312D00",false]}' \
| python3 -m json.tool | head -30
# Bitcoin, если поднят свой узел: сырой заголовок 80 байт в hex
bitcoin-cli getblockheader <blockhash> false
Полезное упражнение: возьмите сырой заголовок Bitcoin из последней команды, прогоните через sha256d кодом из этой статьи, разверните байты — и убедитесь, что получили ровно тот хеш, по которому блок запрашивали. После этого «блок нельзя изменить незаметно» перестаёт быть лозунгом и становится арифметикой.
Мини-итог
- Блокчейн — реплицированный append-only лог плюс правило согласования порядка записей; осмыслен там и только там, где доверенный центр невозможен.
- Блок = заголовок (маленький, фиксированный, достаточный для проверки цепочки) + тело с транзакциями. У Bitcoin заголовок — ровно 80 байт.
- Хеш-цепочка делает подмену истории заметной; неподделываемой её делает сочетание с дорогим консенсусом и независимыми наблюдателями.
- Дерево Меркла сжимает список до 32 байт и даёт доказательство включения за
O(log n); строите своё — разделяйте домены листьев и узлов и проверяйте дубликаты. - Изменяемому состоянию нужен trie, а не список: отсюда Merkle Patricia Trie и
stateRoot. - Цепочка — это дерево: форки и реорганизации штатны, приложение обязано их переживать; в цепочке хранят хеши, а не данные.
Источники
- Satoshi Nakamoto. Bitcoin: A Peer-to-Peer Electronic Cash System — разделы 4, 7 и 8 прямо про эту статью.
- Bitcoin Developer Reference: Block Chain — точная раскладка полей заголовка и правила merkle root.
- Andreas Antonopoulos. Mastering Bitcoin, 2-е изд., глава 9 — открытый исходник книги.
- Ethereum Yellow Paper — формальное определение заголовка блока и Merkle Patricia Trie (приложение D).
- ethereum.org: Merkle Patricia Trie.
- S. Haber, W. S. Stornetta. How to Time-Stamp a Digital Document, Journal of Cryptology, 1991 — хеш-цепочка за 17 лет до биткоина.
- RFC 6962: Certificate Transparency — как правильно строить Merkle-лог с разделением доменов.
Что дальше
Мы разобрали, как данные складываются в блоки и как блоки сшиваются хешами. Осталась вторая опора конструкции: кто и чем доказывает, что запись сделал именно владелец средств. Дальше — Криптография Web3: хеши, пары ключей, подписи, адреса: эллиптические кривые, ECDSA и его подводные камни, как из публичного ключа получается адрес и почему повторное использование одного nonce при подписи мгновенно раскрывает приватный ключ.