#словарь_райта · №3 - Консенсус
В web2 эта задача давно решена: Raft, Paxos, ZooKeeper. Пять реплик твоей базы голосуют, кто лидер и в каком порядке применять записи. Работает, потому что все узлы - твои: их подняла твоя команда, они живут в твоём облаке и доверяют друг другу по определению.
Консенсус в блокчейне - та же задача на максимальной сложности: тысячи узлов, у которых нет общего хозяина, договариваются о каждом следующем блоке. Доверие заменено экономикой: хочешь участвовать - вносишь залог (стейк); подпишешь две противоречащие версии истории - протокол сожжёт залог автоматически. Честность тут не предположение о людях, а самая выгодная стратегия.
Зачем тебе это слово: в посте 2/20 мы оставили консенсус «за скобками». Скобки открыты. А во вторник посмотрим, как твой код разговаривает с этими узлами.
Райт · blockwright.eth
В web2 эта задача давно решена: Raft, Paxos, ZooKeeper. Пять реплик твоей базы голосуют, кто лидер и в каком порядке применять записи. Работает, потому что все узлы - твои: их подняла твоя команда, они живут в твоём облаке и доверяют друг другу по определению.
Консенсус в блокчейне - та же задача на максимальной сложности: тысячи узлов, у которых нет общего хозяина, договариваются о каждом следующем блоке. Доверие заменено экономикой: хочешь участвовать - вносишь залог (стейк); подпишешь две противоречащие версии истории - протокол сожжёт залог автоматически. Честность тут не предположение о людях, а самая выгодная стратегия.
Зачем тебе это слово: в посте 2/20 мы оставили консенсус «за скобками». Скобки открыты. А во вторник посмотрим, как твой код разговаривает с этими узлами.
Райт · blockwright.eth
У блокчейна нет REST API. Есть кое-что постарше и, внезапно, проще - JSON-RPC, протокол из середины нулевых.
Когда MetaMask показывает твой баланс, он не «лезет в блокчейн». Он спрашивает ноду - сервер, который хранит копию того самого append-only лога из поста 2 и отвечает на запросы:
Один эндпоинт, имя метода в теле, никаких /v2/users/:id. Весь web3 - кошельки, дексы, эксплореры - ходит в сеть вот этим протоколом: eth_getBalance, eth_call, eth_sendRawTransaction. Скоро ты будешь дёргать их сам.
Вопрос только один: чью ноду спрашивать? Три пути:
1. Публичные RPC - бесплатные эндпоинты без регистрации. Плюс: ноль порога. Минусы: жёсткие лимиты, падают когда хотят, и - про это забывают - оператор видит твой IP и адреса, которыми ты интересуешься. Для учёбы годится, для продукта - нет.
2. Провайдеры (Alchemy, Infura, QuickNode): free-tier с головой хватает на пет-проекты, стабильность, дашборды запросов. Минус концептуальный: твоё «децентрализованное» приложение разговаривает с миром через одну компанию, которая видит каждый его запрос.
3. Своя нода. Полный контроль и приватность, цена - сотни гигабайт диска и вечная синхронизация. Для нашей серии оверкилл, но знать, что этот путь есть, обязательно: это опция выхода, на которой держится вся модель доверия.
Для Копилки возьмём провайдера с бесплатным тарифом плюс публичный эндпоинт как запасной - так и соберём в посте 5, когда отправим первую транзакцию кодом.
И тонкость, ради которой стоило дочитать: MetaMask из коробки ходит через Infura. То есть дефолтный стек «децентрализованного» веба зависит от одной компании чуть более чем полностью. Децентрализация - это спектр, а не бинарник. Запомни эту рамку - она объясняет половину архитектурных решений в web3.
Задание №4: открой настройки сети в своём кошельке и посмотри, какой RPC URL там прописан. Бонус для смелых: дёрни
Райт · blockwright.eth
github.com/blockwright/kopilka
Когда MetaMask показывает твой баланс, он не «лезет в блокчейн». Он спрашивает ноду - сервер, который хранит копию того самого append-only лога из поста 2 и отвечает на запросы:
{
"jsonrpc":"2.0",
"method":"eth_getBalance",
"params": [
"0xТвойАдрес",
"latest"
],
"id":1
}Один эндпоинт, имя метода в теле, никаких /v2/users/:id. Весь web3 - кошельки, дексы, эксплореры - ходит в сеть вот этим протоколом: eth_getBalance, eth_call, eth_sendRawTransaction. Скоро ты будешь дёргать их сам.
Вопрос только один: чью ноду спрашивать? Три пути:
1. Публичные RPC - бесплатные эндпоинты без регистрации. Плюс: ноль порога. Минусы: жёсткие лимиты, падают когда хотят, и - про это забывают - оператор видит твой IP и адреса, которыми ты интересуешься. Для учёбы годится, для продукта - нет.
2. Провайдеры (Alchemy, Infura, QuickNode): free-tier с головой хватает на пет-проекты, стабильность, дашборды запросов. Минус концептуальный: твоё «децентрализованное» приложение разговаривает с миром через одну компанию, которая видит каждый его запрос.
3. Своя нода. Полный контроль и приватность, цена - сотни гигабайт диска и вечная синхронизация. Для нашей серии оверкилл, но знать, что этот путь есть, обязательно: это опция выхода, на которой держится вся модель доверия.
Для Копилки возьмём провайдера с бесплатным тарифом плюс публичный эндпоинт как запасной - так и соберём в посте 5, когда отправим первую транзакцию кодом.
И тонкость, ради которой стоило дочитать: MetaMask из коробки ходит через Infura. То есть дефолтный стек «децентрализованного» веба зависит от одной компании чуть более чем полностью. Децентрализация - это спектр, а не бинарник. Запомни эту рамку - она объясняет половину архитектурных решений в web3.
Задание №4: открой настройки сети в своём кошельке и посмотри, какой RPC URL там прописан. Бонус для смелых: дёрни
eth_blockNumber публичной ноды через curl - готовая команда лежит в репо (docs/rpc.md), пришли в комменты номер блока, который тебе ответили.Райт · blockwright.eth
github.com/blockwright/kopilka
#словарь_райта · №4 - Хеш-функция
Ты пользуешься ей каждый день: хеш git-коммита - это она. Функция, которая превращает любые данные в короткий отпечаток фиксированной длины.
Три свойства делают её фундаментом всего web3.
- Детерминированность: одинаковый вход - всегда одинаковый отпечаток
- Лавинный эффект: поменяй один байт - отпечаток изменится неузнаваемо
- Необратимость: из отпечатка нельзя восстановить данные, только проверить совпадение
Знакомо по web2: контрольные суммы файлов, хеши паролей в базе, ETag в HTTP-кеше
Зачем тебе это слово: «каждый блок несёт хеш предыдущего» из поста 2/20 - теперь ты точно знаешь, почему подмена старого блока рассыпает всю цепочку: лавинный эффект не оставляет шансов сделать это незаметно.
Райт · blockwright.eth
Ты пользуешься ей каждый день: хеш git-коммита - это она. Функция, которая превращает любые данные в короткий отпечаток фиксированной длины.
Три свойства делают её фундаментом всего web3.
- Детерминированность: одинаковый вход - всегда одинаковый отпечаток
- Лавинный эффект: поменяй один байт - отпечаток изменится неузнаваемо
- Необратимость: из отпечатка нельзя восстановить данные, только проверить совпадение
Знакомо по web2: контрольные суммы файлов, хеши паролей в базе, ETag в HTTP-кеше
Зачем тебе это слово: «каждый блок несёт хеш предыдущего» из поста 2/20 - теперь ты точно знаешь, почему подмена старого блока рассыпает всю цепочку: лавинный эффект не оставляет шансов сделать это незаметно.
Райт · blockwright.eth
Четыре поста мы собирали детали: лог (пост 2), ключи (пост 3), ноды и JSON-RPC (пост 4). Сегодня соединяем всё в 20 строк TypeScript - и отправляем настоящую транзакцию в настоящую сеть.
Подготовка. Нужны: тестовый кошелёк из поста 3, немного Sepolia ETH (фосеты - в репо, docs/faucets.md) и приватный ключ этого кошелька в переменной окружения:
bash
Грабля, о которую споткнёшься первым же запуском: MetaMask экспортирует ключ без префикса
И вторая, из поста 4:
Два правила навсегда: ключ живёт в env или .env-файле (который в .gitignore), и это ключ отдельного тестового кошелька - НИКОГДА основного.
Код целиком:
Запуск:
-
-
Что произошло за эти секунды - по мотивам всего блока:
viem подписал транзакцию локально твоим ключом.
1. Приватный ключ не покинул машину - в сеть ушли только подписанные байты.
2. Подписанные байты улетели на ноду тем самым
3. Транзакция подождала в очереди, валидатор включил её в блок - новая запись в append-only логе из поста 2, навсегда
4.
Открой ссылку из вывода: твоя транзакция лежит в публичном логе, виден блок, газ, подпись. Это уже не туториал - это ончейн-факт, который переживёт нас обоих.
Кстати, «отправить самому себе» - не шутка, а честная транзакция: сеть всё равно проверила подпись, взяла газ и записала её в вечность. Идеальный первый шаг без риска промахнуться адресом.
Задание №5: запусти скрипт и пришли хеш своей транзакции в комменты. Соберём под этим постом первую пачку ончейн-артефактов канала. Если фосет жадничает - вспомни docs/faucets.md, там есть PoW-вариант без лимитов.
Блок «Фундамент» закрыт. Со следующего поста - Solidity: пишем контракт Копилки.
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-05
Подготовка. Нужны: тестовый кошелёк из поста 3, немного Sepolia ETH (фосеты - в репо, docs/faucets.md) и приватный ключ этого кошелька в переменной окружения:
bash
export SEPOLIA_PRIVATE_KEY=0x... # ключ ТОЛЬКО тестового кошелька
Грабля, о которую споткнёшься первым же запуском: MetaMask экспортирует ключ без префикса
0x- просто 64 hex-символа. Допиши 0x в начало сам, иначе получишь загадочное «invalid private key, expected hex or 32 bytes».И вторая, из поста 4:
http() без аргумента отправит тебя на дефолтную публичную ноду viem - она под таким rate-limit, что чтение ещё переживёт, а отправку транзакции встретит ошибкой 429. Поэтому в коде нода задана явно - та самая из docs/rpc.md. Не выбрал ноду сам - за тебя выбрали, и плохо.Два правила навсегда: ключ живёт в env или .env-файле (который в .gitignore), и это ключ отдельного тестового кошелька - НИКОГДА основного.
Код целиком:
import { createWalletClient, createPublicClient, http,
parseEther, formatEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { sepolia } from 'viem/chains';
const account = privateKeyToAccount(
process.env.SEPOLIA_PRIVATE_KEY as `0x${string}`
);
// нода — из docs/rpc.md. Дефолтная у viem — под rate-limit (урок поста 4!)
const RPC = 'https://ethereum-sepolia-rpc.publicnode.com';
const publicClient = createPublicClient({ chain: sepolia, transport: http(RPC) });
const wallet = createWalletClient({ account, chain: sepolia, transport: http(RPC) });
const balance = await publicClient.getBalance({ address: account.address });
console.log('Баланс:', formatEther(balance), 'ETH');
// отправляем 0.001 тестового ETH... самому себе. Для первого раза — идеально
const hash = await wallet.sendTransaction({
to: account.address,
value: parseEther('0.001'),
});
console.log('Отправлена:', hash);
console.log('Смотри: https://sepolia.etherscan.io/tx/' + hash);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('В блоке:', receipt.blockNumber, '· статус:', receipt.status);Запуск:
-
npx tsx send.ts - если ключ экспортирован в шелле,-
npx tsx --env-file=.env send.ts - если лежит в .env-файлеЧто произошло за эти секунды - по мотивам всего блока:
viem подписал транзакцию локально твоим ключом.
1. Приватный ключ не покинул машину - в сеть ушли только подписанные байты.
2. Подписанные байты улетели на ноду тем самым
eth_sendRawTransaction из поста 43. Транзакция подождала в очереди, валидатор включил её в блок - новая запись в append-only логе из поста 2, навсегда
4.
waitForTransactionReceipt дождался этого блока и принёс квитанциюОткрой ссылку из вывода: твоя транзакция лежит в публичном логе, виден блок, газ, подпись. Это уже не туториал - это ончейн-факт, который переживёт нас обоих.
Кстати, «отправить самому себе» - не шутка, а честная транзакция: сеть всё равно проверила подпись, взяла газ и записала её в вечность. Идеальный первый шаг без риска промахнуться адресом.
Задание №5: запусти скрипт и пришли хеш своей транзакции в комменты. Соберём под этим постом первую пачку ончейн-артефактов канала. Если фосет жадничает - вспомни docs/faucets.md, там есть PoW-вариант без лимитов.
Блок «Фундамент» закрыт. Со следующего поста - Solidity: пишем контракт Копилки.
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-05
#словарь_райта · №5 - Мемпул
Вчера в посте 5/20 ты отправил первую транзакцию. Между «нажал отправить» и «легла в блок» она жила в мемпуле.
Мемпул - очередь задач перед воркером, только распределённая: каждый узел держит свой список транзакций-кандидатов, которые ещё не попали в блок. Знакомая картина: message queue перед консьюмером, задачи в Redis перед обработчиком.
Одно важное отличие: это не FIFO, а priority queue, где приоритет покупается. Валидатор собирает блок из самых «дорогих» транзакций — кто предложил больше за газ, тот и первый. Заплатил мало - висишь в очереди, пока сеть не разгрузится.
Зачем тебе это слово: «транзакция зависла» почти всегда означает «стоит в мемпуле с низкой ценой газа». Теперь ты знаешь, где именно она стоит.
Райт · blockwright.eth
Вчера в посте 5/20 ты отправил первую транзакцию. Между «нажал отправить» и «легла в блок» она жила в мемпуле.
Мемпул - очередь задач перед воркером, только распределённая: каждый узел держит свой список транзакций-кандидатов, которые ещё не попали в блок. Знакомая картина: message queue перед консьюмером, задачи в Redis перед обработчиком.
Одно важное отличие: это не FIFO, а priority queue, где приоритет покупается. Валидатор собирает блок из самых «дорогих» транзакций — кто предложил больше за газ, тот и первый. Заплатил мало - висишь в очереди, пока сеть не разгрузится.
Зачем тебе это слово: «транзакция зависла» почти всегда означает «стоит в мемпуле с низкой ценой газа». Теперь ты знаешь, где именно она стоит.
Райт · blockwright.eth
#словарь_райта · №6 - Nonce
У каждого аккаунта в Ethereum есть счётчик отправленных транзакций: первая - nonce 0, вторая - nonce 1, и так далее. Обычный автоинкремент, как ID в таблице.
Зачем он нужен? Две вещи
- Порядок: транзакция с nonce 5 не исполнится раньше, чем 4 - как миграции в базе, строго по номерам
- Защита от повтора: без nonce подписанную транзакцию «перевести 100» можно было бы отправить в сеть ещё раз — и ещё. Replay-атака в чистом виде; nonce делает каждую подпись одноразовой.
Побочный эффект, о который спотыкаются все: отправил транзакцию с nonce 4 и низким газом — она застряла, и все следующие (5, 6, 7...) ждут её. Дырка в автоинкременте блокирует очередь.
Зачем тебе это слово: без него не прочитать ни один разбор «почему у меня зависли все транзакции».
Райт · blockwright.eth
У каждого аккаунта в Ethereum есть счётчик отправленных транзакций: первая - nonce 0, вторая - nonce 1, и так далее. Обычный автоинкремент, как ID в таблице.
Зачем он нужен? Две вещи
- Порядок: транзакция с nonce 5 не исполнится раньше, чем 4 - как миграции в базе, строго по номерам
- Защита от повтора: без nonce подписанную транзакцию «перевести 100» можно было бы отправить в сеть ещё раз — и ещё. Replay-атака в чистом виде; nonce делает каждую подпись одноразовой.
Побочный эффект, о который спотыкаются все: отправил транзакцию с nonce 4 и низким газом — она застряла, и все следующие (5, 6, 7...) ждут её. Дырка в автоинкременте блокирует очередь.
Зачем тебе это слово: без него не прочитать ни один разбор «почему у меня зависли все транзакции».
Райт · blockwright.eth
Блок «Фундамент» закрыт - мы отправили транзакцию из кода. Начинаем блок 2: пять постов Solidity, в конце которых у нас будет живой контракт Копилки в тестовой сети. Сегодня - карта местности: что в Solidity узнаёшь с первого взгляда, а что кусается.
Знакомое. Solidity родился с оглядкой на JS, и первые минуты он выглядит как строгий TypeScript:
Типы, функции, модификаторы видимости - всё на месте. Но сходство обманчиво, и разница не в синтаксисе, а в семантике.
Коварное - три вещи, которых НЕТ:
- Нет float. Вообще. Ни
Нет Date. Время - это
Нет undefined и null. У каждого типа есть нулевое значение, и оно есть всегда: несуществующий ключ в
И два места, где семантика другая:
- Целочисленное деление отбрасывает остаток:
- Переполнение. В JS
Задание №6: что окажется в
Ответы - в комменты. Разбор — в следующем посте, где мы наконец напишем Копилку целиком: deposit, withdraw, payable и msg.sender
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-06
Знакомое. Solidity родился с оглядкой на JS, и первые минуты он выглядит как строгий TypeScript:
contract Kopilka {
bool public locked; // boolean
string public name; // string
address public owner; // такого в TS нет, но смысл ясен
uint256 public balance; // number? НЕТ. И вот тут начинается
mapping(address => uint256) deposits; // почти Map
}Типы, функции, модификаторы видимости - всё на месте. Но сходство обманчиво, и разница не в синтаксисе, а в семантике.
Коварное - три вещи, которых НЕТ:
- Нет float. Вообще. Ни
0.5, ни 1e-3. Все деньги - целые числа в минимальных единицах: 1 ETH = 10¹⁸ wei. Это тот же приём, что хранить цены в копейках, только доведённый до абсолюта. Причина - детерминизм: тысячи узлов должны получить байт-в-байт одинаковый результат, а float-арифметика с её округлениями этого не гарантирует. Помнишь bigint, который viem вернул нам в посте 5 вместо number? Вот поэтому: number в JS безопасен только до 2⁵³, а в сети живут числа до 2²⁵⁶Нет Date. Время - это
block.timestamp: целые секунды, и не «сейчас», а момент, когда валидатор собрал блокНет undefined и null. У каждого типа есть нулевое значение, и оно есть всегда: несуществующий ключ в
mapping молча вернёт 0, а не упадёт и не вернёт undefined. Проверка «а есть ли такая запись?» - уже архитектурная задача, а не оператор ?..И два места, где семантика другая:
- Целочисленное деление отбрасывает остаток:
7 / 2 == 3. Порядок операций внезапно начинает влиять на деньги: 7 / 2 * 2 == 6, но 7 * 2 / 2 == 7. Правило: сначала умножай, потом дели- Переполнение. В JS
2**53 + 1 тихо теряет точность. В Solidity с версии 0.8 переполнение — это revert: транзакция откатывается целиком. Строже, зато без тихо испорченных балансовЗадание №6: что окажется в
x, и почему это больно для реальных денег?uint256 fee = (amount / 100) * 3; // комиссия 3%, amount = 250Ответы - в комменты. Разбор — в следующем посте, где мы наконец напишем Копилку целиком: deposit, withdraw, payable и msg.sender
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-06
#словарь_райта · №7 - Финальность
В web2 всё просто:
Свежий блок - ещё не приговор: при редкой перестройке цепочки (реорге) последние блоки могут замениться, и твоя транзакция вернётся в мемпул. Поэтому «в блоке» - это скорее «записано в реплику», а не «закоммичено».
Финальность - момент, после которого протокол гарантирует необратимость. В Ethereum она наступает через ~13 минут: блок финализирован - откатить его можно, только уничтожив треть всего залога сети. Практически - никогда.
Знакомая механика: eventual consistency, только с чётким моментом «eventually наступило».
Зачем тебе это слово: теперь понятно, почему биржи «ждут подтверждений», а не зачисляют депозит с первого блока.
Райт · blockwright.eth
В web2 всё просто:
COMMIT прошёл - данные в базе, точка. В блокчейне между «транзакция в блоке» и «это уже не отменить» есть зазор - и у него есть имя.Свежий блок - ещё не приговор: при редкой перестройке цепочки (реорге) последние блоки могут замениться, и твоя транзакция вернётся в мемпул. Поэтому «в блоке» - это скорее «записано в реплику», а не «закоммичено».
Финальность - момент, после которого протокол гарантирует необратимость. В Ethereum она наступает через ~13 минут: блок финализирован - откатить его можно, только уничтожив треть всего залога сети. Практически - никогда.
Знакомая механика: eventual consistency, только с чётким моментом «eventually наступило».
Зачем тебе это слово: теперь понятно, почему биржи «ждут подтверждений», а не зачисляют депозит с первого блока.
Райт · blockwright.eth
Сначала - разбор задания №6.
А теперь главное. Помнишь манифест? Мы строим Копилку. Сегодня она рождается:
Три вещи, которых нет в твоём web2-стеке:
msg.sender - кто вызвал функцию. Похоже на session.user, но за ним стоит не кука, а криптографическая подпись из поста 3: подделать нельзя.
payable - функция принимает деньги вместе с вызовом. Не «создаёт платёж через Stripe API», а буквально: в вызов функции вложены деньги, и
require - вышибала на входе. Условие не выполнено → revert: транзакция откатывается целиком, как будто её не было. Никаких «половина изменений применилась»
Деплой - без единой строчки настройки. Открой remix.ethereum.org → новый файл Kopilka.sol → вкладка Solidity Compiler (0.8.x) → Compile → вкладка Deploy & Run → Environment: Injected Provider (MetaMask с Sepolia из поста 3) → в поле рядом с Deploy впиши цель, например 1000000000000000 (0.001 ETH в wei) → Deploy → подтверди транзакцию. Всё: у твоего кода теперь есть адрес и баланс
Обрати внимание: в deposit нет ни одной строчки про хранение денег. Баланс - свойство самого контракта (
Задание №7: задеплой свою Копилку в Sepolia и пришли адрес контракта в комменты. Проверим друг у друга progress() - это же публичный лог.
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-07
(250 / 100) * 3 даёт 6: деление отработало первым и срезало остаток (250/100 = 2, а не 2,5). Правильно - 250 * 3 / 100 = 7. На комиссии 3% мы молча потеряли каждый пятый wei. Сначала умножай, потом дели - теперь вы видели, почему.А теперь главное. Помнишь манифест? Мы строим Копилку. Сегодня она рождается:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// Класть могут все. Забрать — только владелец,
/// и только когда цель достигнута.
contract Kopilka {
address public owner; // кто создал копилку
uint256 public goal; // цель накопления, в wei
constructor(uint256 goal_) {
owner = msg.sender; // владелец = тот, кто задеплоил
goal = goal_;
}
// payable = функция умеет принимать ETH вместе с вызовом
function deposit() external payable {
require(msg.value > 0, "empty deposit");
}
function withdraw() external {
require(msg.sender == owner, "not the owner");
require(address(this).balance >= goal, "goal not reached");
(bool ok, ) = owner.call{value: address(this).balance}("");
require(ok, "transfer failed");
}
function progress() external view returns (uint256 current, uint256 target) {
return (address(this).balance, goal);
}
}
Три вещи, которых нет в твоём web2-стеке:
msg.sender - кто вызвал функцию. Похоже на session.user, но за ним стоит не кука, а криптографическая подпись из поста 3: подделать нельзя.
owner = msg.sender в конструкторе - самый распространённый паттерн владения в web3payable - функция принимает деньги вместе с вызовом. Не «создаёт платёж через Stripe API», а буквально: в вызов функции вложены деньги, и
msg.value - их сумма. В web2 таких функций не существуетrequire - вышибала на входе. Условие не выполнено → revert: транзакция откатывается целиком, как будто её не было. Никаких «половина изменений применилась»
Деплой - без единой строчки настройки. Открой remix.ethereum.org → новый файл Kopilka.sol → вкладка Solidity Compiler (0.8.x) → Compile → вкладка Deploy & Run → Environment: Injected Provider (MetaMask с Sepolia из поста 3) → в поле рядом с Deploy впиши цель, например 1000000000000000 (0.001 ETH в wei) → Deploy → подтверди транзакцию. Всё: у твоего кода теперь есть адрес и баланс
Обрати внимание: в deposit нет ни одной строчки про хранение денег. Баланс - свойство самого контракта (
address(this).balance), сеть ведёт его сама. Поэтому наш deposit стоит 21 272 газа - всего на 272 дороже голого перевода. Что такое эти «272» и почему за каждую строчку платят - следующий пост.Задание №7: задеплой свою Копилку в Sepolia и пришли адрес контракта в комменты. Проверим друг у друга progress() - это же публичный лог.
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-07
#словарь_райта · №8 - Детерминизм
Чистая функция: один и тот же вход - всегда один и тот же выход. Без random, без обращений к часам, без сетевых запросов. В web2 это стиль кода, который хвалят на ревью. В web3 - закон физики.
Почему: твой смарт-контракт исполняют тысячи узлов независимо, и все обязаны получить байт в байт одинаковый результат - иначе консенсус развалится. Поэтому в контрактах нет
Отсюда растёт целый класс инфраструктуры: если контракту нужен курс доллара или случайное число, их приходится заносить в блокчейн транзакцией - через оракулы. Но о них отдельно.
Зачем тебе это слово: Копилку v1 мы уже написали - и теперь понятно, почему в Solidity нет половины привычных API. Не отняли, а вычли всё, что мешает тысячам узлов сойтись в одном ответе.
Райт · blockwright.eth
Чистая функция: один и тот же вход - всегда один и тот же выход. Без random, без обращений к часам, без сетевых запросов. В web2 это стиль кода, который хвалят на ревью. В web3 - закон физики.
Почему: твой смарт-контракт исполняют тысячи узлов независимо, и все обязаны получить байт в байт одинаковый результат - иначе консенсус развалится. Поэтому в контрактах нет
Math.random(), нет fetch(), нет настоящего «текущего времени». Любой недетерминизм = сеть не может договориться.Отсюда растёт целый класс инфраструктуры: если контракту нужен курс доллара или случайное число, их приходится заносить в блокчейн транзакцией - через оракулы. Но о них отдельно.
Зачем тебе это слово: Копилку v1 мы уже написали - и теперь понятно, почему в Solidity нет половины привычных API. Не отняли, а вычли всё, что мешает тысячам узлов сойтись в одном ответе.
Райт · blockwright.eth
#словарь_райта · №9 - Merkle Tree
Ты им уже пользуешься: git хранит каталоги как tree-объекты, где хеш папки считается из хешей файлов и вложенных папок. Если изменится один файл, тогда изменятся хэши по цепочке вверх до корня
Merkle tree - та же конструкция: листья - хеши данных, каждый узел - хеш своих детей, наверху - один корневой хеш, «отпечаток» всего набора данных
Суперсила - доказательство включения: чтобы убедиться, что твоя транзакция входит в блок с миллионом транзакций, не нужно скачивать все, достаточно корня и цепочки хешей от твоего листа вверх, это логарифм: ~20 хешей вместо миллиона
Зачем тебе это слово: именно так лёгкие клиенты (кошелёк в твоём телефоне) проверяют блокчейн, не храня его целиком. Корень состояния лежит в каждом блоке
Райт · blockwright.eth
Ты им уже пользуешься: git хранит каталоги как tree-объекты, где хеш папки считается из хешей файлов и вложенных папок. Если изменится один файл, тогда изменятся хэши по цепочке вверх до корня
Merkle tree - та же конструкция: листья - хеши данных, каждый узел - хеш своих детей, наверху - один корневой хеш, «отпечаток» всего набора данных
Суперсила - доказательство включения: чтобы убедиться, что твоя транзакция входит в блок с миллионом транзакций, не нужно скачивать все, достаточно корня и цепочки хешей от твоего листа вверх, это логарифм: ~20 хешей вместо миллиона
Зачем тебе это слово: именно так лёгкие клиенты (кошелёк в твоём телефоне) проверяют блокчейн, не храня его целиком. Корень состояния лежит в каждом блоке
Райт · blockwright.eth
В прошлом посте наш deposit стоил 21 272 газа. Сегодня разберёмся, что это за валюта и кто выставляет курс.
Газ - это единица работы EVM. Каждая операция имеет прайс: сложение - 3 газа, чтение из storage - от 100, запись нового значения в storage - 20 000. Любая транзакция начинается с фиксированных 21 000 - столько стоит сам факт «проверить подпись и записать перевод». Поэтому голый перевод ETH = ровно 21 000, а наш deposit = 21 272: вызов функции, проверка require - вот те 272 сверху
Зачем это вообще? Твой код исполняют тысячи чужих машин. Газ - счётчик, который не даёт написать
Газ ≠ деньги. Деньги = газ × цена газа. Цена измеряется в gwei (10⁻⁹ ETH) и определяется рынком: с EIP-1559 каждый блок имеет base fee - базовую цену, которая автоматически растёт до +12,5% за блок, когда блоки заполнены, и падает, когда пусты. Base fee сжигается - эти деньги не достаются никому. Сверху ты добавляешь чаевые валидатору (priority fee) - обычно копейки
Считаем на живых цифрах. Сегодня base fee в мейннете - около 0,6 gwei, ETH стоит ~$2 500. Деплой нашей Копилки - 243 973 газа:
- сегодня, при 0,6 gwei: 243 973 × 0,6 gwei ≈ 0,00015 ETH ≈ $0,37
- неделю назад, в тихие часы при 0,05 gwei: тот же деплой ≈ 3 цента
- в пике мая 2021, при 200 gwei: ≈ 0,049 ETH ≈ $122 по нынешнему курсу
Один и тот же код. Цена гуляет на три порядка - потому что газ фиксирует работу, а рынок назначает цену работе. Именно поэтому в контрактах экономят не проценты, а слоты storage: запись одного нового слота (20 000) стоит как весь наш deposit
Две вещи, которые все путают:
1. Gas limit - не «сколько спишется», а страховка: сколько работы ты максимум разрешаешь. Не потраченный остаток возвращается. Поставил лимит меньше нужного - транзакция упала с out of gas, а сгоревшая часть не вернётся: работа-то была сделана
2. И revert - не бесплатен. Вышибала require тоже ест газ за всё, что успело исполниться до отказа
Задание №8: открой свою транзакцию из поста 5 (или деплой Копилки из поста 7) на sepolia.etherscan.io и найди три поля: Gas Used, Effective Gas Price и итоговую Transaction Fee. Проверь руками: Used × Price = Fee. Скинь в комменты, у кого дороже всех вышло.
Райт · blockwright.eth
Газ - это единица работы EVM. Каждая операция имеет прайс: сложение - 3 газа, чтение из storage - от 100, запись нового значения в storage - 20 000. Любая транзакция начинается с фиксированных 21 000 - столько стоит сам факт «проверить подпись и записать перевод». Поэтому голый перевод ETH = ровно 21 000, а наш deposit = 21 272: вызов функции, проверка require - вот те 272 сверху
Зачем это вообще? Твой код исполняют тысячи чужих машин. Газ - счётчик, который не даёт написать
while(true) и повесить всю сеть: кончился газ - исполнение остановилось, транзакция откатилась. Это решение halting problem административным способом: не можешь узнать, остановится ли программа, - продай ей топливоГаз ≠ деньги. Деньги = газ × цена газа. Цена измеряется в gwei (10⁻⁹ ETH) и определяется рынком: с EIP-1559 каждый блок имеет base fee - базовую цену, которая автоматически растёт до +12,5% за блок, когда блоки заполнены, и падает, когда пусты. Base fee сжигается - эти деньги не достаются никому. Сверху ты добавляешь чаевые валидатору (priority fee) - обычно копейки
Считаем на живых цифрах. Сегодня base fee в мейннете - около 0,6 gwei, ETH стоит ~$2 500. Деплой нашей Копилки - 243 973 газа:
- сегодня, при 0,6 gwei: 243 973 × 0,6 gwei ≈ 0,00015 ETH ≈ $0,37
- неделю назад, в тихие часы при 0,05 gwei: тот же деплой ≈ 3 цента
- в пике мая 2021, при 200 gwei: ≈ 0,049 ETH ≈ $122 по нынешнему курсу
Один и тот же код. Цена гуляет на три порядка - потому что газ фиксирует работу, а рынок назначает цену работе. Именно поэтому в контрактах экономят не проценты, а слоты storage: запись одного нового слота (20 000) стоит как весь наш deposit
Две вещи, которые все путают:
1. Gas limit - не «сколько спишется», а страховка: сколько работы ты максимум разрешаешь. Не потраченный остаток возвращается. Поставил лимит меньше нужного - транзакция упала с out of gas, а сгоревшая часть не вернётся: работа-то была сделана
2. И revert - не бесплатен. Вышибала require тоже ест газ за всё, что успело исполниться до отказа
Задание №8: открой свою транзакцию из поста 5 (или деплой Копилки из поста 7) на sepolia.etherscan.io и найди три поля: Gas Used, Effective Gas Price и итоговую Transaction Fee. Проверь руками: Used × Price = Fee. Скинь в комменты, у кого дороже всех вышло.
Райт · blockwright.eth
❤1👍1
#словарь_райта · №10 - ABI
Контракт задеплоен, в блокчейне лежит его байткод. Вопрос: откуда твой JS-код знает, что у контракта есть функция
Ответ - ABI (Application Binary Interface): JSON-описание всех функций, аргументов и событий контракта. Это Swagger/OpenAPI-спека, только не для REST-сервиса, а для байткода: по ней библиотека (viem, ethers) кодирует твой вызов
Разница с REST: сервер со Swagger может и соврать про свой API, а ABI генерируется компилятором из исходников контракта - вместе с байткодом, из одного источника
Зачем тебе это слово: ABI - мост между фронтендом и контрактом Копилки. Когда доберёмся до React-части, файл
Райт · blockwright.eth
Контракт задеплоен, в блокчейне лежит его байткод. Вопрос: откуда твой JS-код знает, что у контракта есть функция
deposit() и как её позвать?Ответ - ABI (Application Binary Interface): JSON-описание всех функций, аргументов и событий контракта. Это Swagger/OpenAPI-спека, только не для REST-сервиса, а для байткода: по ней библиотека (viem, ethers) кодирует твой вызов
kopilka.deposit() в байты, которые поймёт EVM, и декодирует ответ обратноРазница с REST: сервер со Swagger может и соврать про свой API, а ABI генерируется компилятором из исходников контракта - вместе с байткодом, из одного источника
Зачем тебе это слово: ABI - мост между фронтендом и контрактом Копилки. Когда доберёмся до React-части, файл
Kopilka.abi.json станет твоим постоянным соседомРайт · blockwright.eth
Это самый прикладной пост серии. Одно правило - минус 25% стоимости записи. Проверено замером, цифры ниже
Storage контракта - это слоты по 32 байта. Не куча, не таблица, и компилятор раскладывает твои поля по ним в порядке объявления. Правило простое: поле влезает в остаток текущего слота, значит кладём рядом, ну а если не влезает тогда открываем новый
А теперь вспомни пост 8: первая запись в новый слот - 20 000 газа. Значит каждый лишний слот отожмет двадцать тысяч, умноженные на каждую первую запись каждого пользователя
Смотри, как легко это теряется. Профиль копилки: сумма, дата создания, счётчик пополнений
Поля те же. Типы те же. Изменился только порядок,
Замер
- первая запись: 88 120 → 66 290 газа. Минус 21 830, минус 25%
- повторная запись: 34 020 → 32 090 - теплые слоты дешевле, но слот всё равно экономится
По правде говоря, у упакованной версии деплой чуть дороже (159 577 против 156 533), потому что компилятору нужен лишний код, чтобы вынимать поля из общего слота битовыми масками. Но деплой случается один раз, а записи - тысячи. Это тот же трейдофф, что индекс в Postgres: платишь на записи схемы, выигрываешь на каждом обращении
Три правила упаковки:
- Поля меньше 32 байт - группируй рядом (и в контракте, и внутри struct)
-
- Не мельчи без причины: одиночный
На заре программирования было такое понятие «выравнивание структур», и о нём забыли, потому что память подешевела. В web3 память - самая дорогая в мире, причём с плавающим ценником: сегодня утром килобайт storage стоит ~6 центов, позавчера стоил под доллар, в пике 2021-го - сотни долларов. Ты можешь экономить только слоты, больше ты ничего не контролируешь. Старые навыки снова нужны для экономии денег
Задание №9: упакуй вот это в минимум слотов и напиши в комменты сколько вышло (компилятор поможет проверить):
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-09
Storage контракта - это слоты по 32 байта. Не куча, не таблица, и компилятор раскладывает твои поля по ним в порядке объявления. Правило простое: поле влезает в остаток текущего слота, значит кладём рядом, ну а если не влезает тогда открываем новый
А теперь вспомни пост 8: первая запись в новый слот - 20 000 газа. Значит каждый лишний слот отожмет двадцать тысяч, умноженные на каждую первую запись каждого пользователя
Смотри, как легко это теряется. Профиль копилки: сумма, дата создания, счётчик пополнений
// «как написалось»: 3 слота
contract PiggyRecordBad {
uint64 createdAt; // слот 0: 8 байт, 24 простаивают
uint256 total; // слот 1: 32 байта, целиком
uint64 depositsCount; // слот 2: снова 8 из 32
}
// отсортировали: 2 слота
contract PiggyRecordGood {
uint256 total; // слот 0: целиком
uint64 createdAt; // слот 1 ┐
uint64 depositsCount; // слот 1 ┘ соседи
}
Поля те же. Типы те же. Изменился только порядок,
uint256 перестал разделять два маленьких поля, и они упаковались в один слотЗамер
- первая запись: 88 120 → 66 290 газа. Минус 21 830, минус 25%
- повторная запись: 34 020 → 32 090 - теплые слоты дешевле, но слот всё равно экономится
По правде говоря, у упакованной версии деплой чуть дороже (159 577 против 156 533), потому что компилятору нужен лишний код, чтобы вынимать поля из общего слота битовыми масками. Но деплой случается один раз, а записи - тысячи. Это тот же трейдофф, что индекс в Postgres: платишь на записи схемы, выигрываешь на каждом обращении
Три правила упаковки:
- Поля меньше 32 байт - группируй рядом (и в контракте, и внутри struct)
-
uint256 и bytes32 - по своим слотам, их не сожмёшь; не давай им разрезать мелочь- Не мельчи без причины: одиночный
uint8 в своём слоте не экономит ничего, а битовые маски удорожают каждый доступ. Пакуй то, что реально живёт вместеНа заре программирования было такое понятие «выравнивание структур», и о нём забыли, потому что память подешевела. В web3 память - самая дорогая в мире, причём с плавающим ценником: сегодня утром килобайт storage стоит ~6 центов, позавчера стоил под доллар, в пике 2021-го - сотни долларов. Ты можешь экономить только слоты, больше ты ничего не контролируешь. Старые навыки снова нужны для экономии денег
Задание №9: упакуй вот это в минимум слотов и напиши в комменты сколько вышло (компилятор поможет проверить):
uint32 a; uint256 b; uint64 c; uint128 d; uint8 e; uint64 f;
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-09
#словарь_райта · №11 - Идемпотентность
Операция идемпотентна, если она всегда веозвращает одно и то же:
В web2 идемпотентность - это дисциплина, которую надо строить своими руками (idempotency keys, дедупликация, уникальные индексы)
В блокчейне она встроена на уровне протокола. Помнишь nonce из #словарь_райта · №6? Подписанная транзакция исполняется ровно один раз - повторная отправка тех же байтов отклоняется сетью. Ретраить можно сколько угодно, деньги дважды не уйдут
Зачем тебе это слово: когда начнём писать тесты Копилки, «что будет, если вызвать дважды» - вопрос, который мы зададим каждой функции контракта.
Райт · blockwright.eth
Операция идемпотентна, если она всегда веозвращает одно и то же:
PUT /user/5 {name: "Райт"} - хоть заново, хоть по таймауту, все равно состояние одно и тоже. А вот POST /orders при ретрае создаст второй заказ, а это - привет, двойное списаниеВ web2 идемпотентность - это дисциплина, которую надо строить своими руками (idempotency keys, дедупликация, уникальные индексы)
В блокчейне она встроена на уровне протокола. Помнишь nonce из #словарь_райта · №6? Подписанная транзакция исполняется ровно один раз - повторная отправка тех же байтов отклоняется сетью. Ретраить можно сколько угодно, деньги дважды не уйдут
Зачем тебе это слово: когда начнём писать тесты Копилки, «что будет, если вызвать дважды» - вопрос, который мы зададим каждой функции контракта.
Райт · blockwright.eth
#словарь_райта · №12 - Реентерабельность
Представь: твоя функция перечисляет деньги и только потом обнуляет баланс. А получатель, не пассивный кошелёк, а код, который в момент получения вызывает твою функцию снова. И снова. И снова
Это reentrancy - повторный вход в функцию до того, как она закончила работу. В web2 похожее ловят в конкурентном коде. Колбэк или другой поток влезает между чтением и записью состояния. В web3 это атака №1 в истории: взлом The DAO в 2016-м увёл треть эфира проекта ровно этим приёмом и расколол Ethereum на две цепи
Лекарство простое и железное: сначала меняй состояние, потом переводи деньги (паттерн checks-effects-interactions)
Зачем тебе это слово: в посте 16 мы намеренно взломаем собственную Копилку этой атакой, а потом починим. Считай это спойлером
Райт · blockwright.eth
Представь: твоя функция перечисляет деньги и только потом обнуляет баланс. А получатель, не пассивный кошелёк, а код, который в момент получения вызывает твою функцию снова. И снова. И снова
Это reentrancy - повторный вход в функцию до того, как она закончила работу. В web2 похожее ловят в конкурентном коде. Колбэк или другой поток влезает между чтением и записью состояния. В web3 это атака №1 в истории: взлом The DAO в 2016-м увёл треть эфира проекта ровно этим приёмом и расколол Ethereum на две цепи
Лекарство простое и железное: сначала меняй состояние, потом переводи деньги (паттерн checks-effects-interactions)
Зачем тебе это слово: в посте 16 мы намеренно взломаем собственную Копилку этой атакой, а потом починим. Считай это спойлером
Райт · blockwright.eth
Наша Копилка работает, но она немая. Деньги приходят, а фронтенд, который мы напишем в блоке 3, узнать об этом не может, только опрашивать баланс по таймеру. Знакомо? Это же polling против websockets
Сегодня добавляем контракту голос
События - это дешёвые логи для тех, кто снаружи:
И это дёшево. deposit с событием сьест 22 962 газа против 21 272 без
Событие стоит ~1 690 газа, а это, на минуточку, в двенадцать раз дешевле, чем запись одного слота storage. А все потому, что лог write-only для контракта. Сети не нужно держать его в состоянии, доступном EVM
Правило архитектуры:
- Storage - это то, что контракт должен читать сам
- События - это то, что нужно людям и фронтендам
Ошибки повзрослели. В посте 7 мы писали
Это дешево (4 байта селектора вместо строки) и, что намного главнее для нас, типизированных, потому что это структурированная ошибка с параметрами. Фронтенд разберёт
Врезка: почему TODO-лист плохой смарт-контракт. Классическая ошибка новичка, тащить в блокчейн всё подряд, но мы теперь умеем считать. Каждая задача - слот storage (20 000 газа), каждая правка - транзакция с подписью и ожиданием блока. On-chain должно жить только то, что требует общего недоверчивого состояния, например, деньги, права, обязательства. TODO-лист не требует. Копилка, наоборот, требует. Она держит чужие деньги по правилам, которые никто не может переписать
Блок 2 закрыт. Мы умеем писать контракт, деплоить, считать его стоимость, упаковывать storage и говорить с внешним миром. В блоке 3 Копилка получит всё остальное: нормальное окружение разработки, тесты и React-фронтенд
Задание №10: Копилке просится третье событие -
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-10
Сегодня добавляем контракту голос
События - это дешёвые логи для тех, кто снаружи:
event Deposited(address indexed from, uint256 amount, uint256 total);
event Withdrawn(address indexed to, uint256 amount);
function deposit() external payable {
if (msg.value == 0) revert EmptyDeposit();
emit Deposited(msg.sender, msg.value, address(this).balance);
}
emit пишет запись в лог блока. Контракт сам прочитать её не может, зато внешний мир - вполне себе да. Фронтенд подписывается на события и обновляет UI мгновенно, индексаторы строят историю операций, Etherscan показывает человекочитаемую лентуИ это дёшево. deposit с событием сьест 22 962 газа против 21 272 без
Событие стоит ~1 690 газа, а это, на минуточку, в двенадцать раз дешевле, чем запись одного слота storage. А все потому, что лог write-only для контракта. Сети не нужно держать его в состоянии, доступном EVM
Правило архитектуры:
- Storage - это то, что контракт должен читать сам
- События - это то, что нужно людям и фронтендам
Ошибки повзрослели. В посте 7 мы писали
require(msg.sender == owner, "not the owner") - строка хранится в байткоде и улетает в ответ целиком. В Solidity 0.8.4 появились custom errors:error NotOwner();
error GoalNotReached(uint256 current, uint256 goal);
if (msg.sender != owner) revert NotOwner();
Это дешево (4 байта селектора вместо строки) и, что намного главнее для нас, типизированных, потому что это структурированная ошибка с параметрами. Фронтенд разберёт
GoalNotReached(3, 10) и покажет «накоплено 3 из 10», а не голый текст. Это исключения с типами против throw "строка"Врезка: почему TODO-лист плохой смарт-контракт. Классическая ошибка новичка, тащить в блокчейн всё подряд, но мы теперь умеем считать. Каждая задача - слот storage (20 000 газа), каждая правка - транзакция с подписью и ожиданием блока. On-chain должно жить только то, что требует общего недоверчивого состояния, например, деньги, права, обязательства. TODO-лист не требует. Копилка, наоборот, требует. Она держит чужие деньги по правилам, которые никто не может переписать
Блок 2 закрыт. Мы умеем писать контракт, деплоить, считать его стоимость, упаковывать storage и говорить с внешним миром. В блоке 3 Копилка получит всё остальное: нормальное окружение разработки, тесты и React-фронтенд
Задание №10: Копилке просится третье событие -
GoalReached. Напиши в комменты его сигнатуру и место, где его emit'итьРайт · blockwright.eth
github.com/blockwright/kopilka · tag post-10
Блок 3 открыт. Копилка написана и задеплоена - но всё это время мы работали как туристы: Remix в браузере, кнопка Deploy, ручные клики. Для первого контракта - идеально. Для продукта - нет. Нужны тесты, повторяемый деплой и сеть, которую не жалко ломать. Сегодня репо перестаёт быть витриной и становится рабочим станком
Инструмент - Hardhat. Это webpack/vite мира контрактов: компиляция, локальная сеть, тесты, деплой - одной CLI. Для нас выбор очевидный ещё и потому, что третья версия дружит с viem - тем самым, которым мы отправляли транзакцию в посте 5. Честности ради: есть и Foundry - быстрее, тесты пишутся на самом Solidity, его любят аудиторы. Но мы - JS-разработчики, и наш станок будет говорить на TypeScript
Ставим (Node 22.13+):
Получаем
Главный подарок - своя сеть. Одна команда:
- и у тебя личный Ethereum на localhost: мгновенные блоки, 20 аккаунтов с фейковыми 10 000 ETH на каждом, любые эксперименты бесплатно. Помнишь, как мы клянчили тестовый эфир из фосетов? Здесь ты сам себе фосет
Деплой - не клик, а код. Модуль для Hardhat Ignition:
Деплой стал воспроизводимым артефактом в git, а не действием, которое ты вспоминаешь по скриншотам. А для деплоя в Sepolia шаблон ждёт переменные
Задание №11: подними локальную ноду, задеплой в неё Копилку и пришли в комменты адрес, который выдал Ignition. Подсказка: он у всех будет одинаковый и это не баг. Кто первым объяснит почему - тому уважение под постом
Со следующего поста этот станок начнёт окупаться: пишем тесты - в web3 баг равен потерянным деньгам
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-11
Инструмент - Hardhat. Это webpack/vite мира контрактов: компиляция, локальная сеть, тесты, деплой - одной CLI. Для нас выбор очевидный ещё и потому, что третья версия дружит с viem - тем самым, которым мы отправляли транзакцию в посте 5. Честности ради: есть и Foundry - быстрее, тесты пишутся на самом Solidity, его любят аудиторы. Но мы - JS-разработчики, и наш станок будет говорить на TypeScript
Ставим (Node 22.13+):
npx hardhat --init
# шаблон: node-test-runner-viem, и проект готов
Получаем
hardhat.config.ts (конфиг - тоже TypeScript), папки contracts/, test/, ignition/. Кладём нашу Kopilka.sol в contracts/ - и:npx hardhat compile
Главный подарок - своя сеть. Одна команда:
npx hardhat node
- и у тебя личный Ethereum на localhost: мгновенные блоки, 20 аккаунтов с фейковыми 10 000 ETH на каждом, любые эксперименты бесплатно. Помнишь, как мы клянчили тестовый эфир из фосетов? Здесь ты сам себе фосет
Деплой - не клик, а код. Модуль для Hardhat Ignition:
// ignition/modules/Kopilka.ts
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
import { parseEther } from "viem";
export default buildModule("KopilkaModule", (m) => {
const goal = m.getParameter("goal", parseEther("0.001"));
return { kopilka: m.contract("Kopilka", [goal]) };
});
npx hardhat ignition deploy ignition/modules/Kopilka.ts
Деплой стал воспроизводимым артефактом в git, а не действием, которое ты вспоминаешь по скриншотам. А для деплоя в Sepolia шаблон ждёт переменные
SEPOLIA_RPC_URL и SEPOLIA_PRIVATE_KEY - да, ровно те, что лежат у нас в .env с поста 5. Совпадение? Нет, стандарт де-фактоЗадание №11: подними локальную ноду, задеплой в неё Копилку и пришли в комменты адрес, который выдал Ignition. Подсказка: он у всех будет одинаковый и это не баг. Кто первым объяснит почему - тому уважение под постом
Со следующего поста этот станок начнёт окупаться: пишем тесты - в web3 баг равен потерянным деньгам
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-11
❤1
#словарь_райта · №13 - Proof of Work
Из комментариев: «вычисление хешей - основной момент энергозатрат в блокчейне». Разберём, потому что энергию жгло не вычисление
На поиск одного хеша нужны наносекунды, это копеечная операция. Proof of Work - это лотерея, чтобы получить право записать блок, майнер перебирает варианты, пока хеш блока не окажется меньше заданного порога. Квинтиллионы попыток в секунду по всей сети ради одного «красивого» хеша. Дорога не функция - дорог перебор
Зачем так странно? Это способ сделать запись в общий лог дорогой без всякого начальника, если хочешь переписать историю - пересчитай всю работу заново, а сеть за это время уйдёт вперёд. Защита не паролем, а физикой. Атака стоит электричества больше, чем принесёт
Зачем тебе это слово: Bitcoin живёт на PoW до сих пор. А Ethereum ушёл с него в 2022. Куда и почему, в следующем выпуске
Райт · blockwright.eth
Из комментариев: «вычисление хешей - основной момент энергозатрат в блокчейне». Разберём, потому что энергию жгло не вычисление
На поиск одного хеша нужны наносекунды, это копеечная операция. Proof of Work - это лотерея, чтобы получить право записать блок, майнер перебирает варианты, пока хеш блока не окажется меньше заданного порога. Квинтиллионы попыток в секунду по всей сети ради одного «красивого» хеша. Дорога не функция - дорог перебор
Зачем так странно? Это способ сделать запись в общий лог дорогой без всякого начальника, если хочешь переписать историю - пересчитай всю работу заново, а сеть за это время уйдёт вперёд. Защита не паролем, а физикой. Атака стоит электричества больше, чем принесёт
Зачем тебе это слово: Bitcoin живёт на PoW до сих пор. А Ethereum ушёл с него в 2022. Куда и почему, в следующем выпуске
Райт · blockwright.eth
#словарь_райта · №14 - Proof of Stake
Proof of Work делает запись в общий лог дорогой через электричество. Proof of Stake делает её дорогой через залог
Хочешь стать валидатором Ethereum - заблокируй 32 ETH депозитом. Сеть случайно выбирает, кто предлагает следующий блок, и шанс пропорционален стейку. Никакого перебора хешей, подписал блок - получил награду. По энергии валидатор - обычный сервер, а не завод.
Где же защита, если работать стало дёшево? В том, что депозит можно потерять. За нарушение протокола сеть сожжёт часть твоего стейка автоматически. Злоумышленнику теперь нужно не железо, а капитал - и этот капитал сгорает при атаке. Электричество заменили деньгами под залогом
Зачем тебе это слово: весь Ethereum, где живёт наша Копилка, работает так с 2022 года. А механизм сожжения залога называется слэшинг и про него следующий выпуск
Райт · blockwright.eth
Proof of Work делает запись в общий лог дорогой через электричество. Proof of Stake делает её дорогой через залог
Хочешь стать валидатором Ethereum - заблокируй 32 ETH депозитом. Сеть случайно выбирает, кто предлагает следующий блок, и шанс пропорционален стейку. Никакого перебора хешей, подписал блок - получил награду. По энергии валидатор - обычный сервер, а не завод.
Где же защита, если работать стало дёшево? В том, что депозит можно потерять. За нарушение протокола сеть сожжёт часть твоего стейка автоматически. Злоумышленнику теперь нужно не железо, а капитал - и этот капитал сгорает при атаке. Электричество заменили деньгами под залогом
Зачем тебе это слово: весь Ethereum, где живёт наша Копилка, работает так с 2022 года. А механизм сожжения залога называется слэшинг и про него следующий выпуск
Райт · blockwright.eth
В web2 у бага есть жизненный цикл: заметили в проде, откатили, захотфиксили, извинились в постмортеме, все счастливы. В web3 этого цикла нет. Контракт после деплоя не изменить, а внутри лежат чужие деньги. Помнишь словарь №12 про The DAO? Тот код тоже прошёл ревью. Тесты здесь - не «культура разработки», а инстинкт выживания
Хорошая новость: станок из прошлого поста уже всё умеет. Тесты пишутся на TypeScript, раннер - родной
Обрати внимание, что мы утверждаем: не «вернулась ошибка», а «транзакция откатилась именно с NotOwner». Custom errors из поста 10 внезапно оказались нужны не только фронтенду - тесты по ним проверяют, что контракт отказал по правильной причине. Отказал - но не по той причине - это тоже баг, просто затаившийся
А вот главный тест любого контракта с деньгами - куда они пришли:
Одна строка утверждает: после withdraw владелец стал богаче ровно на сумму цели, с поправкой на газ. Такой ассерт невозможно обмануть красивым логом.
Полный набор - семь тестов в репо: деплой, приём депозита с событием, нулевой депозит, чужой withdraw, withdraw до цели (с проверкой цифр в GoalNotReached), вывод всей суммы, событие Withdrawn. Каждый тест поднимает свежую сеть в памяти и живёт миллисекунды - прогон быстрее, чем ты читаешь это предложение
Чему учит сам список: тестируй отказы прилежнее, чем успехи. Успешный путь ты и так прогонял руками сто раз. Деньги теряют на путях, по которым никто не ходил: нулевые значения, вызов не тем аккаунтом, состояние «на границе» цели
Задание №12: посмотри на Копилку и предложи в комментах edge case, который я не покрыл. Их осталось минимум три, один из них - с сюрпризом. Лучший разберу в следующем посте
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-12
Хорошая новость: станок из прошлого поста уже всё умеет. Тесты пишутся на TypeScript, раннер - родной
node:test, никакого нового фреймворка учить не надоit("withdraw чужаком откатывается: NotOwner", async () => {
const kopilka = await deploy();
await kopilka.write.deposit({ value: GOAL });
await viem.assertions.revertWithCustomError(
kopilka.write.withdraw({ account: stranger.account }),
kopilka,
"NotOwner",
);
});Обрати внимание, что мы утверждаем: не «вернулась ошибка», а «транзакция откатилась именно с NotOwner». Custom errors из поста 10 внезапно оказались нужны не только фронтенду - тесты по ним проверяют, что контракт отказал по правильной причине. Отказал - но не по той причине - это тоже баг, просто затаившийся
А вот главный тест любого контракта с деньгами - куда они пришли:
await viem.assertions.balancesHaveChanged(
kopilka.write.withdraw(),
[{ address: owner.account.address, amount: GOAL }],
);
Одна строка утверждает: после withdraw владелец стал богаче ровно на сумму цели, с поправкой на газ. Такой ассерт невозможно обмануть красивым логом.
Полный набор - семь тестов в репо: деплой, приём депозита с событием, нулевой депозит, чужой withdraw, withdraw до цели (с проверкой цифр в GoalNotReached), вывод всей суммы, событие Withdrawn. Каждый тест поднимает свежую сеть в памяти и живёт миллисекунды - прогон быстрее, чем ты читаешь это предложение
Чему учит сам список: тестируй отказы прилежнее, чем успехи. Успешный путь ты и так прогонял руками сто раз. Деньги теряют на путях, по которым никто не ходил: нулевые значения, вызов не тем аккаунтом, состояние «на границе» цели
Задание №12: посмотри на Копилку и предложи в комментах edge case, который я не покрыл. Их осталось минимум три, один из них - с сюрпризом. Лучший разберу в следующем посте
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-12