В качестве криптовалюты возьмем Electroneum. Это довольно перспективная криптовалюта из семейства Monero. Как заверяют разработчики, она защищена от майнинга на специальном оборудовании, точнее, оборудование будет стоить больше, чем можно получить прибыли. Это дает примерно равные шансы всем майнерам. Так как в качестве основы была использована Monero, многое из написанного будет правдиво и для других криптовалют этого семейства.
Рекомендуем ознакомиться со статьей «Mining Pool на Java: кодим распределенный биткойн-майнер».
Для начала разберемся, что же такое майнинг. По сути это проверка транзакций различных пользователей криптовалют. Нет никакого центрального органа, а подтвердить, что один участник сети не использовал свои деньги дважды или не попытался как-то еще обмануть систему, могут все остальные. За это майнеры получают награду в виде небольшого количества криптоденег. В эту сумму входит награда за создание нового блока и оплата за транзакции, которая взимается с пользователей, проводящих транзакцию, и уже включена в нее.
Создание нового блока представляет собой решение определенной математической задачи. Необходимо найти такой хеш блока, который был бы меньше значения, определяемого сетью. Это значение называется сложность (difficulty). Оно регулируется сетью, чтобы время создания блока было более-менее предсказуемо. Майнер, который первый решит задачу, получает всю награду. Награда за блок на сегодняшний день составляет 11 300,93 ETN, что примерно равно 146,2 доллара.
В блоке не обязательно должны быть транзакции других пользователей, может быть только одна транзакция создания новых денег. Зачем нужно просто раздавать деньги? Во-первых, это привлекает больше участников сети, во-вторых, снижает риск атаки на сеть, так как заработать легально получается проще.
Чтобы стать участником сети Electroneum, необходимо скачать пакет программ с официального сайта. Выбираем direct miner для своей платформы. После скачивания и распаковки нужно синхронизироваться с сетью — скачать все уже сгенерированные блоки. Для разработки и тестирования лучше пользоваться тестовой сетью с пониженной сложностью.
К сожалению, синхронизация «из коробки» может зависнуть на блоке 155750. Это связано с найденным критичным багом и кардинальными изменениями из-за этого в сети Electroneum (подробнее). Поэтому прежде чем запускать синхронизацию, нужно скачать файлик с правильной цепочкой блоков и положить его в папку .electroneum/testnet/export/blockchain.raw. Затем выполнить импорт:
1 |
> ./electroneum-blockchain-import --testnet --verify 0 |
Теперь смело запускаем синхронизацию:
1 |
> ./electroneumd --testnet |
Далее создаем кошелек для начисления заработка:
1 |
> electoneum-wallet-cli --testnet |
Ответив на все вопросы, получаем публичный адрес в файлике <название кошелька>.address.txt. Если лениво заморачиваться с развертыванием сервера Electroneum, можно воспользоваться онлайн-сервисом nodes.hashvault.pro:26968.
Настало время запустить свой любимый редактор и приступать к кодированию. Для связи с сервисом Electroneum используется протокол jsonrpc. Нам понадобится всего две команды: получить шаблон блока и отправить решение. Начнем с простого HTTP-клиента:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public String sendRpcCommand(String command) { // Определяем URL для связи с сервером. Для реальной, не тестовой сети порт будет 26968 URL url = new URL("http://127.0.0.1:34568/json_rpc"); HttpURLConnection con = (HttpURLConnection) url.openConnection(); // Задаем параметры соединения. Разрешаем вывод, чтобы забрать ответ сервера con.setDoOutput(true); // Передавать будем данные в формате JSON con.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); con.setRequestProperty("Accept", "application/json"); con.setRequestMethod("POST"); // Отправляем команду OutputStream os = con.getOutputStream(); os.write(command.getBytes("UTF-8")); os.close(); StringBuilder sb = new StringBuilder(); int HttpResult = con.getResponseCode(); if (HttpResult == HttpURLConnection.HTTP_OK) { // Если соединение успешно, то забираем ответ сервера BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8")); String line; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } br.close(); return sb.toString(); } else { // Если соединение не удалось, то бросаем исключение с описанием проблемы throw new IOException(con.getResponseMessage()); } } |
Чтобы получить шаблон блока для вычисления, отправим команду
1 2 3 4 5 6 7 8 9 |
{ "jsonrpc":"2.0", "id":"0", "method":"get_block_template", "params":{ "wallet_address":"44GBHzv6ZyQdJkjqZje6KLZ3xSyN1hBSFAnLP6EAqJtCRVzMzZmeXTC2AHKDS9aEDTRKmo6a6o9r9j86pYfhCWDkKjbtcns", "reserve_size":8 } } |
В качестве параметра wallet_address указываем адрес из файла <название кошелька>.address.txt. Адрес используется, чтобы сразу сгенерировать транзакцию получения награды за расчет блока. Параметр reserve_size задает, сколько выделить зарезервированных байтов, которые потом можно использовать при майнинге. Максимальное число — 255 байт.
В результате получаем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "id": "0", "jsonrpc": "2.0", "result": { "blockhashing_blob": "070784e5dbda054486739aac8830906e18272012b97b98993afccf89d0044241193d1788f760cb0000000057754af7e8324054869263b355ede600c2381cbdf6acf2dc8f2b26f4a9a82bae14", "blocktemplate_blob": "070784e5dbda054486739aac8830906e18272012b97b98993afccf89d0044241193d1788f760cb000000000183d71401fff1d61401eff844020117b5d2cead5bd512ab4b0a2e73377049c49c69ffc916687e811bbb0f5f65322b01d67fec53c3f1cab976537a4ab4ebba03c89849d554963df6ed1a0023e6d5d9e90208000000000000000013d5c0b347172631f9b0175365936f98b00198d8caf3dbb77edc6c002dbb6c302776e7d543da92fdf5c30e91d4b21762eb6fe5daf8959b519f3de65a3cd80adda1e5674fedeb2a5038577ea2fe9eb6a3fd2162a3a09cbe6d3b62c9b04a29d47c5c14c119f0812448ab4e14a76f1c2ddc2ff6ac0b97f1fb9e4cabf0ef2adf79221a3e865b8d9252f41f31e110326b78b0c506e9f18eb094305b6216221c2bd3f9d996bedf54dbb4c0bfe4fea6f2240181c91789270a48cae44d7662e1a13aae45c3edc3247736879f6aa2670b8816e551856b912f11269979fac1c97203365247eaee476ed815e3fa597b5230db7e0162816b55b23d2bfb8b9506492e8359f8ba33807eab0972a7837893163cadf314888dbb64190fa00553156dc7b05574eacd3b9a268666201ab202b23ecf960565c01a6a61fe5f03ba5b6c22d7e6639e7708941c876ecdc191cec4c5797e520855d9cc34ef9c3866ded9a4722c6437363bb7a47c9dbd303c15a18dfb72028054cd438924978f5c5d32be3bcbc622e0fb4b9aef865fea52a09f518952ec0aa94bbfa969f192a93b80a50fe7af2728cbd76e739e9af80aee2644fb2bbe1c82724bdc4678a5a206a945a3e49dabcb10ae0f25d473aa76e0275c4f9fa1cffc3e1d8748278561b99953966606a5d891717b4fb0366a77e38db4c267c3724e994532ae97fc7b12842157d8a11bc97926eb9978c82a07afc573a04660247a94c5c4f14556fbcc9aa367b7bef4fdf18b626b4342d4e84850f133076dcd26c16d3efe9f85fa29c757acda5dff2fe26fbf87d937be455d4053e4246a3055ace5fcb6d6545aa3cd0b2e21ea3648f0dd6cde386933381b7116", "difficulty": 237219196877, "expected_reward": 1129583, "height": 338801, "prev_hash": "4486739aac8830906e18272012b97b98993afccf89d0044241193d1788f760cb", "reserved_offset": 126, "status": "OK" } } |
Рассмотрим подробнее первые два поля.
Electroneum предоставляет две возможности для майнинга. Можно использовать готовый для расчета хеша blockhashing_blob, подбирая четыре байта nonce. Из достоинств — не нужно рассчитывать самому корень Меркле для транзакций. Из недостатков — довольно скудный набор возможных значений, среди которых может и не найтись нужного.
Второй вариант — использовать сырой блок blocktemplate_blob. Тут уже можно перебирать как четыре байта nonce, так и значение блока дополнительных данных, что заметно расширяет вероятность нахождения нужного значения. Но приходится считать хеш первой транзакции и корень Меркле, а только потом рассчитывать хеш самого блока.
Для начала попробуем первый вариант. Напишем небольшой метод, который будет перебирать значения nonce.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public static boolean findProperNonce(byte[] blockheader, int nonceByteIndex, long difficulty) { byte nonceByte = Byte.MIN_VALUE; while (nonceByte != Byte.MAX_VALUE) { blockheader[39 + nonceByteIndex] = nonceByte; if (nonceByteIndex < 3) { boolean found = findProperNonce(blockheader, nonceByteIndex + 1, difficulty); if (found) { return true; } } else { byte[] hash = calculateHash(blockheader); if (hasRequiredDifficulty(hash, difficulty)) return true; } nonceByte++; } return false; } |
Electroneum использует алгоритм хеширования CryptoNight. Описание алгоритма можно посмотреть тут. Хорошая новость — есть много готовых реализаций, плохая — практически все они написаны на С. К счастью, Java-машина прекрасно умеет запускать код на С. Поэтому, чтобы сократить время, возьмем готовую реализацию алгоритма и сделаем для нашего майнера подключаемую DLL’ку.
Для этого нам понадобится Cygwin. Это набор опенсорсных линуксовых утилит, которые можно запускать под виндой. При установке нужно выбрать пакеты mingw64-x86_64-gcc-core и mingw64-x86_64-gcc-g++.
Для загрузки библиотеки создадим класс CryptoNight в пакете com.gogaworm.electroneumminer.
1 2 3 4 5 6 7 8 9 |
public class Cryptonight { // Загружаем библиотеку с именем minerhashing, расширение писать не нужно static { System.loadLibrary("minerhashing"); } // Метод расчета хеша из библиотеки public native static void calculateHash(byte[] output, byte[] input); } |
Метод calculateHash объявлен как native, это означает, что он реализован на другом языке. Далее нужно сгенерировать файл заголовка:
1 |
> %JAVA_HOME%\bin\javah.exe -jni -v -d com/gogaworm/electroneumminer com.gogaworm.electroneumminer.Cryptonight |
В результате получим файл com_gogaworm_electroneumminer_Cryptonight.h. В нем объявлен метод Java_com_gogaworm_electroneumminer_Cryptonight_hash, который нужно реализовать на С. Для этого создадим файл с таким же именем, но расширением .c. Оба файла нужно перенести в папку с исходниками libcryptonight.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Обязательно нужно включить файл — заголовок для jni. Он находится в папке с установленной Java #include <jni.h> JNIEXPORT void JNICALL Java_com_gogaworm_electroneumminer_Cryptonight_calculateHash (JNIEnv *env, jclass clazz, jbyteArray output, jbyteArray input) { // Копируем массивы данных в новую область памяти. Работать с массивами напрямую в JavaHeap нельзя, так как сборщик мусора может перемещать их в памяти unsigned char* inputBuffer = (*env)->GetByteArrayElements(env, input, NULL); unsigned char* outputBuffer = (*env)->GetByteArrayElements(env, output, NULL); // Определяем размеры массивов jsize inputSize = (*env)->GetArrayLength(env, input); jsize outputSize = (*env)->GetArrayLength(env, output); // Рассчитываем хеш cryptonight_hash(outputBuffer, inputBuffer, inputSize); // Освобождаем область памяти, использованную для массивов (*env)->ReleaseByteArrayElements(env, input, inputBuffer, JNI_ABORT); (*env)->ReleaseByteArrayElements(env, output, outputBuffer, JNI_COMMIT); } |
Теперь запускаем Cygwin-консоль и собираем DLL:
1 |
> x86_64-w64-mingw32-gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/win32" -shared -o minerhashing.dll -g com_gogaworm_electroneumminer_Cryptonight.c cryptonight.c crypto/aesb.c crypto/c_blake256.c crypto/c_groestl.c crypto/c_jh.c crypto/c_keccak.c crypto/c_skein.c crypto/oaes_lib.c |
Чтобы наш майнер увидел библиотеку, необходимо определить системную переменную Java java.library.path=<путь к библиотеке>.
Проверим, что библиотека работает правильно. В документе, описывающем алгоритм CryptoNight, есть два примера для проверки. Запустим один из них:
1 2 3 4 5 6 7 |
@Test public void testHashMethod() throws UnsupportedEncodingException { byte[] outputBuffer = new byte[32]; byte[] input = hexStringToByteArray(block); Cryptonight.calculateHash(outputBuffer, "This is a test".getBytes("US-ASCII")); assertEquals("a084f01d1437a09c6985401b60d43554ae105802c5f5d8a9b3253649c0be6605", bytesToHex(outputBuffer).toLowerCase()); } |
Остался метод проверки, найдено ли нужно значение. Команда get_block_template вернула в результате параметр difficulty. Этот параметр показывает условный коэффициент сложности нахождения нужного хеша. По спецификации сложность = ( 2^265 — 1 ) / целевое значение (target). Для этой формулы хеш блока нужно перевести из больших индейцев в мелкие. Затем сравним с текущей сложностью, чтобы понять, найдено ли нужное значение:
1 2 3 4 5 6 7 8 9 |
public static boolean hasRequiredDifficulty(byte[] hash, BigInteger difficulty) { BigInteger diff1 = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16); BigInteger reversed = new BigInteger(bytesToHex(Arrays.reverse(hash)), 16); BigInteger hashdiff = diff1.divide(difficulty); if (hashdiff.compareTo(difficulty) >= 0) { return true; } return false; } |
Чтобы проверить, верно ли работает метод, испытаем его на уже готовом блоке из сети. Получить его можно командой getblock. Возьмем блок с высотой 338 401. Его хеш равен
1 |
13b3cf8b04b6bb78f0c7c1a50f7e8656963c1f48a56ba89999eddf0531750b15 |
а сложность — 252087628780. В результате вычислений получаем, что hashdiff больше difficulty.
Когда найдено нужное значение nonce, можно отправлять блок в сеть. Это делает команда
1 2 3 4 5 6 7 8 |
{ "jsonrpc":"2.0", "id":"0", "method":"submitblock", "params":{ "Block ":"blob template с нужным nonce" } } |
Осталось перенести методы работы с сервером и майнинга в отдельные потоки, и простой майнер готов.
Вместо заключения
Как заявляют разработчики криптовалюты Electroneum, ее можно майнить даже на смартфонах. Приложение для майнинга уже лежит в Google Play. Но на самом деле там только симуляция майнинга: вместо того чтобы решать сложную криптографическую задачу, измеряют доступную производительность CPU, которую теоретически можно было бы использовать для майнинга, и на основе этого значения начисляется заработок. Поэтому майнер для Андроида будет выглядеть несколько иначе.
Но это уже совсем другая история.