Перейти к основному содержимому
Блокчейн33ГЛАВА 33Свой узел: приватная сеть на gethdatadirchain · state · keys012:8545

Свой узел: приватная сеть на geth

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Тридцать две главы подряд слово «сеть» было фигурой речи. Контракты компилировались, выводы примеров приходили откуда-то, а куда именно уезжала транзакция — не обсуждалось.

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

Эта глава — про то, как из одного файла описания и одной команды получается работающая сеть: со своими счетами, своими деньгами и своим номером. И про то, почему без этого файла следующие четыре главы читать бессмысленно: инструментам нужно, куда подключаться.

Узел — это программа с пятью хранилищами​

Узел (node) — программа, которая держит у себя копию сети и умеет её продолжать. Внутри неё пять разных вещей, и путать их дорого.

ОДНА ПРОГРАММА, ПЯТЬ РАЗНЫХ ХРАНИЛИЩцепочка блоковзаголовки и транзакциирастёт и не переписываетсясостояниебалансы, код, памятьпересчитывается каждым блокомочередьпринятые, но не в блокеживёт только в памятиkeystoreзашифрованные ключифайлы на дискепорт 8545 — единственная дверь снаружи: вопрос по HTTP, ответ в JSONпапка datadir — это и есть вся сеть: удалили её — удалили цепочку, счета и историюпрограмма одна, а сетей на машине может быть сколько угодно: у каждой свой datadir и свой порт
Цепочка, состояние, очередь и ключи живут в одной папке; наружу торчит один порт

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

Состояние. Балансы, код контрактов и их хранилище — то самое, из главы про слоты. Состояние не хранится в блоке: блок хранит только его отпечаток, а само состояние узел пересчитывает, применяя транзакции.

Очередь. Транзакции, которые приняты, но ещё не попали ни в один блок. Живёт в памяти; убили узел — очередь исчезла.

Хранилище ключей (keystore). Зашифрованные файлы с приватными ключами. Отдельная папка, отдельный пароль.

Дверь наружу. Один порт, по умолчанию 8545. Всё, что мы делали в главе про запросы к узлу, — стук именно в эту дверь.

Всё перечисленное лежит в одной папке — каталоге данных (datadir). Удалили папку — удалили сеть целиком: цепочку, балансы, историю. Ничего «в облаке» не остаётся, потому что облака нет.

Важно

Один компьютер спокойно держит несколько независимых сетей: у каждой свой каталог данных и свой порт. Это удобно на тренировках — испортили сеть экспериментом, удалили папку, подняли заново за десять секунд.

Ставим geth и проверяем, что он на месте​

geth (go-ethereum) — официальная реализация узла, написанная на Go. Это один исполняемый файл без зависимостей: ни Node, ни Python ему не нужны.

geth version
Geth
Version: 1.17.5-stable
Architecture: arm64
Go Version: go1.26.5
Operating System: darwin

Проверка обязательна до всего остального: половина инструкций в интернете написана под версии 1.10–1.13, а там всё иначе. Ниже — всё под 1.17.

genesis.json: вся сеть одним файлом​

Блок ноль (genesis) не приходит ни от кого — его пишет тот, кто создаёт сеть. Это единственный блок, содержимое которого выбирают руками; все остальные вырастают из него по правилам.

{
"config": {
"chainId": 1337,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"terminalTotalDifficulty": 0,
"shanghaiTime": 0,
"cancunTime": 0,
"pragueTime": 0,
"blobSchedule": {
"cancun": { "target": 3, "max": 6, "baseFeeUpdateFraction": 3338477 },
"prague": { "target": 6, "max": 9, "baseFeeUpdateFraction": 5007716 }
}
},
"difficulty": "0x0",
"gasLimit": "0x1c9c380",
"alloc": {
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266": { "balance": "1000000000000000000000" },
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8": { "balance": "500000000000000000000" },
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": { "balance": "0" }
}
}
Нажми на часть выражения

Три адреса в примере выбраны не случайно. Первый и второй — стандартные учебные счета, чьи ключи опубликованы во всех руководствах; их мы дальше положим в хранилище узла. Третий — просто адрес с нулём: он показывает, что запись в alloc можно сделать и без денег.

Важно

Учебные ключи публичны: их знают все. В приватной сети на своей машине это безопасно и удобно. В любой настоящей сети адрес с таким ключом опустошают за секунды — там ключ генерируют сами и не показывают никому.

Отвечено вопросов: 0 из 2
Откуда в новой приватной сети берутся деньги?
Что произойдёт, если поменять одну цифру в alloc и создать сеть заново?
Отвечено верно: 0 из 2

geth init: файл превращается в блок 0​

geth init --datadir ./chain genesis.json
INFO [09-11|23:55:44.710] Successfully wrote genesis state database=chaindata hash=8d1502..b73a92
Нажми на часть выражения
ЧТО ИЗ GENESIS.JSON ВО ЧТО ПРЕВРАЩАЕТСЯgenesis.jsonchainId: 1337gasLimit: 0x1c9c380difficulty: 0x0alloc: { A: 1000 ETH }blobSchedule: { … }geth initблок 0 в базе узлаeth_chainId → 0x539gasLimit → 30000000difficulty → 0eth_getBalance(A) → 1000 ETHhash → 0x8d15024c…b73a92хеш блока 0 — отпечаток всего файла: изменили одну цифру в alloc — получили другую сетьпоэтому узлы с разным genesis не договорятся никогда, даже если chainId у них совпал
Каждое поле описания превращается в конкретный ответ узла; хеш блока 0 — отпечаток файла целиком

Теперь положим в хранилище узла ключ от первого адреса из alloc:

echo "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" > key.txt
echo "edu" > pass.txt
geth account import --datadir ./chain --password pass.txt key.txt
Address: {f39fd6e51aad88f6f4ce6ab8827279cfffb92266}

Адрес совпал с тем, что записан в alloc с тысячей эфира, — значит, ключ тот. Файл с ключом после импорта можно удалить: узел хранит его уже у себя, зашифрованным паролем.

Запуск: флаги по одному​

geth --datadir ./chain --dev --password pass.txt \
--http --http.addr 127.0.0.1 --http.port 8545 --http.api eth,net,web3
Нажми на часть выражения
Интересный факт

Слово --dev пугает: кажется, что это игрушечный режим. На самом деле отличий от «настоящего» узла ровно три: блоки подписывает один участник, соседей искать не нужно и первый счёт открыт без пароля. Формат блоков, правила выполнения, расход газа и хранилище — те же самые.

Кто отвечает на 8545​

Первая модель стучится в узел обычными запросами — теми самыми, что разбирались в главе про запросы:

── кто отвечает на порту 8545 ──
клиент : Geth/v1.17.5-stable/darwin-arm64/go1.26.5
eth_chainId : 0x539 = 1337
счета в keystore : 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 0x70997970c51812dc3a010c7d01b50e0d17dc79c8

── блок 0: тот самый genesis ──
хеш : 0x8d15024c2ea8d66936234abd1058a6d447e863c1d70d366ca58e7988a9b73a92
родитель : 0x0000000000000000000000000000000000000000000000000000000000000000
gasLimit : 30000000 ← из genesis.json: 0x1c9c380
difficulty : 0
транзакций : 0
высота цепи : 0

── балансы: ровно то, что записано в alloc ──
A 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1000 ETH
B 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 ETH
C 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC 0 ETH

Четыре строки стоят того, чтобы на них остановиться.

chainId вернулся шестнадцатеричным — 0x539. Это 1337, ровно то, что записано в описании. Узлы отвечают числами в шестнадцатеричном виде почти всегда; библиотеки переводят сами, curl — нет.

Родитель блока 0 — сплошные нули. Больше ни у одного блока в сети такого родителя не будет: это признак самого первого.

Высота цепи — ноль. Сеть поднята, но не сделала ни одного блока. В режиме --dev по умолчанию блок появляется только тогда, когда для него есть транзакция: пустых блоков сеть не печатает.

Балансы совпали с alloc до единицы. Второй адрес получил деньги, хотя ключа от него узел не знает: alloc раздаёт по адресу, а не по ключу.

Два отказа, которые встречают первыми​

── два отказа, которые встречают первыми ──
с адреса без ключа : -32000 unknown account
с запертого счёта : -32000 authentication needed: password or unlock

Оба отказа приходят на попытку сказать узлу «отправь транзакцию от такого-то адреса».

unknown account — у узла вообще нет ключа от этого адреса. Деньги на адресе есть, адрес существует, но подписать за него некому.

authentication needed: password or unlock — ключ есть, но счёт заперт. В режиме --dev открыт только первый счёт хранилища; остальные ждут отдельной разблокировки.

Из этого следует главное в главе: просить узел подписать за вас — не единственный и не основной способ.

ДВА СПОСОБА ОТПРАВИТЬ ТРАНЗАКЦИЮподписывает узелeth_sendTransactionключ лежит в keystore узласчёт должен быть разблокированиначе: authentication neededподписываем самиeth_sendRawTransactionключ у нас, узел его не видитузел получает готовые байтытак работают все библиотекив подпись входит chainId: та же транзакция для другой сети не годитсяinvalid sender: invalid chain id for signer: have 7777 want 1337это и есть защита от переноса подписанной транзакции в чужую сеть
Подпись у узла требует ключа в его хранилище; подпись у себя не требует от узла ничего, кроме приёма готовых байтов

Второй способ проще и надёжнее: транзакцию подписывают своим ключом у себя, а узлу отдают готовые байты. Узлу не нужно ни хранилища, ни пароля — он просто принимает подписанное. Именно так работают все библиотеки, и именно так через две главы будет работать Hardhat.

Отвечено вопросов: 0 из 2
«unknown account» при отправке — что это значит?
Почему подписывать транзакцию у себя надёжнее, чем просить узел?
Отвечено верно: 0 из 2

Перевод: откуда берётся блок и куда девается комиссия​

Модель отправляет два перевода — один через хранилище узла, второй подписанным у себя:

── перевод 2 ETH с dev-счёта A: узел подписывает сам ──
хеш : 0x1e60739c7b6cc2bc5230a7923cba566e254b9b35844f94f7539a2f50ddcdb2ed
статус : 0x1 (0x1 = исполнена)
блок : 1 ← сделан ровно под неё
газа : 21000
у C стало : 2 ETH

── перевод 3 ETH с запертого B: подписываем ключом у себя ──
адрес из ключа : 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 — совпал с B: true
хеш : 0xc6d16f32f0a1153f7dbe0194d02cf42b572dcf94409e8092c4e93adab7341819
блок : 2
газа истрачено : 21000 ← простой перевод всегда столько
цена газа : 765777977 wei за единицу
комиссия : 16081337517000 wei
у B было : 500000000000000000000 wei
у B стало : 496999983918662483000 wei
списано : 3000016081337517000 wei
это 3 ETH + комиссия? true
у C стало : 5 ETH

── блок, в котором она лежит ──
номер : 2
подписал : 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 ← A, первый счёт keystore
транзакций в блоке : 1
gasUsed / gasLimit : 21000 / 30058619
цепочка родителей до нуля : 0 → 1 → 2

Блок появился ровно под транзакцию. До первого перевода высота была нулём, после первого — единица, после второго — двойка. Это и есть --dev без расписания: узел не тратит время на пустые блоки.

21 000 — не случайное число. Столько стоит любой простой перевод; это фиксированная цена самого факта транзакции, записанная в правилах сети. Всё, что дороже, — это уже работа контракта.

Проверка сошлась до последнего вей. Списано ровно 3 эфира плюс комиссия. Такую арифметику стоит уметь считать руками: на разборе «почему не хватило денег» она отвечает за минуту.

Подписал блок первый счёт хранилища. В режиме --dev он и участник, и тот, кто печатает блоки. Из этого есть неочевидное следствие: если отправлять с того же счёта, часть комиссии возвращается ему же как вознаграждение за блок, и арифметика перестаёт сходиться на глаз. Поэтому в модели проверка сделана на счёте B, который блоков не подписывает.

Совет

Прежде чем звать библиотеки, любой узел стоит проверить двумя запросами: «какой у тебя номер сети» и «какая высота цепи». Если первое не то, что вы ждёте, — вы подключились не туда. Если второе не растёт после отправки — транзакция не принята.

Номер сети спрятан внутри подписи​

Отдельная модель подписывает одну и ту же транзакцию дважды — с правильным номером сети и с чужим — и отдаёт обе узлу:

подписано chainId=7777 → отказ узла: -32000 invalid sender: invalid chain id for signer: have 7777 want 1337
подписано chainId=1337 → принята: 0x0796c4c262b54c14f9bcb81fd7d302596c84f033d32d9459ef21a60e065dc22a

Номер сети не приписан к транзакции сбоку — он входит в подписываемые данные. Поэтому подпись, сделанная для сети 7777, для сети 1337 просто не является подписью: узел не может достать из неё отправителя и отвечает invalid sender.

Смысл этого правила прямой: не будь его, транзакцию, подписанную в тестовой сети, можно было бы взять и повторить в настоящей — с теми же деньгами и тем же результатом.

Практический вывод: номер сети в настройках инструмента обязан совпадать с номером узла. Это первое, что проверяют, когда «всё настроено, а транзакции отклоняются».

Сеть живёт на диске, блоки можно пустить по расписанию​

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

── что есть в сети сейчас ──
последний блок : 2
его хеш : 0x1b213a395421c655f7f14f80da431d83b74ace3868f62dcd50a10a9671977259
баланс C : 0x4563918244f40000
папка chain/ : 2.1M на диске

── узел убит ──
порт 8545: соединение отклонено

── поднимаем заново на том же datadir, но блоки по таймеру: --dev.period 3 ──
блок 2 на месте, хеш совпал : да
баланс C : 0x4563918244f40000 ← тот же

транзакций не шлём, просто смотрим на высоту цепи:
+2 c → блок 3
+4 c → блок 4
+6 c → блок 5
+8 c → блок 5
+10 c → блок 6
+12 c → блок 7

при --dev.period 0 (по умолчанию) все эти шесть строк показали бы одно и то же число

Цепочка пережила перезапуск. Хеш блока 2 совпал, баланс не изменился: узел — это программа, а сеть — папка на диске.

--dev.period 3 заставляет печатать блок каждые три секунды, даже когда отправлять нечего. Шесть замеров дали 3, 4, 5, 5, 6, 7 — блоки идут по таймеру, а не по нашим кнопкам.

Зачем это на демонстрации: с мгновенными блоками всё происходит быстрее, чем успевает обновиться интерфейс, и разобрать, что за чем случилось, невозможно. С расписанием появляется настоящее ожидание — «транзакция отправлена, ждём подтверждения», — и рассказ становится связным.

Отвечено вопросов: 0 из 2
Узел убили и подняли заново на той же папке. Что стало с цепочкой?
Зачем на демонстрации ставить --dev.period 3 вместо мгновенных блоков?
Отвечено верно: 0 из 2

Типичные ошибки​

Отдельная модель ломает описание сети по одному полю и показывает, что говорит geth.

Забыли настройки больших вложений.

Fatal: Failed to write genesis block: invalid chain configuration: missing entry for fork "cancun" in blobSchedule

Это самая частая ошибка при переносе старого примера: в руководствах 2022 года такого поля не было вовсе. Лечится добавлением блока blobSchedule.

Сложность не ноль.

Fatal: Bad developer-mode genesis configuration: difficulty must be 0

Обратите внимание на момент: geth init эту ошибку не поймал — genesis записался успешно. Отказ пришёл только при запуске с --dev. Между двумя командами прошла минута, и связать их между собой на разборе получается не сразу.

Забыли geth init вообще.

chainId : 0x539
хеш блока 0 : 0x790ac206f4e9d3837e8284a6b81d69d832d6974d10735757ccad8ad35086d6ad
ваш хеш блока 0 : 0x8d15024c2ea8d66936234abd1058a6d447e863c1d70d366ca58e7988a9b73a92
баланс вашего A : 0x0

Это самая коварная. Узел поднялся, порт отвечает, номер сети даже совпал с вашим — потому что 1337 у --dev и так по умолчанию. А сеть другая: хеш блока 0 чужой, и ваших денег в ней нет. Выглядит как «почему-то пустой баланс», а причина — сеть не та.

Проверка одной командой — сравнить хеш блока 0 с тем, что выдал geth init. Совпал — сеть ваша.

Слова главы​

  • Узел — программа с копией сети: цепочка, состояние, очередь, ключи, порт.
  • Каталог данных — папка, в которой лежит вся сеть; удалили её — удалили сеть.
  • Блок ноль — единственный блок, который пишут руками; его хеш — отпечаток описания.
  • alloc — кому сколько выдать на старте: единственный источник денег в новой сети.
  • Номер сети — входит в подпись; чужой номер делает подпись недействительной.
  • Хранилище ключей — зашифрованные ключи узла; счета в нём по умолчанию заперты.
  • Режим одиночки — блоки подписывает сам узел, соседей не ищет, первый счёт открыт.
  • Период блоков — либо «только когда есть транзакция», либо по таймеру.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 95 зн/мин, точность ≥ 88%

geth init --datadir ./chain genesis.json

На чемпионате​

Требование звучит как «развернуть приватную сеть», и проверяют его не на словах: просят показать узел, счета и прохождение транзакции.

Порядок, который стоит уметь повторить не задумываясь:

  1. genesis.json — номер сети, потолок газа, нулевая сложность, alloc с вашими адресами;
  2. geth init — и запомнить хеш блока 0 из вывода: он пригодится, чтобы доказать, что сеть ваша;
  3. импортировать ключ первого адреса в хранилище;
  4. запустить с открытым портом и нужным набором команд;
  5. проверить двумя запросами: номер сети и баланс вашего адреса.

Две вещи, которые экономят время на разборе:

  • Держите genesis.json в репозитории решения. Сеть по нему поднимается за десять секунд на любой машине, в том числе на чужой.
  • Не ставьте мгновенные блоки на показ. Три секунды на блок выглядят как работа; ноль секунд выглядит как фокус, которого никто не разглядел.

И отдельно: --dev — это не эмулятор. Эмулятор — это программа, которая делает вид, что она сеть. Здесь настоящий узел настоящей реализации протокола, просто участник в нём один.

Куда углубиться​

  • geth attach — консоль к работающему узлу; те же запросы, но из командной строки.
  • Несколько узлов в одной сети — второй datadir, второй порт, соединение через список соседей.
  • Способ согласия на основе подписей — когда блоки подписывает не один участник, а несколько по очереди.
  • Отдельный порт для внутренних команд — зачем узлы разделяют публичный и служебный доступ.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

Видео3022 Настройка приватной сети EthereumProgramming Innopolis STEM
ВидеоEthereum для начинающих. Geth Intro. Private chain.doIT
ВидеоEthereum для начинающих. Geth JSON-RPC API.doIT
Видео3012 Процесс и мотивация синхронизации состояний узлов в сети Ethereum, Genesis blockProgramming Innopolis STEM
ВидеоPart 1 - Build your Own Private Geth PoA Ethereum Network (Blockholic) - 4K VideoBlockholic

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.