March 1, 2021

Мультиподпись в Биткоине: теория и практика

Пост публикуется в результате голосования на странице Features контракта платформы Etleneum. Вы можете поддержать автора и редактора статьи.

"Его ноутбук зашифрован. Накачай его и бей этим 5$ ключом пока он не скажет пароль"

Зачем пользователю Мультисиг?

В данной статье мы рассмотрим, как работает мультиподписные контракты в Биткоине, а также попробуем воспользоваться ими в популярном Биткоин-клиенте Electrum. Мультиподпись может быть полезна во многих случаях:

  • когда нужно обезопасить себя от использования аппаратных кошельков одного производителя
  • добавить дополнительный фактор аутентификации. Для этого используйте кошелёк для ПК Blockstream Green, в котором мультисиг работает незаметно, как будто вы подтверждаете транзакцию в вашем банке, используя смс или электронную почту
  • когда нужно просто иметь возможность совместного распоряжения средствами — это наш сценарий двух партнёров

Почему Electrum? Это кошелёк для персонального компьютера с графическим интерфейсом, он доступен для любого пользователя. Кошелёк поддерживается для разных операционных систем, и есть версия для Android с практически идентичным интерфейсом. Поэтому данное руководством может быть применимо и к смартфону.

Это руководство можно "усилить", если подключить к Electrum аппаратный кошелёк. В то же время покупка аппаратного кошелька делает настройку мультиподписи дороже. Это руководство допускает виртуально бесплатный мультисиг, не нужно ничего покупать, если есть компьютер.

2021 год можно было бы уже назвать "годом Мультисига". Не смотря на давнюю историю (примерно с 2013 года, если мы не ошибаемся), импульс развитию мультисиг решений, кроме Электрум, был сообщён только командой Nunchuk. Если у вас есть пара аппаратных кошельков, рекомендуем попробовать.

Nunchuk сделал удобную графическую оболочку для работы с стандартным узлом Core и его разработчик Хьюго Нгуен в феврале 2021 инициировал создание протокола для выработки правил работы с мультиподписью на Биткоине. Количество разнообразных кошельков растёт, и стандартизация работы с мультисигом была бы очень полезна для пользователей таких решений.

Теория: контракты мультиподписи в Биткоине

C помощью Bitcoin Script можно создать скрипт, проверяющий подписи транзакции M публичными ключами из N возможных. Этот скрипт называется multisignature script и выглядит так:

OP_M <публичный ключ 1> ... <публичный ключ N> OP_N CHECKMULTISIG
  1. OP_M (опкоды 0x51-0x60) - эта инструкция помещает на стек указанное вместо M число от 1 до 16. Например, OP_5 положит на стек 5. Обозначает, сколько ключей сколько подписей должны успешно проверить для прохождения проверки. Например, если 5 ключей из указанного в скрипте множества из 10 ключей успешно проверят 5 подписей, то проверка завершится успешно.
  2. <публичный ключ 1> ... <публичный ключ N> (инструкции загрузки 33 или 65 байтов данных с опкодами 0x21 или 0x41 + 33 байта или 65 байт compressed- или uncompressed-ключ соответственно) - эти инструкции помещают на стек ключи от 1 до 16 штук. Обозначает всё множество проверяющих подписи ключей. Например, множество из 10 ключей проверяют 5 подписей.
  3. OP_N (опкоды 0x51-0x60) - эта инструкция помещает на стек указанное вместо N число от 1 до 16. Обозначает количество помещённых на стек в шаге 2 ключей.
  4. CHECKMULTISIG (опкод 0xae) - эта инструкция работает так:
    1. Сначала извлекает со стека помещённое в шаге 3 число N. Теперь известно, сколько ключей помещенно на стек в шаге 2.
    2. Теперь по очереди извлекаются со стека помещённые в шаге 2 N ключей. Извлечение происходит в обратном порядке: первым будет ключ N, затем ключ N - 1, и т.д.
    3. После этого извлекается со стека помещённое в шаге 1 число M. На этот момент известно, сколько ключей сколько подписей должны успешно проверить для прохождения проверки.
    4. На этом этапе извлекается помещённые до шага 1 M подписей со стека, тоже в обратном порядке: первая будет подпись M, потом M - 1, и т.д.
    5. Ключи проверяют подписи последовательно. Если ключ успешно проверил подпись, берётся следующий ключ и следующая подпись. Если ключ провалил проверку подписи, то берётся следующий ключ, но та же подпись. Если не меньше M ключей успешно проверили M подписей, то на стек помещается 1, иначе 0.

Pay-to-Multisignature-Script

Этот скрипт можно использовать в scriptPubKey как политику траты, такой вариант использования называется bare multisignature script или Pay-to-Multisignature-Script, описан в BIP 11: M-of-N Standard Transactions. Но так мало кто делает, у этого способа, по крайней мере, четыре существенных недостатка:

  1. Непозволяющие использовать больше 3 ключей в P2MS ограничения на уровне протокола.
  2. Отправитель на P2MS-выход должен знать все ключи этой политики траты.
  3. Основные расходы по оплате комиссий возьмут отправители на P2MS-выход.

Ущерб анонимности и безопасности: после публикации выхода с этой политикой траты все ключи станут известны. Поэтому multisignature script встраивают в P2SH.

Pay-to-Script-Hash

Pay-to-Script-Hash (P2SH) - это формат скрипта скрывает политику траты, включающую в себя сложную логику и называющуюся redeem script, до момента, когда трату UTXO по этой политике опубликуют. Реализован в софтфорке BIP 16: Pay to Script Hash. Посмотрим, что из себя представляет P2SH.

OP_HASH160 <хеш redeem script> OP_EQUAL
  1. OP_HASH160 (опкод 0xa9) - эта инструкция берёт со стека данные и вычисляет от них хеш SHA256, а от этого получившегося хеша хеш RIPEMD160, результат помещается на стек. Здесь данные - redeem script.
  2. <хеш redeem script> (инструкция загрузки 20 байтов данных с опкодом 0x14 + 20 байт хеша) - эта инструкция помешает на стек хеш redeem script. P2SH-адрес формируется с помощью base58-кодирования этого хеша.
  3. Ну и последняя инструкция, OP_EQUAL (опкод 0x87), берёт со стека хеш redeem script, хеш данных от и сравнивает их. Если одинаковы, то на стек помещается 1, иначе 0.

P2SH - это специальный формат скрипта. Когда эти проверки завершаются с успехом, начинается самое интересное: если нода реализует софтфорк BIP 16, то она десериализует лежащий на стеке redeem script и выполнит его, как обычный Bitcoin Script, если нет - на этом всё заканчивается и UTXO разблокируется, даже если проверки в redeem script завершились бы с ошибкой.

Для того, чтобы совместить P2SH и multisignature script, нужно последний использовать в качестве redeem script. Для этого преобразуем инструкции и данные redeem script в их бинарное представление, склеим вместе, хешируем как последовательное применение хеш-функций SHA256 к бинарному представлению redeem script, а затем к результату SHA256 - RIPEMD160. Получившийся хеш используем в качестве <хеш redeem script> в скрипте P2SH.

Pay-to-Witness-Script-Hash

Для SigWit P2SH немного модифицируется, и этот формат скрипта называется Pay-to-Witness-Script-Hash (P2WSH). Вот так он выглядит:

OP_0 <хеш redeem script>
  1. OP_0 (опкод 0x00) - эта инструкция в SegWit-транзакции обозначает версию witness program.
  2. <хеш redeem script> (32 байта) - witness program, SHA256-хеш redeem script в этом контексте.

Как только нода видит OP_0 и 32 байтовый SHA256-хеш redeem script в выходе SegWit-транзакции, она понимает, что имеет дело с P2WSH. За кулисами она извлекает redeem script из стека, хеширует его хеш-функцией SHA256, а затем сравнивает получившийся хеш с хешем, указанный в <хеш redeem script>. Подробнее почитайте вот здесь.

Трата P2SH или P2WSH

Чтобы потратить P2SH или P2WSH UTXO, в scriptSig в случае P2SH или в witness в случае P2WSH помещается этот скрипт:

OP_0 <подпись 1> <...> <подпись M> <redeem script>
  1. OP_0 (опкод 0x00) - эта инструкция кладёт на стек 0. Исправляет баг в CHECKMULTISIG, которая вместо M подписей извлекает M + 1. Поэтому подсовывается недействительная подпись.
  2. <подпись 1> <...> <подпись M> (инструкция загрузки 71 байта данных с опкодом 0x47 + 71 байт подпись, но может быть и больше, подробнее о размерах подписей) - эти инструкции кладут на стек подписи, сформированными учавствовавшими в подписи ключами.
  3. <redeem script> (инструкции загрузки данных от 1 байта до 117 байт с опкодами 0x01-0x75 или OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4 опкодами 0x76-0x78 + 1, 2 или 4 байта, обозначающие длинну данных, если инструкции OP_PUSHDATA* соответственно + redeem script N байт) - эта инструкция кладёт на стек бинарное представление redeem script.

Таким образом, как видно из скриптов, размер транзакции и, соответственно, комиссии за трату P2SH или P2WSH растёт пропорционально количеству ключей N в redeem script и количеству участвующих в подписи ключей M. Ключи непубличного контракта при каждой трате с возратом сдачи нужно ротировать, так как трата раскрывает ключи. Эту проблему решает адаптация иерархических детерминированных кошельков (HD wallet) для P2SH и этот же BIP для P2WSH, за исключением того, что purpose для P2WSH не определён.

Практика: мультиподписные контракты в Electrum

Давайте попробуем поиграться c мультиподписями. Возьмём стандартный пример: супруги имеют общий бюджет и хотят его тратить только с согласием друг друга. Получается контракт 2-из-2. И так, создадим кошелёк с этим контрактом.

Откройте Electrum. Если вы создаёте кошелёк в первый раз, вы
увидите окно создания кошелька:

В поле "Кошелёк" вводим желаемое название кошелька. Например:

И нажмите кнопку "Далее". Если какой-нибудь другой кошелёк уже до этого существовал, вы увидите такое окно:

Нажмите кнопку "Создать новый кошелёк", и вы перейдёте к окну создания кошелька. После того, как вы назвали свой новый кошелёк, вы увидите это окно, где вы должны нажать "Кошелек с несколькими подписями", чтобы создать мультиподписной кошелёк:

Нажмите кнопку "Далее". Теперь появится окно, где нужно настроить количество требуемых подписей и количество со-подписантов. Двигая первый ползунок, можно изменить общее количество человек, со-подписантов, могущие принимать решение о трате денег, а второй ползунок меняет количество людей из этого общего множества со-подписантов, которые должны согласиться, чтобы трата стала возможной: если транзакция не набирает этого необходимого количества подписей из множества со-подписантов, то транзакция просто не попадёт в цепочку блоков Биткоина. В этом случае 2 со-подписанта, и они должны друг с другом согласиться о тратах, если они не согласятся, то и трата не возможна, если с одним из со-подписантом что-то случится, что он не сможет подписать транзакцию, то и деньги навсегда окажутся замороженными.

А здесь 8 со-подписантов, и 5 из них должны согласиться о тратах:

Хорошо, вернёмся к нашему случаю, выберите мультиподписной контракт 2-из-2, то есть первый ползунок установлен "От 2 со-подписантов", а второй ползунок установлен "Требуется 5 подписей". Нажмите "Далее". Теперь вы увидите окно, где вам предлагают варианты создания seed-фразы. Вы можете создать новую, выбрав "Создать новую seed-фразу", или использовать существующую, выбрав "У меня уже есть seed-фраза". Для простоты рассмотрим только первый вариант, выберите его и нажмите "Далее".

Тут предлагают выбрать тип кошелька: "Legacy" (P2SH) или "SegWit" (P2WSH). Выберите второй тип, это снижает комиссии и нажмите "Далее".

В этом окне вам предлагаю сохранить seed-фразу на физическом носителе. Ни в коем случае не копируйте её в буфер обмена, запишите её на бумажку и нажмите "Далее".

Перепишите с вашего листка бумаги seed-фразу в это поле. Ни в коем случае не вставляйте из буффера обмена, нажмите далее.

Скопируйте этот открытый ключ и сохраните, в будущем он понадобится, и нажмите "Далее".

Внимание: храните обе копии xpub или zbup вместе с резервной копией сид-фразы. Без открытого ключа вы не сможете восстановить кошелёк мультиподписи, если у вас нет всех сид-фраз на момент восстановления!

К этому моменту супруг(а) должен(а) пройти те же шаги, что написаны выше. Возьмите открытый ключ вашего партнёра, который он передал вам, а ваш открытый ключ передайте ему. В этом окне введите ключ вашего партнёра и нажмите "Далее". Ваш партнёр должен сделать тоже самое с вашим открытым ключом, который он вам передал.

Да, кстати, очень желательно, если вы свой открытый ключ будете передавать только со-подписантам. Кроме очевидной потери приватности, это может также повлечь проблемы для безопасности в некоторых случаях, подробности можно посмотреть здесь. Добейтесь от со-подписантов того же.

Также не забудьте сохранить на физическом носителе открытые ключи всех со-подписантов. Если для схемы 2-из-2 не имеет особой практической пользы, то для любой другой схемы, где количество требуемых подписей меньше количества со-подписантов, это может помочь восстановить доступ к средствам, если вы потеряли кошелёк (кроме seed и открытых ключей), а один или несколько со-подписантов больше не могут подписывать транзакции (например, потерял доступ к seed и приватному ключу или умер), но при этом консенсус всё ещё может быть достигнут, так как можно достигнуть требуемого количества подписей.

В этом окне введите ключ вашего партнёра и нажмите "Далее". Ваш партнёр должен сделать тоже самое.

Теперь установите пароль на ваш кошелёк и не забудте его зашифровать.

Поздравляю, вы создали кошелёк!

Откройте вкладку "Адреса" и посмотрите их.

Если вы и ваш партнёр всё сделали правильно, то у вашего партнёра будет тот же список адресов.

Чтобы не тратить свои деньги зря, я продемонстрирую работу мультиподписных кошельков на тестнете Биткоина. Адреса отличаются от тех, что были на скриншотах выше, но действия абсолютно такие же. После создания кошелька, зайдите на эту страницу и пополните адрес бесплатными токенами тестнета. Я открыл вкладку "Отправка" в одном из своих тестовых кошельков и вставил адрес в поле "Получатель" один из адресов для приёма денег мультиподписного кошелька.

Деньги пришли, что можно увидеть во вкладке "Адреса", где на одном из адресов будет ненулевой баланс:

Партнёр тоже увидит приход этих денег:

Посмотрите эту транзакцию в Биткоин-эксплорере Blockstream, вы увидите в SCRIPTPUBKEY(ASM) хеш мультиподписного контракта, он же является адресом, но в bech32-кодировании.

Откройте вкладку "История". Если вы увидите, что больше 6 подтверждений набралось, а это и обозначает зелёная галочка возле записи, то это значит, что транзакция с очень очень большой вероятностью необратима.

У партнёра тоже средства, которые пришли, помечены, как подтверждённые:

Попробуйте их отправить. И так, откройте вкладку "Отправка", возьмите какой-нибудь адрес, на который вам нужно отправить деньги, введите его в поле "Получатель" и отправьте ему нужную сумму, например, 0.00001 BTC, введя эту сумму в поле "Сумма", нажмите "Оплатить":

Вам любезно открылось окно с детализацией транзакции. Тут можно поиграться с комиссиями, если вам нужно быстрее, то рядом со всплывающим меню "Мемпул" слева есть ползунок, меняющий комиссии. Двигаете его влево, комиссии уменьшаются, вправо - увеличиваются. Для простоты поменяйте "Мемпул" на "Ожидаемое время" и установите ползунок, чтобы транзакция с большой вероятностью была принята, например, в течении двух минут. Также не забудьте включить "Replace by fee", чтобы комиссию можно было увеличить, если транзакция зависла. Соглашайтесь со всем, если всё правильно, и финализируйте транзакцию, нажав "Finalize".

В этом окне вам предлагают поставить свою подпись. Поставьте, нажав "Подписать".

Вводим пароль от кошелька:

Транзакция подписана вашей подписью. Но ещё нужна подпись партнёра, без неё средства нельзя потратить. Вы же помните, что мы указали, что подписей требуется 2 для траты из 2 возможных? Нажмите "Экспортировать" -> "Export to file" (прошу прощения, всплывающие меню упорно не хотят скриншотиться), чтобы сохранить частично подписанную транзакцию в файл. Сохраните его, передайте этот файл партнёру.

Партнёр жмёт "Инструменты" -> "Загрузить транзакцию" -> "Из файла", выбирает файл, который вы ему отправили, видит детализацию транзакции, жмёт "Подписать", чтобы поставить свою подпись:

Вводит пароль от кошелька:

Две требуемых подписи стоят на транзакции. Теперь доступна кнопка "Разослать", партнёр её жмёт, либо таким же образом экспортирует транзакцию, как в одном из шаге выше, и отправляет вам. В любом случае транзакция в конце концов попадает в мемпул.

Хорошо, транзакция оказалась в истории, пока что ещё не попала хотя бы в один блок:

Партнёр видит тоже самое:

Часть денег ушла к получателю, а часть осталась у вас в виде сдачи на специальном для этого адресе:

У партнёра тоже самое:

В WINTESS вы увидите пустой элемент, две подписи и redeem script. В P2WSH WITNESS SCRIPT multisignature script двумя ключами.

Хорошо, транзакция набрала 6 подтверждений:

У партнёра тоже:

Ради эксперимента, отправьте деньги во второй раз. Интересно, ключи в redeem script изменятся? На этот раз транзакция инициируется вашим партнёром, я продемонстрирую только краткое описание скриншотов, поскольку все действия точно такие же.

Появилось окно с детализацией. Партнёр смотрит, правильные ли адреса, играется с комиссиями, финализирует транзакцию.

Подписывает транзакцию:

Вводит пароль, чтобы подписать:

Экспортирует частично подписанную транзакцию в файл:

Партнёр вам передаёт файл с частично подписанной транзакцией, вы его импотрируете и подписываете:

Введите пароль для подписи:

Отправьте транзакцию в сеть:

Транзакция отправилась:

Партнёр тоже увидел эту транзакцию в мемпуле:

Так как первый адрес сдачи был уже использован, для того, чтобы исключить переиспользование адреса сдачи, Electrum использует новый адрес сдачи:

У партнёра тоже деньги оказались на другом адресе сдачи

Кстати, в P2WSH WITNESS SCRIPT лежат другие публичные ключи. Первая трата была с первого принимающего адреса, а в этот раз вы потратили первый адрес сдачи. Эти два адреса использует разные публичные ключи: как ваши, так и партнёра. Поэтому хеш redeem script отличается, а, соответственно, и адрес. Каждый адрес приёма и сдачи имеет разный уникальный набор ключей, о котором договариваются подписанты транзакции при формировании сдачи благодаря HD wallet.

Хорошо, транзакция набрала 6 подтверждений:

У партнёра тоже:

Заключение

Таким образом, мультиподписные кошельки являются безопасным способом вести общий бюджет: ни одна сторона не может потратить деньги без согласования с другой стороной. Рассмотренная схема 2-из-2 имеет проблему: деньги застрянут, если ваш партнёр больше не может подписывать транзакции. Поэтому рекомендую использовать схемы, где количество подписей меньше количества со-подписантов, например, схема 2-из-3, где третий со-подписант - доверенный член семьи. Но такая схема уязвима к сговору, эта проблема разрешима с помощью более сложных политик трат, например:

  1. Либо могут потратить супруги в схеме 2-из-2.
  2. Либо супруг и один из родственников супруги в схеме 1 + 2-из-3 после таймлока в год. То есть супруг с согласием 2 из 3 родственников супруги могут потратить UTXO, если он через год не был потрачен.
  3. Либо супруга и один из родственников супруга в схеме 1 + 2-из-3 после таймлока в год. То есть супруга с согласием 2 из 3 родственников супруга могут потратить UTXO, если он через год не был потрачен.
  4. Либо дети с согласием друг друга после таймлока в полтора года.
  5. Либо родственники супругов, допустим, в схеме 4-из-6 после таймлока в 2 года. То есть 3 родственника выступают от каждой стороны, согласие 4 требуется для траты: например, одна сторона полностью достигла согласия, включая 1 согласие с другой стороны, либо 2 согласия от каждой стороны, и они могут потратить UTXO, если он через 2 года не был потрачен.

Эта схема более устойчива к сговору, при этом менее уязвима к потерям или похищениям ключей, забытым паролям, недееспособности или смерти одного из со-подписантов. Можно добавить дополнительные условия трат, и так далее. В общем, мультиподписные контракты в Биткоине позволяют во многих случаях обойтись без юристов. В новом обновлении Taproot они станут ещё гибче и приватнее в использовании, их стоимость будет менее зависеть от сложности политики траты, а подписи Шнорра позволят в будущем внедрить MuSig2 и пороговые подписи, что будет ещё большим улучшением в приватность, а всё множество ключей в скрипте свернётся в один общий ключ, поэтому стоимость траты мультиподписного UTXO не будет зависеть ни от размера множества со-подписантов, ни от количества требуемых подписей.

Автор: Sergey Sherkunov