Перейти к основному содержимому

Практикум — шаг 1: цепочка блоков

Разработчику

← Обзор практикума · Шаг 1 · Шаг 2 →

Создаём ядро реестра: блок, хеш, цепочка и простейший proof-of-work. Логика совпадает с интерактивом на вводной странице, но здесь — полноценный Python-модуль.


Файлы этого шага

В каталоге из обзора создайте пакет ledger_lab/ и два файла:

  • block.py — блок и хеширование;
  • chain.py — цепочка, проверка и учебный PoW.

В начале block.py понадобятся импорты: hashlib, json, time, dataclasses.


Блок и канонический JSON

Хеш должен считаться от однозначного представления данных. Используем json.dumps с sort_keys=True:

def canonical_json(data: Any) -> str:
return json.dumps(data, sort_keys=True, separators=(",", ":"))

def sha256_hex(payload: str) -> str:
return hashlib.sha256(payload.encode("utf-8")).hexdigest()

Класс Block хранит index, previous_hash, timestamp, transactions, nonce. После заполнения полей вычисляется hash:

@dataclass
class Block:
index: int
previous_hash: str
timestamp: float
transactions: list[dict[str, Any]]
nonce: int = 0
hash: str = field(default="", init=False)

def calculate_hash(self) -> str:
return sha256_hex(canonical_json(self.header_dict()))

Genesis-блок — нулевой блок с фиксированным previous_hash из нулей (как в Bitcoin, только упрощённо):

def genesis_block() -> Block:
return Block(
index=0,
previous_hash="0" * 64,
timestamp=time.time(),
transactions=[{"type": "genesis", "message": "Crypto Ledger Lab"}],
)
Связь с теорией

В Bitcoin заголовок богаче (Merkle root, bits, version). У нас в transactions пока лежит весь список операций — для учебного объёма этого достаточно. Merkle-дерево разобрано в главе 1.


Цепочка и проверка

Blockchain держит список блоков и параметр difficulty (сколько hex-нулей в начале хеша):

@dataclass
class Blockchain:
chain: list[Block] = field(default_factory=list)
difficulty: int = 2

def is_valid(self) -> tuple[bool, str]:
prefix = "0" * self.difficulty
for i, block in enumerate(self.chain):
if block.hash != block.calculate_hash():
return False, f"блок {i}: хеш не совпадает с содержимым"
if i > 0 and block.previous_hash != self.chain[i - 1].hash:
return False, f"блок {i}: разорвана ссылка на предыдущий"
if i > 0 and not block.hash.startswith(prefix):
return False, f"блок {i}: не выполнен учебный PoW"
return True, "ok"

Если изменить transactions в старом блоке без пересчёта hash и перемайнинга хвоста — is_valid() вернёт ошибку. Это тот же принцип, что в демо BlockchainChainPlay, когда подмена ломает цепочку.


Учебный майнинг (PoW)

def mine_block(self, transactions: list[dict]) -> Block:
previous = self.last_block
block = Block(
index=previous.index + 1,
previous_hash=previous.hash,
timestamp=time.time(),
transactions=transactions,
)
prefix = "0" * self.difficulty
while not block.hash.startswith(prefix):
block.nonce += 1
block.hash = block.calculate_hash()
self.chain.append(block)
return block
difficultyПоведение
1Быстро, подходит для тестов
2Заметная задержка на ноутбуке
4+Наглядно показывает цену PoW

В Ethereum после Merge консенсус — PoS; PoW здесь только чтобы почувствовать работу майнера из обзора консенсуса.


Практическое задание

  1. В REPL или маленьком скрипте создайте Blockchain(difficulty=1), добавьте блок с транзакцией {"type": "transfer", "amount": 42}.
  2. Вызовите is_valid() до и после ручной подмены transactions в блоке 1.
  3. Увеличьте difficulty до 3 и засеките время mine_block.

Проверка в REPL после mine_block:

ok, msg = chain.is_valid()
assert ok, msg

Что дальше

Шаг 2 — криптография и подписи переводов: ключи Ed25519 и защита секрета в «vault».

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").