Если вам нужно развернуть приватную сеть Ethereum на базе Geth, то это можно сделать, например, создавая узлы сети на физических серверах или виртуальных машинах. Однако намного легче использовать готовые образы (Images) контейнеров Geth из репозитория Docker hub. В этом случае можно установить целую сеть узлов даже на одной виртуальной машине или на одном физическом сервере.

В этой статье мы расскажем как установить Docker на сервере с ОС Debian 9 (виртуальном или физическом), как создать несколько контейнеров с работающим Geth и объединить их в приватную сеть. Мы приведем пример скрипта для Node.js, который обращается к узлам Geth, работающим в контейнерах.

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


Установка Docker описана на официальном сайте. Для Debian 9 и 10 вы найдете подробные инструкции на сайте https://docs.docker.com/install/linux/docker-ce/debian/.

Установка Docker
Прежде всего, обновите пакеты:

# apt-get update

Далее установите необходимые пакеты следующей командой:

# apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common

Добавьте официальный ключ GPG:

# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -

Убедитесь, что вы получили ключ с отпечатком 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88:

# apt-key fingerprint 0EBFCD88
pub   4096R/0EBFCD88 2017-02-22
Key fingerprint = 9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88
uid  Docker Release (CE deb) docker@docker.com
sub   4096R/F273FCD8 2017-02-22

Добавьте стабильный репозиторий Docker:

# add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"

Обновите пакеты и установите Docker:

# apt-get update
# apt-get install docker-ce docker-ce-cli containerd.io

Теперь остается только проверить, что все сделано правильно. Для этого запустите образ (Image) hello-world:

# docker run hello-world

Эта команда загрузит нужный образ, и запустит на выполнение. Как и следовало ожидать, вы увидите на консоли сообщение «Hello from Docker!».

Как видите, ничего сложного! Теперь можно приступить к созданию приватной сети Geth с узлами, работающими в контейнерах.

Создаем пользователя, каталоги и файлы


Создайте на сервере пользователя book, а в его домашнем каталоге — следующие подкаталоги:

/home/book/dock-test
/home/book/dock-test/distr

Далее в каталоге /home/book/dock-test создайте файл Dockerfile:

Листинг 1. Файл /home/book/dock-test/Dockerfile
FROM ubuntu:16.04
LABEL version="1.0"
LABEL maintainer="alexandre@frolov.pp.ru"
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install --yes software-properties-common
RUN add-apt-repository ppa:ethereum/ethereum
RUN apt-get update && apt-get install --yes geth
RUN adduser --disabled-login --gecos "" eth_book

COPY distr /home/eth_book/distr
RUN chown -R eth_book:eth_book /home/eth_book/distr

USER eth_book
WORKDIR /home/eth_book
RUN geth --nousb init distr/genesis.json
ENTRYPOINT bash


Этот файл будет использован при создании контейнеров Docker.

Еще вам нужно будет создать файл /home/book/dock-test/distr/genesis.json, необходимый для инициализации узлов сети Ethereum:

Листинг 2. Файл /home/book/dock-test/distr/genesis.json
{
 "config": {
 "chainId": 98760,
 "homesteadBlock": 0,
 "eip150Block": 0,
 "eip155Block": 0,
 "eip158Block": 0
 },
 "difficulty": "10",
 "gasLimit": "5100000",
 "alloc": {}
}


Здесь мы задаем идентификатор нашей приватной сети 98760. Для облегчения майнинга мы указали в параметре difficulty значение 10. Это позволит работать на виртуальных машинах с относительно небольшим объемом памяти (например, 4 Гбайта).

Создаем сеть и контейнеры


Для того чтобы наши узлы могли обмениваться данными между собой, создадим сеть между контейнерами:

# docker network create PRIVATENET

Далее нужно сделать текущим каталог /home/book/dock-test, в котором расположен файл Dockerfile. Затем создайте нужное количество контейнеров, например, три:

# docker build -t node01 .
# docker build -t node02 .
# docker build -t node03 .

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

Теперь откройте четыре консольных окна. В трех из них мы будем работать с контейнерами, а в четвертом — получать необходимые нам данных о контейнерах и сети.

В первом консольном окне выдайте такую команду:

# docker run --rm -it -p 8545:8545 --net=PRIVATENET node01

Вы увидите приглашение вида:

eth_book@304bf4f09063:~$

Во втором и третьем консольном окне введите, соответственно, следующие команды:

# docker run --rm -it -p 8546:8546 --net=PRIVATENET node02
# docker run --rm -it -p 8547:8547 --net=PRIVATENET node03

Создание узлов приватной сети Ethereum и аккаунтов


На данный момент у нас работают три контейнера с Geth. Давайте создадим в каждом из них узел нашей приватной сети Ethereum и аккаунт.

Введите в первых трех консольных окнах команду:

$ geth account new

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

После создания аккаунтов на консоли появятся публичные адреса ключей, похожие на эти:

Public address of the key:   0xc5Df10a76Bb559332c385F8cA789C0F37dD77A54
Public address of the key:   0x0C976006a5762779bA36AC590D1D8Ebac1Ca2981
Public address of the key:   0xaB627feab4e962222a3333F3b09182dF68bB9422

Сохраните адреса (у вас будут другие), т.к. они нам понадобятся для запуска узлов.

Теперь нужно запустить инициализацию узлов. Это делается при помощи команды:

$ geth --nousb init distr/genesis.json

Введите эту команду во всех консольных окнах наших контейнеров. Параметр --nousb отключает все коммуникации с USB-устройствами.

Запуск узлов Geth


Теперь откройте по очереди каждое из консольных окон и запустите там Geth с помощью приведенных ниже команд.

Первое окно:

$ geth --identity="Node01" --etherbase "0xc5Df10a76Bb559332c385F8cA789C0F37dD77A54" --mine --minerthreads 1 --verbosity 3 --networkid 98760 --rpc --rpcaddr 127.0.0.1 --nousb --rpcapi="db,eth,net,web3,personal,web3" console

Второе окно:

$ geth --identity="Node02" --etherbase "0x0C976006a5762779bA36AC590D1D8Ebac1Ca2981" --mine --minerthreads 1 --verbosity 3 --networkid 98760 --rpc --rpcaddr 127.0.0.1 --rpcport=8546 --nousb --rpcapi="db,eth,net,web3,personal,web3" console

Третье окно:

$ geth --identity="Node03" --etherbase "0xaB627feab4e962222a3333F3b09182dF68bB9422" --mine --minerthreads 1 --verbosity 3 --networkid 98760 --rpc --rpcaddr 127.0.0.1 --rpcport=8547 --nousb --rpcapi="db,eth,net,web3,personal,web3" console

В каждом из открытых окон появятся сообщения о генерации DAG:

…
INFO [12-19|17:57:44.072] Generating DAG in progress               epoch=0 percentage=34 elapsed=29.740s
INFO [12-19|17:57:44.898] Generating DAG in progress               epoch=0 percentage=35 elapsed=30.566s
INFO [12-19|17:57:45.671] Generating DAG in progress               epoch=0 percentage=36 elapsed=31.339s
…

Дождитесь, пока генерация будет завершена. После этого eth.hashrate и eth.blockNumber будут отличны от 0:

> eth.hashrate
4
> eth.blockNumber
2

Проверить текущий баланс аккаунта можно так:

> web3.fromWei( eth.getBalance(eth.coinbase) )

Чтобы вам не мешали сообщения о создании новых блоков и другие сообщения в консольных окнах, завершите работу Geth командой exit и перезапустите заново, указав значение параметра verbosity, равным 1.

Объединение узлов в сеть


Для начала мы посмотрим список запущенных контейнеров. Выдайте из четвертого консольного окна следующую команду:

# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMES
fa70a5418618        node03              "/bin/sh -c bash"   2 hours ago         Up 2 hours          0.0.0.0:8547->8547/tcp   gifted_curran
49a028744b4b        node02              "/bin/sh -c bash"   2 hours ago         Up 2 hours          0.0.0.0:8546->8546/tcp   reverent_wescoff
5a9ade2947eb        node01              "/bin/sh -c bash"   2 hours ago         Up 2 hours          0.0.0.0:8545->8545/tcp   clever_ellis

Здесь для каждого контейнера показывается его идентификатор, название образа, имя контейнера и другая информация.

Для объединения контейнеров в сеть вам потребуются адреса IP контейнеров в нашей приватной сети PRIVATENET. Вы можете получить эти адреса по идентификатору контейнера, например, следующим образом:

# docker inspect 5a9ade2947eb | grep IPAddress
            "SecondaryIPAddresses": null,
            "IPAddress": "",
                    "IPAddress": "172.21.0.2",

Также можно воспользоваться командой docker network inspect PRIVATENET:

docker network inspect PRIVATENET
[
    {
        "Name": "PRIVATENET",
        "Id": "576ec7edba5b4c228740deaf7fabb5e2ba003d310086153dd7f15e2c7de0c1b2",
        "Created": "2019-12-20T11:52:07.90695857+03:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.21.0.0/16",
                    "Gateway": "172.21.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "49a028744b4b6073f6dbca23e78625bc58fc0cdacadec7cded4bb0e888c7e37b": {
                "Name": "reverent_wescoff",
                "EndpointID": "11006b596b5a46df9bf9f95a9456784795d333a3e6901b15bd2db746fd4b5513",
                "MacAddress": "02:42:ac:15:00:03",
                "IPv4Address": "172.21.0.3/16",
                "IPv6Address": ""
            },
            "5a9ade2947ebd8e55594ede9763aac71f5e6529c03e762ef723adb2c592c5ccd": {
                "Name": "clever_ellis",
                "EndpointID": "41ef69a0a93b5b1de495836028bac1742c303de92ffe42a0855ed32c93c28953",
                "MacAddress": "02:42:ac:15:00:02",
                "IPv4Address": "172.21.0.2/16",
                "IPv6Address": ""
            },
            "fa70a54186185de01db3647e7333bf6c71250162fafefb78dbe9998e5ac93f34": {
                "Name": "gifted_curran",
                "EndpointID": "d368c032bc0886c27ad4895d1856e4f00cf1b25ce040f3b42393dbff778c18e5",
                "MacAddress": "02:42:ac:15:00:04",
                "IPv4Address": "172.21.0.4/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

Сопоставляя данные, полученные этими командами, составим список IP-адресов для наших контейнеров:

node01 - 172.21.0.2
node02 - 172.21.0.3
node03 - 172.21.0.4

Разумеется, у вас будет другой список, и при перезапуске контейнеров эти адреса могут меняться.

Получив список адресов, перезапустите geth во всех контейнерах, указав в параметре --rpcaddr адрес своего контейнера. Можно, конечно, задать адрес 0.0.0.0, но это плохо с точки зрения безопасности — к узлу сможет подключиться кто или что угодно. Например, это могут быть боты, которые попытаются «увести» все средства из этих узлов, дождавшись, когда узел будет разблокирован.

Для объединения узлов вам нужно будет воспользоваться командой admin.addPeer. В качестве параметра этой команде нужно передать URL в формате enode. Получите этот URL для каждого контейнера с помощью команды admin.nodeInfo.enode:

> admin.nodeInfo.enode
"enode://0a84e562c9b22e43269b7dca215cf2ed8c20bbf35da67bae8d5ee81b36d8bbb69e3ec704b9b6f7501059fe861843a836b2fbab641f36616cdd77365b1a522d5b@62.152.63.28:30303?discport=1350"

"enode://ee49f69e25c068e006fec4a8d74370370b1d2be9715b86eddd99f97a3a5a9c692a265ab7d01fb36410d59c3f6e2b253a22f652ecbf1941eef0b3f1d30b19a535@62.152.63.28:30303?discport=1345"

"enode://156d43648b47078439c7481e54f697bbf1c6b6e762029ba2969f1556ceb94e51ad03f8bd2bed35f466073165810600f52925d155f0fceef832ae86fc39a8c135@62.152.63.28:30303?discport=1348"

Полученные адреса сохраните.

Для того чтобы подключить один узел к другому, откройте консольное окно первого узла и введите там команду следующего вида:

> admin.addPeer( "enode://0a84e562c9b22e43269b7dca215cf2ed8c20bbf35da67bae8d5ee81b36d8bbb69e3ec704b9b6f7501059fe861843a836b2fbab641f36616cdd77365b1a522d5b@172.21.0.3:30303")

Здесь мы передали адрес URL, указав в нем адрес IP подключаемого узла. Проделайте аналогичную процедуру и на других узлах сети.

Для того чтобы проверить, что соединение установлено, воспользуйтесь командой admin.peers. Если соединений нет, команда вернет пустой результат:

> admin.peers
[]

В нашем случае команда показывает, что первый узел с адресом 172.21.0.2 подключен к узлам с адресами 172.21.0.3 и 172.21.0.4:

Посмотреть результат, когда есть соединения
> admin.peers

[{
    caps: ["eth/63", "eth/64"],
    enode: "enode://156d43648b47078439c7481e54f697bbf1c6b6e762029ba2969f1556ceb94e51ad03f8bd2bed35f466073165810600f52925d155f0fceef832ae86fc39a8c135@172.21.0.4:30303",
    id: "4dac1d10cb6ae8bfc1fdebd3f5334b24ee62ec38a50bc92c89104cfc3251b5fc",
    name: "Geth/Node03/v1.9.9-stable-01744997/linux-amd64/go1.13.4",
    network: {
      inbound: false,
      localAddress: "172.21.0.2:40652",
      remoteAddress: "172.21.0.4:30303",
      static: true,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 98414119,
        head: "0x6b31a5bb9cde06fab5a8cc1ae9b18bada30de0d1b76cb3286c1081e76dbf5b83",
        version: 64
      }
    }
}, {
    caps: ["eth/63", "eth/64"],
    enode: "enode://ee49f69e25c068e006fec4a8d74370370b1d2be9715b86eddd99f97a3a5a9c692a265ab7d01fb36410d59c3f6e2b253a22f652ecbf1941eef0b3f1d30b19a535@172.21.0.3:30303",
    id: "b74277d278c15317fa7f7fa492daca60492ea22053bfc53281dd0071eba1c16b",
    name: "Geth/Node02/v1.9.9-stable-01744997/linux-amd64/go1.13.4",
    network: {
      inbound: false,
      localAddress: "172.21.0.2:42576",
      remoteAddress: "172.21.0.3:30303",
      static: true,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 99041423,
        head: "0x0ec44735bbb425cb8db96103f52300dfaae1147ba0e03aa4892d041250ce4408",
        version: 64
      }
    }
}]

Обратите внимание, что после успешного объединения узлов и их синхронизации команда eth.blockNumber будет возвращать одно и то же значение при запуске в любом из наших трех контейнеров.

Команда web3.fromWei( eth.getBalance(eth.coinbase) ) напротив, будет показывать разный баланс на разных узлах, т.к. у каждого узла свой собственный аккаунт.

Работа с узлами сети с помощью Node.js


В листинге 3 мы привели простой скрипт, работающий под управлением Node.js, который показывает на консоли список аккаунтов указанного узла и баланс каждого из них:

Листинг 3. Файл /home/book/ list_accounts.js
var Web3 = require('web3')
var web3 = new Web3(new Web3.providers.HttpProvider("http://172.21.0.2:8545"));

web3.eth.getAccounts()
.then(accList => {
    return accList;
})
.then(function (accounts) {
  var balancePromeses = [];
  for(let i = 0; i < accounts.length; i++) {
    balancePromeses[i] = web3.eth.getBalance(accounts[i]);
  }

  Promise.all(balancePromeses).then(values => {
     for(let i = 0; i < values.length; i++) {
       console.log('Account: ', accounts[i], 'balance: ', values[i], 'wei, ', web3.utils.fromWei(values[i], 'ether'), 'ether');
     }
   });
})
.catch(function (error) {
  console.error(error);
});


Этот скрипт вы можете запустить в отдельном, например, в четвертом консольном окне:

# node list_accounts.js
Account:  0x0C976006a5762779bA36AC590D1D8Ebac1Ca2981 balance:  3350000000000000000000 wei,  3350 ether

Что дальше


Тема разработки ПО для криптовалют вообще и для Ethereum в частности довольно увлекательна. Если после прочтения этой статьи у вас появились вопросы и захотелось узнать больше, читайте мою книгу «Создание смарт-контрактов Solidity для блокчейна Ethereum. Практическое руководство», которая вышла в издательстве Литрес.

Также возможно, вам пригодится хорошая шпаргалка по командам Docker.

Буду благодарен за любые замечания и дополнения по статье и книге!

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


  1. Undiabler
    22.12.2019 14:40

    Уместно было бы добавить юзкейсы применения такой сети. Сейчас статья читается как «давайте сделаем штуковину, ну вот у нас получилась штуковина». И это при наличии опыта работы с geth и со смарт-контрактами. Для остальных читателей, подозреваю, может быть еще менее понятно что тут происходит и зачем.


    1. AlexandreFrolov Автор
      22.12.2019 15:52

      Это может пригодится в том случае, если вам нужно быстро развернуть приватную сеть узлов Ethereum для тестирования или в учебных целях, а не ограничиваться только одним узлом. Docker сокращает затраты времени и ресурсов на создание такой сети. Или если вам просто нужно быстро развернуть приватную сеть на большом количестве узлов.

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


      1. Undiabler
        22.12.2019 16:16

        нужно быстро развернуть приватную сеть узлов Ethereum для тестирования или в учебных целях
        Вопрос не в докере, а как раз в том зачем может понадобиться приватная сеть узлов? Какие задачи она решает? Чем это лучше чем обычная тестовая нода? Какие дает преимущества перед уже готовым и развернутым тестнетом (и не одним)?


        1. AlexandreFrolov Автор
          22.12.2019 16:54

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

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

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

          Разумеется, можно отлаживать смарт-контракты в тестовых сетях, например, в Rinkeby, однако намного легче это делать именно в приватной сети. Что же касается сетей для нужд организаций или частных лиц, то по понятным причинам едва ли их стоит реализовывать на бесплатных тестовых сетях.

          Один узел — это вырожденный случай, пригодный лишь для тестирования.

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