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

Выбираем валюту и готовим инструментарий

В качестве криптовалюты мы выбрали биткойны — из-за их высокой стоимости и относительно хорошей документации. Так как биткойн-сеть не имеет централизованного хранилища данных, чтобы с ней работать, нам понадобятся средства синхронизации с другими узлами сети. К счастью, есть готовое решение — пакет программ 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. Находим в нем и редактируем следующие параметры:

## Говорим серверу использовать тестовую сеть, а не настоящую
testnet=1 
## Имя пользователя и пароль, конечно же, нужно поменять на сложные
rpcuser=rpcuser
rpcpassword=rpcpassword

Ты хотел бы сделать свой пул для майнинга?

Загрузка ... Загрузка ...

Чтобы проверить настройки, можно воспользоваться готовым клиентом bitcoin-cli. Например, выполнив команду getinfo (предварительно запустив bitcoind). Подробное описание всех команд можно посмотреть тут.


Наш JSON-RPC-клиент напишем, используя фреймворк Vert.x, потому что он простой, код занимает мало места и в нем реализованы все необходимые нам функции.

Итак, создаем HTTP-клиент. Bitcoind использует базовую аутентификацию, поэтому конвертируем в Base64 строку с логином и паролем.

client = vertx.createHttpClient();
requestOptions = new RequestOptions()
        .setHost(host)
        .setPort(port)
        .setURI("/");
base64Key = Base64.getEncoder().encodeToString((user + ':' + password).getBytes());

Пишем простой метод для вызова методов bitcoind. Будем передавать в него команду в JSON-формате и handler, чтобы реагировать на полученный ответ от сервера.

private void executeRpc(String command, final Handler<Buffer> handler) {
    client
        .post(requestOptions, result &rarr; {
            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.

executeRpc(new JsonObject(ImmutableMap.of(
        "id", "1",
        "method", "getblocktemplate",
        "params", "",
        "jsonrpc", "1.0")).toString(), buffer &rarr; {
    createJobData(buffer.toJsonObject());
});

В результате получим JSON-объект.

{
  "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 и ограничения на изменения времени создания блока приводят к ограниченному диапазону перебора возможных значений. Чтобы его увеличить, предусмотрена возможность изменения транзакций, в результате чего меняется корень Меркле, увеличивая тем самым варианты перебора. Задача пула — распределить работу между майнерами таким образом, чтобы их работа не дублировалась.

Первой транзакцией в блоке обычно идет так называемая coinbase-транзакция, которая отвечает за генерацию новых денег. Она отличается от обычной транзакции тем, что не имеет входа, описывающего, откуда пришли деньги. Кроме того, в каждой транзакции есть поля scriptPubKey и scriptSig. В них содержится небольшая программа на языке Script, которая отвечает за валидацию транзакции. Чтобы транзакция считалась валидной, вызывается сначала scriptSig из старой транзакции, а затем scriptPubKey из новой транзакции. Если выполнение успешно, то транзакция считается валидной. Так как в coinbase-транзакции нет входящей транзакции, то поле scriptSig называется coinbase и в него можно писать любую информацию. Этим и пользуются пулы, чтобы увеличить диапазон перебираемых значений.

Описание полей coinbase-транзакции
Описание полей coinbase-транзакции

Разработчики Bitcoin немного перемудрили с представлением данных — то прямой, то обратный порядок байтов, поэтому, чтобы не сойти с ума, воспользуемся для генерации coinbase-транзакции Java-библиотекой bitcoinj.

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. Создадим обработчик соединения с майнерами.

vertx.createNetServer().connectHandler(netSocket &rarr; {
    // Конец команды обозначается символом перевода каретки
    RecordParser parser = RecordParser.newDelimited("\n", netSocket);
    parser
            .endHandler(v &rarr; netSocket.close())
            .exceptionHandler(throwable &rarr; {
                throwable.printStackTrace();
                netSocket.close();
            })
            .handler(buffer &rarr; {
                String inputCommand = buffer.toString("UTF-8");
                processCommand(new JsonObject(inputCommand));
            });
}).listen(33333);

Где лучше организовать пул для майнинга?

Загрузка ... Загрузка ...

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

// Сохраним информацию о майнере
Miner miner = new Miner(netSocket);
miner.setExtranonce1(minerCounter++);

Информацию об extranonce и уникальный идентификатор подписки отправляем клиенту в ответ на команду mining.subscribe:

if (command.getString("method").equalsIgnoreCase("mining.subscribe")) {
    String answer = "{\"jsonrpc\":\"2.0\",\"result\":[[\"mining.notify\",\"" + subscriptionId + "\"],\"" 
        + extraNonce1 + "\"," + extraNonce2.length + "],\"id\":1}";

Сформируем задание для майнера.

{
 "params": 
  ["<идентификатор задания>", 
  "<хеш предыдущего блока>", 
  "<coinbase1>", 
  "<coinbase2>", 
  ["<хеш транзакции 1>", ... "<хеш транзакции х>"], 
  "<версия>", 
  "<bits>", 
  "<время создания блока>", 
  <если true, то необходимо переключиться на новую задачу>], 
  "id": "1", "method": "mining.notify"
}

Когда майнер найдет решение, он пришлет вторую часть extranonce, время создания блока и nonce. Пулу необходимо собрать новый блок и отправить его в bitcoin-сеть.

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 &rarr; {
    JsonObject result = buffer.toJsonObject();
    if (result.getString("result") == null) {
        System.out.println("Блок успешно вставлен!");
    }
});

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

// Сервер подписывается на события
vertx.eventBus().consumer("miner.notify", message &rarr; {
    for (Miner miner : miners) {
        miner.getSocket().write(message.body().toString());
    }
});
...
// Планировщик заданий отправляет уведомление о новой работе
vertx.eventBus().publish("miner.notify", jobNotification);

Как думаешь, затраты на пул окупятся?

Загрузка ... Загрузка ...

Вуаля!

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

8 комментариев

  1. Аватар

    Владиславище

    26.02.2018 в 13:00

    Это необходимо сделать в организации, где ты работаешь СисАдмином на подряде!!! И запретить на ночь выключать компы! :)))

  2. Аватар

    john_

    09.03.2018 в 19:56

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

  3. Аватар

    valpamaxim

    15.03.2018 в 14:03

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

  4. Аватар

    renok

    09.04.2018 в 14:42

    Уважаемый автор, очень актуальная статья получилась (если bitcoin на hodlcoin поменять), к сожалению очень трудно оказалось повторить, то что в ней описано. Будет ли работать код в Vert.x 3, как подключить библиотеку bitcoinj, в каком приложении можно отладить сей код?

  5. Аватар

    mrbl

    16.04.2018 в 01:25

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

  6. Аватар

    mrbl

    16.04.2018 в 01:32

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

  7. Аватар

    gondagar

    17.02.2020 в 22:04

    Где линк на Github?

Оставить мнение