AtomicDEX “под капотом” или как на самом деле работают атомарные свопы?
В этой статье мы попытаемся рассмотреть как на самом деле работают атомарные свопы от 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.
Запускаем 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):
- Стороны “договариваются” (negotiate) о том, что они хотят совершить обмен RICK <-> MORTY, а также обмениваются всей необходимой для этого информацией. Maker придумывает секрет, 32-х байтное число, которое также входит в информацию, которой обмениваются участники сделки.
- Taker платит Trading Fee в MORTY, т.е. в той валюте которую он продает.
- Maker проверяет, что taker отправил trading fee и составляет Reedem Script для P2SH адреса с использованием хеша придуманного на первом шаге секрета RIPEMD-160(SHA-256(secret)). После чего отправляет RICK на этот P2SH адрес. Тут важно понимать, что на данный момент только Maker знает сам секрет, однако, хеш секрета является публичной информацией, которая известна Taker’у.
- Используя известный ему хеш секрета Taker также составляет Redeem Script и отправляет средства (MORTY) на получившийся с помощью него P2SH адрес. Шаблоны Reedem Script’ов на которые отправляют монеты Maker и Taker схожи, используется один и тот же известный всем хеш секрета, но разные timestamp’ы для возврата средств в случае неудачной сделки, а также разный порядок other_side_pubkey и our_pubkey в “шаблоне”.
- Как только Maker увидел, что средства были отправлены Taker’ом он забираем себе MORTY с адреса из предыдущего пункта и при этом разглашает значение самого секрета на блокчейне. Т.е. для расходования средств с составленного подобным образом P2SH адреса он должен предъявить секрет в поле ScriptSig транзакции расходования.
- Ну и последний шаг, когда 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 .
Полезные ссылки:
- Market Maker 2: The Engine That Powers The AtomicDEX Protocol
- How To Become a Liquidity Provider on AtomicDEX
- Скачать AtomicDEX для Android из Google Play
- Скачать AtomicDEX для iOS (Test Flight)