Масштабируемость биткоина является одной из его главных проблем, над решением которой активно работают. Одним из представителей этих решений является, например, технология Lightning network, но ее реализация пока что не представляется возможной ввиду некоторых уязвимостей. Другое решение — Segregated Witness также направлено на повышение масштабируемости, но ко всему прочему решает еще и целый ряд проблем, включая ту самую уязвимость, мешающую реализации лайтнинга. В этой статье мы рассмотрим основные преимущества Segregated Witness, а также опишем механизм его работы.
Segregated witness, или как многие его называют SegWit, это софт-форк, описанный в серии BIP'ов (141, 142, 143, 144 и 145), главной целью которого является оптимизация структуры транзакций и блоков с помощью вынесения подписей (то, что называют 'scriptSig', 'witness' или же 'unlocking script') из транзакции в отдельную струтуру. Это не только позволяет уменьшить размер транзакций, делая блоки более вместительными, но и решает проблему их "изменяемости" (transaction malleability, именно об этой уязвимости мы и говорили выше), что очень важно для технологий наподобие платежных каналов или лайтнинга, пологающихся на строение транзакции биткоина.
How it works
Before we begin
Для начала кратко напомним, что из себя представляет платежная система в биткоине. В ней нет ничего наподобие списка балансов как это может быть реализовано в каком-нибудь банке. Вместо этого, баланс каждого адреса представлен набором транзакций, отправленных на этот адрес, где транзакция — это структура, в которой главными являются входы (inputs) и выходы (outputs). Входы — это транзакции, на которые мы ссылаемся, чтобы потратить (если точнее то это не транзакции полностью, а их конкретные выходы, т.к в одной транзакции мы можем переводить средства на несколько адресов), а выходы — это адреса, на которые мы хотим перевести средства. Вот так выглядит структура транзакции биткоина:
Поле PubKey script (далее scriptPubKey) в выходах это то, что называют locking script. Оно нужно для того, чтобы только владелец адреса, на который перечисляются срества, смог воспользоваться этим выходом. Поле Signature Script (далее scriptSig) еще называют unlocking script — оно "отпирает" locking script, предоставляя доказательство владения адресом.
Подробнее о транзакциях, а также о работе запирающего и отпирающего скрипта можно почитать здесь.
Backward compability
На самом деле, Segregated Witness меняет не только строение транзакции, но и ее выходы. Это, однако, не значит, что в одной и той же транзакции не могут быть потрачены как традиционные UTXO (unspent transaction outputs), так и сегвитовские — просто первые будут ожидать "доказательства" внутри входа (поле scriptSig), а вторые — снаружи.
Так как Segregated Witness все-таки является софт-форком, его обновления могут быть проигнорированы, а значит более старые системы должны как-то обрабатывать сегвитовские выходы. Дело в том, что для старых нод или кошельков эти выходы выглядят как доступные всем, то есть они могут быть потрачены с пустой подписью, что все еще валидно. Принявшие обновление ноды и кошельки конечно же будут искать все подписи снаружи входов в специальном поле 'witness'.
Examples
Pay-to-Witness-Public-Key-Hash
Теперь давайте взглянем на примеры транзакций и на то, как они изменятся с Segregated Witness. Мы начнем со стандартной Pay-to-Public-Key-Hash (P2PKH) транзакции.
Нас интересуют выходы, а именно их поля "scriptPubKey". Рассмотрим типичный locking script:
OP_DUP OP_HASH160 <PubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
C Segregated Witness он будет выглядеть так:
0 <PubKeyHash>
Как видите, сегвитовский выход намного проще традиционного — он состоит из двух значений, которые будут помещены на стэк исполнения скрипта. Как мы уже упомянули ранее, для старых версий клиента биткоина этот выход будет виден как доступный любому, так как он не требует подписи. А вот для обновленного клиента первое число интерпретируется как номер версии, а второе как аналог запирающего скрипта (witness program). На самом деле, здесь должен быть использован хеш сжатого публичного ключа, об этом мы расскажем немного позже.
Теперь давайте рассмотрим транзакцию, в которой этот выход будет потрачен. В традиционном случае это выглядело бы так:
[...]
"Vin" : [
{
"txid": "8adbca5e652c68f8f3c30ac658115bc4af395d0cc7e6beaea18168295c29d011",
"vout": 0,
"scriptSig": "<our scriptSig>"
}
]
[...]
Однако, чтобы потратить сегвитовский выход, транзакция должна иметь пустое поле sriptSig и содержать все подписи в отдельном месте:
[...]
"Vin" : [
{
"txid": "8adbca5e652c68f8f3c30ac658115bc4af395d0cc7e6beaea18168295c29d011",
"vout": 0,
"scriptSig": ""
}
]
[...]
"witness": "<Witness data>"
[...]
Warning
Несмотря на то, что традиционные клиенты могут обрабатывать сегвитовские транзакции (напомню, что их выходы выглядят как доступные всем для старых клиентов), они не могут тратить их выходы, так как они просто не знают, как это сделать: кошелек старого типа попытается потратить сегвитовский выход с пустой подписью, однако эта транзакция на самом деле не валидна (ноды, имеющие Segregated Witness, не пропустят такую транзакцию). Это значит, что отправитель должен знать, поддерживает ли кошелек получателя segwit или нет, чтобы создавать выходы нужного типа.
Начиная с BIP 143 выходы должны быть созданы с помощью хеша сжатого публичного ключа. Если вы создадите выход используя обычный адрес или несжатый публичный ключ, этот выход будет неиспользуемым.
Pay-to-Witness-Script-Hash
Следующим очень важным типом транзакции является P2SH — он позволяет отправлять транзакции на хеш скрипта вместо хеша публичного ключа (обычный адрес биткоина). Чтобы потратить выход P2SH транзакции нужно предоставить скрипт (его называют redeem script), хеш которого совпадает с хешем скрипта на который отправлены средства, а также подписи/пароли/что-то еще в зависимости от скрипта. Используя такой подход можно отправлять биткоины на адрес, защищенный способом, о котором нам ничего не известно, а также сильно экономить место — в случае, например, кошелька с мультиподписью (multisignature wallet) locking script был бы действительно большим, если бы мы хранили все "замки" полностью, а не только их хеш.
Итак, рассмотрим в качестве примера кошелек с мультиподписью, требующий наличия 2ух подписей из 5. В традиционном виде locking script выхода P2SH транзакции выглядит так:
HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e EQUAL
Чтобы потратить его, нужно предоставить redeem script, определяющий условие мультиподписи 2 из 5, а также 2 подписи и все это должно находится во входе транзакции:
[...]
"Vin" : [
"txid": "abcdef12345...",
"vout": 0,
"scriptSig": "<SigA> <SigB> <2 PubA PubB PubC PubD PubE 5 CHECKMULTISIG>",
]
Теперь давайте взглянем, как бы все это выглядело, если бы и отправитель и получатель использовали Segregated Witness.
Locking script выхода:
0 9592d601848d04b172905e0ddb0adde59f1590f1e553ffc81ddc4b0ed927dd73
Опять же, как и в случае с P2PKH транзакцией полученный скрипт намного проще. Здесь 1ое значение это номер версии, а второе — 32 байтный SHA256 хеш redeem script'a (witness program). Эта хеш-функция была выбрана для того, чтобы как-то отличать witness program P2WPKH от оной для P2WSH по длине хеша (32 байта SHA256 и 20 байт RIPEMD160(SHA256(script))).
Транзакция, использующая этот выход:
[...]
"Vin" : [
"txid": "abcdef12345...",
"vout": 0,
"scriptSig": "",
]
[...]
"witness": "<SigA> <SigB> <2 PubA PubB PubC PubD PubE 5 CHECKMULTISIG>"
[...]
Embedding Segregated Witness inside P2SH
Итак, мы убедились, что использование Segregated Witness имеет свои преимущества, однако для приведенных выше примеров и отправитель и получатель должны быть обновлены, что возможно далеко не всегда. Рассмотрим, например, такую ситуацию:
Алиса хочет отправить биткоинов Бобу, но у нее нет сегвит-кошелька, в то время как у Боба он есть. Конечно же, они могут просто использовать стандартную транзакцию, однако Боб хочет использовать segwit для сокращения комиссии.
В таком случае Боб может создать P2SH адрес, содержащий в себе segwit скрипт. Для Алисы он будет виден как самый обычный P2SH адрес и она сможет без каких либо проблем перевести на него средства, а Боб сможет потратить этот выход используя сегвит-транзакцию и получив скидку на комиссию (о новой метрике установления комиссии для сегвит транзакций мы расскажем ниже).
Таким образом оба типа сегвит-транзакций — P2WSH и P2WPKH могут быть реализованы внутри P2SH.
P2SH(P2WPKH)
Для использования P2WPKH транзакции внутри P2SH Бобу нужно создать witness program из своего публичного ключа. Затем результат нужно хешировать и преобразовать в P2SH адрес:
Создаем witness program:
0 ab68025513c3dbd2f7b92a94e0581f5d50f654e7
Как обычно — 1ое число это версия и 2ое это 20 байтный хеш публичного ключа. Полученный скрипт затем хешируется SHA256 и потом RIPEMD160, выдавая очередной 20 байтный хеш.
HASH160 от witness program P2WPKH:
3e0547268b3b19288b3adef9719ec8659f4b2b0b
И преобразовываем в адрес:
37Lx99uaGn5avKBxiW26HjedQE3LrDCZru
Locking script выхода отправленного на этот адрес будет выглядеть как скрипт для обычного P2SH адреса:
HASH160 3e0547268b3b19288b3adef9719ec8659f4b2b0b EQUAL
Теперь рассмотрим как Боб может потратить этот выход:
[...]
"Vin" : [
{
"txid": "8adbca5e652c68f8f3c30ac658115bc4af395d0cc7e6beaea18168295c29d011",
"vout": 0,
"scriptSig": "0 ab68025513c3dbd2f7b92a94e0581f5d50f654e7"
}
]
[...]
"witness": "<Witness data>"
[...]
Сначала предоставленный нами redeem script (в нашем случае это witness program) будет хеширован и, если он равен хешу, указанному в locking script'e, то он будет выполнен и будут проверены подписи в поле "witness".
P2SH(P2WSH)
Точно также любой P2WSH скрипт может быть реализован внутри P2SH. Возьмем multisig скрипт 2 из 5, рассмотренный ранее. Все шаги будут практически идентичны случаю P2SH(P2WPKH):
Для начала, опять же, создаем witness program:
0 9592d601848d04b172905e0ddb0adde59f1590f1e553ffc81ddc4b0ed927dd73
1ое число — версия, 2ое — 32 байтный SHA256 хеш нашего скрипта мультиподписи. Далее шаги повторяются — берем HASH160 от witness program и преобразовываем в обычный P2SH адрес. Для использования выхода, отправленного на этот адрес, в scriptSig нужно записать witness program, а все подписи и полный скрипт мультисига в поле witness.
Segregated witness benefits
Теперь, когда мы разобрались с технической частью, рассмотрим основные преимущеста segregated witness.
Transaction malleability
Одной из самых важных проблем, которые решает segwit является "изменяемость" транзакций, а если точнее, то их ID, являющиеся хешами. Разберемся немного подробнее.
В традиционном случае подписи, находящиеся внутри транзакции во входах, могут быть изменены третьей стороной без их инвалидации. Это позволяет изменять ID транзакции, являющийся ее хешем, не меняя никаких "фундаментальных" полей вроде входов/выходов/количества средств. Таким образом транзакция все еще валидна, однако теперь имеет другой ID, что создает возможность для разного рода атак, например denial-of-service.
Segwit решает эту проблему, т.к все подписи находятся снаружи транзакции, а значит не хешируются и их изменение никак не повлияет на ID транзакции. Также вводится отдельный идентификатор wtxid — он хеширует не только транзакцию но и всю witness часть, так что если транзакция передается без witness данных, то txid равен wtxid.
Решение данной проблемы позволяет создавать цепочки неподтвержденных транзакций без какого-либа риска — очень важное свойство для таких протоколов как Lightning Network.
Network and Storage Scaling
Witness данные зачастую составляют самую большую часть транзакции. В скриптах наподобие multisig'a они могут занимать до 75% места используемого транзакцией. Благодаря segwit'y передача подписей становится опциональной — нода запрашивает их только если собирается проводить валидацию транзакции. SPV (simple payment verification) клиенты или ноды, не поддерживающие сегвит, в таком случае могут не загружать лишние данные, экономя место на диске.
Block size increase and lower transaction fees
Segwit транзакции обходятся дешевле, нежели традиционные за счет скидки на хранение witness данных. Если быть точнее, то было изменено само понятие "размера" для segwit транзакций. Вместо обычного размера для них было введено понятие "виртуального размера" (virtual size) — все данные, хранящиеся в "witness", учитываются с коэффицентом в 0.25, что также позволяет разместить в блоке больше транзакций. Рассмотрим на примере.
Пусть у нас есть традиционная транзакция размером в 200 байт. В блок размера 1 МB поместится 5000 таких. Теперь возьмем ее segwit эквивалент, где примерно 120 байт это witness данные. Тогда ее vsize = 80 + 0.25 * 120 = 110 и теперь уже 9090 таких транзакций влезут в блок. Также при комиссии, допустим, в 40 satoshi/byte для 1ой транзакции мы получим комисси в 8000 сатоши, а для 2ой 4400 сатоши, что практически в два раза меньше.
Script Versioning
Как вы уже могли заметить, каждый locking script имеет байт, отвечающий за версию скрипта. Использование версий позволяет вносить дополнения и изменения (изменения в синтаксисе, новые операторы и тд.) в виде софт-форков.
Signature Verification Optimization
Segregated Witness также оптимизирует работу алгоритмов с подписями (CHECKSIG, CHECKMULTISIG и тд.). До segwit'a количество хеш-вычислений увеличивалось квадратично от количества подписей, в то время как в обновлении сложность алгоритма понижена до O(n).
So what is the problem?
Так если все так хорошо, в чем же проблема? Обновление имеет немало противников в сети, так как несмотря на все очевидные преимущества, оно имеет и свои слабые стороны. Рассмотрим некоторые из аргументов против.
- Так как segwit является софт-форком, обновлены будут далеко не все клиенты, а значит в сети будут находится одновременно два вида UTXO, и такие важные изменения как устранение уязвимости id транзакций и хеширование за линейное время не будут применены к не сегвитовским выходам, а значит сеть все еще будет уязвима к атакам, основанным на изменяемости id транзакций, а также к проблеме квадратичного времени хеширования.
- Segwit может уменьшить безопасность сети. Количество нод, проводящих полную валидацию, сильно уменьшится, так как только принявшие segwit смогут проверять witness часть транзакций.
- Segwit не может быть отменен. Если его отменить и откатить все изменения обратно, все segwit-выходы станут доступными каждому.
- Segwit пытается решить все проблемы сразу и, как следствие, огромное количество кода изменено. Это усложняет дальнейшую работу и увеличивает вероятность появления багов, которые будет сложнее устранить.
Conclusion
Несмотря на то, что, возможно, для проблем, решаемых SW, могут быть предоставлены более элегантные решения, мы считаем, что на данный момент это отличный способ повысить масштабируемость сети, а также сделать возможным внедрение таких технологий как Lightning Network, подробный разбор которой мы также проведем в следующих статьях.
References
"Mastering bitcoin" — Andreas M. Antonopoulos
MrBreath
Спасибо за статью
Многое разъяснили, однако некоторые вещи так и остались непонятны
Где собственно храниться поле witness?
В стороне от блокчена, на каждой ноде?
Т.е. используя SegWit итоговое количество хранщихся данных не уменьшаеться, но позволяет старым версиям хранить меньше информации в ущерб функциональности?
Так же интересно, что именно из себя представляет «witness data» для траты транзации.
Не ясно за счет чего достигается Signature Verification Optimization.
30mb1 Автор
Хорошие вопросы! Итак, по порядку:
1. Поле witness отделяется от транзакции. Вся witness-data хранится в отдельном merkle tree как и транзакции и является частью блока. Также стоит заметить, что witness merkle tree root не включается в заголовок блока — он содержится в выходе coinbase транзакции как дополнительные данные, после OP_RETURN.
2. Количество хранимых данных действительно уменьшается. Подписи нужны только для проведения полной валидации, поэтому, например, для новой ноды, которая только начинает синхронизировать блокчейн, отсутствует необходимость загружать witness данные для старых блоков, так как она не будет проверять их на валидность (проверять будут только некоторое число последних блоков).
30mb1 Автор
В продолжение предыдущего ответа:
3. Witness data для траты транзакции это тот же самый scriptSig (например, signature + public key в случае P2WPKH).
4. На данный момент для каждого входа транзакции проводится следующая процедура:
4.1. Все входы удаляются, это происходит за O(n).
4.2. На место текущего проверяемого входа вставляется subScript (это locking script выхода, который мы тратим).
4.3. Берется двойной SHA-256 хеш и сравнивается с предоставленной подпиьсю при помощи публичного для проверки корректности.
Так как имеем n входов, получаем итоговую сложность в O(n^2), что может вылиться в очень долгое время валидации (транзакция, время валидации которой составляет 25 сек. из-за большого кол-ва входов). Сегвит решает эту проблему изменяя способ хеширования транзакции таким образом, что каждый байт транзакции будет хеширован не более 2ух раз.