Биткоин: транзакции, пластичность, SegWit и масштабируемость

Dmitry Laptev
6 min readAug 24, 2017

This article in English.

Итак, сообщество биткоина добилось своего: майнеры согласились на SegWit и он таки активировался сегодня в сети. Паника немного улеглась, цена подросла… Пришла пора разобраться, что же такое этот SegWit, почему все о нем говорят, и зачем он нужен.

Дисклеймер. Я сознательно опускаю/упрощаю некоторые технические моменты. А еще я не эксперт, и могу банально ошибаться.

Segregated Witness (SegWit) — это собирательное название для нескольких изменений в протоколе биткоин. Многие из этих изменений заслуживают отдельного внимания, но я буду фокусироваться на первом и главном: изменение того, как считается идентификатор транзакции.

Хотя в последнее время все говорят о SegWit только в контексте споров о масштабировании, изначальное предложение решало совсем другую проблему: transaction malleability, или, как принято это переводить на русский, пластичность транзакций.

Но обо всем по порядку: что такое транзакция?

Транзакция

Для простоты будем рассматривать транзакцию с адреса A на адрес B, с одним входом и одним выходом. Пример такой транзакции:

Input:
Previous tx: f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6
Index: 0
scriptSig: 304502206e21798a42fae0e854281abd38bacd1aeed3ee3738d9e1446618c4571d1090db022100e2ac980643b0b82c0e88ffdfec6b64e3e6ba35e7ba5fdd7d5d6cc8d25c6b241501
Output:
Value: 5000000000
scriptPubKey: OP_DUP OP_HASH160 404371705fa9bd789a2fcd52d2c580b65d35549d OP_EQUALVERIFY OP_CHECKSIG
  • Previous tx — идентификатор предыдущей транзакции на адрес A;
  • Index — номер входа (в этом примере один вход с номером 0);
  • scriptSig — первая часть проверяющего скрипта (об этом ниже);
  • Value — количество биткоинов, указанное в сатоши (один биткоин = 100 миллионов сатоши) — 50 биткоинов в примере;
  • scriptPubKey — вторая часть проверяющего скрипта, которая также содержит адрес получателя B.

Транзакции всегда выстраиваются в цепочку (на самом деле в дерево, но это детали). Чтобы потратить биткоины с адреса A, нужно потратить выход каких-то транзакций на адрес A (за исключением coinbase транзакций, у которых ноль входов, но это тоже детали).

Чтобы проверить, что мы можем тратить выход предыдущей транзакции, мы должны:

  1. Соединить scriptSig новой транзакции со scriptPubKey предыдущей транзакции,
  2. Проверить, что получившийся скрипт (на специальном языке программирования Script) выполняется и возвращает True.

В самом простом случае (как в примере выше) scriptSig содержит подпись транзакции приватным ключем, а scriptPubKey просто проверяет совпадение публичного ключа и валидность подписи для данного адреса (основная операция: OP_CHECKSIG).

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

Пластичность транзакций

Что же конкретно содержится в поле Previous tx, спросите вы? Что это за идентификатор транзакции (transaction identifier, txid)?

txid — это sha256d хэш от всех полей данных транзакции.

Ключевым моментом является зависимость от scriptSig. Потому что scriptSig сама по себе как правило содержит данные о транзакции подписанные приватным ключем, включая txid. scriptSig зависит от txid, поэтому txid не может зависеть от scriptSig.

Любой держатель полной ноды биткоин (не только майнер) помогает собирать и распространять по сети данные о транзакциях. В процессе он может изменить scriptSig таким образом, чтобы скрипт подписи оставался корректным, результат транзакции оставался тем же, но txid поменялся.

Например, к любой подписи можно добавить операцию OP_NOP (не делает ничего), или для изощренности добавить две операции OP_DUP OP_DROP (первая дублирует подпись на стеке, а вторая удаляет верхний элемент стека). Подпись останется корректной, но txid поменяется.

И вот тут-то кроется проблема. Точнее две.

1. txid — плохой идентификатор

Такая изменяемость идентификатора не позволяет использовать txid по назначению: как идентификатор. Злоумышленник может перехватить нормальную транзакцию и распространить модифицированную версию по сети. С некоторой вероятностью майнеры могут включить в блок модифицированную транзакцию вместо изначальной.

Зачем это нужно злоумышленнику, спросите вы? В идеальном мире — особо незачем. Но в реальном мире люди любят использовать идентификаторы для уникальной идентификации. Это и произошло (по одной из версий) с биржей MtGox.

Злоумышленник выводил биткоины с этой печально известной биржи, перехватывал транзакцию, менял ей txid. Транзакция оставалась валидной, злоумышленник получал свои биткоины. Но биржа видела, что изначальный txid не вошел в блок и не уменьшала баланс злоумышленника.

2. Цепочки транзакций в одном блоке

Есть еще и вторая проблема. А точнее упущенная возможность.

Теоретически биткоин позволяет тратить выход транзакций, которые еще не были включены в блок. Я могу перевести средства с адреса A на адрес B. А потом, не дожидаясь подтверждения первой транзакции, перевести биткоины с адреса B на адрес C. И обе эти транзакции могли бы быть проверены и одновременно включены в блок, если бы не пластичность.

Если txid первой транзакции может измениться, значит Previous tx второй транзакции может измениться, а значит и подпись может измениться. А значит вторую транзакцию проверить нельзя, пока первая не будет включена в блок (тогда ее txid будет зафиксирован).

И это проблема больше, чем кажется на первый взгляд, но об этом в самом конце.

Segregated Witness

На самом деле теперь все становится очень просто.

SegWit предлагает: давайте вынесем всю пластичную информацию из транзакции отдельно (witness data), а txid будем считать без нее. Идентификатор станет уникальным, проблемы будут решены.

Транзакция до и после. scriptSig теперь всегда пуст, данные перемещаются в отдельную структуру “Witness data”. Все остальное остается также: txid и размер транзакции вычисляются из тех же полей, что и раньше.

Элементарное решение, не правда ли? Но оно меняет определение транзакции (которое почти не менялось с самого начала), меняет механизмы валидации, требует переписать огромное количество очень стабильного кода. А еще неплохо было бы подумать про сохранение совместимости.

Поэтому (а также из-за политических игр, про которые я не буду здесь рассказывать) принятие SegWit заняло так долго.

При чем здесь масштабирование?

Оказывается, этот фикс имеет огромное влияние на масштабирование биткоина. По двум причинам.

1. Больше транзакций в блоке

Посмотрите еще раз на пример транзакции выше по тексту. Больше половины информации в ней — это scriptSig. Вынося эту информацию отдельно, мы по сути снижаем размер транзакции. И эффективно увеличиваем размер блока.

Конечно, это читерство, потому что подпись мы тоже будем хранить, хоть и отдельно. Но при вычислении размера блока эти данные не учитываются. Почему не учитываются? Не вдаваясь в детали, потому что только так можно было реализовать SegWit через soft fork. Чуть больше про форки.

Теоретически, в блок можно будет впихнуть в четыре раза больше транзакций. Практически — примерно в два раза больше. Вполне себе ощутимое масштабирование.

2. Транзакции вне блокчейна и lightning network

На данный момент абсолютно все транзакции хранятся в блокчейне. Тысячи компьютеров по всему миру хранят информацию о том, что я купил кофе за 0.0015 биткоина. Это дорого для узлов сети. Это дорого и долго для меня: каждый раз мне нужно ждать 10 минут подтверждения транзакции за кофе.

Один из вариантов решения этой проблемы — проводить небольшие транзакции вне основного блокчейна. И время от времени синхронизировать баланс на главном блокчейне. Общее название этого подхода — second layer networks (сети второго уровня).

Одним из рабочих решений в этом стиле на данный момент считается Lightning network. Работает это примерно так:

  • два человека открывают между собой канал микроплатежей, у обоих блокируется по некоторой сумме;
  • они совершают много небольших транзакций между собой;
  • честное поведение гарантируется за счет умной криптографии, конфликты разрешаются из заблокированных средств;
  • как только канал закрывается — на главном блокчейне появляется одна транзакция, которая синхронизирует балансы людей, замороженные суммы возвращаются.

Настоящая сила каналов проявляется, когда их много. Каналы формируют ребра в графе людей. Если между двумя людьми есть путь, даже через несколько каналов — они могут совершать транзакции.

Причем тут SegWit, спросите вы? Он действительно не обязателен, но делает имплементацию намного удобнее.

Не углубляясь в детали… Для работы канала микроплатежей необходимо создать транзакцию подписанную двумя сторонами (double-signed transaction). В самом начале на адрес с двумя ключами отправляются средства. Но чтобы предотвратить мошенничество, double-signed transaction должна быть подписана до того, как туда переведут деньги.

Но чтобы провернуться такое, необходимо собрать выходы транзакций, которые еще не были подтверждены на главном блокчейне. А это как раз тот сценарий, который на данный момент невозможен из-за пластичности транзакций (смотрите пункт “2. Цепочки транзакций в одном блоке” выше по тексту). И тут-то SegWit приходит на помощь.

Заключение

TL;DR. SegWit активирован. На самом деле он решает проблему пластичности транзакций, но еще оказывается полезен для масштабирования. Краткосрочно он позволит включать больше транзакций в блоки. Долгосрочно — открывает новые возможности масштабирования через off-chain транзакции. С нетерпением ждем, что из этого получится.

Комментируйте, критикуйте, задавайте вопросы, подписывайтесь! Больше интересных статей в телеграме: https://t.me/cryptohodl. Больше обо мне: http://laptev.ch.

--

--