Всем привет! Меня зовут Oleksandr Pelykh, и я работаю в роли QA уже почти 7 лет. В этой статье поделюсь своим небольшим опытом тестирования криптовалютной биржи. А именно части, касающейся переводов криптовалюты, её хранения и все такое. Несмотря на то, что рассматривается отдельный модуль, в материале также будет базовая информации о принципах построения криптобиржи.
Статья нацелена на тех, кто уже обладает базовыми знаниями о криптовалюте и имеет опыт работы с ней. Хотя по тексту есть много гиперссылок с подробным объяснением рассматриваемых понятий.
И предупреждаю, это очень лонгрид.
Если вы жмете на кликабельный текст и видите 404, 503 или что-то, кроме нужного контента — это, скорее всего, проблема ресурса, на который идет редирект. Ничто не вечно. Напишите в коммент, и я обновлю ссылку в статье.
Примечание: здесь вы не найдете того, как разрабатывать/тестировать сам блокчейн. Это отдельная огромная тема. Мы рассматриваем только тестирование криптобиржи — некой системы, построенной на базе блокчейна. Так что, если в ваших целях разобраться с разработкой/тестированием своего блокчейна — лучше поискать более подходящий теме материал. Ну а тем, кто остался читать дальше, добро пожаловать в криптовалютный мир!
Здесь мы видим процессы депозита и вывода средств (слева), горячий и холодный кошельки (справа, обведены желтым). Остальной функционал (логин, регистрация, трейдинг и все такое) не рассматривается (обведен красным). Позже мы эту схему разберем подробнее.
Поехали.
Для адекватного восприятия статьи вы должны понимать значения следующих понятий: блокчейн, криптовалюта, биткоин (биток, BTC), майнинг, транзакция, публичный/приватный ключи, горячий кошелек, холодный кошелек.
Кроме того, нужно понимать разницу между coin и token, знать, кто такой Виталик Бутерин, что такое POW, что считают майнеры (кроме заработанных денег) и сколько будет стоить биткоин через год.
Перейдем, наконец, к первому большому подразделу — «Депозит».
Под «депозитом» в данной статье имеется ввиду пополнение счета, а не банковский депозит (подразумевающий хранение денег и начисление процентов на них).
Процесс депозита на криптобирже похож на вышеописанный. Пользователь регистрируется → ему создается счет (криптовалютный кошелек) → пользователь пополняет этот счет по адресу (публичному ключу).
Примечание: Описанный в данной статье процесс депозита рассматривает только пополнение счета в валюте крипты. Пополнение «фиатными» деньгами происходит через платежные сервисы и не требует обработки в блокчейне, так что работа с фиатом рассматриваться не будет.
Поэтому перейдем к следующему предположению — создавать кошелек после нажатия на кнопку «Депозит». Этим действием пользователь показывает свое намерение внести средства. Важное условие, что внести средства юзер хочет в конкретной валюте (например, биткоинах). Так что процесс пополнения счета должен включать выбор этой валюты.
Таким образом, мы считаем это предположение самым оптимальным. Потому что оно требует генерации только одного кошелька (а не сотен для каждой поддерживаемой валюты) и только после того, как пользователь показал свое намерение внести средства на счет. Что позволяет нам избежать выполнения ненужных операций.
Но в мире блокчейна такое зачастую невозможно. Ведь все транзакции публичны. Невозможность определить автора транзакции вынуждает нас сделать данное предположение (использовать один кошелек для всех пользователей) неверным.
Но! Для некоторых валют такой подход используется. Об этом позже.
Предположение: нужно создать пользователю кошелек с ETH, чтобы хранить купленный им эфириум.
Это неверно, потому что транзакции и хранение средств пользователей происходят на внутренних счетах биржи. (Почему? Отвечу дальше в статье.)
Идем дальше по user flow — пользователь хочет вывести ETH. Теперь точно нужно создать ему кошелек!
Опять нет. Вывод средств осуществляется на личный кошелек пользователя. Если его сгенерирует биржа, то она и будет иметь доступ к средствам на этом кошельке.
Вывод: создание кошельков для пользователей необходимо только при внесении средств на биржу извне, то есть только при депозите (пополнении счета) в крипте.
Окей, с этим разобрались. Теперь попробуем ответить на следующий вопрос.
Стоп! А зачем вообще переводить средства на какой-то горячий кошелек? Для начала нужно понимать предназначение и разницу между горячим и холодным криптокошельками. На эту тему есть масса статей.
Если в двух словах: средства на горячем кошельке активно используются, даже очень. А на холодном просто хранятся. Главное требование к горячему кошельку — простота и скорость использования, к холодному — надежность.
Почему же после депозита просто не оставить средства пользователя на кошельке, который мы для него сгенерировали и который им был пополнен?
Давайте представим ситуацию. У нас четыре пользователя. Три из них пополнили свои счета в BTC на 0.1, 0.2 и 0.3 биткоина соответственно. А четвертый пользователь запросил вывод 0.5 BTC. Нам нужно теперь агрегировать с имеющихся трех кошельков куш в 0.5 биткоина и перевести пользователю № 4. А еще нужно запомнить, сколько же средств осталось на кошельках пользователей № 1, № 2 и № 3, чтобы при следующем чьём-то withdrawal-запросе понимать, откуда собирать остатки «битков». По сути, роль такого кошелька-агрегатора и играет горячий кошелек. К тому же, когда у нас не четыре, а 400 тысяч пользователей, да еще и сотни валют, необходимость в кошельке такого типа (или даже нескольких для каждой из валют) становится явной.
Как вариант, можно:
и попробуем понять следующее:
1. Проверять в блокчейне все кошельки пользователей по всем валютам с определенной периодичностью (например, 5 минут):
Это работает
Очень затратно по ресурсам
2. Проверять баланс кошелька пользователя определенное время после того, как кошелек был сгенерирован
Экономнее в плане ресурсов
Работает только для первого депозита (а пользователь может пополнить счет как через 5 минут, так и через неделю после того, как мы сгенерировали ему кошелек)
3. Добавить кнопку «Я пополнил, проверьте баланс моего кошелька»
Очень экономно (проверяем только те кошельки, которые потенциально были только что пополнены)
Дополнительное действие от пользователя
4. Проверять транзакции в каждом новом блоке (а точнее список получателей в этих транзакциях) и обнаруживать, есть ли в этом списке адреса кошельков пользователей биржи
Относительно эффективно в плане ресурсов
Будут проверяться и те кошельки, которые никогда не пополняются
Теперь рассмотрим процесс вывода средств.
Если до этого момента не стало очевидно, то акцентирую ваше внимание на следующем — важно различать такие виды кошельков:
Есть еще один момент, на который биржи обращают внимание: чем меньше нагрузка на блокчейн, тем меньше стоимость транзакции. Таким образом, биржи могут влиять на стоимость транзакции в сети за счет уменьшения количества самих транзакций. Достигается это увеличением минимальной суммы вывода и/или ограничением на количество выводов за единицу времени.
Например, пользователь может сделать не больше одного запроса на вывод BTC в день. Или минимальная сумма для вывода — 0.001 BTC.
К счастью, проблема эта решается просто, нужно только определить количество необходимых подтверждений транзакций для каждой из валют, которая поддерживается.
Посмотрим на график ниже (он для BTC). По вертикальной шкале — вероятность осуществления вышеупомянутой double spending attack, по горизонтальной — время (в минутах).
Источник
Видим, что спустя час (за это время в сети биткоина успеет сгенерироваться шесть блоков) вероятность атаки стремится к нулю.
В таблице ниже приведены количество необходимых подтверждений и среднее время их достижения для нескольких популярных криптовалют.
The account you tried to send transaction from does not have enough funds. Required 700022023750000000 and got: 700000000000000000
В таком случае будет логичным решение использовать один кошелек для всех пользователей (или хотя бы для большой группы пользователей). И к счастью, для некоторых блокчейнов такой подход можно реализовать.
Несмотря на то, что блокчейн позиционируется как децентрализованная технология, некоторые блокчейны являются приватными, полный доступ к ним имеет только владелец.
В случае тестирования такого рода проектов ваши возможности как QA-инженера будут ограничиваться уровнем приватности самого блокчейна.
Попробую очень просто объяснить на примере: вам пришли 0.3 биткоина. Вы хотите 0.1 отправить своему другу Пете. Для этого в качестве получателя необходимо указать Петю и сумму 0.1 биткоина, вторым получателем надо указать себя же (адрес, с которого осуществляется перевод) и сумму 0.2 биткоина. То есть ваши 0.3 биткоина вначале рассматриваются как целый кусок и вышеописанным образом вы можете его разделить.
Для простоты понимания рассматриваемая ситуация была упрощена, а также не учитывалась комиссия.
Касательно биржи важно помнить, что ключи от холодных кошельков настоятельно рекомендуется хранить отдельно от горячих и подальше от других данных биржи. Чтобы при взломе минимизировать возможный ущерб.
Еще важно помнить, что у токенов этого стандарта существует такое понятие, как decimals, определяющее, на сколько частей будет делиться токен.
В качестве примера приведу форматы адресов кошельков в сети BTC:
Старый:
1BUrDeWstWetqBFn5Au8m4JFg2xJaKVN4
Новый:
bc1uf5tdn87k2uz7r2kl5zrfww362ch3746lq5vse7
В etherscan их можно увидеть на соответствующей вкладке.
Если забыть про этот момент, то можно отправить пользователю, скажем, 10 биткоинов вместо 10 сатоши, разница между которыми составляет 108.
Как же в таких условиях осуществлять депозиты/вывод средств?! Спокойствие, все продумано. Для этих целей есть специальные поля при создании транзакции. Например, в Ripple это Destination tag:
В Monero — Payment ID:
Стоит обратить внимание, что это поле является необязательным (мы видим подписи Only required if... и Optional). Ведь у нас должна быть возможность все-таки сделать транзакцию анонимной, если необходимо.
Генерируя для каждого пользователя уникальный Destination tag, мы понимаем, от кого пришла какая транзакция. И это позволяет нам использовать один кошелек для большой группы пользователей.
На этом предлагаю данный раздел закончить и перейти к более интересной теме — тестового окружения.
Данная схема является упрощенным изображением реальной архитектуры. На ней нет, к примеру, модулей валидации, обработки невалидных транзакций, работы со смарт-контрактами, контроля балансов кошельков и прочих важных штук. Всё в целях простоты (+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 тестами).
А теперь перейдем к самой захватывающей части данного раздела «Где тестировать». И есть у нас три варианта:
Несмотря на то, что данный вид тестирования подразумевает развертывание системы локально, есть несколько нюансов. Очевидно, что для взаимодействия с блокчейном (даже если это его эмуляция, которую можно запустить на одной машине) нужна нода этого самого блокчейна (минимум одна). Решается это достаточно просто — берем и стартуем ноду. Но проблема состоит в том, что в тестировании у нас сотни разных валют. И почти каждая требует свою ноду. Понятно, что никакой ПК/лэптоп такого не потянет. Приходится использовать облако. А это дорого. А учитывая необходимые мощности, это очень дорого. Да и локальным теперь это не назовешь.
Теперь подумаем, как же все это автоматизировать. Контент будет основан на моем личном опыте.
Используемые инструменты/языки:
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-тесты без особых проблем.
Примечание: поскольку тест является end-to-end, вместо ключевого test (или it) я использовал step.
Код продемонстрирован частично. Во-первых, из-за NDA. Во-вторых, для простоты понимания. Поэтому, если вы видите необъявленную переменную, изменение свойства объекта, которого раньше не было, и другие странности — это норм.
Шаг 0: Создание пользователя
Поскольку наш модуль менеджмента средств предполагает наличие уже существующего пользователя, мы можем включить этот код в preconditions. Либо просто выбрать любого пользователя из базы, если в ней уже есть тестовые данные. В общем, вариантов много и все зависит от реализации в конкретном случае. Результат этого шага — существует пользователь в системе.
Шаг 1: Пользователь намеревается сделать депозит
Есть тысяча и один способ создать и отобразить пользователю адрес кошелька, на который он должен внести средства. Рассмотрим один из них.
Юзер заходит на страницу депозита, выбирает валюту (из дропдауна или поиском) → ему отображается адрес кошелька и минимальная сумма депозита. (Помним, что могут быть и дополнительные поля, такие как payment id).
Именно это событие — намерение пользователя пополнить счет — является триггером для нас. В этот момент один из модулей (находящийся в Other exchange services на диаграмме выше) отправляет в Kafka сообщение, что юзеру с id X нужно создать кошелек в валюте Y. Напомню, что по факту наш модуль менеджмента средств — это единственный модуль, который общается с блокчейном, и только он может создать кошелек.
В итоге для реализации данного шага мы внутри нашего блока Tests (на той же диаграмме выше) имитируем действие, описанное в предыдущем абзаце. То есть отправляем сообщение в Kafka.
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 в нашем случае).
step('created wallet is written to DB', async () => {
const walletInDB = await walletTable.getWalletByAddress(kafkaMessage.wallets[0].address);
});
Шаг 3: Отправка и перехват сообщения с адресом кошелька
Адрес кошелька отправляется в Kafka, чтобы соответствующий модуль биржи смог отобразить этот адрес юзеру. И мы в модуле Tests это сообщение перехватываем.
step('intercept message about wallet creation', async () => {
data.currency = currency;
interceptKafkaMessage(externalConsumerTopics.userWalletsCreate.topic, data);
});
Шаг 4.0: Precondition (да, еще один)
Чтобы юзер пополнил свой кошелек на бирже, у него должны быть средства. Чтобы симитировать депозит, нужен какой-то кошелек, с которого мы будем пополнять пользовательский кошелек биржи.
Особенность некоторых локально поднятых блокчейнов — в них нет монет. Поскольку это имитация реального блокчейна, монеты надо намайнить. Эту задачу мы и решаем на текущем шаге.
Пример для 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. Депозит (пополнение счета)
Пополняем баланс кошелька, который для нас сгенерировал 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. Определение неподтвержденной транзакции
На этом шаге 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-менеджера — сохранение информации о транзакции в базе:
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) подтверждениями транзакций занимаются майнеры. Здесь очень кратко, но понятно описан процесс. В случае локально работающего блокчейна единственный возможный майнер — это мы сами.
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 определяет, что транзакция была подтверждена (на основании минимально необходимого количества подтверждений в конфигурации).
step('transaction with CONFIRMED status is saved to DB', async () => {
transaction = await transactionTable.getTransactionByHash(txId);
expect(transaction.status).toEqual('CONFIRMED');
});
Данные о транзакции обновляются в базе.
На всякий случай можем сами обратиться к блокчейну и проверить, что баланс пополненного кошелька соответствует сумме, которую мы перевели:
step('user wallet balance is updated', async () => {
const userWalletBalance = await regtestnetwork[currency].getWalletBalance(depositAddress);
expect(userWalletBalance).toEqual(depositAmount);
});
Шаг 8. Сообщение об успешном депозите
Теперь время сообщить бирже, что депозит был успешен, чтобы можно было обновить баланс пользователя по конкретной валюте и позволить ему торговать на эту сумму. Опять мы это сообщение перехватываем и выполняем нужные проверки.
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 над тестированием с локальными нодами заключается в возможности использовать публичные 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:
Еще одна проблема (точнее задача) тестнета — нужно где-то брать средства для осуществления транзакций. Напомню, что в regtest мы можем уже запустить ноду с кошельком, полным крипты, или запустить очень быстрый майнинг, или просто сгенерировать необходимое нам количество блоков за доли секунды. В тестнете это не прокатит, так как это имитация реального блокчейна и все транзакции должны подтверждаться майнерами.
Так где же брать средства в testnet?
Первый способ — использовать так называемые fausets (краны) — бесплатную раздачу крипты. Но у кранов есть ряд недостатков:
Есть и хорошая новость: некоторые блокчейны дают тестовые монеты, причем в таком количестве, что их точно хватит для целей тестирования. Среди них ETH, XRP.
Например, так вы можете получить тестовые монеты с помощью MetaMask:
Еще одним недостатком тестнета является нестабильность. Даже самые надежные блокчейны иногда лежат.
{
"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 — наличие работающих, так называемых blockchain explorers (обозреватель блокчейна). Они позволяют получать информацию о транзакциях, балансах, блоках и дают много другой полезной информации. Обозреватель BTC, для примера. Из недостатков могу указать на то, что зачастую такие обозреватели предоставляют не разработчики блокчейна, а сторонние разработчики.
Стоит заметить, что обозреватели существуют и для testnet. Пример для BTC. Так как в идеале мы всегда имеем два параллельно работающих блокчейна — mainnet и testnet, с отдельными транзакциями и кошельками (а иногда и форматами кошельков).
Выводы: использовать mainnet целесообразно, когда транзакции очень дешевые и/или у блокчейна нет testnet/regtest.
Первая часть этой статьи — это своеобразный чек-лист, в котором я попытался агрегировать:
Статья нацелена на тех, кто уже обладает базовыми знаниями о криптовалюте и имеет опыт работы с ней. Хотя по тексту есть много гиперссылок с подробным объяснением рассматриваемых понятий.
И предупреждаю, это очень лонгрид.
Почему я?
Во-первых, потому что контента на эту тему нет. Ладно, он есть, но обладает недостатками. Несмотря на это, я настоятельно рекомендую вам, кроме моей статьи, использовать также другие материалы:- здесь поверхностно описан процесс тестирования всей криптобиржи (с акцентом на безопасность), можно взять эту статью за основу;
- здесь общий чек-лист, в котором уже больше технических нюансов;
- материал, похожий на первый, но на английском;
- это качественная статья от ребят из Hacken, в основном тоже касающаяся безопасности; это лучший материал, который я нашел по этой теме, к тому же пункты чек-листа кликабельны и поведут вас сразу на страницы с описанием того, как тестировать конкретные кейсы.
Если вы жмете на кликабельный текст и видите 404, 503 или что-то, кроме нужного контента — это, скорее всего, проблема ресурса, на который идет редирект. Ничто не вечно. Напишите в коммент, и я обновлю ссылку в статье.
Примечание: здесь вы не найдете того, как разрабатывать/тестировать сам блокчейн. Это отдельная огромная тема. Мы рассматриваем только тестирование криптобиржи — некой системы, построенной на базе блокчейна. Так что, если в ваших целях разобраться с разработкой/тестированием своего блокчейна — лучше поискать более подходящий теме материал. Ну а тем, кто остался читать дальше, добро пожаловать в криптовалютный мир!
План
О чем я расскажу:- что тестировать (чек-лист);
- где тестировать (окружения);
- как тестировать (примеры кода).
- процесс депозита (пополнения счета);
- процесс вывода средств с биржи.
- вопросы, которые нужно задать;
- проблемы, с которыми предстоит столкнуться.
Scope
Для наглядности рассмотрим схему ниже:Здесь мы видим процессы депозита и вывода средств (слева), горячий и холодный кошельки (справа, обведены желтым). Остальной функционал (логин, регистрация, трейдинг и все такое) не рассматривается (обведен красным). Позже мы эту схему разберем подробнее.
Поехали.
Что тестировать
Что вообще такое криптовалютная биржа, можно почитать здесь.Для адекватного восприятия статьи вы должны понимать значения следующих понятий: блокчейн, криптовалюта, биткоин (биток, BTC), майнинг, транзакция, публичный/приватный ключи, горячий кошелек, холодный кошелек.
Кроме того, нужно понимать разницу между coin и token, знать, кто такой Виталик Бутерин, что такое POW, что считают майнеры (кроме заработанных денег) и сколько будет стоить биткоин через год.
Перейдем, наконец, к первому большому подразделу — «Депозит».
Депозит
Для начала вспомним, как это работает в банке. Вы приходите в банк, подписываете какие-то бумажки. Вам открывают счет в определенной валюте (или нескольких), и с этого момента вы можете пополнять свой счет по его номеру (в последнее время это IBAN).Под «депозитом» в данной статье имеется ввиду пополнение счета, а не банковский депозит (подразумевающий хранение денег и начисление процентов на них).
Процесс депозита на криптобирже похож на вышеописанный. Пользователь регистрируется → ему создается счет (криптовалютный кошелек) → пользователь пополняет этот счет по адресу (публичному ключу).
Примечание: Описанный в данной статье процесс депозита рассматривает только пополнение счета в валюте крипты. Пополнение «фиатными» деньгами происходит через платежные сервисы и не требует обработки в блокчейне, так что работа с фиатом рассматриваться не будет.
Какие вопросы необходимо задать при тестировании депозита
Когда создавать пользователю кошелек для депозита?
Давайте начнем с предположений. Первое предположение — создавать кошелек сразу после регистрации. И это плохая идея. Потому что: 1) придется генерировать кошелек для каждой валюты, а их много; 2) пользователь после регистрации может не пополнить счет. Зачем же тогда ему все эти кошельки?Поэтому перейдем к следующему предположению — создавать кошелек после нажатия на кнопку «Депозит». Этим действием пользователь показывает свое намерение внести средства. Важное условие, что внести средства юзер хочет в конкретной валюте (например, биткоинах). Так что процесс пополнения счета должен включать выбор этой валюты.
Таким образом, мы считаем это предположение самым оптимальным. Потому что оно требует генерации только одного кошелька (а не сотен для каждой поддерживаемой валюты) и только после того, как пользователь показал свое намерение внести средства на счет. Что позволяет нам избежать выполнения ненужных операций.
А почему бы не использовать один кошелек для всех пользователей?
Так происходило до недавних пор (пока лавочку не прикрыли). Вы переводите деньги на счет, говорите продавцу товара время и сумму, таким образом доказываете, что автор перевода — вы.Но в мире блокчейна такое зачастую невозможно. Ведь все транзакции публичны. Невозможность определить автора транзакции вынуждает нас сделать данное предположение (использовать один кошелек для всех пользователей) неверным.
Но! Для некоторых валют такой подход используется. Об этом позже.
Когда еще нужно создавать пользователю кошелек?
Ситуация следующая: пользователь успешно пополнил свой BTC-счет, после этого совершил торговую операцию, продав биткоины и купив на них эфириум (продал BTC → получил ETH).Предположение: нужно создать пользователю кошелек с ETH, чтобы хранить купленный им эфириум.
Это неверно, потому что транзакции и хранение средств пользователей происходят на внутренних счетах биржи. (Почему? Отвечу дальше в статье.)
Идем дальше по user flow — пользователь хочет вывести ETH. Теперь точно нужно создать ему кошелек!
Опять нет. Вывод средств осуществляется на личный кошелек пользователя. Если его сгенерирует биржа, то она и будет иметь доступ к средствам на этом кошельке.
Вывод: создание кошельков для пользователей необходимо только при внесении средств на биржу извне, то есть только при депозите (пополнении счета) в крипте.
Окей, с этим разобрались. Теперь попробуем ответить на следующий вопрос.
Что делать со средствами юзера после депозита?
- переводить сразу на горячий кошелек;
- хранить некоторое время и скидывать на горячий кошелек после: X пополнений, если баланс превышает Y, спустя Z время.
Стоп! А зачем вообще переводить средства на какой-то горячий кошелек? Для начала нужно понимать предназначение и разницу между горячим и холодным криптокошельками. На эту тему есть масса статей.
Если в двух словах: средства на горячем кошельке активно используются, даже очень. А на холодном просто хранятся. Главное требование к горячему кошельку — простота и скорость использования, к холодному — надежность.
Почему же после депозита просто не оставить средства пользователя на кошельке, который мы для него сгенерировали и который им был пополнен?
Давайте представим ситуацию. У нас четыре пользователя. Три из них пополнили свои счета в BTC на 0.1, 0.2 и 0.3 биткоина соответственно. А четвертый пользователь запросил вывод 0.5 BTC. Нам нужно теперь агрегировать с имеющихся трех кошельков куш в 0.5 биткоина и перевести пользователю № 4. А еще нужно запомнить, сколько же средств осталось на кошельках пользователей № 1, № 2 и № 3, чтобы при следующем чьём-то withdrawal-запросе понимать, откуда собирать остатки «битков». По сути, роль такого кошелька-агрегатора и играет горячий кошелек. К тому же, когда у нас не четыре, а 400 тысяч пользователей, да еще и сотни валют, необходимость в кошельке такого типа (или даже нескольких для каждой из валют) становится явной.
Какая минимальная сумма депозита?
Важно её определить для каждой валюты. И понимать, что мы будем делать, если пользователь пополнил кошелек на меньшую сумму.Как вариант, можно:
- игнорировать пополнение,
- выводить сообщение, что сумма недостаточна.
и попробуем понять следующее:
Как определить, что кошелек пользователя был пополнен?
То есть зашел пользователь в раздел «Депозит» на сайте биржи, выбрал валюту, сгенерировали мы ему кошелек. Как понять, что он этот кошелек пополнил? Этот вопрос сложный, и однозначного ответа на него нет. Но накидаю несколько вариантов решения этой задачи:1. Проверять в блокчейне все кошельки пользователей по всем валютам с определенной периодичностью (например, 5 минут):
Это работает
Очень затратно по ресурсам
2. Проверять баланс кошелька пользователя определенное время после того, как кошелек был сгенерирован
Экономнее в плане ресурсов
Работает только для первого депозита (а пользователь может пополнить счет как через 5 минут, так и через неделю после того, как мы сгенерировали ему кошелек)
3. Добавить кнопку «Я пополнил, проверьте баланс моего кошелька»
Очень экономно (проверяем только те кошельки, которые потенциально были только что пополнены)
Дополнительное действие от пользователя
4. Проверять транзакции в каждом новом блоке (а точнее список получателей в этих транзакциях) и обнаруживать, есть ли в этом списке адреса кошельков пользователей биржи
Относительно эффективно в плане ресурсов
Будут проверяться и те кошельки, которые никогда не пополняются
Теперь рассмотрим процесс вывода средств.
Вывод средств
Дополним уже существующую схему. Новый флоу — перевод средств с горячего кошелька на пользовательский (личный). Это и есть процесс вывода средств (withdrawal).Если до этого момента не стало очевидно, то акцентирую ваше внимание на следующем — важно различать такие виды кошельков:
- Кошелек пользователя для депозита — пополняется при депозите. Приватный ключ принадлежит бирже.
- Личный кошелек пользователя. На него производится вывод средств. Приватный ключ принадлежит пользователю.
На какие вопросы нужно найти ответы
Как часто юзер может выводить средства? Минимальная сумма вывода?
Вопросы связаны между собой, одно влияет на другое. Зачастую в плане комиссии все тривиально и всё оплачивается пользователем.Есть еще один момент, на который биржи обращают внимание: чем меньше нагрузка на блокчейн, тем меньше стоимость транзакции. Таким образом, биржи могут влиять на стоимость транзакции в сети за счет уменьшения количества самих транзакций. Достигается это увеличением минимальной суммы вывода и/или ограничением на количество выводов за единицу времени.
Например, пользователь может сделать не больше одного запроса на вывод BTC в день. Или минимальная сумма для вывода — 0.001 BTC.
Величина комиссии
Во-первых, определяться она должна отдельно для каждой валюты, потому что зависит от многих факторов (технология, нагрузка на сеть, общее количество криптовалюты в сети и прочее). А во-вторых, комиссию за транзакцию можно либо захардкодить, либо рассчитывать динамически:- hardcoded (основываясь на исторических данных);
- динамическая и определяется: 1) в момент создания withdrawal запроса; 2) в момент создания транзакции в сети.
Подводные камни
В этом разделе рассмотрим проблемные моменты, на которые нужно обратить внимание при тестировании депозита и вывода средств.Double-spending attack
Подробно о Double-spending можно почитать здесь. Если кратко, один и тот же биткоин вы можете отправить на два разных адреса. При этом оба получателя будут видеть, что биткоин «летит» к ним. Но «прилетит» он в итоге только одному.К счастью, проблема эта решается просто, нужно только определить количество необходимых подтверждений транзакций для каждой из валют, которая поддерживается.
Посмотрим на график ниже (он для BTC). По вертикальной шкале — вероятность осуществления вышеупомянутой double spending attack, по горизонтальной — время (в минутах).
Источник
Видим, что спустя час (за это время в сети биткоина успеет сгенерироваться шесть блоков) вероятность атаки стремится к нулю.
В таблице ниже приведены количество необходимых подтверждений и среднее время их достижения для нескольких популярных криптовалют.
Некоторые валюты не требуют подтверждений
Существуют блокчейны, транзакции в которых осуществляются мгновенно, и они не требуют подтверждений (просто подписываются приватным ключом).Суммы на вашем счету недостаточно для совершения транзакции
Проблема возникает в момент, когда вы, например, забыли учесть комиссию. Давайте рассмотрим ситуацию: пользователь пополнил депозитный кошелек на 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 в том числе). Но адекватных причин использовать этот функционал при депозите нет. Так что рекомендую с подозрением относиться к таким транзакциям.Приватные блокчейны
Несмотря на то, что блокчейн позиционируется как децентрализованная технология, некоторые блокчейны являются приватными, полный доступ к ним имеет только владелец.
В случае тестирования такого рода проектов ваши возможности как QA-инженера будут ограничиваться уровнем приватности самого блокчейна.
Fee (комиссия)
В большинстве блокчейнов при создании транзакции можно указать размер комиссии (fee). Но, например, в Monero ее размер известен только после того, как транзакция выполнена. Это накладывает ограничение на контроль допустимого для нас уровня комиссии.Валидация номеров кошельков
Валидация — почти всегда хорошо. И проверка адресов, которые предоставляет нам пользователь — не исключение. Так мы сможем предотвратить:- Ошибки в блокчейне, например:
- перевод на кошелек другой валюты;
- неправильный формат кошелька;
- потерянный символ.
- Количество обращений в поддержку (ведь пользователь будет жаловаться, даже если средства пропадут по его вине).
Нетипичное поведение пользователя
В этом небольшом блоке я покажу несколько странных примеров поведения юзеров:- пользователь хочет вывести средства на свой же депозитный кошелек;
- пользователь хочет вывести средства на депозитный кошелек другого пользователя биржи.
Не забывайте возвращать сдачу
Подробнее о «сдаче» можно почитать здесь, а здесь пользователь задал вопрос на эту тему, но в его вопросе уже содержится много ответов и объяснений.Попробую очень просто объяснить на примере: вам пришли 0.3 биткоина. Вы хотите 0.1 отправить своему другу Пете. Для этого в качестве получателя необходимо указать Петю и сумму 0.1 биткоина, вторым получателем надо указать себя же (адрес, с которого осуществляется перевод) и сумму 0.2 биткоина. То есть ваши 0.3 биткоина вначале рассматриваются как целый кусок и вышеописанным образом вы можете его разделить.
Для простоты понимания рассматриваемая ситуация была упрощена, а также не учитывалась комиссия.
Менеджмент горячих (hot) и холодных (cold) хранилищ
Я уже говорил о предназначении хранилищ каждого из типов. К слову, это разделение актуально не только на криптобиржах. Даже рядовой пользователь криптовалюты должен знать, что основные средства стоит хранить на холодных носителях. А на горячих держать лишь небольшую, активно используемую их часть.Касательно биржи важно помнить, что ключи от холодных кошельков настоятельно рекомендуется хранить отдельно от горячих и подальше от других данных биржи. Чтобы при взломе минимизировать возможный ущерб.
Максимальная нагрузка/пропускная способность
Конечно, куда же без тестирования нагрузки/производительности. Но сразу замечу, что ваши возможности будут, скорее всего, ограничены самим блокчейном. Так, блокчейн BTC способен обрабатывать семь транзакций в секунду (у других блокчейнов этот показатель может достигать десятков тысяч).Сколько и какие валюты поддерживать?
Обычно биржа поддерживает несколько сотен валют. Важно иметь такой список, так как многие валюты обладают уникальными особенностями, часть из которых рассмотрена в данной статье.Поддержка токенов
В самом начале статьи я уже упоминал о необходимости понимать разницу между coin (монетой) и token (токеном). Так что не забудем и про поддержку последних. Кстати, самым популярным стандартом для токенов является ERC-20 (на базе блокчейна эфириума).Еще важно помнить, что у токенов этого стандарта существует такое понятие, как decimals, определяющее, на сколько частей будет делиться токен.
Форматы адресов
Они часто меняются, добавляются. По возможности нужно поддерживать все существующие форматы, чтобы удовлетворить потребности как можно большего количества пользователей.В качестве примера приведу форматы адресов кошельков в сети BTC:
Старый:
1BUrDeWstWetqBFn5Au8m4JFg2xJaKVN4
Новый:
bc1uf5tdn87k2uz7r2kl5zrfww362ch3746lq5vse7
Lightning Network
Это тянет на отдельную статью. Но в двух словах — это сеть поверх блокчейна, которая позволяет уменьшить издержки на транзакции. Подробнее можно почитать здесь.ETH Internal transactions
Из названия видно, что это особенность сети эфириума, где существуют так называемые Internal Transactions, обладающие несколькими свойствами:- являются результатом выполнения смарт-контрактов;
- сохраняются не как обычные транзакции, но так же могут изменять состояние блокчейна (в том числе баланс).
В etherscan их можно увидеть на соответствующей вкладке.
Единицы измерения
Еще одна важная тема. Когда мы говорим о биткоине или его стоимости, мы измеряем биткоины именно в биткоинах (что не странно). То же самое касается других валют, например, эфириум (ETH), bitcoin cash (BCH). Но при совершении транзакций (особенно с помощью консольных клиентов, не UI) зачастую используются другие единицы измерения. Например, в сети BTC это сатоши, а в сети ETH — wei.Если забыть про этот момент, то можно отправить пользователю, скажем, 10 биткоинов вместо 10 сатоши, разница между которыми составляет 108.
GAS
Для некоторых валют стоимость транзакции зависит от стоимости сопутствующей сущности, часто называемой газом (GAS). К таким относятся ETH, ETC, NEO и другие. Это уже не единица измерения основной валюты, а, скорее, другая криптовалюта внутри основной, обладающая своей стоимостью, зависящей, как правило, от текущей нагрузки на сеть.Инкогнито
Некоторые криптовалюты анонимны — нельзя определить связь отправитель-получатель. Самые популярные из них Monero, zCash, Grin.Как же в таких условиях осуществлять депозиты/вывод средств?! Спокойствие, все продумано. Для этих целей есть специальные поля при создании транзакции. Например, в Ripple это Destination tag:
В Monero — Payment ID:
Стоит обратить внимание, что это поле является необязательным (мы видим подписи Only required if... и Optional). Ведь у нас должна быть возможность все-таки сделать транзакцию анонимной, если необходимо.
Генерируя для каждого пользователя уникальный Destination tag, мы понимаем, от кого пришла какая транзакция. И это позволяет нам использовать один кошелек для большой группы пользователей.
Обработка неуспешных транзакций
Что-то может пойти не так по нескольким причинам (этот список можно продолжить):- транзакция была отменена (например, неправильный формат данных);
- возникла ошибка при выполнении транзакции;
- зависла из-за низкой fee;
- недостаточно средств (обычно следствие double spending attack или ошибки в сумме транзакции).
Большие числа
Помним, что транзакции обычно осуществляются в единицах, намного меньших, чем единица самой валюты. Например, в одном ETH квинтиллион wei. Это тысяча квадриллионов или миллион триллионов. Короче, это много. И Integer здесь будет маловато. Вот классная статья на эту тему.̶Б̶ы̶т̶ь̶ ̶и̶л̶и̶ ̶н̶е̶ ̶б̶ы̶т̶ь̶ Строка или число
Под каждую криптовалюту существуют свои библиотеки работы с ними. Разнообразие усиливается количеством языков программирования. И разработчики разделились на тех, кто привык передавать, например, суммы транзакций как строки, и тех, кто предпочитает числа. Просто обращайте на это внимание во время разработки и тестирования.А как же безопасность?
Конечно, это важный аспект. Ведь количество оборачиваемых на бирже средств вынуждает обращать на безопасность внимание и выделять значительные средства на ее обеспечение. Но, во-первых, эта тема заслуживает отдельной статьи. Во-вторых, безопасность обеспечивается в основном другими системами криптобиржи (например, KYC, 2FA), работа которых не рассматривается в данной статье. К тому же в начале статьи есть ссылки на отличные материалы по этой теме.На этом предлагаю данный раздел закончить и перейти к более интересной теме — тестового окружения.
Где тестировать
Структура проекта
Перед обсуждением непосредственно окружений рассмотрим, как обстояли дела на проекте, где мне посчастливилось трудиться в качестве QA-инженера, покрывая все нужды проекта, начиная от мануального тестирования и заканчивая автоматизированными end-to-end тестами. Обзор схемы позволит понять логику тестов, которые будут рассмотрены ниже.Данная схема является упрощенным изображением реальной архитектуры. На ней нет, к примеру, модулей валидации, обработки невалидных транзакций, работы со смарт-контрактами, контроля балансов кошельков и прочих важных штук. Всё в целях простоты (+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 на несколько порядков);
- вы полностью контролируете блокчейн: запускаете/останавливаете майнинг, генерируете блоки, нужное количество крипты в кошельке, делаете сколько угодно транзакций и мгновенно их подтверждаете или не подтверждаете.
Несмотря на то, что данный вид тестирования подразумевает развертывание системы локально, есть несколько нюансов. Очевидно, что для взаимодействия с блокчейном (даже если это его эмуляция, которую можно запустить на одной машине) нужна нода этого самого блокчейна (минимум одна). Решается это достаточно просто — берем и стартуем ноду. Но проблема состоит в том, что в тестировании у нас сотни разных валют. И почти каждая требует свою ноду. Понятно, что никакой ПК/лэптоп такого не потянет. Приходится использовать облако. А это дорого. А учитывая необходимые мощности, это очень дорого. Да и локальным теперь это не назовешь.
Теперь подумаем, как же все это автоматизировать. Контент будет основан на моем личном опыте.
Используемые инструменты/языки:
- JavaScript/TypeScript;
- Jest;
- Axios;
- Docker;
- библиотеки работы с блокчейнами (например, эта);
- публичные API (примеры ниже);
- faucets (например, этот);
- обозреватели блокчейнов (пример будет ниже);
- Metamask и др.
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.
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 в нашем случае).
step('created wallet is written to DB', async () => {
const walletInDB = await walletTable.getWalletByAddress(kafkaMessage.wallets[0].address);
});
Шаг 3: Отправка и перехват сообщения с адресом кошелька
Адрес кошелька отправляется в Kafka, чтобы соответствующий модуль биржи смог отобразить этот адрес юзеру. И мы в модуле Tests это сообщение перехватываем.
step('intercept message about wallet creation', async () => {
data.currency = currency;
interceptKafkaMessage(externalConsumerTopics.userWalletsCreate.topic, data);
});
Шаг 4.0: Precondition (да, еще один)
Чтобы юзер пополнил свой кошелек на бирже, у него должны быть средства. Чтобы симитировать депозит, нужен какой-то кошелек, с которого мы будем пополнять пользовательский кошелек биржи.
Особенность некоторых локально поднятых блокчейнов — в них нет монет. Поскольку это имитация реального блокчейна, монеты надо намайнить. Эту задачу мы и решаем на текущем шаге.
Пример для 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. Депозит (пополнение счета)
Пополняем баланс кошелька, который для нас сгенерировал 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. Определение неподтвержденной транзакции
На этом шаге 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-менеджера — сохранение информации о транзакции в базе:
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) подтверждениями транзакций занимаются майнеры. Здесь очень кратко, но понятно описан процесс. В случае локально работающего блокчейна единственный возможный майнер — это мы сами.
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 определяет, что транзакция была подтверждена (на основании минимально необходимого количества подтверждений в конфигурации).
step('transaction with CONFIRMED status is saved to DB', async () => {
transaction = await transactionTable.getTransactionByHash(txId);
expect(transaction.status).toEqual('CONFIRMED');
});
Данные о транзакции обновляются в базе.
На всякий случай можем сами обратиться к блокчейну и проверить, что баланс пополненного кошелька соответствует сумме, которую мы перевели:
step('user wallet balance is updated', async () => {
const userWalletBalance = await regtestnetwork[currency].getWalletBalance(depositAddress);
expect(userWalletBalance).toEqual(depositAmount);
});
Шаг 8. Сообщение об успешном депозите
Теперь время сообщить бирже, что депозит был успешен, чтобы можно было обновить баланс пользователя по конкретной валюте и позволить ему торговать на эту сумму. Опять мы это сообщение перехватываем и выполняем нужные проверки.
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 иногда платны,
- иногда нестабильны,
- нет поддержки многих валют.
Еще одна проблема (точнее задача) тестнета — нужно где-то брать средства для осуществления транзакций. Напомню, что в regtest мы можем уже запустить ноду с кошельком, полным крипты, или запустить очень быстрый майнинг, или просто сгенерировать необходимое нам количество блоков за доли секунды. В тестнете это не прокатит, так как это имитация реального блокчейна и все транзакции должны подтверждаться майнерами.
Так где же брать средства в testnet?
Первый способ — использовать так называемые fausets (краны) — бесплатную раздачу крипты. Но у кранов есть ряд недостатков:
- не для каждой криптовалюты существуют;
- часто не работают;
- нередко требуют ввода капчи (что сильно усложняет автоматизацию тестирования);
- имеют ограничение на количество транзакций (часто лишь одна в день).
Есть и хорошая новость: некоторые блокчейны дают тестовые монеты, причем в таком количестве, что их точно хватит для целей тестирования. Среди них ETH, XRP.
Например, так вы можете получить тестовые монеты с помощью MetaMask:
Еще одним недостатком тестнета является нестабильность. Даже самые надежные блокчейны иногда лежат.
{
"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 есть у всех криптовалют, и мы можем ожидать от него стабильной работы. В то же время он обладает рядом свойств, ограничивающих наши возможности тестирования внутри него:- дорого;
- очень дорого;
- долго.
Но подход тестирования в mainnet можно смело использовать как минимум в двух случаях:
- у валюты нет testnet и/или regtest,
- транзакции в mainnet очень дешевые.
Опять-таки для некоторых валют mainnet — единственный вариант тестирования.
Еще одно преимущество mainnet — наличие работающих, так называемых blockchain explorers (обозреватель блокчейна). Они позволяют получать информацию о транзакциях, балансах, блоках и дают много другой полезной информации. Обозреватель BTC, для примера. Из недостатков могу указать на то, что зачастую такие обозреватели предоставляют не разработчики блокчейна, а сторонние разработчики.
Стоит заметить, что обозреватели существуют и для testnet. Пример для BTC. Так как в идеале мы всегда имеем два параллельно работающих блокчейна — mainnet и testnet, с отдельными транзакциями и кошельками (а иногда и форматами кошельков).
Выводы: использовать mainnet целесообразно, когда транзакции очень дешевые и/или у блокчейна нет testnet/regtest.
Где чек-лист?!
По сути тестирование криптобиржи не отличается от тестирования других приложений. Используются стандартные техники тест-дизайна. Особенность — наличие большого количества кейсов, но это скорее следствие сложности системы, а не наличия блокчейна. В общем, все сводится к тому, чтобы проверить положительные флоу и максимально продумать и проверить негативные. Здесь вам и придется применить все навыки тестировщика.Первая часть этой статьи — это своеобразный чек-лист, в котором я попытался агрегировать:
- Максимально общие вещи, характерны для всех криптовалютных бирж.
- Негативные кейсы, рассмотрение которых должно и помочь вам в тестировании, и натолкнуть на поиск других кейсов.
- Разные подходы и разнообразие решений.
DOU
DOU – Найбільша спільнота розробників України. Все про IT: цікаві статті, інтервʼю, розслідування, дослідження ринку, свіжі новини та події. Спілкування на форумі з айтівцями на найгарячіші теми та технічні матеріали від експертів. Вакансії, рейтинг IT-компаній, відгуки співробітників, аналітика...
dou.ua