AtomicDEX “под капотом” или как на самом деле работают атомарные свопы?

Decker
7 min readOct 12, 2019

В этой статье мы попытаемся рассмотреть как на самом деле работают атомарные свопы от Komodo, а именно проведем некий “reverse engineering” протокола обмена на основе данных блокчейна, чтобы понять что именно происходит в AtomicDEX “под капотом”. Забегая вперед, скажу что на эту тему есть довольно неплохая статья на английском Market Maker 2: The Engine That Powers The AtomicDEX Protocol, мы к ней обязательно еще вернемся, ну а пока просто предположим что мы установили приложение AtomicDEX из Google Play и хотим разобраться как это работает и насколько это безопасно.

Для определенности будем считать что у нас есть 200 монет ассета MORTY и мы хотим обменять его на ассет RICK по выгодному для нас курсу. RICK и MORTY являются тестовыми ассетами, предназначенными как раз для тестирования atomic swaps, если кто-то вдруг захочет повторить мой эксперимент или же просто поэкспериментировать с AtomicDEX, то получить их можно из faucet (крана) непосредственно в приложении или на https://www.atomicexplorer.com в разделе faucet.

Выбираем подходящий ордер и нажимаем кнопку Trade

Запускаем AtomicDEX, выбираем тот актив, который мы хотим продать, в нашем случае это MORTY, указываем количество - 200, находим подходящий ордер (я взял первый из списка с ценой 1) и видим что за 200 MORTY при курсе обмена 1 RICK = 1 MORTY мы получим 200 RICK. Также мы видим что AtomicDEX оценивает transaction fee как 0.0002 MORTY и trading fee, как 0.25740026 MORTY. После нажатия кнопки Trade и Confirm (обратите внимание, что нажатие кнопки Confirm запускает процесс обмена, прервать который уже невозможно) процесс обмена запущен.

И первая (1) транзакция которую мы видим, это - 6c07d32957fe9c8a3416dac1b43bfca9e6a7c891c0fe25150316685c8bfa69fa в блокчейне MORTY. Здесь с нашего адреса RTCVGuoSNehKG8YYxcoskC7LK1yZhgvQRV отправляется trading fee 0.25740025 на адрес RThtXup6Zo7LZAi8kRWgjAyi1s4u6U9Cpf.

Следующая транзакция (2) которая уходит с нашего адреса - это cd0d3640e141b927ff331aa1b536f973d8a5a4ac482e510973ae1526bd92ef40 , перевод в блокчейне MORTY 200 монет с нашего адреса на некий P2SH адрес - bGhuTK6b86fLp71k33SGg9kzW6uGGfpk7T. На данный момент мы ничего не знаем ни о том как этот адрес получился, ни о том каким образом наше приложение узнало о том, что монеты нужно перевести именно туда, вообщем пока никакой дополнительной информации нет. Однако, уже сейчас мы можем предположить два факта - другая сторона сделки должна будет получить монеты с этого адреса, т.е. в будущем должна быть транзакция с адреса bGhuTK6b86fLp71k33SGg9kzW6uGGfpk7T на адрес получателя, мы должны каким-то образом получить свои монеты RIСK на адрес RTCVGuoSNehKG8YYxcoskC7LK1yZhgvQRV. Поэтому дальнейшие наши действия - это просто мониторинг нашего адреса и адреса на который мы отправили MORTY. Посмотрим что будет дальше. Да, единственное, я буду помечать время каждой транзакции, чтобы была понятна последовательность в которой они происходили в реальности. Это поможет нам восстановить всю картину после завершения обмена. Так, транзакция (1) c trading fee была Oct 10, 2019 1:00:18 AM, а вторая (2) транзакция Oct 10, 2019 1:03:42 AM .

Далее мы видим что 200 MORTY с адреса bGhuTK6b86fLp71k33SGg9kzW6uGGfpk7T уходят на адрес R9ViegsR8qrthx81NJdANfnkLoQxomzHAM в db79a1b1fb17dce748dc14d82771644317464923329dc089b05e28434c6191a9 , это у нас получается (3) и произошла она Oct 10, 2019 1:05:56 AM.

Взглянем на эту транзакцию чуть более подробно, а именно посмотрим в поле scriptSig, чтобы понять каким был reedem script для разблокирования средств (т.е. какой скрипт и какие подписи, pubkey’и использовались для перевода bGhuTK6b86fLp71k33SGg9kzW6uGGfpk7T -> R9ViegsR8qrthx81NJdANfnkLoQxomzHAM). Здесь мы видим:

  • 30450221..5918cf00fd - sig (подпись)
  • [ALL]- флаг SIGHASH, указывающий на то, что подпись применяется ко всем фрагментам входных и выходных данных.
  • 3c9f8118cb83beef7d375ffcf956522c6ee4f37ee5f81b7668d5476473a0e3cd - некое 32-х байтное число.
  • 0
  • 63049b769e5db175210301b2324698f3dacd6e076be09f06b58e5462d77e902c3304c078f6d6a367df97ac6782012088a914d2d3b0dde21244cd5a3e7d518c1697b708037169882102b86508ab996ca87d863c907e44fe495d96adf7e95ad0823bac16c3e55758f902ac68 - скрипт.

Давайте декодируем его:

Или можно вручную, например так:

  • 0x63 - OP_IF (условие)
  • 0x04 - OP_PUSHDATA (поместить в стек следующие 4 байта)
  • 0x9B, 0x76, 0x9e, 0x5D - число 0x5D9E769B или 1570666139 в десятичной системе.
  • 0xB1 - OP_CHECKLOCKTIMEVERIFY (OP_CLTV, ранее OP_NOP2)- помечает транзакцию как некорректную, если значение элемента на вершине стека больше, чем значение поля транзакции nLockTime, иначе скрипт продолжает выполняться, как если бы был выполнен оператор OP_NOP (к слову, у рассматриваемой транзакции поле nLockTime установлено в 1570655046).
  • 0x75 - OP_DROP (извлекает верхний элемент стека)
  • 0x21- OP_PUSHDATA (поместить в стек следующие 33 байта)
  • 0301b2324698f3dacd6e076be09f06b58e5462d77e902c3304c078f6d6a367df97 - 33 байта, pubkey нашего адреса RTCVGuoSNehKG8YYxcoskC7LK1yZhgvQRV.

Ну и т.д., здесь отметим что 02b86508ab996ca87d863c907e44fe495d96adf7e95ad0823bac16c3e55758f902 - это pubkey адреса R9ViegsR8qrthx81NJdANfnkLoQxomzHAM .

Глядя на структуру этого скрипта мы понимаем, что монеты MORTY с P2SH адреса bGhuTK6b86fLp71k33SGg9kzW6uGGfpk7T может забрать либо сам отправитель RTCVGuoSNehKG8YYxcoskC7LK1yZhgvQRV, но не ранее момента времени 1570666139 (unix timestamp), либо вторая сторона участвующая в обмене при предъявлении валидного секрета. В данном случае, как мы видим, владелец адреса R9ViegsR8qrthx81NJdANfnkLoQxomzHAM предъявил секрет 3c9f8118cb83beef7d375ffcf956522c6ee4f37ee5f81b7668d5476473a0e3cd и он валиден, т.к.:

  • SHA-256 hash(secret)=8f04adae7f3feaa5fbae414dc1e853cab1d73bb06507e5a18e699b9515edb66b
  • RIPEMD-160 Hash(SHA-256 hash(secret)) = d2d3b0dde21244cd5a3e7d518c1697b708037169

Убедиться в этом можно выполнив следующий несложный код:

Итак, отлично, мы выяснили что скрипт с помощью которого другая сторона обмена получила свои монеты выглядит следующим образом:

OP_IF timestamp OP_CTLV OP_DROP our_pubkey OP_CHECKSIG OP_ELSE OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 hash_of_secret OP_EQUALVERIFY other_side_pubkey OP_CHECKSIG OP_ENDIF

Где hash_of_secret = RIPEMD-160(SHA-256(secret)) и получить она их смогла только благодаря тому что уже обладала секретом. Давайте теперь посмотрим что происходило на нашей стороне, а именно на то, как мы получали свои монеты RICK.

Мы получили наши RICK абсолютно с другого P2SH адреса bVa4Pn4uwFw4DXd7UT6oMB515cKTiExL61 в (4) f3ba65d485a344128a276313468135d687298ed5090f109f83de3a1810c64249 - Oct 10, 2019 1:06:22 AM.

Если посмотреть подробнее, то мы забрали наш RICK используя следующий скрипт:

OP_IF timestamp OP_CTLV OP_DROP other_side_pubkey OP_CHECKSIG OP_ELSE OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 hash_of_secret OP_EQUALVERIFY our_pubkey OP_CHECKSIG OP_ENDIF

И тот же секрет 3c9f8118cb83beef7d375ffcf956522c6ee4f37ee5f81b7668d5476473a0e3cd .

А отправлены они были на него в (5) be2cbb9089fc36e0aaceb52023d6a1e7e30603a10d1dbd5475b5048cac705a69 - Oct 10, 2019 1:00:55 AM c адреса R9ViegsR8qrthx81NJdANfnkLoQxomzHAM .

Восстановив хронологическую последовательность транзакций получаем следующее:

  • (1) Oct 10, 2019 1:00:18 AM - мы отправляем trading fee в MORTY на адрес RThtXup6Zo7LZAi8kRWgjAyi1s4u6U9Cpf .
  • (5) Oct 10, 2019 1:00:55 AM - другая сторона обмена отправляет RICK на P2SH адрес bVa4Pn4uwFw4DXd7UT6oMB515cKTiExL61.
  • (2) Oct 10, 2019 1:03:42 AM - мы отправляем MORTY на P2SH адрес bGhuTK6b86fLp71k33SGg9kzW6uGGfpk7T (Taker Payment ID)
  • (3) Oct 10, 2019 1:05:56 AM - другая сторона забираем MORTY с адреса bGhuTK6b86fLp71k33SGg9kzW6uGGfpk7T.
  • (4) Oct 10, 2019 1:06:22 AM - мы забираем наш RICK с адреса bVa4Pn4uwFw4DXd7UT6oMB515cKTiExL61 (Maker Payment ID)

Итого, вышеописанная схема работает следующим образом (в данном случае мы Taker, а та сторона чей уже существующий ордер мы выбрали Maker):

  1. Стороны “договариваются” (negotiate) о том, что они хотят совершить обмен RICK <-> MORTY, а также обмениваются всей необходимой для этого информацией. Maker придумывает секрет, 32-х байтное число, которое также входит в информацию, которой обмениваются участники сделки.
  2. Taker платит Trading Fee в MORTY, т.е. в той валюте которую он продает.
  3. Maker проверяет, что taker отправил trading fee и составляет Reedem Script для P2SH адреса с использованием хеша придуманного на первом шаге секрета RIPEMD-160(SHA-256(secret)). После чего отправляет RICK на этот P2SH адрес. Тут важно понимать, что на данный момент только Maker знает сам секрет, однако, хеш секрета является публичной информацией, которая известна Taker’у.
  4. Используя известный ему хеш секрета Taker также составляет Redeem Script и отправляет средства (MORTY) на получившийся с помощью него P2SH адрес. Шаблоны Reedem Script’ов на которые отправляют монеты Maker и Taker схожи, используется один и тот же известный всем хеш секрета, но разные timestamp’ы для возврата средств в случае неудачной сделки, а также разный порядок other_side_pubkey и our_pubkey в “шаблоне”.
  5. Как только Maker увидел, что средства были отправлены Taker’ом он забираем себе MORTY с адреса из предыдущего пункта и при этом разглашает значение самого секрета на блокчейне. Т.е. для расходования средств с составленного подобным образом P2SH адреса он должен предъявить секрет в поле ScriptSig транзакции расходования.
  6. Ну и последний шаг, когда Taker увидел что Maker забрал его средства, он видит использовавшийся для этого секрет в блокчейне MORTY и использует тот же самый секрет для перевода себе RICK.

Вышеописанную схему можно увидеть и в исходниках ядра AtomicDEX, т.е. в MM2.0 (marketmaker): https://github.com/KomodoPlatform/atomicDEX-API/blob/92b9bae77cd02cd074f0353fc61e8d8bab72541e/mm2src/lp_swap.rs#L22

Здесь:

  • Alice = Buyer = Liquidity receiver = Taker
  • Bob = Seller = Liquidity provider = Market maker

Т.е. фактически в нашем swap’е мы выступали в роли Alice (покупателя) и покупали RICK за MORTY, а другая сторона была Bob’ом (продавцом) и продавала нам RICK за MORTY.

Обратите внимание, что безопасность сделки в данном случае обеспечивается самим протоколом обмена и достигается за счет используемых для разблокировки средств скриптов на блокчейне. Так, например, maker может забрать MORTY, которые мы ему отправили только в одном случае - разгласив секрет, с помощью которого “заблокированы” RICK, которые он отправил нам. Как только Maker забирает наши MORTY, мы получаем значение секрета для того, чтобы забрать RICK. При этом Maker не может сам взять и забрать свои RICK раньше наступления момента времени 1570673940 (10-Oct-19 02:19:00 UTC), а Taker не может забрать свои MORTY раньше 1570666139 (10-Oct-19 00:08:59 UTC). Время на которое блокируются средства у Maker’а в 2 раза больше, чем время на которое блокируются средства у Taker’а. Обмануть другого участника сделки не получится ни у одной из сторон, при условии что другая сторона заинтересована в строгом соблюдении протокола.

Ну и еще немного из исходников:

  • Функция для составления redeem script’а называется payment_script, вторым параметром у нее как раз идет тот самый secret_hash, который известен и maker’у (т.к. он придумал secret) и taker’у (он получает его на этапе предварительных договоренностей об обмене).
  • Trading Fee отправляется по фиксированному адресу, соответствующему pubkey 03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc06 , что в сети KMD (и ассетов) как раз соответствует адресу RThtXup6Zo7LZAi8kRWgjAyi1s4u6U9Cpf , ну а в сети Bitcoin - 1KRhTPvoxyJmVALwHFXZdeeWFbcJSbkFPu .

Полезные ссылки:

--

--