Привет, Хабр!


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


В этой статье я постараюсь максимально подробно, без акцента на каком-либо языке программирования, описать, как сделать приём платежей Bitcoin (а также, при желании — Litecoin, Dash, Bitcoin Cash, Steep, ONION и т.п.), начиная с разворачивания полной ноды и заканчивания проверкой поступления платежа.


Предварительные требования


Подразумевается, что вы имеете сайт, размещенный на VPS, к которой у вас есть root доступ, а также готовы тратить по $15+ на оплату сервера для кошелька.


Установка кошелька


Первым делом надо выделить отдельный сервер для размещения кошелька. Почему именно отдельный сервер? Отдельный сервер позволит снизить риски вывода всех ваших средств злоумышленником в случае взлома основного сайта. Ну и не стоит забывать, что для хранения blockchain требуется много места на диске (~150Gb места на диске и т. п. — подробности по ссылке).


Какие есть варианты дешевых серверов? Их масса, на мой взгляд самый адекватный — сервера от hetzner.de или chipcore.com. На chipcore.com, например, можно взять dedicated с диском на 500Gb (хватит на BTC и еще пару блокчейнов) всего за 990 рублей (примерно 17 баксов). Если знаете что-то дешевле — пишите в комментариях, очень интересно (думаю, не только мне).


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


На сервере должна быть установлена любая подходящая операционная система, самый простой вариант — Ubuntu 16.10 (да, на самом деле — это не лучший выбор, лучше установить 16.04 либо дождаться 18.04 и подождать еще пару месяцев для стабилизации). Как правило, заморачиваться с разбивкой диска нет смысла и можно смело использовать 2-4Gb на swap и остальное пускать на корневой раздел (/ или root).


После того, как сервер будет доступен, первое, что надо сделать — отключить авторизацию по паролям и настроить авторизацию по ssh ключам. Сделать это достатчно просто, есть хорошее описание от DigitalOcean.


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


Устанавливаем bitcoind


sudo apt-add-repository ppa:bitcoin/bitcoin
sudo apt-get update
sudo apt-get install bitcoind

Это всё, что требуется для установки ноды


Настройка bitcoind


Первым делом надо создать пользователя bitcoin:


adduser bitcoin
# везде выбираем стандартные значения и указываем какой-нибудь сложный пароль

и создать служебные директории:


mkdir -p /etc/bitcoin
chown bitcoin: /etc/bitcoin
mkdir -p /run/bitcoind
chown bitcoin: /run/bitcoind
mkdir -p /var/lib/bitcoind
chown bitcoin: /var/lib/bitcoind

Теперь осталась самая мелочь – корректно настроить ноду для приёма JSON RPC запросов.


Минимальный конфиг будет выглядеть так:


rpcuser=USERNAME
rpcpassword=PASSWORD
rpcbind=127.0.0.1
rpcallowip=127.0.0.1/32

Его надо положить по адресу /etc/bitcoin/bitcoin.conf. И не забыть установить корректного владельца:


chown bitcoin: /etc/bitcoin/bitcoin.conf

Важно: использование USERNAME и PASSWORD — deprecated метод и немного не безопасный. Более правильно использовать rpcauth, пример можете найти по ссылке.


Далее, достаточно настроить systemd сервис для запуска ноды (в том числе после перезагрузки).


Для этого можно просто скопировать юнит файл, размещенный по адресу в директорию /etc/systemd/system/:


wget https://raw.githubusercontent.com/bitcoin/bitcoin/master/contrib/init/bitcoind.service -O /etc/systemd/system/bitcoind.service

После чего запустить его и настроить автозапуск:


systemctl daemon-reload
systemctl start bitcoind
systemctl enable bitcoind

Теперь можно проверить рабостопособность ноды:


curl --data-binary '{"jsonrpc": "1.0", "method": "getinfo", "params": [] }'  -H 'Content-Type: application/json' http://USERNAME:PASSWORD@127.0.0.1:8332/

Если всё ок — в ответ придёт примерно такое сообщение:


{"result":{"balance":0.000000000000000,"blocks":59952,"connections":48,"proxy":"","generate":false,
     "genproclimit":-1,"difficulty":16.61907875185736},"error":null,"id":"curltest"}

Настройка сервера основного сайта


Осталось только настроить сервер, на котором расположен ваш сайт.


Наиболее безопасный и простой способ сделать доступным на бекенде API кошелька — прокинуть ssh туннель через сервис systemd (ну или любой другой init сервис). В случае использования systemd конфигурация сервиса максимально проста:


[Unit]
Description=SSH Tunnel BTC
After=network.target

[Service]
Restart=always
RestartSec=20
User=tunnel
ExecStart=/usr/bin/ssh -NT -o ServerAliveInterval=60 -L 127.0.0.1:8332:127.0.0.1:8332 tunnel@YOUR_SERVER_IP

[Install]
WantedBy=multi-user.target

Эту конфигурацию нужно разместить по пути /etc/systemd/system/sshtunnel-btc.service.


После этого ставим сервис в автозапус и запускаем:


systemctl enable sshtunnel-btc.service
systemctl start sshtunnel-btc.service

Для проверки можно постучаться на порт локалхоста и проверить, что всё ок:


curl --data-binary '{"jsonrpc": "1.0", "method": "getinfo", "params": [] }'  -H 'Content-Type: application/json' http://USERNAME:PASSWORD@127.0.0.1:8332/

Документация API


Со списком всех методов удобнее всего ознакомиться по ссылке.


Вызывать их очень просто даже через curl, пример запроса мы уже использовали ранее при получении информации о ноде методом getinfo.


Есть два варианта передачи параметров — массивом либо словарём.


Ниже можно увидеть примеры запроса на получение нового адреса с передачей параметров массивом и словарём:


# array
curl --data-binary '{"jsonrpc": "1.0", "method": "getnewaddress", "params": ["test"] }'  -H 'Content-Type: application/json' http://USERNAME:PASSWORD@127.0.0.1:8332/
# object
curl --data-binary '{"jsonrpc": "1.0", "method": "getnewaddress", "params": {"account": "test"} }'  -H 'Content-Type: application/json' http://USERNAME:PASSWORD@127.0.0.1:8332/

Простой API клиент


Для использования удобно написать простую обертку с нужными нам функциями (либо использовать имеющуюся библиотеку для вашего языка). Пример для ruby:


class Btc
  class BtcError < StandardError; end

  SUPPORTED_METHODS = %w(get_new_address get_addresses_by_account get_info get_net_totals get_balance get_received_by_address send_to_address list_transactions get_transaction)

  class << self
    def method_missing(method_name, *args)
      if SUPPORTED_METHODS.include?(method_name.to_s)
        send_request(method_name.to_s, args.empty? ? nil : args)
      else
        super
      end
    end

    def respond_to_missing?(method_name, _include_private = false)
      SUPPORTED_METHODS.include?(method_name) || super
    end

    protected

    def host
      ENV["HOST"] || "http://username:password@127.0.0.1:8332"
    end

    private

    def send_request(method, params = nil)
      uri = URI.parse(host)
      request = Net::HTTP::Post.new(uri)
      request.basic_auth uri.user, uri.password
      request.body = JSON.dump(
        jsonrpc: "1.0",
        method: method.tr("_", ""),
        params: params
      )

      req_options = { use_ssl: uri.scheme == "https" }

      response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
        http.request(request)
      end

      begin
        result = JSON.parse(response.body)
      rescue JSON::ParserError
        raise BtcError.new(response.body)
      end
      raise BtcError.new(result["error"].to_json) if result["error"]

      result["result"].is_a?(Hash) ? result["result"].deep_symbolize_keys : result["result"]
    end
  end
end

После этого можно удобно пользоваться им в примерно таком виде:


# run with HOST env variable (HOST=http://username:password@localhost:8332)
Btc.get_info
# => {result: {...}}

Аналогиный пример для node.js:


var http = require('http');

function BtcApi(host, port, username, password) {
  this.host = host;
  this.port = port;
  this.username = username;
  this.password = password;
};
BtcApi.methods = [
  'getNewAddress',
  'getAddressesByAccount',
  'getInfo',
  'getNetTotals',
  'getBalance',
  'getReceivedByAddress',
  'sendToAddress',
  'listTransactions',
  'getTransaction',
];
BtcApi.prototype.sendRequest = function(method, params, callback) {
  if (BtcApi.methods.indexOf(method) === -1) {
    throw new Error('wrong method name ' + method)
  };

  if (callback == null) {
    callback = params;
  };

  var body = JSON.stringify({
    jsonrpc: '1.0',
    method: method.toLowerCase(),
    params: params,
  });

  var auth = 'Basic ' + Buffer.from(this.username + ':' + this.password).toString('base64');

  var options = {
    host: this.host,
    port: this.port,
    path: '/',
    method: 'POST',            
    headers: {
      'Content-Type': 'application/json',
      'Authorization': auth
    },
  };

  var request = http.request(options, function (response) {
    var result = '';
    response.setEncoding('utf8');

    response.on('data', function (chunk) {
      result += chunk;
    });

    // Listener for intializing callback after receiving complete response
    response.on('end', function () {
      try {
        callback(JSON.parse(result));
      } catch (e) {
        console.error(e);
        callback(result);
      }
    });
  });

  request.write(body)
  request.end()
};

for (var i = 0; i < BtcApi.methods.length; i++) {
  BtcApi.prototype[BtcApi.methods[i]] = function (method) {
    return function (params, callback) {
      this.sendRequest(method, params, callback)
    }
  }(BtcApi.methods[i])
}

module.exports = BtcApi

Который можно использовать примерно следующим образом:


var BtcApi = require('./btc');

var client = new BtcApi('127.0.0.1', 8332, 'username', 'password');

client.listTransactions({ count: 1 }, function (response) {
  console.log('response: ', JSON.stringify(response));
});

// {"result":[{...}]}

Для Python всё еще проще – официальный способ — использование:


from jsonrpc import ServiceProxy

access = ServiceProxy("http://user:password@127.0.0.1:8332")
access.getinfo()

Собственно, с PHP также нет никаких проблем (рекомендуется использовать http://jsonrpcphp.org/):


  require_once 'jsonRPCClient.php';

  $bitcoin = new jsonRPCClient('http://user:password@127.0.0.1:8332/');

  echo "<pre>\n";
  print_r($bitcoin->getinfo()); echo "\n";
  echo "Received: ".$bitcoin->getreceivedbylabel("Your Address")."\n";
  echo "</pre>";

Хорошая подборка документации находится здесь.


Приведенные выше примеры являются немного доработанными версиями перечисленных по ссылке.


Интеграция с сайтом


Осталась достаточно простая часть — настроить обработку получения платежей и генерации адресов для пополнения.


Сам процесс интеграции приема платежей криптой выглядит примерно так:


  • При запросе на оплату от пользователя показываем ему адрес, куда переводить средства
  • В фоновом режиме (самый простой вариант — по cron) проверяем список транзакций кошелька и при поступлении новой — начисляем средства / меняем статус оплаты.

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


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


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


Для генерации адреса пополнения нужно вызвать метод getnewaddress, который в ответе вернёт новый адрес для пополнения. Для удобства можно передать аккаунт в качестве параметра (account), к которому будет привязан созданный адрес. Иногда это может быть удобно для просмотра транзакций по конкретному пользователю.


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


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


Важный момент при проверке транзакций — корректно указать кол-во подтверждений для защит от различных атак. Для большинства криптовалют обычно их можно найти в White Paper.


Для bitcoin рекомендуемое значение на данный момент — 6 подтверждений для небольших сумм. Здесь всё хорошо описано.


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


Заключение


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

Комментарии (15)


  1. nikitasius
    04.03.2018 21:52
    +1

    В биткоин клиенте нету давно аккаунтов, но архаизмы остались. Весь инет рекомендует оперировать raw транзакциями:


    1. генерим аддресс для юзера getrawchangeaddress (SO discussion)
    2. listunspent 6 (например транзакции с 6 конфирмами), в этом архиве отбираем все транзакции, которые пришли на сгенеренные адреса, и все.

    Всего 2 команды :)


    Отправка биткоинов (если дойдет до этого) делается через rawtransactions: createrawtransaction/signrawtransaction/sendrawtransaction, в первой указываются архивом транзакции на валете, которые надо использовать (через айди и vout). Следом там указыватся архив получателей. Комиссия — это недоданные биткоины. То бишь если там в сумме 0.9, а отправлено юзерам 0.8, то 0.1 уйдет как miner fee.


    curl --data-binary

    обычная тоже проходит: curl -u login:passwd -X POST -H "Content-Type: application/json" --data 'data_here' http://ip:port


    Заголовок спойлера

    у меня своя либа для нескольких валлетов (btc/eth/xmr) и 3 трейд сайтов. Самый кривой апи — это у ETH.


    1. joochooz Автор
      05.03.2018 00:51

      Да, действительно, на аккаунты лучше не опираться, как оказалось. Если кому-то интересно — то вот issue на github с доп. информацией.

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

      А по curl — я вообще обычно для краткости пишу -d 'data_here' ).

      А можно в личку скинуть линк на либу? Или она проприетарная?


      1. nikitasius
        05.03.2018 02:32

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

        Все платежи надо делать через них, иначе логику (выбор транзакций для создания новой) контролирует сам клиент (bitcoin core client). Комиссия — как я писал выше, это просто недоздача. Через core клиент получаем размер комиссию через estimatesmartfee 2, что даст стоимость для 1кб данных чтобы максимум через через 2 блока оказаться в blockchain. Следом расчитать размер комиссии по формуле IN146+OUT34+11 bytes, где IN — колво входых адресов, а OUT — выходных. Вот по этой формуле расчитывается комиссия, чтобы за 7-8 минут получить первый конфирм. Далее — многие используют усредненные комиссии, там 30 минут — час, когда несрочно.


        Транзакция, сделанная руками (тестнет, там комиссия от балды, но обычно 0.01): https://live.blockcypher.com/btc-testnet/tx/ffa1b85c05f52205d432f290680b2ccb09c93d5049186cdcfbc33953deb224c2/


        {"jsonrpc":"2.0","id":"23767114995403","method":"createrawtransaction","params":[[{"txid":"9f0ff312df1c879594c89f5c25c634ef11e95a61591daf295e7b931ac32b12ec","vout":0}],{"n3wm3yNqXURGzAHHjEMUzrtHtH2KKVgmLx":0.08}]} — 0.01 ушло на комиссию сети.


        затем вывод подписывается через signrawtransaction и ее вывод отправляется через sendrawtransaction.


        Кстати, про комиссию, если например надо отправить 3 BTC, комиссия 0.05BTC, и на входе мы имеем 5 BTC, то надо сделать raw и отправить 3 получателю, 1.95 себе на новый адресс. Это, например, биткоин клиент генерит сам (когда на автомате посылает). Так же в случае с raw можно делать транзакции от транзакций с 0 уровнем подтверждения, то бишь там пришли бабки, и надо с ним сделать неспешных платежей. То можно спокойно генерить новые tx от tx, которые были отправлены недавно.


        А можно в личку скинуть линк на либу? Или она проприетарная?

        Не могу расшарить, это ядро проекта, с пулом и прочими пряниками. Кстати пул очень хорошая вещь. И через raw надо группировать платежи юзерам. Это уменьшает комиссию им + можно очень много плюшек вводить (как создана логика пула). Даже далеко ходить не надо — просто группировать все на 1 адрес это уже пул. И потраченная комиссия потом просто взымается с юзеров. А если биткоины юзеров "простаивают", то они и так уже в системе. Но это к теме статьи не относится :)


        1. antonverov
          06.03.2018 16:32

          Вот по этой формуле расчитывается комиссия, чтобы за 7-8 минут получить первый конфирм

          Это довольно сложно, если учесть, что среднее время генерации блока 10 минут :)

          А что у вас за проект, если не секрет?


      1. nikitasius
        05.03.2018 02:55

        Да и либ в сети полно, как нормальных, так и не очень.
        Для узкоспециализорованных проектов надо как правио 5-7 вызовов реализовать из всего перечня. Далее — выходной формат унифицируется как возможно. Хотя для разных вылют разная точность.


  1. antonverov
    05.03.2018 20:58

    Для приема платежей можно запускать bitcoind с опцией -prune=551. В этом случае он будет занимать около 4Gb. И это все равно будет полная нода — с полной валидацией всех блоков локально.


    1. joochooz Автор
      05.03.2018 21:19

      Спасибо за способ, действительно может сильно снизить требования к серверу!


    1. nikitasius
      06.03.2018 16:04

      -prune=колво_мегабайт
      c 8192 он занимает 9.9


      Так что 4 с 551 мегабайтами — что-то там не так.


      1. antonverov
        06.03.2018 16:20

        sudo du -h
        77M ./blocks/index
        516M ./blocks
        424K ./database
        2.9G ./chainstate
        3.4G .

        551M — это размер под блоки.
        2.9G — chainstate — база непотраченных выходов (балансов).
        Всего — 3.4G


        1. nikitasius
          07.03.2018 13:41

          на тест лаптопе


          nikitas@pentagon:~/.bitcoin$ du -h
          72M ./blocks/index
          8.0G    ./blocks
          3.4G    ./chainstate
          259M    ./testnet3/blocks/index
          8.3G    ./testnet3/blocks
          954M    ./testnet3/chainstate
          9.2G    ./testnet3
          21G .

          В любом случае 500 метров под блоки какая-то экономия на спичках.


      1. antonverov
        06.03.2018 16:22

        Еще под кошелек (у меня его нет) мегабайт 500.


  1. vagran
    05.03.2018 21:13

    Для простого приёма оплаты по-моему проще подключить сторонний платёжный сервис типа BitPay. Некоторые из них позволяют принимать платежи в сотнях криптовалют, что самому точно не имеет смысла делать.


    1. joochooz Автор
      05.03.2018 21:20

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


  1. aml
    05.03.2018 21:34

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


    1. joochooz Автор
      05.03.2018 21:55

      Да, действительно, тоже хороший вариант, правда, я в своё время не смог найти быстро простого и удобного способа использовать BIP-32, плюс была необходимость в совершении автоматизированных платежей.