Multisig-контракты в современных децентрализованных сетях — это мощный инструмент, который позволяет просто и надёжно защищать средства на коллективных счетах, а также проводить сделки с несколькими участниками. Если вам интересно, как использовать такие адреса, то вы попросту обязаны понимать механику владения ими и прекрасно представлять себе порядок транзакций. Для работы с такими адресами требуется участие нескольких аккаунтов.
Несмотря на одинаковое название и схожую логику работы, внутренние алгоритмы и способы взаимодействия с адресами, защищёнными мультиподписью, довольно сильно различаются в Bitcoin и Ethereum. Именно об этом внутреннем устройстве и пойдёт речь в данной статье.
Мы будем говорить о двух сетях: Bitcoin и Ethereum. В других блокчейнах multisig-доступ к криптоактивам может быть реализован совершенно иначе.
Введение
Предположим, у нас есть задача защитить от несанкционированного доступа средства на адресе (счёте, если вам ближе банковская терминология), где хранятся деньги организации. Доступ к адресу (возможность создать транзакцию с него на внешний) есть у нескольких людей, к примеру у гендира, финдира и главбуха. Чтобы защитить деньги, если кого-то из этой троицы силой заставят подписать вывод, существует multisig. В Bitcoin и Ethereum можно создать адрес, вывод средств с которого требует не одного, а нескольких подтверждений.
Теперь каждый из трёх участников, желая создать транзакцию на вывод, должен предоставить свою подпись, подтверждающую его согласие с транзакцией. Когда набирается достаточно подписей, средства переводятся. Такая логика и носит название multisig: отправка N из M подписей (M > N) для подтверждения операции.
Для децентрализованных сетей multisig — родной паттерн, так как любая валидная транзакция, отправленная на некоторый адрес, by design содержит в себе электронную подпись, созданную секретным ключом отправителя, так что практически вся нужная для multisig-адресов функциональность уже на борту большинства современных блокчейнов.
Наиболее интересен простейший multisig-адрес 2/3, вывод с которого возможен только по согласию двух из трёх участников. Такой multisig способен сделать удобными и безопасными многие сделки с тремя сторонами и решать задачи, когда двое участников доверяют третьему рассудить их. Практически любые финансовые услуги, в которых есть третья доверенная сторона, реализуются с помощью multisig 2/3. Для примера возьмём аккредитив: плательщик вносит деньги и не может забрать их, если не убедит либо банк (куда не доехали документы о покупке), либо получателя (по доброй воле) с этим согласиться. Получатель также не сможет забрать деньги, если либо продавец их не отдаст (по доброй воле), либо банк не сообщит, что нужные документы до него дошли и он их проверил. В схеме сервис банка до неприличия прост: увидев договор о покупке квартиры, он, вместо того чтобы нести ключ от ячейки в хранилище, просто отправляет approve на multisig-адрес.
Задачи учёта и оплаты услуг тоже тяготеют к multisig 2/3: там часто требуется, чтобы третья сторона решила, была ли оказана услуга и в каком объёме. Multisig, где одна из трёх подписей принадлежит организации, которая в итоге решает, была услуга оказана или нет (например, суду), означает, что суд, приняв решение в пользу одной из сторон, попросту подписывает своё решение и шлёт транзакцию, автоматически разблокируя средства для выигравшей стороны. Для сервисов B2C типа Uber или AirBnB мультисиг, хоть и неявно, основной паттерн работы: когда услуга оказана, именно подпись запроса сервисом инициирует отправку средств от клиента водителю или хозяину квартиры.
В общем, удобно реализованный multisig — как автомат Калашникова: дёшево и сердито, подходит для использования в домашних условиях и для сборов в миллионы долларов. А в блокчейне всё это «без регистрации и СМС» (с точки зрения безопасности это, между прочим, довольно серьёзный тезис, если задуматься).
Мы будем рассматривать два блокчейна — Bitcoin и Ethereum. Надо сказать, что внутреннее устройство multisig в них довольно сильно отличается, всё работает по-разному. Но не будем забегать вперёд.
Bitcoin multisig
Bitcoin на самом деле не так уж и прост, как может показаться многим. Его дизайн позволяет реализовывать легковесные и при этом очень безопасные схемы получения доступа к средствам. Ну и добавлю, что, чисто по моему личному мнению, структуры и алгоритмы в Bitcoin отлично продуманы, оптимальны и попросту красивы. Стандартный перевод X биткоинов с адреса на адрес — это всего лишь default’ный тип транзакции, верхушка айсберга потенциальных способностей Bitcoin. Дефолтная транзакция по переводу BTC с точки зрения кода представляет собой простейший multisig 1/1: чтобы воспользоваться X биткоинами с адреса а1, необходимо приложить к транзакции электронную подпись, созданную с помощью секретного ключа, который принадлежит адресу a1. Давайте посмотрим на структуру транзакции Bitcoin и немного глубже копнём, как она работает.
В Bitcoin есть требование: все BTC, хранящиеся на некотором адресе, всегда тратятся целиком. То есть, подтвердив своё право владеть адресом, транзакция тратит весь баланс, распределив его на несколько output’ов. В реале ещё и оставив разницу между input’ами и output’ами майнеру в качестве комиссии, но для данной статьи это несущественно.
Представим ситуацию. Васе на адрес пришло 50 BTC. Вася хочет послать Пете 0,5 BTC. Для этого он обязан потратить все 50 BTС. Чтобы получить «сдачу», Вася берёт один input (50 BTC) и создает два output’а:
- 0,5 BTC на адрес Пети;
- 49,5 BTC на один из собственных адресов.
Когда говорят «Вася подписывает транзакцию» — это не совсем точно для Bitcoin. Вася подписывает все input’ы в транзакции, причём каждый input — это отдельный bitcoin-адрес, и для каждого из них нужен соответствующий секретный ключ. Предположим, у Васи есть три адреса, на которые три его друга прислали по 0,1 BTC. Чтобы потратить 0,25 BTC, Васе придётся подписать три input’а (каждый по 0,1) и сформировать два output’a (0,25 на адрес получателя и 0,05 себе в качестве сдачи). Такая схема позволяет, в частности, никогда не использовать дважды один и тот же адрес и возвращать сдачу каждый раз на новый. Некоторые кошельки, например Electrum, позволяют владельцу самому выбрать стратегию для переиспользования адресов, более приватную (каждый раз новый адрес) либо более простую, но с постоянными адресами (такая схема удобней, если вы прибегаете к сложным регулярным транзакциям). Напоминаю, что разница между суммой input’ов и output’ов — это одновременно комиссия майнеру: просто и надёжно.
Теперь я вместо «Вася подписывает транзакцию» говорю «Вася подписывает input». Но я специально упустил ещё несколько моментов, чтобы не усложнять общую схему. Дело в том, что адреса в Bitcoin — это не public key в чистом виде, а его хеш (дважды хеш с несколькими служебными байтами и версией, подробности в статье Bitcoinwiki), то есть при обычной транзакции отправитель даже не знает публичного ключа получателя. Поэтому Вася, подписывая input, кроме самой подписи, предъявляет биткоиновскому Script’у ещё и свой публичный ключ, тем самым раскрывая его. Естественно, зная публичный ключ, можно без проблем сгенерировать Bitcoin-адрес.
Помимо подписи inputs, Вася ещё и ставит условие для каждого output’а, как можно потратить BTC с этого конкретного output’а. Но «требования» ему предоставил тот, кто получит биткоины: это его забота, как он будет тратить свои BTC. Это крайне важный момент. Адрес Bitcoin содержит в себе хеш от кода, который будет принимать решение, разрешить ли тратить BTC с этого адреса.
Если ещё искать аналогии, то Вася к каждому output’у (который ему сообщили те, кто получит BTC) прицепляет данные, которые отвечают на вопрос «Какой код и на каких данных должен вернуть true, чтобы подписывающий считался владельцем этого output’а?». Вот ещё аналогия для питонистов: можно представить, что каждый output содержит lambda-функцию, которая, исполненная Петей, предоставившим ей аргументы, вернёт true либо false. Если возвращается true, то Петя может использовать output как input для последующих транзакций. В default’ном варианте программа может взять два предоставленных Петей аргумента — подпись output’a и Петин public key — и проверить подпись. Если вернулось true, значит, Петя имеет право на output и может его потратить; следовательно, транзакция валидна. Глубже в рамках данной статьи в Script нырять необязательно, но крайне желательно. Кстати, redeem script — это и есть тот самый smart contract, то есть формальное правило, по которому право на BTC переходит от одного адреса к другому. Клиенты сети, процесся транзакции, исполняют каждый script в каждой транзакции, проверяя, валидна ли она. Если валидна — валидна и неоспорима трата биткоинов с определённого адреса.
Вот первая отличная статья по этой теме, сильно рекомендую прочитать, там отлично расписан весь raw-механизм с примерами на питоне: Bitcoins the hard way: Using the raw Bitcoin protocol. А вот ещё одна: Bitcoin in a nutshell — Transaction.
Вернёмся к мультисигу 2/3, отметив, что обычную транзакцию с переводом биткоинов можно рассматривать и как multisig 1/1. Предположим, Петя хочет, чтобы Вася просто перевёл ему BTC. Петя выдаёт Васе адрес, в который зашит хеш default’ного script’а, и биткоины с этого адреса Петя сможет забрать, предоставив стандартный код проверки подписи и саму подпись. Такие «традиционные» адреса в Bitcoin начинаются с цифры 1 (единицы). Адреса, в которые зашит «нетрадиционный» код (проверяющий multisig script), называются pay-to-script и начинаются с 3 (тройки).
Bitcoin позволяет нам создать адрес, в который будет зашит хеш НАШЕГО СОБСТВЕННОГО redeem script’a. Ведь это наша проблема, как мы потом будем тратить свои BTC, а не отправляющего. Нам неважно, кто переведёт биткоины на адрес, а важно, что, когда мы захотим их потратить, нам придётся предоставить код, соответствующий адресу. То есть, попросив перевести мне BTC на pay-to-script адрес, я обязуюсь предоставить сам скрипт потом, в транзакции, использующей данный input.
Согласно документации, для multisig используется код такого вида:
-----------------------------------------------------------
Pubkey script: OP_HASH160 <Hash160(redeemScript)> OP_EQUAL
Signature script: <sig> [sig] [sig...] <redeemScript>
-----------------------------------------------------------
(Продолжение статьи о script и multisig)
Нам нужен такой script, чтобы все необходимые подписи сразу были представлены в тратящей транзакции. Если, к примеру, наш multisig 2/3 содержит адреса трёх участников (генерального директора, главного бухгалтера и сисадмина) и секретные ключи для подписей лежат на их компьютерах раздельно, то для формирования тратящей транзакции сисадмину придётся:
- сформировать тратящую транзакцию;
- сформировать собственную подпись;
- передать содержимое транзакции второму подписанту (например, сходить в бухгалтерию или к генеральному с флешкой);
- попросить второго подписанта добавить подпись к транзакции;
- отправить в сеть транзакцию с двумя из трёх подписей.
Это не сильно простой способ, но он позволяет использовать в качестве одного из подписантов компьютер, который вообще отключён от сети и гарантированно не сольёт секретный ключ. В этом случае вывод средств совсем «ручной», что для больших сумм весьма неплохо. XXI век, друзья, сейчас модно класть в сейф нулёвый ноутбук без подключения к сети и с единственным установленным софтом — кошельком Bitcoin.
Ethereum multisig
Реализация multisig в Ethereum сильно отличается от таковой в Bitcoin из-за разного дизайна внутренних алгоритмов клиента сети, но есть и параллели. В Ethereum специальная транзакция типа "create_contract" создаёт в сети адрес, к которому привязан код конкретного контракта. У этого адреса также есть баланс эфира. Если послать туда эфир — адрес «примет его на баланс». И наоборот, если выслать с адреса эфир — тот «снимется с баланса».
Я сразу накидаю несколько довольно точных аналогий для понимания весьма простой и логичной кухни. Если забыть о консенсусе и о том, как ноды обновляют цепочку блоков, и рассматривать блокчейн как абстрактное хранилище, то размещение контракта можно сравнить, например, с инстанцированием объекта, то есть превращением класса в реальный объект в оперативной памяти. В аналогии память — это блокчейн, а код класса — код контракта. После инстанцирования объект (контракт) получает собственный адрес в «памяти», и сети известен интерфейс к нему, представленный в виде описанных в классе методов.
Приходящие на адрес транзакции в такой схеме — это методы-writer’ы, они изменяют внутреннее состояние объекта контракта. Методы-reader’ы, которые просто читают данные из состояния контракта, любой клиент сети выполняет локально, ибо уверен, что его копия объекта верна и подтверждена консенсусом block-producer’ов. Очень важно также понимать, что код контракта выполняется тогда, когда майнер «применяет» пришедшую транзакцию к существующему контракту, а в нашем варианте — попросту вызывает один из методов контракта с аргументами, вложенными в транзакцию.
Ну а раз у контракта есть внутреннее состояние (поля объекта в нашей аналогии), то он может реализовывать мультисиг без необходимости присылать подписи одной транзакцией. Создаём контракт, в нём хардкодим адреса подписантов, когда приходят транзакции на вывод — переводим контракт последовательно в состояние ожидания нужного числа подтверждений. Если речь идёт о multisig 2/3, всё может выглядеть примерно так:
- сисадмин деплоит в сеть multisig-контракт и кладёт в него адреса генерального директора и главного бухгалтера;
- счастливые клиенты шлют тонны эфира в контракт;
- главный бухгалтер собирается сделать большой платёж и шлёт в контракт транзакцию с желанием перевести сколько-то эфира на внешний адрес;
- multisig-контракт переходит в состояние «ожидаю N подтверждений», в нашем случае достаточно одного;
- генеральный директор хочет помочь главному бухгалтеру, но забыл, в каком ноутбуке нужный секретный ключ;
- сисадмин шлёт транзакцию — подтверждение вывода средств в контракт;
- контракт, приняв транзакцию от сисадмина, понимает, что набралось нужное N подтверждений, и высылает эфир по заказанному главным бухгалтером адресу, одновременно возвращаясь в состояние простого приёма средств.
В зависимости от реализации multisig-контракта много вещей можно реализовать по-разному. Это касается складирования неисполненных заявок на вывод, интервала времени ожидания вывода, удобных ограничений. Например, можно разрешать выводить суммы до определённого лимита без мультиподписи и требовать подтверждения, если лимит превышен.
Есть несколько вариантов кода контрактов multisig-кошельков, вы без проблем найдёте их. Самый официальный из них этот. Существует ещё множество модификаций multisig-контрактов, а также контракты, которые представляют собой multisig, но даже не знают об этом, ибо любой контракт с несколькими ролями в философском смысле и есть multisig.
Заключение
Мы рассмотрели схему работы одного из самых важных кирпичиков для построения сложных систем контрактов и организации многосторонних сделок. Где проще всего пощупать multisig-адреса вживую:
- для Bitcoin: в кошельке Electrum есть подробная пошаговая инструкция со скриншотами, как создать и сконфигурировать multisig-адрес и как им воспользоваться. Просто поищите «Electrum multisig»;
- для Ethereum: в стандартном кошельке «Ethereum wallet» есть раздел «Contracts», и там можно легко создать multisig. Также multisig наверняка будет во многих видах представлен на платформах для запуска стандартных контрактов, например на нашей Smartz.io сейчас уже один есть.
Как видите, на блокчейне эти схемы весьма технологичны и просты, так что используйте их в своё удовольствие и защищайте свои криптобогатства правильно.
4tune
Спасибо за подробности!