Развёртывание: скрипты, Ignition и своя сеть
- Зачем это нужно
- Развёртывание — это транзакция без получателя
- Скрипт развёртывания
- Прогон в свою сеть
- Ignition: описание вместо шагов
- Повторный запуск: ничего нового
- Работать с уже развёрнутым
- Четыре отказа, и все про сеть
- Почему Ignition потребовал пять подтверждений
- Что показывать на демонстрации
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Куда углубиться
- Экзамен по блоку «Инструменты»
- Куда дальше: проверенные ресурсы
Зачем это нужно
Сеть поднята, проект собран, тесты зелёные. Осталось последнее: вынести контракт в сеть и получить адрес, по которому с ним можно говорить.
Это и есть шаг, который показывают на демонстрации. Задание требует не только развернуть, но и провести транзакции, показать изменение состояния и подтвердить, что адрес контракта не менялся при обновлении.
Эта глава закрывает блок инструментов: два способа развернуть, файл с адресами, работа с уже развёрнутым — и четыре отказа, которые встречают на этом шаге чаще всего. Все они про сеть, а не про контракт.
Развёртывание — это транзакция без получателя
Обычная транзакция идёт кому-то: есть адрес получателя, есть сумма. У транзакции развёртывания поле получателя пустое, а в поле данных лежит байт-код.
Сеть, увидев такое, поступает так: считает адрес для нового контракта, выполняет код развёртывания (в нём отрабатывает конструктор) и кладёт по этому адресу результат — тело контракта.
Отсюда важное следствие. Адрес не случаен: он вычисляется из адреса отправителя и номера его транзакции. Проверка:
nonce 0 → 0x8464135c8F25Da09e49BC8782676a84730C318bC
nonce 1 → 0x71C95911E9a5D330f4D621842EC243EE1343292e
nonce 2 → 0x948B3c65b89DF0B4894ABE91E6D02FE579834F8F
nonce 3 → 0x712516e61C8B383dF4A63CFe83d7701Bce54B03e
фактически развёрнуто скриптом : 0x8464135c8F25Da09e49BC8782676a84730C318bC
фактически развёрнуто Ignition : 0x948B3c65b89DF0B4894ABE91E6D02FE579834F8F
Первое развёртывание было нулевой транзакцией счёта — адрес совпал со строкой nonce 0. Между ними прошёл ещё один вызов (вклад), поэтому второе развёртывание стало третьей транзакцией, и адрес совпал со строкой nonce 2.
Номер транзакции (nonce) — счётчик отправленных этим счётом транзакций. Он же не даёт повторить одну и ту же транзакцию дважды.
Скрипт развёртывания
import { network } from "hardhat";
const { ethers } = await network.connect();
const [deployer] = await ethers.getSigners();
const vault = await ethers.deployContract("Vault", [500n]);
const tx = vault.deploymentTransaction()!;
await vault.waitForDeployment();
const addr = await vault.getAddress();
const rc = await ethers.provider.getTransactionReceipt(tx.hash);
Запускается с указанием сети:
npx hardhat run scripts/deploy.ts --network local
Прогон в свою сеть
── куда и чем ──
сеть : local chainId 1337
разворачиваем с: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
баланс до : 500.0 ETH
высота цепи до : 0
── развернули ──
адрес контракта: 0x8464135c8F25Da09e49BC8782676a84730C318bC
хеш транзакции : 0x9d47c30f735412f6fe851e13f2fcfa36e12503e6197c5f5149406c446eabe283
номер (nonce) : 0
блок : 1
газа истрачено : 586384
цена газа : 875000001 wei
заплачено : 0.000513086000586384 ETH
── что теперь лежит по адресу ──
длина кода : 2253 байт
начало : 0x60806040526004361061007a …
до развёртывания по этому адресу было: 0x (пусто)
── контракт отвечает ──
owner() : 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
rate() : 500
totalDeposits(): 0
── положили 1 ETH ──
блок : 2
газа : 67481
totalDeposits(): 1.0 ETH
баланс контракта: 1.0 ETH
Четыре числа здесь встречались раньше, и это не совпадение.
586 384 газа — ровно столько же показала таблица газа в тестах. Развёртывание стоит одинаково в симуляторе и в настоящей сети: правила одни.
2253 байта — длина тела контракта из артефакта в главе про настройки. То, что собрал компилятор, и то, что лежит в сети, — один и тот же набор байтов.
67 481 газа на вклад — снова та же цифра из таблицы.
owner() — адрес развернувшего. Конструктор записал msg.sender, а отправителем был наш счёт.
Строка «до развёртывания по этому адресу было пусто» — не украшение. Пока транзакция не попала в блок, по адресу ничего нет, и любой вызов туда вернёт пустоту вместо ошибки. Отсюда классическая жалоба «контракт развёрнут, а функции не отвечают»: не дождались подтверждения.
Ignition: описание вместо шагов
Скрипт — это список действий. Когда контрактов несколько и они зависят друг от друга, список становится хрупким: упало на третьем шаге — что уже сделано, что нет?
Второй способ описывает желаемый результат, а порядок действий выводится сам:
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
export default buildModule("VaultModule", (m) => {
const rate = m.getParameter("rate", 500n);
const vault = m.contract("Vault", [rate]);
m.call(vault, "setRate", [700n]);
return { vault };
});
npx hardhat ignition deploy ignition/modules/Vault.ts --network local
? Confirm deploy to network local (1337) › yes
Hardhat Ignition 🚀
Deploying [ VaultModule ]
Batch #1
Executed VaultModule#Vault
Batch #2
Executed VaultModule#Vault.setRate
[ VaultModule ] successfully deployed 🚀
Deployed Addresses
VaultModule#Vault - 0x948B3c65b89DF0B4894ABE91E6D02FE579834F8F
Работа разбита на пачки. В первой — развёртывание, во второй — вызов, который от него зависит. Независимые действия попали бы в одну пачку и ушли бы одновременно.
Подтверждение спрашивается. Для любой сети, кроме симулятора: развёртывание стоит денег, и случайный запуск не туда — дорогая ошибка.
Повторный запуск: ничего нового
=== повторный запуск того же модуля ===
[ VaultModule ] Nothing new to deploy based on previous execution
stored in ./ignition/deployments/chain-1337
Deployed Addresses
VaultModule#Vault - 0x948B3c65b89DF0B4894ABE91E6D02FE579834F8F
Второй запуск ничего не развернул и назвал тот же адрес. Состояние хранится на диске:
ignition/deployments/chain-1337/deployed_addresses.json
ignition/deployments/chain-1337/journal.jsonl
ignition/deployments/chain-1337/artifacts/VaultModule#Vault.json
ignition/deployments/chain-1337/build-info/solc-0_8_34-111f9245….json
{ "VaultModule#Vault": "0x948B3c65b89DF0B4894ABE91E6D02FE579834F8F" }
Папка названа по номеру сети — chain-1337. Для каждой сети своё состояние: одно и то же описание разворачивается в свою сеть и в чужую независимо.
Журнал — построчная запись всего, что происходило. Если развёртывание оборвалось на середине, следующий запуск читает журнал и продолжает с места остановки, а не начинает заново.
Артефакт и слепок сборки лежат рядом. То есть в папке развёртывания хранится не только адрес, но и чем именно этот адрес был собран.
Работать с уже развёрнутым
Адрес больше не нужно переписывать руками — он лежит в файле:
const addresses = JSON.parse(
readFileSync("ignition/deployments/chain-1337/deployed_addresses.json", "utf8"),
);
const vault = await ethers.getContractAt("Vault", addresses["VaultModule#Vault"]);
── состояние контракта в сети ──
owner() : 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
rate() : 700 ← модуль вызвал setRate(700) сразу после развёртывания
totalDeposits(): 0.0 ETH
── положили 2 ETH ──
блок : 57
totalDeposits(): 2.0 ETH
проценты по 7 %: 0.14 ETH
баланс контракта: 2.0 ETH
── все события Deposited с начала цепочки ──
блок 57 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 | 2.0 ETH
Ставка 700, а не 500. Модуль после развёртывания вызвал setRate(700) — и это состояние в сети, а не в скрипте.
Блок 57. Узел работает с блоками по таймеру, и за время работы их накопилось полсотни. Номер блока — не «сколько было транзакций», а «сколько прошло блоков».
Четыре отказа, и все про сеть
Узел не отвечает.
Error HHE703: Cannot connect to the network "dead".
Please make sure your node is running, and check your internet connection
and networks config
Узел не запущен, порт другой или адрес в настройках с опечаткой. Проверяется одним запросом высоты цепи.
Номер сети не совпал.
Error HHE708: Hardhat was set to use chain id "9999", but connected to
a chain with id "1337".
Отдельно ценно, что это ловится до отправки. Если бы не ловилось, транзакция ушла бы и была бы отвергнута узлом с невнятным invalid sender — тем самым, что мы видели в главе про свой узел.
На счету ноль.
адрес: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC баланс: 0.0 ETH
ProviderError: insufficient funds for transfer
Ключ в настройках верный, адрес существует, но денег на нём нет. Лечится не в проекте, а в genesis.json: адрес вписывают в alloc и поднимают сеть заново.
Свои же транзакции мешают.
Error HHE10402 in plugin hardhat-ignition: You have sent transactions from
0x70997970… and they interfere with Hardhat Ignition. Please wait until they
get 5 confirmations before running Hardhat Ignition again.
Этот отказ был получен по-настоящему и объясняется не сразу.
Почему Ignition потребовал пять подтверждений
Ignition отслеживает свои транзакции по номерам. Если тем же счётом кто-то отправил транзакции мимо него — а мы именно это и сделали скриптом развёртывания, — его представление о номерах может разойтись с действительностью. Поэтому он требует, чтобы посторонние транзакции отстоялись пять блоков, и только потом берётся за работу.
И тут выяснилось неприятное: узел был запущен без расписания блоков. Новых блоков нет — значит, подтверждения не набираются никогда, и Ignition будет отказываться вечно.
Лечение — то самое расписание из главы про свой узел:
geth --datadir ./chain --dev --dev.period 2 …
Через несколько секунд подтверждения набрались, и развёртывание прошло.
Общее правило: для работы инструментов узел должен печатать блоки сам. Режим «блок только под транзакцию» удобен в опытах руками и мешает всему, что ждёт подтверждений. Двух секунд на блок достаточно.
Что показывать на демонстрации
Задание требует показать не «контракт развёрнут», а цепочку событий. Порядок, который читается с экрана без пояснений:
- Узел работает — высота цепи растёт сама.
- Развёртывание — адрес, хеш транзакции, номер блока, истраченный газ.
- По адресу действительно код — длина кода ненулевая; до развёртывания там было пусто.
- Контракт отвечает — чтение полей: владелец, параметры.
- Транзакция меняет состояние — было, стало, номер блока.
- Событие нашлось — поиск по цепочке показывает, кто и сколько.
- Адрес не изменился — для шага с обновлением логики из главы про прокси.
Всё это — один скрипт, который печатает по-человечески. Писать его лучше заранее: на демонстрации думать некогда, а вывод, который сам себя объясняет, экономит половину времени защиты.
Слова главы
- Транзакция развёртывания — без получателя, с байт-кодом в данных.
- Номер транзакции (nonce) — счётчик у счёта; вместе с адресом задаёт адрес будущего контракта.
- Подтверждение (receipt) — блок, газ, цена; появляется только после попадания в блок.
- Дождаться развёртывания — до этого по адресу пусто, и вызовы возвращают пустоту.
- Описание развёртывания — желаемый результат вместо списка шагов; порядок выводится сам.
- Состояние развёртывания — папка по номеру сети: адреса, журнал, артефакты.
- Взять по адресу — связать ABI с уже существующим контрактом, ничего не разворачивая.
- Пять подтверждений — условие Ignition после посторонних транзакций тем же счётом.
Тренажёр: набрать вслепую
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
const vault = await ethers.deployContract("Vault", [500n]);
На чемпионате
Одна команда на всё. Разворачивать руками из консоли — потерять время и сбиться. Скрипт или описание запускается одной строкой и повторяется сколько угодно раз.
Адреса — только из файла. Переписанный руками адрес рано или поздно окажется от прошлого развёртывания, и полчаса уйдёт на «почему функция возвращает ноль».
Узел с расписанием блоков. Иначе половина инструментов встанет в ожидании подтверждений, которых не будет.
Сохраните вывод развёртывания. Адрес, хеш транзакции, номер блока — это доказательства, которые пригодятся при защите, и восстановить их потом сложнее, чем скопировать сразу.
И про порядок работы: развернуть в симулятор проще и быстрее, но проверять надо в своей сети. Разница между ними — не в контракте, а во всём остальном: счетах, подтверждениях, времени блока. Ошибки живут именно там.
Куда углубиться
- Развёртывание по заранее известному адресу — способ, при котором адрес не зависит от номера транзакции.
- Подтверждение исходников — как связать адрес в сети с исходником в обозревателе.
- Продолжение оборванного развёртывания — журнал и команда состояния.
- Развёртывание связки контрактов — зависимости, параметры, пачки.
- Обновление логики через прокси — как это выглядит из описания развёртывания.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Экзамен по блоку «Инструменты»
Пять глав: от genesis-блока своей сети до развёрнутого контракта, отвечающего на вызовы. Проверка — по всему блоку.
Сводная проверка по 5 главам блока: 15 вопросов, 10:00 на всё, по одному вопросу, без возврата. Пересдавать можно сколько угодно раз.
Готовность: экзамены глав сданы 0 из 5. Блок можно сдавать и так, но шанс выше после всех глав.
Куда дальше: проверенные ресурсы
- Hardhat Ignition — описания развёртывания, параметры, состояние.
- Hardhat: скрипты — запуск и выбор сети.
- ethers: ContractFactory — что происходит при развёртывании.
- Ethereum: адреса контрактов — как считается адрес и что попадает в сеть.
- EIP-1014: CREATE2 — развёртывание по адресу, не зависящему от номера транзакции.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.