Биткоин: работа продолжается. Глава 10
Технические инновации из окопов
Перевод NADO Book by Sjors Provoost.
Проект перевода организован HypeCoinNews.
Script, P2SH и Miniscript
В этой главе мы поговорим о Miniscript и о том, как он значительно упрощает использование Bitcoin Script. Мы расскажем, как работает Script, как с его помощью можно делать более сложные и даже абсурдные вещи, и как с целью сделать транзакции менее сложными и более безопасными появился Miniscript. Кроме того, в главе будет рассказано о том, что такое язык для описания политик, и как он может упростить создание скриптов.
Ограничения
Скрипты — это о том, как блокчейн Биткоина ограничивает возможности траты некой конкретной монеты: когда вы хотите получить биткоин, вы сообщаете человеку, отправляющему его, какие правила применяются к транзакции. Например, без каких-либо ограничений любой может посмотреть, что находится в вашем кошельке. Обычно вы добавляете ограничение, что только вы можете тратить эти монеты. Или, если быть более точным, «эти монеты могут быть потрачены только в том случае, если транзакция включает в себя подпись, сделанную с помощью определенного открытого ключа, а именно моего ключа».
Таким образом, вы должны сообщить человеку, отправляющему биткоины, либо ваш открытый ключ, либо его хэш. Для этого и нужны адреса, как мы объясняли в главе 1. Затем отправитель должен поместить их в блокчейн с пометкой о том, что только владелец этого приватного ключа может тратить биткоины.
Однако, хотя это, безусловно, самый распространенный тип ограничения, существует множество других типов ограничений, и вы даже можете указать несколько различных вариантов для того, кто тратит, например: «Я могу потратить это, но для этого моей маме также нужно подписать транзакцию. Но через два года, может быть, я смогу подписать ее и в одиночку». В таком сценарии, если вы хотите потратить деньги, вам нужно указать, какой вариант вы используете, и выполнить только определенные критерии для этого варианта.
Как работает Script
Script — это язык, основанный на стеке, поэтому думайте о нем как о стопке тарелок. На стопку можно ставить тарелки, можно снимать верхнюю тарелку, но нельзя манипулировать тарелками посередине.
Стек работает иначе, чем обычная память, где вы можете читать и записывать произвольные адреса (например, жесткий диск или ОЗУ — память с произвольным доступом). Стек легче реализовать и представить.
Напротив, смарт-контракты Ethereum имеют стек, а также обычную память и даже долгосрочное хранилище. Как следствие, разработчикам гораздо труднее представлять себе его поведение.
Наиболее часто используемый (до SegWit, см. главу 3) биткоин-скрипт выглядит следующим образом:
OP_DUP
(как в дубликате)OP_HASH160
(который дважды берет хэш SHA-256, а затем хэш RIPEMD-160)pubKeyHash
(хэш приватного ключа)OP_EQUAL_VERIFY
OP_CHECKSIG
Значение для pubKeyHash
генерируется кошельком получателя путем выполнения двойного хэширования SHA-256, за которым следует RIPEMD-160, и результат вставляется в приведенный выше скрипт. Как объяснялось в главе 1, биткоин-адрес содержит только pubKeyHash
; остальное подразумевается. На самом деле именно кошелек отправителя генерирует полный скрипт перед его публикацией в блокчейне.
В блокчейне этот скрипт заканчивается выходом транзакции. Выход транзакции, то есть монета, состоит из этого скрипта, которым она заблокирована (scriptPubKey
) и суммы. Теперь, если вы хотите потратить монеты, вы создаете вход транзакции, который дает указание блокчейну добавить определенные вещи в стек перед выполнением вышеприведенного скрипта.
Интерпретатор Биткоина увидит, что вы поместили в стек, и начнет выполнять программу, начиная с выхода. В этом случае то, что вы помещаете в стек — это ваша подпись и ваш открытый ключ, потому что исходный скрипт не имел вашего открытого ключа; у него был только его хэш.
Продолжая вышеприведенный пример, мы начнем со стопки, состоящей из двух тарелок. Тарелка внизу это ваша подпись, а сверху тарелка с вашим открытым ключом, а дальше скрипт говорит OP_DUP
. Это производит операцию pop, т. е. скрипт берет верхний элемент из стека — верхнюю тарелку, которая является открытым ключом — и дублирует его. Затем оригинал и дубликат помещаются в стек. Итак, теперь у вас две тарелки с публичным ключом наверху стека, а ваша подпись по-прежнему внизу.
Следующая инструкция — «OP_HASH160». Она извлекает один из этих двух дубликатов открытых ключей из стека и хэширует его, а затем помещает этот хэш в стек.
Подпись все еще внизу, потом публичный ключ, а потом хэш публичного ключа (три тарелки).
Следующая операция — «pubKeyHash», которая помещает хэш вашего открытого ключа в стек. Итак, теперь хэш вашего открытого ключа дважды повторен на вершине стека.
Операция OP_EQUAL_VERIFY
извлекает оба этих хэша из стека и проверяет, совпадают ли они.
В стеке остается ваша подпись и ваш открытый ключ, поэтому OP_CHECKSIG
проверяет подпись с помощью вашего открытого ключа, после чего стек оказывается пустым.
Именно так, в двух словах, работает программа на языке Script, и в ходе ее выполнения вы можете делать сколь угодно сложные вещи.
Хэш скрипта и P2SH
Вообще, всякий раз, когда вы хотите получить от кого-то монеты, вы должны точно указать, какой скрипт использовать. В приведенном выше примере все, что нужно — это предоставить хэш открытого ключа в стандартном формате адреса, и кошелек отправителя создаст корректный скрипт.
Но в более сложном примере, приведенном ранее, с альтернативными условиями, такими как наличие подписи родителя через несколько лет, сообщать об этом становится неудобно. Даже если бы для подобного существовал стандарт адреса, ради учета всех возможных ограничений это был бы чрезвычайно длинный адрес.
К счастью, есть альтернатива передаче контрагенту (отправителю) полного скрипта — вы можете передать ему хэш скрипта, который всегда имеет одинаковую длину, причем точно такую же длину, что и обычный адрес.
В 2012 году был введен стандарт Pay-to-Script-Hash (P2SH). Эти виды транзакций позволяют вам использовать в качестве адреса отправления хэш скрипта (такой адрес должен начинаться с 3), вместо отправки на хэш открытого ключа (такой адрес начинался с 1).
Находящийся на другом конце должен скопипастить этот адрес в свой биткоин-кошелек и отправить на него деньги. Теперь, когда вы хотите потратить эти деньги, вам нужно открыть блокчейну фактический скрипт, который ваш кошелек далее автоматически обработает. Поскольку все, что вам для этого нужно, это хэш, человеку, который отправляет вам деньги, не нужно заботиться о том, что на самом деле скрывается за этим хэшем. Только когда вы тратите монеты, вам нужно раскрыть ограничения. С точки зрения приватности это намного лучше, чем сразу вставлять скрипт в блокчейн. В главе 11 объясняется, как Taproot идет еще дальше.
Как и в случае с обычными адресами P2PKH, то, что вы сообщаете отправителю - это просто хэш скрипта. Прежде чем кошелек отправителя поместит его в блокчейн, он добавляет в начало OP_HASH160
и в конец OP_EQUAL
. Так что это, по сути, скрипт внутри скрипта. Внешний скрипт, который кошелек помещает в блокчейн, сообщает блокчейну, что существует внутренний скрипт, который должен быть раскрыт и исполнен получателем, и тогда с него можно тратить деньги.
Это последнее требование на самом деле не следует из скрипта в блокчейне, для которого требуется только совпадение хэша скрипта. Вот почему новый тип адреса P2SH появился с софт-форком, чтобы гарантировать, что, когда такой скрипт находится внутри скрипта, он также выполняется. Обычно это означает, что плательщик помещает в стек не только скрипт, но и ингредиенты, необходимые для выполнения скрипта, такие как подпись.
Реально абсурдные вещи
Script — это язык программирования, который был представлен в Биткоине, хотя он напоминает ранее существовавший язык, известный как Forth. Также он, похоже, исправлялся на ходу под влиянием запоздалых соображений. Фактически, многие операции, которые были частью языка, потребовалось удалить почти сразу, потому что они обеспечивали множество способов, которыми можно было просто сломать узел сети, или сделать другие плохие вещи.
К сожалению, в случае с Биткоином нельзя просто начать с черновика языка, а позже довычистить его. Но это стало ясно только тогда, когда разработчики осознали, что единственный безопасный способ обновить Биткоин — это тщательно продуманные софт-форки. Каждое изменение должно быть обратно совместимым и не ломать уже существующие скрипты. Но разработчики не всегда могут знать назначение уже существующих скриптов, и, что еще хуже, как объяснялось выше, большинство скриптов хэшируются, поэтому они могут содержать что угодно.
В результате попытки убедиться, что обновления языка Script не делают ничего плохого или непредсказуемого, оказывались полным кошмаром. Если выяснялось, что на существующие узлы могло быть оказано негативное влияние, то есть они падали из-за какого-то непонятного скрипта, разработчикам приходилось очень тщательно обходить эту трудность; они должны были решать проблему, не делая случайно монеты непригодными для использования и не добавляя новых ошибок, в том числе в любом потенциально неизвестном (хэшированном) скрипте.
Что еще хуже, поскольку Биткоин — это живая система, и пользователей нельзя заставить обновляться всем одновременно, в идеале исправление не должно подсказывать злоумышленнику, в чем проблема. Но в то же время Биткоин это открытая и прозрачная система, в которую нельзя вносить изменения без публичного обоснования. Это делает ответственное раскрытие информации очень сложным. Так что действительно в первую очередь лучше сделать все возможное, чтобы вовсе избежать таких проблем.
Язык Script достаточно разнообразен, чтобы допускать странные вещи. Если вы просто хотите, чтобы кто-то отправил вам деньги, вам нужен лишь вышеописанный очень простой стандартный сценарий: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
.
Но допустим, вы сотрудничаете и хотите сделать мультиподпись. Чтобы потратить монеты, необходимо предоставить две подписи, а не одну. Теперь вы можете просто использовать OP_CHECKMULTISIG
, но, допустим, его еще не существует. Вместо этого вы можете взять вышеприведенный скрипт для одной подписи и более или менее продублировать его, например: <KEY_A> OP_CHECKSIGVERIFY <KEY_B> OP_CHECKSIG
. В этом примере у вас ключ B, второй подлежащий проверке ключ (мы также не беспокоимся о хэшировании открытых ключей).
По сути, если вы начинаете с этих двух открытых ключей и двух подписей в стеке и запускаете скрипт по одной инструкции за раз, то, если A и B поместят в стек действительные подписи, все будет хорошо. Это будет такая мультиподпись для нищих.
Однако злоумышленник может вставить код с именем OP_RETURN
в середине скрипта: <KEY_A> OP_CHECKSIGVERIFY OP_RETURN <KEY_B> OP_CHECKSIG
.
Этот код OP_RETURN
указывает блокчейну прекратить выполнение программы — другими словами, пропустить проверку подписи B - вашей подписи.
Если вы наивно смотрели на этот скрипт, то могли подумать, что ваша подпись проверяется в конце, и поэтому остальная часть скрипта не актуальна. Если бы у вас был бдительный электронный юрист (т. е. человек или компьютерная программа, которые проводят комплексную проверку транзакций), который должным образом проверил бы, выполняет ли этот «умный контракт» то, о чем он говорит, он мог бы сказать: «Осторожно, тут не проверяют действительность вашей подписи». Этот гипотетический электронный юрист должен увидеть написанное мелким шрифтом OP_RETURN, и предупредить вас. Но проблема в том, что существует бесчисленное множество способов, при которых скрипты могут сработать некорректно, поэтому нам нужен стандартизированный способ работы с подобными скриптами.
В интервью Bitcoin Magazine Эндрю Поэлстра сказал: «В биткоин-скрипте есть коды операций, которые делают действительно абсурдные вещи, например, интерпретируют подпись как значение true/false, разветвляются по этому значению; преобразовают это логическое значение в число, а затем индексируют стек и сортируют его по этому числу. И конкретные правила того, как скрипт это делает, просто сумасшедшие».
Эта цитата иллюстрирует сложность потенциальных способов возни с языком Script.
Возвращаясь к аналогии со стопкой тарелок, представьте, что вы берете молоток и разбиваете одну, а затем прячете две где-то в стопке и красите одну в красный цвет, и после этого скрипт все еще работает, если вы проделаете шаги должным образом. Это совершенно абсурдно.
Если вам этого мало, посмотрите двухчасовую презентацию Эндрю Поэлстры на London Bitcoin Devs, где он продолжает и продолжает рассказывать о проблемах в Script: https://www.youtube.com/watch?v=_v1lECxNDiM
Это было долгое рассуждение о проблемах Script, а если вкратце, то: легко сделать ошибки или скрыть ошибки и создать всевозможные сложные механизмы, которые люди могут заметить или не заметить. И тогда ваши деньги уходят туда, куда вы не хотите. Мы уже видели это в других проектах, например, во взломе Ethereum DAO и последующем хард-форке насколько плохи могут оказаться дела, если у вас очень сложный язык, который делает то, чего вы совсем не ожидаете. Но Биткоин увернулся от многих пуль в первые дни, и, несмотря на его относительную простоту, он по-прежнему требует бдительности.
Внедрение Miniscript
Miniscript — это проект, разработанный несколькими инженерами Blockstream: Питером Вуилле, Эндрю Поэлстрой и Санкетом Канжалкаром. Это «язык для написания (подмножества) биткоин-скриптов в структурированном виде, обеспечивающем анализ, композицию, подписи и многое другое». Вы можете увидеть примеры и попробовать разобраться с ним сами на https://bitcoin.sipa.be/miniscript.
Miniscript состоит из нескольких десятков фрагментов на Script, каждый из которых представляет собой последовательность кодов операций. Эти фрагменты можно комбинировать. Если отдельные коды операций на Script подобны алфавиту, то фрагменты на Miniscript подобны словам. Создавая скрипт, который использует только эти слова, а не любую комбинацию букв алфавита, вы теряете некоторые возможности Script, но получаете определенные гарантии безопасности и правильного поведения скриптов.
Простым примером кода на Miniscript является pkh(A)
, который состоит только из одного фрагмента. Это эквивалент стандартного сценария P2PKH, проанализированного выше (OP_DUP OP_HASH160 <pubKeyHashA> OP_EQUALVERIFY OP_CHECKSIG
). А приведенная выше мультиподпись для нищих требует уже нескольких фрагментов на Miniscript: and_v(v:pk(pubKeyA),pk(pubKeyB))
.
"Грабли" - небезопасные фрагменты кода, из-за которого пользователи "стреляют себе в ногу". Ранний разработчик Биткоина Грегори Максвелл использовал этот термин еще в 2012 году, см., например, bitcoin/bitcoin#1889 (comment), но, возможно, он еще старше.
Miniscript следит за тем, чтобы в коде не было всяких приколов мелким шрифтом. Он удаляет некоторые грабли, но помимо этого он также позволяет безопасно делать очень классные вещи. В частности, он вводит такие выражения, как И
. Таким образом, вы можете сказать, что условие «А» должно быть истинным, и условие «Б» должно быть истинным. Аналогично, вы можете делать и что-то вроде «ИЛИ». И все, что находится внутри «И» или внутри «ИЛИ», может быть сколь угодно сложным.
Вместо этого в Bitcoin Script у вас есть операторы if
и else
, но если вы не будете осторожны, эти операторы if
и else
будут делать не то, что вы предполагаете, потому что за этими выражениями скрывается сложная начинка.
Между тем шаблоны Miniscript гарантируют, что вы делаете только то, что и предполагаете делать. Допустим, вы — компания и предлагаете полукастодиальное решение для кошелька, в котором у вас есть один из ключей пользователя, а у самого пользователя — два ключа. У вас нет большинства ключей, но, скажем, есть пятилетний тайм-аут, после которого у вас появляется контроль - на случай, если пользователь умрет или произойдет что-то подобное.
У вас получается нечто похожее на настройку мультиподписи. Обычно, когда вы настраиваете мультиподпись, каждый дает свой открытый ключ^[Обычно каждый предоставляет не один открытый ключ, а целую серию открытых ключей, например, используя расширенный открытый ключ (так называемый xpub)], и вы создаете простой скрипт, который требует три ключа, подписанные тремя людьми. Но проблема в том, что вы крупный бизнес, предлагающий услуги, у вас очень сложная внутренняя бухгалтерия, и вы, возможно, захотите иметь пять разных подписей от конкретных людей, согласно правилам различной сложности.
То есть вам нужно пройти сложную процедуру согласования, а то, что получается на выходе, должно считаться одним ключом.
Здесь есть проблема: как клиент узнает, что скрипт в порядке? Ему придется нанять собственного электронного юриста, чтобы проверить, нет ли в скрипте каких-то мелких уловок.
Miniscript позволяет это проверить. Некий футуристический кошелек может показать вам небольшую круговую диаграмму, говоря: «Вы — это один фрагмент диаграммы, и есть еще один фрагмент, который действительно сложен, но вам не нужно об этом беспокоиться. Он не собирается делать ничего подлого».
Язык политик
Язык политик — это способ выразить свои намерения. Это проще, чем писать Miniscript напрямую, не говоря уже о непосредственном написании на Bitcoin Script. Далее тяжелую работу выполняет компилятор.
Наш предыдущий пример с мультиподписью для нищих был найден именно с его помощью. Мы изложили политику and(pk(KEY_A),pk(KEY_B))
, которую компилятор превратил в and_v(v:pk(KEY_A),pk(KEY_B))
, что эквивалентно скрипту <KEY_A> OP_CHECKSIGVERIFY <KEY_B> OP_CHECKSIG
. Оказывается, такой скрипт и впрямь производит транзакцию с более низкой комиссией, чем <KEY_A> <KEY_B> 2 OP_CHECKMULTISIG
. Это тип оптимизации, который человек может не заметить, и именно для этого хороши компиляторы.
По сути, вы пишете на языке политик, который похож на язык программирования более высокого уровня, который компилятор превращает в коды операций низкого уровня. Это инструкции, подобные тем, которые мы описали выше, для извлечения элементов из стека и их дублирования. Miniscript также живет на этом очень низком уровне, даже если он и немного легче читается и намного безопаснее. Задача компилятора — взять язык высокого уровня, такой как язык политик, и превратить его в наиболее эффективный низкоуровневый код.
В случае мультиподписи вы можете сказать: «Мне просто нужны две подписи из двух. Мне все равно, как ты это сделаешь». Компилятор знает, что есть несколько способов исполнить это намерение. И тогда вопрос лишь в том, который выбрать? Ответ на этот вопрос зависит от веса транзакции и возможных комиссий.
Однако вы также можете сказать компилятору: «Хорошо, я думаю, что в большинстве случаев это условие A, но только в 10% случаев — условие B». Затем компилятор рассчитает комиссию за условие A, умножит на 9, прибавит комиссию за условие B и разделит общую сумму на 10, чтобы получить среднюю ожидаемую комиссию. Он может оптимизировать код для типичных случаев использования, наихудших сценариев и всего такого, а затем выдает Miniscript, который затем можно преобразовать в Bitcoin Script.^[Технический термин для перехода от Miniscript к Bitcoin Script или для преобразования исходного кода любого языка в другой аналогичный — это транспиляция, которая как правило может осуществляться в двух направлениях. Таким образом, вы можете перейти от Miniscript к Script или от Script к Miniscript, но вы не можете столько же легко вернуться к языку политик. Тем не менее, используя инструменты автоматического анализа, вы часто можете выяснить, какой язык политик использовался для создания данного фрагмента Miniscript.]
С помощью Taproot (см. главу 11) вместо разделения различных условий с помощью и / или их можно разделить на дерево скриптов Меркла. Вам не нужно беспокоиться о том, как построить дерево Меркла, так как об этом позаботится компилятор. В принципе, каждый лист также может содержать операторы и/или. Есть ли смысл это делать? Или лучше придерживаться одного условия на лист? Кто знает? Будущий компилятор Miniscript может просто попробовать все перестановки и решить, какая из них оптимальна.
Ограничения
При этом при использовании языка политик или Miniscript в целом существуют некоторые ограничения.
Чтобы гарантировать безопасность Miniscript и соответствующего ему кода на Bitcoin Script, в нем заблокирован доступ ко всей мощи Script. Однако иногда безопасные действия приводят к тому, что скрипт становится неприемлемо длинным и дорогим для выполнения. В этом случае человек может найти лучшее решение, чем компилятор. Что касается примера, упомянутого Поэлстрой, о том, как транзакции в сети Lightning обрабатывают временные блокировки, хэши или одноразовые номера, там как раз есть подобные оптимизации. Как он выразился: «Ага, вы делаете какое-то странное переключение стека и интерпретируете вещи не такими, как они есть». Вы подаете в стек публичный ключ, но интерпретируете его как число — такие примерно странные трюки.
Такие вещи довольно трудно себе представлять, но человек сможет это сделать, а компилятор Miniscript не сможет, то есть компилятор в конечном итоге получит потенциально более длинные сценарии Lightning. Возможно, когда-нибудь Miniscript можно будет расширить, чтобы он также мог уметь срезать углы таким образом. Но разработчики Miniscript должны быть осторожны, потому что они действительно хотят убедиться, что в Miniscript нет ничего, что возвращает пугающие свойства базового языка.
Еще одно ограничение заключается в том, что язык политик это лишь один из нескольких инструментов, необходимых для практической реализации очень сложных кошельков с мультиподписью. Есть еще вопросы, на которые нужно ответить, например: как именно вы их настраиваете? Что вы пишете друг другу по электронной почте? Вы отправляете по электронной почте сразу свои ключи, или сперва что-то немного более абстрактное, с чем сначала соглашаетесь, а затем обмениваетесь ключами? Это практические вещи, которые не решаются внутри Miniscript.
На момент написания статьи интеграция Miniscript в кошелек Bitcoin Core все еще находилась в стадии разработки.
Поддержите проект(ы) на цепочке
HCN имеет две активные краудфандинговые компании на TallyCoin, которые собирают средства ончейн:
https://tallycoin.app/@hypecoinnews/
Например из @LightningTipBot в Телеграме
/send 100 [email protected]
Или начните пользоваться LN кошельком типа Valet.