Как тестировать перемещение средств пользователя на криптобирже

Kate

Administrator
Команда форума
Всем привет! Меня зовут Oleksandr Pelykh, и я работаю в роли QA уже почти 7 лет. В этой статье поделюсь своим небольшим опытом тестирования криптовалютной биржи. А именно части, касающейся переводов криптовалюты, её хранения и все такое. Несмотря на то, что рассматривается отдельный модуль, в материале также будет базовая информации о принципах построения криптобиржи.

Статья нацелена на тех, кто уже обладает базовыми знаниями о криптовалюте и имеет опыт работы с ней. Хотя по тексту есть много гиперссылок с подробным объяснением рассматриваемых понятий.

И предупреждаю, это очень лонгрид.

👨 Почему я?​

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

  • здесь поверхностно описан процесс тестирования всей криптобиржи (с акцентом на безопасность), можно взять эту статью за основу;
  • здесь общий чек-лист, в котором уже больше технических нюансов;
  • материал, похожий на первый, но на английском;
  • это качественная статья от ребят из Hacken, в основном тоже касающаяся безопасности; это лучший материал, который я нашел по этой теме, к тому же пункты чек-листа кликабельны и поведут вас сразу на страницы с описанием того, как тестировать конкретные кейсы.
Я решил дополнить эти статьи своим опытом. Здесь будут диаграммы, код и чек-листы (их подобие). В основном внимание будет акцентироваться на сложностях, нюансах, проблемах, особенностях, с которыми вам придется столкнуться при работе с блокчейном. Контент включает как ручное тестирование, так и автоматизацию с примерами кода.

Если вы жмете на кликабельный текст и видите 404, 503 или что-то, кроме нужного контента — это, скорее всего, проблема ресурса, на который идет редирект. Ничто не вечно. Напишите в коммент, и я обновлю ссылку в статье.

Примечание: здесь вы не найдете того, как разрабатывать/тестировать сам блокчейн. Это отдельная огромная тема. Мы рассматриваем только тестирование криптобиржи — некой системы, построенной на базе блокчейна. Так что, если в ваших целях разобраться с разработкой/тестированием своего блокчейна — лучше поискать более подходящий теме материал. Ну а тем, кто остался читать дальше, добро пожаловать в криптовалютный мир!

📝 План​

О чем я расскажу:

  • что тестировать (чек-лист);
  • где тестировать (окружения);
  • как тестировать (примеры кода).
Первый пункт сильно пересекается с принципами построения криптобиржи. Так что мой чек-лист будет создаваться именно в процессе анализа данных принципов. Конкретно будут затронуты следующие темы:

  • процесс депозита (пополнения счета);
  • процесс вывода средств с биржи.
И следующие два блока, касающиеся предыдущих тем:

  • вопросы, которые нужно задать;
  • проблемы, с которыми предстоит столкнуться.
Анализируя эти сущности, я постепенно буду формировать список необходимых проверок (читай чек-лист).

Scope​

Для наглядности рассмотрим схему ниже:

image2_z7GWwDv.jpg


Здесь мы видим процессы депозита и вывода средств (слева), горячий и холодный кошельки (справа, обведены желтым). Остальной функционал (логин, регистрация, трейдинг и все такое) не рассматривается (обведен красным). Позже мы эту схему разберем подробнее.

Поехали.

🤔 Что тестировать​

Что вообще такое криптовалютная биржа, можно почитать здесь.

Для адекватного восприятия статьи вы должны понимать значения следующих понятий: блокчейн, криптовалюта, биткоин (биток, BTC), майнинг, транзакция, публичный/приватный ключи, горячий кошелек, холодный кошелек.

Кроме того, нужно понимать разницу между coin и token, знать, кто такой Виталик Бутерин, что такое POW, что считают майнеры (кроме заработанных денег) и сколько будет стоить биткоин через год.

Перейдем, наконец, к первому большому подразделу — «Депозит».

💰 Депозит​

Для начала вспомним, как это работает в банке. Вы приходите в банк, подписываете какие-то бумажки. Вам открывают счет в определенной валюте (или нескольких), и с этого момента вы можете пополнять свой счет по его номеру (в последнее время это IBAN).

Под «депозитом» в данной статье имеется ввиду пополнение счета, а не банковский депозит (подразумевающий хранение денег и начисление процентов на них).

Процесс депозита на криптобирже похож на вышеописанный. Пользователь регистрируется → ему создается счет (криптовалютный кошелек) → пользователь пополняет этот счет по адресу (публичному ключу).

image6_aB0iUkH.jpg


Примечание: Описанный в данной статье процесс депозита рассматривает только пополнение счета в валюте крипты. Пополнение «фиатными» деньгами происходит через платежные сервисы и не требует обработки в блокчейне, так что работа с фиатом рассматриваться не будет.

❓ Какие вопросы необходимо задать при тестировании депозита​

Когда создавать пользователю кошелек для депозита?​

Давайте начнем с предположений. Первое предположение — создавать кошелек сразу после регистрации. И это плохая идея. Потому что: 1) придется генерировать кошелек для каждой валюты, а их много; 2) пользователь после регистрации может не пополнить счет. Зачем же тогда ему все эти кошельки?

Поэтому перейдем к следующему предположению — создавать кошелек после нажатия на кнопку «Депозит». Этим действием пользователь показывает свое намерение внести средства. Важное условие, что внести средства юзер хочет в конкретной валюте (например, биткоинах). Так что процесс пополнения счета должен включать выбор этой валюты.

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

А почему бы не использовать один кошелек для всех пользователей?​

Так происходило до недавних пор (пока лавочку не прикрыли). Вы переводите деньги на счет, говорите продавцу товара время и сумму, таким образом доказываете, что автор перевода — вы.

image33_jE82XBL.png



Но в мире блокчейна такое зачастую невозможно. Ведь все транзакции публичны. Невозможность определить автора транзакции вынуждает нас сделать данное предположение (использовать один кошелек для всех пользователей) неверным.
Но! Для некоторых валют такой подход используется. Об этом позже.

Когда еще нужно создавать пользователю кошелек?​

Ситуация следующая: пользователь успешно пополнил свой BTC-счет, после этого совершил торговую операцию, продав биткоины и купив на них эфириум (продал BTC → получил ETH).

Предположение: нужно создать пользователю кошелек с ETH, чтобы хранить купленный им эфириум.

Это неверно, потому что транзакции и хранение средств пользователей происходят на внутренних счетах биржи. (Почему? Отвечу дальше в статье.)

Идем дальше по user flow — пользователь хочет вывести ETH. Теперь точно нужно создать ему кошелек!
Опять нет. Вывод средств осуществляется на личный кошелек пользователя. Если его сгенерирует биржа, то она и будет иметь доступ к средствам на этом кошельке.

Вывод: создание кошельков для пользователей необходимо только при внесении средств на биржу извне, то есть только при депозите (пополнении счета) в крипте.

Окей, с этим разобрались. Теперь попробуем ответить на следующий вопрос.

Что делать со средствами юзера после депозита?​

  • переводить сразу на горячий кошелек;
  • хранить некоторое время и скидывать на горячий кошелек после: X пополнений, если баланс превышает Y, спустя Z время.
Наша схема с депозитом немного дополняется:

image8_c12URlM.jpg



Стоп! А зачем вообще переводить средства на какой-то горячий кошелек? Для начала нужно понимать предназначение и разницу между горячим и холодным криптокошельками. На эту тему есть масса статей.
Если в двух словах: средства на горячем кошельке активно используются, даже очень. А на холодном просто хранятся. Главное требование к горячему кошельку — простота и скорость использования, к холодному — надежность.

Почему же после депозита просто не оставить средства пользователя на кошельке, который мы для него сгенерировали и который им был пополнен?

Давайте представим ситуацию. У нас четыре пользователя. Три из них пополнили свои счета в BTC на 0.1, 0.2 и 0.3 биткоина соответственно. А четвертый пользователь запросил вывод 0.5 BTC. Нам нужно теперь агрегировать с имеющихся трех кошельков куш в 0.5 биткоина и перевести пользователю № 4. А еще нужно запомнить, сколько же средств осталось на кошельках пользователей № 1, № 2 и № 3, чтобы при следующем чьём-то withdrawal-запросе понимать, откуда собирать остатки «битков». По сути, роль такого кошелька-агрегатора и играет горячий кошелек. К тому же, когда у нас не четыре, а 400 тысяч пользователей, да еще и сотни валют, необходимость в кошельке такого типа (или даже нескольких для каждой из валют) становится явной.

Какая минимальная сумма депозита?​

Важно её определить для каждой валюты. И понимать, что мы будем делать, если пользователь пополнил кошелек на меньшую сумму.

Как вариант, можно:

  • игнорировать пополнение,
  • выводить сообщение, что сумма недостаточна.
А теперь вернемся немного назад...
1XRGM6L.jpg

и попробуем понять следующее:

Как определить, что кошелек пользователя был пополнен?​

То есть зашел пользователь в раздел «Депозит» на сайте биржи, выбрал валюту, сгенерировали мы ему кошелек. Как понять, что он этот кошелек пополнил? Этот вопрос сложный, и однозначного ответа на него нет. Но накидаю несколько вариантов решения этой задачи:

1. Проверять в блокчейне все кошельки пользователей по всем валютам с определенной периодичностью (например, 5 минут):

➕ Это работает
➖ Очень затратно по ресурсам

2. Проверять баланс кошелька пользователя определенное время после того, как кошелек был сгенерирован

➕ Экономнее в плане ресурсов
➖ Работает только для первого депозита (а пользователь может пополнить счет как через 5 минут, так и через неделю после того, как мы сгенерировали ему кошелек)

3. Добавить кнопку «Я пополнил, проверьте баланс моего кошелька»

➕ Очень экономно (проверяем только те кошельки, которые потенциально были только что пополнены)
➖ Дополнительное действие от пользователя

4. Проверять транзакции в каждом новом блоке (а точнее список получателей в этих транзакциях) и обнаруживать, есть ли в этом списке адреса кошельков пользователей биржи

➕ Относительно эффективно в плане ресурсов
➖ Будут проверяться и те кошельки, которые никогда не пополняются

Теперь рассмотрим процесс вывода средств.

🏦 Вывод средств​

Дополним уже существующую схему. Новый флоу — перевод средств с горячего кошелька на пользовательский (личный). Это и есть процесс вывода средств (withdrawal).
image28_wZgPUXV.jpg


Если до этого момента не стало очевидно, то акцентирую ваше внимание на следующем — важно различать такие виды кошельков:

  • Кошелек пользователя для депозита — пополняется при депозите. Приватный ключ принадлежит бирже.
    Ppc7gdT.jpg
  • Личный кошелек пользователя. На него производится вывод средств. Приватный ключ принадлежит пользователю.
    yIuNxxV.jpg
Вернемся к вопросам, на которые необходимо найти ответы.

❓ На какие вопросы нужно найти ответы​

Как часто юзер может выводить средства? Минимальная сумма вывода?​

Вопросы связаны между собой, одно влияет на другое. Зачастую в плане комиссии все тривиально и всё оплачивается пользователем.

Есть еще один момент, на который биржи обращают внимание: чем меньше нагрузка на блокчейн, тем меньше стоимость транзакции. Таким образом, биржи могут влиять на стоимость транзакции в сети за счет уменьшения количества самих транзакций. Достигается это увеличением минимальной суммы вывода и/или ограничением на количество выводов за единицу времени.

Например, пользователь может сделать не больше одного запроса на вывод BTC в день. Или минимальная сумма для вывода — 0.001 BTC.

Величина комиссии​

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

  • hardcoded (основываясь на исторических данных);
  • динамическая и определяется: 1) в момент создания withdrawal запроса; 2) в момент создания транзакции в сети.

🗿 Подводные камни​

В этом разделе рассмотрим проблемные моменты, на которые нужно обратить внимание при тестировании депозита и вывода средств.

Double-spending attack​

Подробно о Double-spending можно почитать здесь. Если кратко, один и тот же биткоин вы можете отправить на два разных адреса. При этом оба получателя будут видеть, что биткоин «летит» к ним. Но «прилетит» он в итоге только одному.

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

Посмотрим на график ниже (он для BTC). По вертикальной шкале — вероятность осуществления вышеупомянутой double spending attack, по горизонтальной — время (в минутах).

image30_V7ubkZ4.png

Источник
Видим, что спустя час (за это время в сети биткоина успеет сгенерироваться шесть блоков) вероятность атаки стремится к нулю.

В таблице ниже приведены количество необходимых подтверждений и среднее время их достижения для нескольких популярных криптовалют.

image13_0pQ5rxB.png



Некоторые валюты не требуют подтверждений​

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

image18_0hKFbFp.jpg


Суммы на вашем счету недостаточно для совершения транзакции​

Проблема возникает в момент, когда вы, например, забыли учесть комиссию. Давайте рассмотрим ситуацию: пользователь пополнил депозитный кошелек на 0.2 BTC, мы эту сумму переводим на горячий кошелек (раньше мы уже поняли зачем). И по факту мы сможем уже перевести не 0.2, а, к примеру, 0.199 BTC (зависит от комиссии). И если упустим эту особенность, то при попытке совершить транзакцию получим ошибки по типу
The account you tried to send transaction from does not have enough funds. Required 700022023750000000 and got: 700000000000000000

Активация кошелька​

Некоторые валюты (например, тот же Ripple) не позволяют использовать кошелек, пока на нем не появится определенная сумма средств (20 XRP, что на момент написания статьи ~5$), которая еще и блокируется. А пользователей у нас много (например, 100 тысяч), итого мы потратим полмиллиона долларов только на то, чтобы активировать кошельки пользователей.

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

Несколько получателей в одной транзакции​

Да, такое возможно в некоторых блокчейнах (в BTC в том числе). Но адекватных причин использовать этот функционал при депозите нет. Так что рекомендую с подозрением относиться к таким транзакциям.

image24_yUP29rV.png


Приватные блокчейны​

image3_0oCH48L.jpg

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

В случае тестирования такого рода проектов ваши возможности как QA-инженера будут ограничиваться уровнем приватности самого блокчейна.

Fee (комиссия)​

В большинстве блокчейнов при создании транзакции можно указать размер комиссии (fee). Но, например, в Monero ее размер известен только после того, как транзакция выполнена. Это накладывает ограничение на контроль допустимого для нас уровня комиссии.

Валидация номеров кошельков​

Валидация — почти всегда хорошо. И проверка адресов, которые предоставляет нам пользователь — не исключение. Так мы сможем предотвратить:

  1. Ошибки в блокчейне, например:
    • перевод на кошелек другой валюты;
    • неправильный формат кошелька;
    • потерянный символ.
  2. Количество обращений в поддержку (ведь пользователь будет жаловаться, даже если средства пропадут по его вине).

Нетипичное поведение пользователя​

В этом небольшом блоке я покажу несколько странных примеров поведения юзеров:

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

Не забывайте возвращать сдачу​

Подробнее о «сдаче» можно почитать здесь, а здесь пользователь задал вопрос на эту тему, но в его вопросе уже содержится много ответов и объяснений.

2bVzOPp.png


Попробую очень просто объяснить на примере: вам пришли 0.3 биткоина. Вы хотите 0.1 отправить своему другу Пете. Для этого в качестве получателя необходимо указать Петю и сумму 0.1 биткоина, вторым получателем надо указать себя же (адрес, с которого осуществляется перевод) и сумму 0.2 биткоина. То есть ваши 0.3 биткоина вначале рассматриваются как целый кусок и вышеописанным образом вы можете его разделить.

Для простоты понимания рассматриваемая ситуация была упрощена, а также не учитывалась комиссия.

Менеджмент горячих (hot) и холодных (cold) хранилищ​

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

Максимальная нагрузка/пропускная способность​

Конечно, куда же без тестирования нагрузки/производительности. Но сразу замечу, что ваши возможности будут, скорее всего, ограничены самим блокчейном. Так, блокчейн BTC способен обрабатывать семь транзакций в секунду (у других блокчейнов этот показатель может достигать десятков тысяч).

Сколько и какие валюты поддерживать?​

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

Поддержка токенов​

В самом начале статьи я уже упоминал о необходимости понимать разницу между coin (монетой) и token (токеном). Так что не забудем и про поддержку последних. Кстати, самым популярным стандартом для токенов является ERC-20 (на базе блокчейна эфириума).

Еще важно помнить, что у токенов этого стандарта существует такое понятие, как decimals, определяющее, на сколько частей будет делиться токен.

image12_Zw1dS3R.png


Форматы адресов​

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

В качестве примера приведу форматы адресов кошельков в сети BTC:

Старый:
1BUrDeWstWetqBFn5Au8m4JFg2xJaKVN4

Новый:
bc1uf5tdn87k2uz7r2kl5zrfww362ch3746lq5vse7

Lightning Network​

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

ETH Internal transactions​

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

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

В etherscan их можно увидеть на соответствующей вкладке.

zFHwgA2.png


Единицы измерения​

Еще одна важная тема. Когда мы говорим о биткоине или его стоимости, мы измеряем биткоины именно в биткоинах (что не странно). То же самое касается других валют, например, эфириум (ETH), bitcoin cash (BCH). Но при совершении транзакций (особенно с помощью консольных клиентов, не UI) зачастую используются другие единицы измерения. Например, в сети BTC это сатоши, а в сети ETH — wei.

image17_iduF0Ur.png


image10_JHmzlu3.png


Если забыть про этот момент, то можно отправить пользователю, скажем, 10 биткоинов вместо 10 сатоши, разница между которыми составляет 108.

GAS​

Для некоторых валют стоимость транзакции зависит от стоимости сопутствующей сущности, часто называемой газом (GAS). К таким относятся ETH, ETC, NEO и другие. Это уже не единица измерения основной валюты, а, скорее, другая криптовалюта внутри основной, обладающая своей стоимостью, зависящей, как правило, от текущей нагрузки на сеть.

Инкогнито​

Некоторые криптовалюты анонимны — нельзя определить связь отправитель-получатель. Самые популярные из них Monero, zCash, Grin.

VLGqgMi.jpg

Как же в таких условиях осуществлять депозиты/вывод средств?! Спокойствие, все продумано. Для этих целей есть специальные поля при создании транзакции. Например, в Ripple это Destination tag:

image20_Eyp0Ms6.png


В Monero — Payment ID:
image27_uWPU4y9.png


Стоит обратить внимание, что это поле является необязательным (мы видим подписи Only required if... и Optional). Ведь у нас должна быть возможность все-таки сделать транзакцию анонимной, если необходимо.

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

Обработка неуспешных транзакций​

Что-то может пойти не так по нескольким причинам (этот список можно продолжить):

  • транзакция была отменена (например, неправильный формат данных);
  • возникла ошибка при выполнении транзакции;
  • зависла из-за низкой fee;
  • недостаточно средств (обычно следствие double spending attack или ошибки в сумме транзакции).
Некоторых из вышеперечисленных проблем можно избежать, а некоторые предвидеть и протестировать крайне сложно. Соглашусь, что звучит слишком обобщенно. Но цель данной статьи — указать на потенциальные ошибки, а за подробностями уже придется идти к Google.

Большие числа​

Помним, что транзакции обычно осуществляются в единицах, намного меньших, чем единица самой валюты. Например, в одном ETH квинтиллион wei. Это тысяча квадриллионов или миллион триллионов. Короче, это много. И Integer здесь будет маловато. Вот классная статья на эту тему.

̶Б̶ы̶т̶ь̶ ̶и̶л̶и̶ ̶н̶е̶ ̶б̶ы̶т̶ь̶ Строка или число​

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

А как же безопасность?​

Конечно, это важный аспект. Ведь количество оборачиваемых на бирже средств вынуждает обращать на безопасность внимание и выделять значительные средства на ее обеспечение. Но, во-первых, эта тема заслуживает отдельной статьи. Во-вторых, безопасность обеспечивается в основном другими системами криптобиржи (например, KYC, 2FA), работа которых не рассматривается в данной статье. К тому же в начале статьи есть ссылки на отличные материалы по этой теме.

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

🏖️ Где тестировать​

Структура проекта​

Перед обсуждением непосредственно окружений рассмотрим, как обстояли дела на проекте, где мне посчастливилось трудиться в качестве QA-инженера, покрывая все нужды проекта, начиная от мануального тестирования и заканчивая автоматизированными end-to-end тестами. Обзор схемы позволит понять логику тестов, которые будут рассмотрены ниже.

TBYwykT.jpg


Данная схема является упрощенным изображением реальной архитектуры. На ней нет, к примеру, модулей валидации, обработки невалидных транзакций, работы со смарт-контрактами, контроля балансов кошельков и прочих важных штук. Всё в целях простоты (+NDA)

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

Blockchain nodes — содержит в себе ноду каждой валюты, которые поддерживает наша биржа. Это основной пожиратель ресурсов системы.

PostgreSQL — используемая БД. В нее заносится огромное количество информации, например, публичные и приватные ключи кошельков (пользовательских, горячих), информация о проведенных транзакциях и их статусах и так далее.

Kafka — брокер сообщений. Главная роль — связующее звено между всей правой частью диаграммы и левой частью — другими сервисами биржи. В Kafka мы получаем сообщения типа «нужно создать пользователю с id 123 кошелек в валюте BTC», «пользователю с id 234 вывести 0.5 ETH на адрес кошелька ******». В то же время во внешние сервисы отправляются сообщения типа «пользователь с id 345 пополнил свой DOGE-кошелек на 2000 DOGE», «вывод 0.3 XRP для пользователя с id 456 успешно завершен».

Other exchange services — огромный набор модулей биржи, отвечающих за все, кроме управления созданием кошельков и перемещением средств между ними. Например, есть модуль регистрации, KYC (верификации личности), логина, трейдинга и много других. Общение с некоторыми из них происходит посредством Kafka.

Tests — модуль, написанный мной и используемый для проведения тестирования. Его особенность в том, что он общается с каждым из модулей данной схемы (кроме other services). Таким образом мы можем получать/отправлять статусы/команды к другим модулям посредством API запросов и проверять их корректную работу. Данный модуль используется как для облегчения ручного тестирования, так и для автоматизации (начиная с API, заканчивая end-to-end тестами).

А теперь перейдем к самой захватывающей части данного раздела «Где тестировать». И есть у нас три варианта:

  • regtes (локально);
  • testnet;
  • mainnet (production).
Рассмотрим каждый из них.

🖥️ Локально (ну почти)​

Преимущества:

  • легко (не всегда);
  • быстро (тоже не всегда, но однозначно быстрее testnet/mainnet на несколько порядков);
  • вы полностью контролируете блокчейн: запускаете/останавливаете майнинг, генерируете блоки, нужное количество крипты в кошельке, делаете сколько угодно транзакций и мгновенно их подтверждаете или не подтверждаете.
Недостаток: не сильно соответствует работе на проде (mainnet).

Несмотря на то, что данный вид тестирования подразумевает развертывание системы локально, есть несколько нюансов. Очевидно, что для взаимодействия с блокчейном (даже если это его эмуляция, которую можно запустить на одной машине) нужна нода этого самого блокчейна (минимум одна). Решается это достаточно просто — берем и стартуем ноду. Но проблема состоит в том, что в тестировании у нас сотни разных валют. И почти каждая требует свою ноду. Понятно, что никакой ПК/лэптоп такого не потянет. Приходится использовать облако. А это дорого. А учитывая необходимые мощности, это очень дорого. Да и локальным теперь это не назовешь.

Теперь подумаем, как же все это автоматизировать. Контент будет основан на моем личном опыте.

Используемые инструменты/языки:

  • JavaScript/TypeScript;
  • Jest;
  • Axios;
  • Docker;
  • библиотеки работы с блокчейнами (например, эта);
  • публичные API (примеры ниже);
  • faucets (например, этот);
  • обозреватели блокчейнов (пример будет ниже);
  • Metamask и др.
Для начала рассмотрим docker-compose файл:

services:
db:
image: postgres
container_name: db
ports:
- 5432:5432

kafka:
image: wurstmeister/kafka
container_name: kafka
ports:
- 9092:9092

blockchain-node:
image: ruimarinho/bitcoin-core:0.20.1
container_name: blockchain-node
hostname: node
ports:
- 18443:18443

blockchain-manager:
image: blockchain-manager
ports:
- 8001:8001

tests:
image: tests
ports:
- 8080:8080

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

Данный docker-compose файл сильно упрощен. В действительности конфигурация каждого из контейнеров выглядит примерно так.

Напомню, что пока мы рассматриваем локальное тестирование. Дальше будет пример end-to-end теста с использованием regtest нод.

Почему именно E2E-тесты? Потому что они самые сложные. Кто-то возразит: но начинать-то лучше с API. И я соглашусь, но раз уж вы дошли до этого места статьи, я уверен, сможете реализовать API-тесты без особых проблем.

E2E автоматизированный тест проверки депозита​

Флоу рассматриваемого теста изображен на блок-схеме (картинка кликабельна):



Примечание: поскольку тест является end-to-end, вместо ключевого test (или it) я использовал step.

Код продемонстрирован частично. Во-первых, из-за NDA. Во-вторых, для простоты понимания. Поэтому, если вы видите необъявленную переменную, изменение свойства объекта, которого раньше не было, и другие странности — это норм.

Шаг 0: Создание пользователя

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

Шаг 1: Пользователь намеревается сделать депозит

Есть тысяча и один способ создать и отобразить пользователю адрес кошелька, на который он должен внести средства. Рассмотрим один из них.

Юзер заходит на страницу депозита, выбирает валюту (из дропдауна или поиском) → ему отображается адрес кошелька и минимальная сумма депозита. (Помним, что могут быть и дополнительные поля, такие как payment id).

Именно это событие — намерение пользователя пополнить счет — является триггером для нас. В этот момент один из модулей (находящийся в Other exchange services на диаграмме выше) отправляет в Kafka сообщение, что юзеру с id X нужно создать кошелек в валюте Y. Напомню, что по факту наш модуль менеджмента средств — это единственный модуль, который общается с блокчейном, и только он может создать кошелек.

В итоге для реализации данного шага мы внутри нашего блока Tests (на той же диаграмме выше) имитируем действие, описанное в предыдущем абзаце. То есть отправляем сообщение в Kafka.

image7_bnZeptY.png


step('send message to kafka', async () => {
data.currency = currency;
produceKafkaMessage(externalProducerTopics.userWalletCreate.topic, data);
});

Таким образом запускаем работу нашего модуля, и он начинает предпринимать активные действия, реализуя поставленную задачу.

Шаг 2: Создание кошелька

Blockchain manager обращается к ноде нужной валюты и генерирует кошелек (публичный и приватный ключи).

К слову, правильность создания кошелька Blockchain-менеджером — это отдельная тема, реализовать это лучше отдельными API-тестами.

const blockchainManagerAPI = new BlockchainAdapterAPI(currency);
const wallet = await blockchainAdapterAPI.createWallet(userID);

Внутри же end-to-end теста мы проверим, что кошелек создался и записался в базу (PostgreSQL в нашем случае).

image16_1s3sKNF.png


step('created wallet is written to DB', async () => {
const walletInDB = await walletTable.getWalletByAddress(kafkaMessage.wallets[0].address);
});

Шаг 3: Отправка и перехват сообщения с адресом кошелька

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

image9_iOR75xA.png


step('intercept message about wallet creation', async () => {
data.currency = currency;
interceptKafkaMessage(externalConsumerTopics.userWalletsCreate.topic, data);
});

Шаг 4.0: Precondition (да, еще один)

Чтобы юзер пополнил свой кошелек на бирже, у него должны быть средства. Чтобы симитировать депозит, нужен какой-то кошелек, с которого мы будем пополнять пользовательский кошелек биржи.

Особенность некоторых локально поднятых блокчейнов — в них нет монет. Поскольку это имитация реального блокчейна, монеты надо намайнить. Эту задачу мы и решаем на текущем шаге.

image5_eHbhGwE.png


Пример для BTC:

step('generate wallet with funds', async () => {
walletWithFunds = await blockchainManager.generateWallet(currency, true);
await testnetworkBTC.generateBlocks(walletWithFunds.address, 100);
});

А в ETH перед запуском ноды мы можем просто указать такой кошелек и необходимое количество крипты. Сделать это можно в конфигурационном файле при создании genesis блока:


"alloc": {
"0x8c21A547AEcC36921E31BcBF61931CA434D38eE6": {"balance": "0x13370000000000000000000"}
}


Как видим, для разных валют процесс генерации начального количества крипты отличается. И часто это вызывает сложности.

Шаг 4.0.1 Precondition (опять)

Данный шаг отсутствует на схеме с user flow.

В блокчейне Ripple необходимо активировать кошелек до того, как его можно будет использовать.

await XRPBlockchain.activateWallet(depositAddress, 20);

Важно о таком не забывать. Как и о том, что Ripple — лишь один из блокчейнов подобного рода.

Наконец-то можем перейти к следующему важнейшему шагу.

Шаг 4. Депозит (пополнение счета)

image15_tWKhFel.png


Пополняем баланс кошелька, который для нас сгенерировал Blockchain manager. Это как раз то действие, которое будет совершать пользователь. С одной стороны — все просто, но на практике видим следующее:

step('make deposit to user wallet', async () => {
switch (currency) {
case 'BTC':
case 'BCH':
case 'DASH':
case 'LTC':
...
await BTCNetwork.transferFunds(walletWithFunds.address, walletWithFunds.privateKey, depositAddress, depositAmount);
break;
case 'ETH':
case 'ETC':
await ETHNetwork.transferFunds(walletWithFunds.address, walletWithFunds.privateKey, depositAddress, depositAmount);
break;
case 'XRP':
await XRPNetwork.transferFunds(depositAddress, depositAmount, depositTag);
break;
}
...
});

Проблема 1 — разные библиотеки. Для каждого из блокчейнов существуют свои библиотеки для работы, и нет единых стандартов/паттернов.

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

Шаг 5. Определение неподтвержденной транзакции

image32_sqKd71w.png


На этом шаге Blockchain manager должен понять, что была входящая транзакция на кошелек пользователя. «Зачем?» — спросите вы. У нас уже есть и сумма, и хэш транзакции с предыдущего шага. Мы же сами выполнили эту транзакцию.

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

Поэтому наша задача — ждать, пока Blockchain manager сделает свою работу — обнаружит транзакцию. После того как транзакция определена, Blockchain manager отправляет сообщение в Kafka о неподтвержденной транзакции. Если все еще возникают вопросы по поводу того, что такое неподтвержденная транзакция — вам сюда или в Google (или сюда — очень кратко на русском).

Мы это сообщение перехватываем, конечно, нашим модулем Tests и можем выполнить необходимые проверки.

step('send info about unconfirmed transaction to kafka', async () => {
const depositInfo = await receiveKafkaMessage(producerTopics.depositStatus,
{userId:userId });
expect(jsonSchemaValidator(jsonSchemas.depositStatus, depositInfo)).toBeTruthy();
expect(depositInfo.status).toEqual('PENDING');
expect(String(depositInfo.sum)).toEqual(String(depositAmount));
expect(depositInfo.currency).toEqual(currency);
expect(depositInfo.address).toEqual(depositAddress);
txId = depositInfo.txId;
});

И еще одно действие от Blockchain-менеджера — сохранение информации о транзакции в базе:

image11_CllxQBb.png


step('transaction is saved to DB', async () => {
const transaction = await transactionTable.getTransactionByHash(txId);
expect(transaction.status).toEqual('PENDING');
expect(transaction.type).toEqual(‘Deposit');
expect(transaction.tx_sum).toEqual(Helpers.transformAmount(depositAmount, currency));
expect(transaction.destination).toEqual(depositAddress);
});

Шаг 6. Подтверждение транзакции

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

image14_YWObCRr.png


step('confirm transaction', async () => {
switch (currency) {
case 'BTC':
case 'BCH':
case 'DASH':
case 'LTC':
// ...
await testnetworkBTC.generateBlocks(walletWithFunds.address, 100);
break;
case 'ETH':
case 'ETC':
await ETHnode.doMining(1);
break;
case 'XRP':
// do nothing
break;
}
// ...
});

Как видим, для BTC и подобных ему блокчейнов мы вызываем функцию, которая просто генерирует блоки. Для ETH/ETC необходимо в ноду отправить команду майнинга, которая еще и заберет у вас одно ядро процессора (по дефолту), и транзакции будут подтверждаться больше минуты. В XRP мы можем ничего не делать, поскольку помним, что в этом блокчейне транзакции не имеют статуса «неподтвержденная» и к тому же выполняются мгновенно. Поэтому для XRP этот и предыдущий шаг по факту объединяются в один.

Это только самые распространенные способы подтверждения транзакций, есть и другие.

Примечание: еще полезно знать, что такое форки. Это упростит процесс тестирования для похожих криптовалют.

Шаг 7. Определение подтвержденной (confirmed) транзакции

Наш старый добрый Blockchain manager определяет, что транзакция была подтверждена (на основании минимально необходимого количества подтверждений в конфигурации).

lca4JeJ.png


step('transaction with CONFIRMED status is saved to DB', async () => {
transaction = await transactionTable.getTransactionByHash(txId);
expect(transaction.status).toEqual('CONFIRMED');
});
Данные о транзакции обновляются в базе.

image4_wMzfZgJ.png


На всякий случай можем сами обратиться к блокчейну и проверить, что баланс пополненного кошелька соответствует сумме, которую мы перевели:

image23_AjZmVxz.png


step('user wallet balance is updated', async () => {
const userWalletBalance = await regtestnetwork[currency].getWalletBalance(depositAddress);
expect(userWalletBalance).toEqual(depositAmount);
});

Шаг 8. Сообщение об успешном депозите

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

image25_eQWjo9S.png


step('intercept kafka message with CONFIRMED deposit status', async () => {
const depositInfo = await getKafkaMessage(kafkaTopics.depositStatus, { userId: userId });
const txId = depositInfo.txId;
expect(jsonSchemaValidator(jsonSchemas.depositStatus, depositInfo)).toBeTruthy();
expect(depositInfo.status).toEqual('CONFIRMED');
expect(depositInfo.sum).toEqual(depositAmount - feeMargin);
expect(depositInfo.currency).toEqual(currency);
expect(depositInfo.userId).toEqual(userId);
expect(depositInfo.address).toEqual(depositAddress);
});

Это всё! (Здесь должна была быть картинка с Гарольдом)

Напомню, что это была всего лишь часть о тестировании локально (нас еще ждет testnet и mainnet). Как видим, проблем и нюансов много.

🧪 Testnet​

Testnet — это альтернативная цепочка криптовалюты исключительно для разработчиков. Она позволяет разработчикам проводить эксперименты без потери реальной валюты. Подробнее здесь. По факту это копия mainnet, где проще проводить тестирование.
Первым значимым преимуществом testnet над тестированием с локальными нодами заключается в возможности использовать публичные API для общения с блокчейнами. Пример такого API.

Что вообще делают эти API? Давайте посмотрим на примере.

Перед нами стоит задача сгенерировать адрес кошелька. Примерно так мы это сделаем, если используем библиотеки для работы с regtest BTC нодой:

import bitcore from 'bitcore-lib';
import nodeclient from '../nodeclient';

export default async function generateBTCaddress() {
const network = bitcore.Networks.regtest;
const privateKey = await new bitcore.PrivateKey(null, network);
const address = await privateKey.toAddress().toString();
await nodeclient.request('importaddress', [address, '', false]);
return address;
};

Здесь еще скрыта реализация собственного модуля nodeclient, которая потянет на сотню строк кода.

Используя API одного из провайдеров в testnet, все это можно заменить на один API-запрос:

Давайте еще один пример с получением баланса кошелька:

используя локальную ноду:

async getWalletBalance(address) {
const utxos =
(await nodeclient.request("listunspent", [6, 100000, [address]])).result;
let sum = 0;
utxos.forEach(u => sum += u.amount);
return sum;
};

используя API testnet:

https://api.cryptoapis.io/v1/bc/btc/mainnet/address/{{address}}
Примечание: важно помнить, что почти всегда кошельки в testnet и mainnet имеют разный формат. Обычно они отличаются всего на один символ. И это частично нам помогает избежать ошибок при работе с ними.

Тот же тест на тестирование депозита, который был реализован с использованием regtest, мы можем реализовать и в testnet.

Преимущества использования публичных API:

  • меньше кода;
  • API часто бесплатны;
  • иногда один API провайдер поддерживает несколько валют (что упрощает жизнь).
Но мы можем взаимодействовать с тестнетом не только посредством API-провайдеров, но и напрямую, имея соответствующую ноду криптовалюты с тестнет-конфигурацией. И нам точно придется так делать, потому что:

  • API иногда платны,
  • иногда нестабильны,
  • нет поддержки многих валют.
Более того, много проектов криптовалют в принципе не имеют работающего тестнета (это как жить без Dev-, Stage-окружений). Речь сейчас не о API, позволяющих работать с тестнетом, а о самом тестнете. И это вполне нормальная ситуация для проектов-стартапов. Поддержка только mainnet уже требует много сил, так что маленькие команды не готовы распыляться на какой-то тестнет.

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

Так где же брать средства в testnet?

Первый способ — использовать так называемые fausets (краны) — бесплатную раздачу крипты. Но у кранов есть ряд недостатков:

  • не для каждой криптовалюты существуют;
  • часто не работают;
  • нередко требуют ввода капчи (что сильно усложняет автоматизацию тестирования);
  • имеют ограничение на количество транзакций (часто лишь одна в день).
Второй способ — майнить. Но это дорого. Мы знаем, что для майнинга в основном нужны либо вычислительные ресурсы (POW), либо средства на счету (POS). И хотя сложность майнинга в тестнете ниже примерно на порядок в сравнении с mainnet, но это все равно долго и дорого. А учитывая отсутствие интереса (крипта из тестнета не стоит ничего), количество майнеров значительно ниже, чем в mainnet, что приводит к увеличению времени подтверждения транзакций в разы в сравнении с mainnet.

Есть и хорошая новость: некоторые блокчейны дают тестовые монеты, причем в таком количестве, что их точно хватит для целей тестирования. Среди них ETH, XRP.

Например, так вы можете получить тестовые монеты с помощью MetaMask:

image29_2BxVIKS.png


Еще одним недостатком тестнета является нестабильность. Даже самые надежные блокчейны иногда лежат.

{
"type": "https://stellar.org/horizon-errors/timeout",
"title:": "Timeout",
"status": 504,
"detail": "Your request timed out before completing. Please try your request again."
}

В mainnet это редкость для хороших проектов.

Кстати, обычно величина комиссии в testnet ниже, чем mainnet. Объясняется это, конечно, количеством пользователей и, как следствие, нагрузкой на сеть. Это важно учитывать при разработке и тестировании. Ожидание того же уровня fee в mainnet может иметь негативные последствия.

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

🌐 Mainnet​

Mainnet — это продакшн блокчейна. Подробнее здесь. Тестируя в mainnet, мы получаем самые достоверные результаты, ведь именно так это будет работать в проде. За редким исключением, mainnet есть у всех криптовалют, и мы можем ожидать от него стабильной работы. В то же время он обладает рядом свойств, ограничивающих наши возможности тестирования внутри него:

  • дорого;
  • очень дорого;
  • долго.
Дорого, потому что если в regtest мы не платим ничего за выполнение транзакций (только за контейнеры в облаке), в testnet мы оперируем фантиками (их, конечно, нужно прежде получить), то в mainnet каждая транзакция платна. Иногда это центы, но, если посмотреть на исторический график стоимости транзакции в сети BTC, её пиковые значения равнялись 50$. Для тестирования такая стоимость является большим стопором, а автоматизировать что-то при таких цифрах становится абсолютно неоправданным. Даже среднее значение в 1$ многие компании заставляет задуматься.

Но подход тестирования в mainnet можно смело использовать как минимум в двух случаях:

  • у валюты нет testnet и/или regtest,
  • транзакции в mainnet очень дешевые.
Один из самых используемых блокчейнов такого рода — DOGE. У него очень нестабильный тестнет, но транзакции в mainnet стоят ничтожно мало. Даже высокоприоритетная обойдется вам в менее чем 0.1 цента.

Опять-таки для некоторых валют mainnet — единственный вариант тестирования.

Еще одно преимущество mainnet — наличие работающих, так называемых blockchain explorers (обозреватель блокчейна). Они позволяют получать информацию о транзакциях, балансах, блоках и дают много другой полезной информации. Обозреватель BTC, для примера. Из недостатков могу указать на то, что зачастую такие обозреватели предоставляют не разработчики блокчейна, а сторонние разработчики.

Стоит заметить, что обозреватели существуют и для testnet. Пример для BTC. Так как в идеале мы всегда имеем два параллельно работающих блокчейна — mainnet и testnet, с отдельными транзакциями и кошельками (а иногда и форматами кошельков).

Выводы: использовать mainnet целесообразно, когда транзакции очень дешевые и/или у блокчейна нет testnet/regtest.

✅ Где чек-лист?!​

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

Первая часть этой статьи — это своеобразный чек-лист, в котором я попытался агрегировать:

  1. Максимально общие вещи, характерны для всех криптовалютных бирж.
  2. Негативные кейсы, рассмотрение которых должно и помочь вам в тестировании, и натолкнуть на поиск других кейсов.
  3. Разные подходы и разнообразие решений.
Что касается автоматизации, то ситуация тут сложнее, чем с типичными веб-приложениями. Поскольку блокчейны обладают рядом характеристик, усложняющим нам, автоматизатором, жизнь. Например, наличие майнинга. Но писать автотесты возможно и нужно.


 
Сверху