Блокчейн
Bitcoin Script: программируемые деньги
Ты отправляешь Bitcoin. Кажется, что транзакция - это просто перевод с одного адреса на другой, как банковский платёж. Но внутри каждой транзакции скрыта мини-программа - скрипт на Forth-подобном языке, который буквально говорит: «Докажи, что ты имеешь право потратить эти монеты». Без циклов, без переменных, с одним только стеком - и этого достаточно, чтобы построить целую платёжную сеть на миллиарды долларов.
- **Bitcoin Lightning Network** - сеть мгновенных платежей с пропускной способностью миллионы транзакций в секунду, построенная целиком на комбинации hashlocks и timelocks в Bitcoin Script
- **Multisig-кошельки** защищают более 50 млрд корпоративных и институциональных средств. Биржи вроде Coinbase хранят клиентские средства в multisig 2-of-3, где один ключ - у клиента
- **Escrow без посредника** - через 2-of-3 multisig покупатель и продавец совершают сделку, где арбитр вмешивается только при споре, но не может украсть средства в одиночку
Предварительные знания
Стековая машина: Forth-подобный язык Bitcoin
В предыдущем уроке мы узнали, что каждый UTXO «заперт» скриптом. Но что за язык используется для этих скриптов? Ответ удивляет: Сатоши Накамото создал **собственный** язык программирования - **Bitcoin Script**.
Bitcoin Script - это **стековый язык**, вдохновлённый Forth (1970). Вместо переменных и функций здесь единственная структура данных - **стек** (stack). Каждая инструкция либо кладёт данные на вершину стека, либо снимает их оттуда, выполняет операцию и кладёт результат обратно.
Каждая Bitcoin-транзакция содержит два скрипта: - **scriptPubKey** (locking script) - «замок» на UTXO. Задаёт условие: *кто может потратить?* - **scriptSig** (unlocking script) - «ключ». Предоставляет данные, удовлетворяющие условию
При валидации транзакции нода **конкатенирует** scriptSig и scriptPubKey и выполняет результат как единую программу. Если после выполнения на вершине стека остаётся **TRUE** (ненулевое значение) - транзакция валидна.
**Bitcoin Script намеренно НЕ является Turing-complete.** В нём нет циклов, рекурсии и доступа к состоянию блокчейна. Это сделано сознательно: Turing-complete язык позволил бы создавать бесконечные циклы, что парализует сеть. Каждый скрипт **гарантированно завершается** за конечное время. Это ограничение - не недостаток, а защитный механизм.
**Forth** - язык, созданный Чарльзом Муром в 1970 году для управления телескопами. Его стековая архитектура оказалась настолько надёжной, что её использовали в космических зондах (Philae, Rosetta) и - спустя 38 лет - в Bitcoin. Минимализм Forth идеально подошёл для денег: чем проще язык, тем меньше поверхность атаки.
Почему Bitcoin Script не поддерживает циклы и рекурсию?
Опкоды: инструкции стековой машины
Bitcoin Script состоит из **опкодов** (operation codes) - элементарных инструкций, каждая из которых выполняет одну операцию со стеком. Всего определено около **100 опкодов**, но активно используется лишь пара десятков.
| Опкод | Действие | Стек до → после |
|---|---|---|
| OP_DUP | Дублирует верхний элемент стека | [a] → [a, a] |
| OP_HASH160 | RIPEMD160(SHA256(x)) - двойное хеширование | [x] → [hash160(x)] |
| OP_EQUAL | Сравнивает два элемента, кладёт TRUE/FALSE | [a, b] → [a == b] |
| OP_EQUALVERIFY | Как OP_EQUAL, но прерывает скрипт если FALSE | [a, b] → [] (или FAIL) |
| OP_CHECKSIG | Проверяет ECDSA-подпись | [sig, pubKey] → [TRUE/FALSE] |
| OP_CHECKMULTISIG | Проверяет m-of-n подписей | [sig1..sigM, M, pub1..pubN, N] → [TRUE/FALSE] |
| OP_RETURN | Помечает output как «неспрашиваемый» (provably unspendable) | - (данные после OP_RETURN игнорируются) |
Рассмотрим пошаговое выполнение простого скрипта: **проверка, что предоставленное значение хешируется в ожидаемый хеш** (hashlock).
**OP_RETURN** - специальный опкод, позволяющий записать **произвольные данные** в блокчейн (до 80 байт). Поскольку output с OP_RETURN неспрашиваем, ноды не хранят его в UTXO-наборе. Этот опкод используют для записи хешей документов, сообщений и метаданных протоколов вроде Ordinals.
**Категории опкодов:** - **Константы**: OP_0, OP_1, ... OP_16, OP_PUSHDATA - кладут данные на стек - **Управление**: OP_IF, OP_ELSE, OP_ENDIF - ветвление (но не циклы!) - **Стек**: OP_DUP, OP_DROP, OP_SWAP - манипуляции со стеком - **Арифметика**: OP_ADD, OP_SUB, OP_EQUAL - вычисления - **Криптография**: OP_HASH160, OP_SHA256, OP_CHECKSIG - хеширование и подписи - **Timelocks**: OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY - условия по времени
Что произойдёт при выполнении: PUSH <data> OP_HASH160 OP_HASH160?
P2PKH: самый распространённый тип транзакции
**Pay-to-Public-Key-Hash (P2PKH)** - классический тип Bitcoin-транзакции, который использовался в подавляющем большинстве переводов до появления SegWit. Адреса P2PKH начинаются с **1** (например, `1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa` - адрес Сатоши).
Логика P2PKH: «Чтобы потратить этот UTXO, предоставь публичный ключ, хеш которого совпадает с указанным, **и** валидную подпись этим ключом». Два уровня защиты: знание ключа + владение секретом.
**Зачем хешировать публичный ключ?** Почему не просто OP_CHECKSIG с публичным ключом? 1. **Компактность**: hash160 = 20 байт, а публичный ключ (сжатый) = 33 байта. При миллионах UTXO это экономия 2. **Постквантовая защита**: пока UTXO не потрачен, публичный ключ неизвестен. Квантовый компьютер не может атаковать то, чего не видит. Ключ раскрывается только в момент траты 3. **Дополнительный слой**: даже если ECDSA взломан, злоумышленнику нужно ещё обратить HASH160
**Адрес Bitcoin** - это Base58Check-кодировка hash160 публичного ключа. Base58 исключает символы `0`, `O`, `I`, `l`, чтобы избежать путаницы при ручном вводе. Префикс `0x00` для mainnet даёт адреса, начинающиеся с **1**.
В P2PKH-скрипте OP_DUP дублирует публичный ключ. Зачем нужна копия, если ключ уже на стеке?
P2SH: хеш скрипта вместо самого скрипта
P2PKH прекрасно работает для простых переводов: один ключ, одна подпись. Но что если условие траты сложнее? Например, multisig 2-of-3 требует скрипт длиной **~250 байт**. И этот скрипт хранится в **каждом UTXO** - отправитель платит за длину скрипта получателя.
**Pay-to-Script-Hash (P2SH)**, предложенный в **BIP-16** (2012), решает эту проблему компактно: вместо самого скрипта в UTXO хранится только его **хеш**. Полный скрипт предоставляется позже - в момент траты.
P2SH-адреса начинаются с **3** (например, `3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy`). Процесс валидации двухэтапный:
**P2SH - универсальная обёртка.** Внутри redeemScript может быть что угодно: multisig, timelock, hashlock, или их комбинация. Отправителю не нужно знать детали - достаточно хеша. Это разделение ответственности: отправитель знает *куда*, получатель определяет *как*.
Компания хочет использовать multisig 2-of-3 для получения платежей. В чём главное преимущество P2SH перед наивным multisig-скриптом в UTXO?
Multisig: m-of-n подписей и знаменитый баг
**OP_CHECKMULTISIG** - опкод, проверяющий, что **m из n** предоставленных подписей валидны. Это основа корпоративных кошельков, escrow-сервисов и совместного управления средствами.
**Знаменитый off-by-one баг.** Заметили `OP_0` в начале scriptSig? Это **dummy-элемент**, который не несёт смысла, но обязателен. OP_CHECKMULTISIG содержит ошибку в оригинальном коде Сатоши: он снимает со стека на **один элемент больше**, чем нужно. Баг обнаружили слишком поздно - исправление сломало бы все существующие транзакции (hard fork). Решение: стандарт **BIP-11** требует, чтобы первым элементом был OP_0. Этот баг живёт в Bitcoin уже 17 лет и, вероятно, переживёт всех нас.
Применение multisig в реальном мире
Три сценария, три конфигурации
1. Корпоративный кошелёк (2-of-3): CEO + CFO = штатный платёж CEO + CTO = экстренный платёж Один ключ украден → средства в безопасности 2. Escrow-сервис (2-of-3): Покупатель + Продавец = сделка без посредника Покупатель + Арбитр = возврат при споре Продавец + Арбитр = подтверждение доставки → Арбитр не может украсть средства в одиночку 3. Семейный траст (3-of-5): 5 членов семьи, для траты нужно согласие большинства. Защита от потери 2 ключей и от сговора меньшинства.
**Ограничения OP_CHECKMULTISIG:** - Максимум **20 публичных ключей** (OP_20 - максимальное значение в Script) - Проверка **линейная**: каждая подпись сравнивается с каждым ключом по порядку. Порядок подписей должен совпадать с порядком ключей - Раскрывает **все** публичные ключи участников - даже тех, кто не подписывал. Taproot (Schnorr + MAST) решает эту проблему приватности
**Gnosis Safe** (ныне Safe) - самый популярный multisig-кошелёк Ethereum, хранящий более **$100 млрд** активов. В Bitcoin аналогичные решения - **Electrum** (2-of-3) и **Unchained Capital** (collaborative custody). Биржа **Bitfinex** в 2016 году потеряла $72 млн из-за уязвимости в своей multisig-конфигурации.
Bitcoin Script - примитивный язык, который не позволяет создавать сложные условия траты, поэтому для «умных контрактов» нужен Ethereum
Bitcoin Script поддерживает **ветвление** (OP_IF/OP_ELSE), **timelocks** (OP_CHECKLOCKTIMEVERIFY), **hashlocks** и **multisig** - комбинация этих примитивов позволяет строить сложные протоколы. **Lightning Network** - полноценная платёжная сеть второго уровня - построена целиком на Bitcoin Script (HTLC = hashlock + timelock). **Taproot** (2021) добавил деревья скриптов (MAST) и Schnorr-подписи, значительно расширив возможности.
Путаница возникает из-за отсутствия Turing-completeness. Но Turing-completeness - не показатель мощности для финансовых контрактов. Bitcoin Script намеренно ограничен, чтобы гарантировать безопасность и предсказуемость. Большинство реальных финансовых контрактов (escrow, timelocks, multisig, payment channels) реализуемы без циклов. Ограниченность - это feature, а не bug.
Почему scriptSig для multisig-транзакции начинается с OP_0 (dummy-элемента)?
Итоги
- **Bitcoin Script** - стековый Forth-подобный язык, намеренно не Turing-complete. scriptSig (ключ) + scriptPubKey (замок) = программа. Если стек завершается с TRUE - транзакция валидна
- **Опкоды** - ~100 инструкций: OP_DUP, OP_HASH160, OP_CHECKSIG и другие. Каждый опкод выполняет одну атомарную операцию со стеком
- **P2PKH** - «покажи публичный ключ + подпись». Адреса начинаются с **1**. Семь шагов: от подписи на стеке до TRUE на вершине
- **P2SH** - хеш скрипта вместо самого скрипта. Уменьшает UTXO с ~250 до 23 байт. Адреса начинаются с **3**
- **Multisig** (OP_CHECKMULTISIG) - m-of-n подписей для корпоративных кошельков и escrow. Тот самый dummy-элемент OP_0 - баг из 2009 года, который переживёт нас всех. А вся эта система, работающая без циклов и переменных, оказалась достаточно мощной, чтобы на ней построили Lightning Network с миллионами транзакций в секунду
Связанные темы
Bitcoin Script связывает модель UTXO с продвинутыми протоколами второго уровня:
- Bitcoin: модель UTXO — Каждый UTXO «заперт» скриптом (scriptPubKey). Понимание Script невозможно без понимания UTXO - скрипт определяет *условие траты* каждого неизрасходованного выхода
- Lightning Network — Payment channels построены на HTLC (Hash Time-Locked Contracts) - комбинации OP_HASH160 (hashlock) и OP_CHECKLOCKTIMEVERIFY (timelock) из Bitcoin Script
- Taproot и Schnorr — Taproot (BIP-340/341/342) заменяет OP_CHECKMULTISIG на Schnorr-подписи и добавляет MAST (Merklized Abstract Syntax Trees) - деревья скриптов, где раскрывается только использованная ветка
Вопросы для размышления
- Bitcoin Script намеренно не Turing-complete, а Ethereum EVM - Turing-complete (с ограничением через gas). Какой подход безопаснее для хранения ценности, и почему оба имеют право на существование?
- Off-by-one баг в OP_CHECKMULTISIG живёт в Bitcoin с 2009 года. Как бы вы поступили, будучи core-разработчиком: исправить через hard fork (рискуя расколом сети) или оставить навсегда? Какие ещё примеры «вечных» багов в критических системах вы знаете?
- P2SH переносит стоимость хранения скрипта с отправителя на получателя. Справедливо ли это экономически? Как эта идея «отложенного раскрытия» применяется в других областях (например, в zero-knowledge proofs)?