Большой рост цен на игровые видеокарты (сравните цены декабря 2017-го и февраля 2018-го, почувствуйте разницу) говорит о том, что предприимчивый пользователь не очень верит в падение курса биткойна. Но даже в такой холодной стране с дешевым электричеством, как Россия, себестоимость майнинга одного намайненного битка, с учетом риска дальнейшего падения и затруднений при обналичивании, не совсем себя оправдывает. Что же делать в такой ситуации? Как обычно — задействовать чужие компьютеры, переходить к распределенному майнингу, освобождать от тяжелой работы свою личную видеокарту, заварить чашку кофе и откинуться на спинку кресла.
РЕКОМЕНДУЕМ:
Как запустить свое ICO
Выбор криптовалюты и подготовка
В качестве криптовалюты мы выбрали биткойны — из-за их высокой стоимости и отличной (по сравнению с друигими криптовалютами) документации. Так как сеть биткойн не владеет централизованым хранилищем данных, чтобы с ней работать, нам потребуются средства синхронизации с другими узлами сети. К счастью, уже есть готовое решение — пакет программ Bitcoin Core. Синхронизация с живой сетью занимает очень много времени и места на диске, поэтому для начала разработки и тестирования лучше использовать тестовую сеть testnet, которую периодически очищают от информации о транзакциях. Итак, запускаем UI-приложение Bitcoin Core (testnet) и подождем, пока синхронизация закончится. В дальнейшем для работы будем использовать консольный сервис bitcoind.
Для общения с bitcoind используется протокол JSON-RPC. Это очень простой протокол поверх HTTP, который позволяет вызывать методы сервера, используя JSON, чтобы задавать имя метода и параметры.
По умолчанию в целях безопасности возможность подключения к bitcoind отключена. Чтобы ее включить, нужно создать файл bitcoin.conf в каталоге Windows: %APPDATA%\Bitcoin\ (например, C:\Users\username\AppData\Roaming\Bitcoin\bitcoin.conf) или Linux: $HOME/.bitcoin/ (например, /home/username/.bitcoin/bitcoin.conf). Готовый файл можно взять с GitHub. Находим в нем и редактируем следующие параметры:
1 2 3 4 5 |
## Говорим серверу использовать тестовую сеть, а не настоящую testnet=1 ## Имя пользователя и пароль, конечно же, нужно поменять на сложные rpcuser=rpcuser rpcpassword=rpcpassword |
Чтобы проверить настройки, можно воспользоваться готовым клиентом bitcoin-cli. Например, выполнив команду getinfo (предварительно запустив bitcoind). Подробное описание всех команд можно посмотреть тут.
Наш JSON-RPC-клиент напишем, используя фреймворк Vert.x, потому что он простой, код занимает мало места и в нем реализованы все необходимые нам функции.
Итак, создаем HTTP-клиент. Bitcoind использует базовую аутентификацию, поэтому конвертируем в Base64 строку с логином и паролем.
1 2 3 4 5 6 |
client = vertx.createHttpClient(); requestOptions = new RequestOptions() .setHost(host) .setPort(port) .setURI("/"); base64Key = Base64.getEncoder().encodeToString((user + ':' + password).getBytes()); |
Пишем простой метод для вызова методов bitcoind. Будем передавать в него команду в JSON-формате и handler, чтобы реагировать на полученный ответ от сервера.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private void executeRpc(String command, final Handler<Buffer> handler) { client .post(requestOptions, result → { if (result.statusCode() == 200) { result.bodyHandler(handler); } else { System.out.println("Failed do post because " + result.statusMessage()); } }) .putHeader(HttpHeaders.CONTENT_TYPE, "application/json") .putHeader(HttpHeaders.AUTHORIZATION, "Basic " + base64Key) .putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(command.length())) .write(command) .end(); } |
Основные команды, которые нам понадобятся, — это получить работу и отправить работу. Но прежде чем приступать к их реализации, рассмотрим подробнее процесс майнинга. Как известно, биткойн базируется на цепочке блоков, в которых хранится информация обо всех транзакциях. Каждый блок состоит из заголовка и списка транзакций. Задача майнера — получить хеш заголовка блока, значение которого меньше заданного. За это он вознаграждается некоторой суммой криптоденег.
Заголовок состоит из нескольких полей:
- версия (определяет версию валидации блока),
- хеш заголовка предыдущего блока,
- корень Меркле (хеш всех транзакций, включенных в блок),
- время создания блока,
- bits (закодированная версия заданного максимального значения хеша блока),
- nonce (произвольное значение).
Всю эту информацию позволяет получить команда getblocktemplate.
1 2 3 4 5 6 7 |
executeRpc(new JsonObject(ImmutableMap.of( "id", "1", "method", "getblocktemplate", "params", "", "jsonrpc", "1.0")).toString(), buffer → { createJobData(buffer.toJsonObject()); }); |
В результате получим JSON-объект.
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 |
{ "result": { "version": <версия блока>, "previousblockhash": "<хеш предыдущего блока>", "transactions": [ <транзакции, которые нужно включить в блок> { "data": "<данные транзакции>", "hash": "<хеш транзакции, который понадобится для построения корня Меркле>", "fee": <разница в биткойнах между входами и выходами>, ... } ... ], "coinbaseaux": { <данные, которые нужно включить в скрипт транзакции генерации новых денег> "flags": "" }, "coinbasevalue": <максимальная награда за майнинг этого блока>, "target": "<целевое значение для хеша блока>", "mintime": <минимальное возможное время создания блока>, "mutable": [ <изменяемые поля> "time", "transactions", "prevblock" ], "noncerange": "<допустимый диапазон nonce>", "curtime": <время создания блока>, "bits": "<упакованное значение target>", "height": <высота текущего блока> }, "error": null, "id": "1" } |
Чтобы добиться нужного значения хеша заголовка, майнер может менять nonce и время создания блока (в ограниченном диапазоне). Размерность nonce и ограничения на изменения времени создания блока приводят к ограниченному диапазону перебора возможных значений. Чтобы его увеличить, предусмотрена возможность изменения транзакций, в результате чего меняется корень Меркле, увеличивая тем самым варианты перебора. Задача пула — распределить работу между майнерами таким образом, чтобы их работа не дублировалась.
РЕКОМЕНДУЕМ:
Создание майнера криптовалюты Electroneum
Первой транзакцией в блоке обычно идет так называемая coinbase-транзакция, которая отвечает за генерацию новых денег. Она отличается от обычной транзакции тем, что не имеет входа, описывающего, откуда пришли деньги. Кроме того, в каждой транзакции есть поля scriptPubKey и scriptSig. В них содержится небольшая программа на языке Script, которая отвечает за валидацию транзакции. Чтобы транзакция считалась валидной, вызывается сначала scriptSig из старой транзакции, а затем scriptPubKey из новой транзакции. Если выполнение успешно, то транзакция считается валидной. Так как в coinbase-транзакции нет входящей транзакции, то поле scriptSig называется coinbase и в него можно писать любую информацию. Этим и пользуются пулы, чтобы увеличить диапазон перебираемых значений.
Разработчики Bitcoin немного перемудрили с представлением данных — то прямой, то обратный порядок байтов, поэтому, чтобы не сойти с ума, воспользуемся для генерации coinbase-транзакции Java-библиотекой bitcoinj.
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 |
TestNet3Params params = TestNet3Params.get(); // Используем тестовую сеть byte[] pubKeyTo = (new ECKey()).getPubKey(); // Тут должен быть валидный кошелек пула Coin coin = Coin.valueOf(blocktemplate.getJsonObject("result").getLong("coinbasevalue")); int height = blocktemplate.getJsonObject("result").getInteger("height"); String coinbaseauxFlags = blocktemplate.getJsonObject("result").getJsonObject("coinbaseaux").getString("flags"); byte[] extranonce = new byte[8]; String message = "Troyanpool Rulez!"; byte[] coinbase = generateCoinbaseTransaction(params, height, extranonce, message, pubKeyTo, coin).bitcoinSerialize(); private Transaction generateCoinbaseTransaction(NetworkParameters params, int height, String coinbaseauxFlags, byte[] extranonce, String message, byte[] pubKeyTo, Coin value) { Transaction coinbase = new Transaction(params); ScriptBuilder inputBuilder = new ScriptBuilder(); inputBuilder.number((long)height); byte[] coinbseauxFlagsData = isNotEmpty(coinbaseauxFlags) ? coinbseauxFlagsData = HEX.decode(coinbaseauxFlags) : new byte[0]; byte[] messageData = message.getBytes(); byte[] data = new byte[coinbseauxFlagsData.length + extranonce.length + messageData.length]; if (coinbseauxFlagsData.length > 0) { System.arraycopy(coinbseauxFlagsData, 0, data, 0, coinbseauxFlagsData.length); } System.arraycopy(extranonce, 0, data, coinbseauxFlagsData.length, extranonce.length); System.arraycopy(messageData, 0, data, coinbseauxFlagsData.length + extranonce.length, messageData.length); inputBuilder.data(data); coinbase.addInput(new TransactionInput(params, coinbase, inputBuilder.build().getProgram())); coinbase.addOutput(new TransactionOutput(params, coinbase, value, ScriptBuilder.createOutputScript(ECKey.fromPublicOnly(pubKeyTo)).getProgram())); return coinbase; } |
Для удобства майнеров (чтобы им лишний раз не делать вычисления) разделим coinbase-транзакцию на три части: coinbase1, extranonce, coinbase2.
Осталось обеспечить канал связи с майнерами. Для этого воспользуемся протоколом Stratum. Он базируется на TCP/IP-сокетах, через которые гоняются данные в формате JSON. Создадим обработчик соединения с майнерами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
vertx.createNetServer().connectHandler(netSocket → { // Конец команды обозначается символом перевода каретки RecordParser parser = RecordParser.newDelimited("\n", netSocket); parser .endHandler(v → netSocket.close()) .exceptionHandler(throwable → { throwable.printStackTrace(); netSocket.close(); }) .handler(buffer → { String inputCommand = buffer.toString("UTF-8"); processCommand(new JsonObject(inputCommand)); }); }).listen(33333); |
Протокол требует авторизации майнеров, а так как наши майнеры не совсем люди и никак не могут зарегистрироваться, можно логин генерировать, пароль же либо зашить в исходный код майнера, либо вычислять по известному пулу и майнеру алгоритму. Подробнее о протоколе Stratum можно почитать тут. Чтобы гарантированно распределять уникальную часть работы каждому майнеру, разделим extranonce на две части и в первой будем проставлять счетчик майнеров, а вторую разрешим изменять.
1 2 3 |
// Сохраним информацию о майнере Miner miner = new Miner(netSocket); miner.setExtranonce1(minerCounter++); |
Информацию об extranonce и уникальный идентификатор подписки отправляем клиенту в ответ на команду mining.subscribe:
1 2 3 |
if (command.getString("method").equalsIgnoreCase("mining.subscribe")) { String answer = "{\"jsonrpc\":\"2.0\",\"result\":[[\"mining.notify\",\"" + subscriptionId + "\"],\"" + extraNonce1 + "\"," + extraNonce2.length + "],\"id\":1}"; |
Сформируем задание для майнера.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "params": ["<идентификатор задания>", "<хеш предыдущего блока>", "<coinbase1>", "<coinbase2>", ["<хеш транзакции 1>", ... "<хеш транзакции х>"], "<версия>", "<bits>", "<время создания блока>", <если true, то необходимо переключиться на новую задачу>], "id": "1", "method": "mining.notify" } |
Когда майнер найдет решение, он пришлет вторую часть extranonce, время создания блока и nonce. Пулу необходимо собрать новый блок и отправить его в bitcoin-сеть.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
TestNet3Params params = TestNet3Params.get(); // Используем тестовую сеть // Не забываем добавить coinbase-транзакцию transactions.add(generateCoinbaseTransaction(params, height, extranonce, message, pubKeyTo, coin)); Block block = new Block(params, blockVersion, prevBlockHash, merkelRoot, ntime, difficulty, nonce, transactions); byte[] blockBytes = block.bitcoinSerialize(); executeRpc(new JsonObject(ImmutableMap.of( "id", "1", "method", "submitblock", "params", HEX.encode(blockBytes), "jsonrpc", "1.0")).toString(), buffer → { JsonObject result = buffer.toJsonObject(); if (result.getString("result") == null) { System.out.println("Блок успешно вставлен!"); } }); |
Когда блок успешно добавлен, нужно уведомить всех клиентов об отмене работы над этим блоком. Для этого используем шину событий.
1 2 3 4 5 6 7 8 9 |
// Сервер подписывается на события vertx.eventBus().consumer("miner.notify", message → { for (Miner miner : miners) { miner.getSocket().write(message.body().toString()); } }); ... // Планировщик заданий отправляет уведомление о новой работе vertx.eventBus().publish("miner.notify", jobNotification); |
Рекомендуем к прочтению: Лучшие ICO-компании
Биткойн майнер готов!
Наш проект завершен. Осталось добавить обработку ошибок, и можно начинать майнить. Однако помните, что пул для майнинга требует хорошего канала и довольно много места на диске. И не забудьте предупредить об этих ограничениях друзей, которые добровольно согласятся вам помочь в майнинге криптовалюты, ведь вы не хотите ненароком их огорчить?