Библиотеки, import и хеши: keccak256 и abi.encode
- Зачем это нужно
- Библиотека — функции без собственной памяти
- internal или external: цена выбора
- import: подключение файлов
- Хеш: односторонняя свёртка
- abi.encode против abi.encodePacked
- Пароль в блокчейне: разбор за пятьдесят миллисекунд
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Что дальше
- Углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
В эталонном контракте задания пароли пользователей хранятся так:
passwordMap["owner"] = keccak256(abi.encode("123"));
Выглядит разумно: не сам пароль, а его отпечаток. На деле здесь три проблемы разного масштаба, и одну из них мы вскроем прогоном — за пятьдесят миллисекунд.
Заодно разберём, как не писать одну и ту же формулу в трёх контрактах: библиотеки, подключение файлов и цену каждого способа в газе.
Библиотека — функции без собственной памяти
library FeeLib {
uint256 internal constant LENDER_SHARE = 70;
function share(uint256 amount, uint256 pct) internal pure returns (uint256) {
return amount * pct / 100;
}
}
Библиотека нужна там, где одна и та же формула повторяется в нескольких контрактах: расчёт доли, перевод в мелкие единицы, проверка границ.
Функции библиотеки можно превратить в «методы» типа:
using FeeLib for uint256;
uint256 lenders = interest.share(70); // то же, что FeeLib.share(interest, 70)
Значение слева от точки становится первым аргументом. Без строки using такая запись не скомпилируется — компилятор не знает, откуда у числа взялся метод.
internal или external: цена выбора
У библиотечной функции есть выбор видимости, и он меняет не только код, но и деньги. Первая модель считает одну и ту же формулу тремя способами:
── байткод в сети, байт ──
формула в теле функции 637
internal-библиотека 666
external-библиотека 765 + отдельный контракт FeeExternal 569
── газ на distribute(1_000_000), второй вызов ──
формула в теле функции 27282
internal-библиотека 27345
external-библиотека 31403
internal против формулы: 63 газа разницы
external против формулы: +4121 газа за delegatecall
lendersPool у всех трёх: 1400000 / 1400000 / 1400000
internal практически бесплатна. Компилятор копирует тело функции прямо в контракт: разница с формулой, написанной руками, — 63 газа. Читаемость даром.
external стоит перехода. Такая библиотека разворачивается отдельным контрактом, и каждый вызов — переход в него: плюс 4 121 газа. Зато её код не дублируется в десяти контрактах, а при обновлении можно развернуть новую версию.
Результат у всех трёх одинаковый. По коду выбор выглядит одинаково — цену видно только в замере.
Внешнюю библиотеку мало развернуть — её адрес нужно подставить в байткод контракта при развёртывании. Это называется «связывание» (linking), и делает его инструмент сборки. Забыли — контракт не развернётся.
import: подключение файлов
import "./FeeLib.sol"; // файл рядом
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // файл из пакета
import {IERC20} from "./interfaces/IERC20.sol"; // только нужное имя
Первая форма берёт файл из той же папки. Вторая — из установленного пакета. Третья, точечная, подключает только названное имя: так меньше шансов на конфликт двух одинаковых названий.
Эталонный контракт задания начинается со второй формы — с подключения библиотеки токенов.
На площадке интернета нет, и установить пакет из сети не получится. Значит, нужные файлы либо уже лежат в образе рабочего места, либо копируются в проект руками. Проверить это стоит в первые минуты модуля, а не за час до сдачи: если подключения нет, базовый токен придётся написать самому — как мы делали в главе про наследование.
Ещё одно следствие версии 0.8: библиотека «безопасной арифметики», которую подключали в старых уроках, больше не нужна — переполнение проверяет сам компилятор.
Хеш: односторонняя свёртка
Хеш-функция берёт байты любой длины и выдаёт ровно 32 байта. Свойства, ради которых её используют:
- одинаковый вход — всегда одинаковый выход;
- изменение одного байта на входе полностью меняет выход;
- обратно хода нет: по хешу нельзя вычислить исходные данные.
В Solidity это keccak256. Он принимает байты, а не строку, поэтому строку сначала кодируют:
bytes32 h = keccak256(abi.encode(pin));
Попытка передать строку напрямую даёт ошибку: функция требует ровно один аргумент типа bytes.
abi.encode против abi.encodePacked
Есть два способа превратить значения в байты, и разница между ними стоит целого раздела.
Вторая модель хеширует одни и те же пары строк обоими способами:
── abi.encodePacked: байты подряд ──
("ab", "c") → 0x616263 (3 байта)
("a", "bc") → 0x616263 (3 байта)
keccak256(abi.encodePacked("ab","c")) = 0x4e03657a…2d6c45
keccak256(abi.encodePacked("a","bc")) = 0x4e03657a…2d6c45
хеши равны: true ← коллизия: разные аргументы, один хеш
── abi.encode: смещение, длина, данные — по 32 байта ──
("ab", "c") → 192 байт
("a", "bc") → 192 байт
keccak256(abi.encode("ab","c")) = 0x8c98d572…b4b7c6
keccak256(abi.encode("a","bc")) = 0x68cd083d…8e532c
хеши равны: false
── тот же хеш можно посчитать без сети ──
ethers.keccak256(abi.encode("ab","c")) = 0x8c98d572…b4b7c6
совпадает с контрактом: true
abi.encodePacked склеивает байты подряд, без длин и выравнивания. Для двух строк «ab» + «c» и «a» + «bc» получаются одинаковые три байта — и, соответственно, одинаковый хеш. Это коллизия: два разных набора данных неотличимы. Если на таком хеше построена проверка прав или уникальности, её можно обойти.
abi.encode пишет каждое поле в 32-байтовые слоты вместе с длинами. Занимает больше места, зато границы между аргументами сохраняются.
Правило: хешируете больше одного аргумента — берите abi.encode. Упакованная форма уместна для одного значения или когда типы фиксированной длины.
Последняя строка вывода — важная сама по себе: тот же хеш считается вне сети, обычной программой на ноутбуке. Хеширование — чистая математика, никакой связи с блокчейном у него нет.
Пароль в блокчейне: разбор за пятьдесят миллисекунд
Теперь к строке из эталонного контракта. Третья модель повторяет её один в один: контракт хранит keccak256(abi.encode(pin)) в приватном поле и открывает замок тому, кто назовёт правильный пин.
── что видит посторонний ──
pinHash объявлен private, getter-а нет
слот 0 хранилища: 0x4c681d1fc3c303a3ed5a49a814434e202c0be374f5ab82670580cad1323be5cf
── перебор 000–999 на ноутбуке ──
найден пин: "742" за 743 попыток, 55.5 мс
все 1000 вариантов целиком: 70.4 мс
оценка на 6 цифр (1 000 000 вариантов): ~70 с
── взломщик открывает замок чужим пином ──
opened = true (вызвал 0x70997970…, не деплоер)
Три проблемы, каждая из которых сама по себе достаточна.
Слово private не прячет данные. Оно закрывает доступ другим контрактам и убирает автоматическую функцию-читалку. Но хранилище контракта публично: любой читает нужный слот напрямую, не отправляя ни одной транзакции.
Короткий вход перебирается мгновенно. Тысяча вариантов — 70 миллисекунд. Миллион — около семидесяти секунд. Односторонность хеша ничего не даёт, если вариантов мало: перебирают не хеш, а вход.
Пароль вообще попадает в сеть. Даже если бы хеш был неуязвим, сама транзакция с паролем видна всем: аргументы вызова хранятся в блоке открытым текстом.
Что делают вместо этого:
- соль — случайные байты, подмешанные к паролю: одинаковые пароли дают разные хеши, а заранее посчитанные таблицы перестают работать;
- длинный непредсказуемый секрет — перебор становится нереальным;
- а по-хорошему — вход подписью кошелька, без паролей вообще: пользователь доказывает, что владеет адресом, и никакой секрет в сеть не уходит.
Для отчёта по информационной безопасности это готовый пункт с числами: «пароли хранятся как хеш в открытом хранилище; трёхзначный пин восстанавливается перебором за 70 мс без транзакций; предлагается вход по подписи».
Типичные ошибки
Изменяемая переменная в библиотеке. Компилятор запрещает: у библиотеки нет своего хранилища.
Вызов метода без using for. Компилятор не знает, откуда у типа взялся такой метод.
keccak256 от строки напрямую. Функция требует ровно один аргумент типа bytes — строку сначала кодируют.
keccak256 с двумя аргументами. Аргумент один; несколько значений сначала объединяют кодировщиком.
abi.encodePacked с двумя динамическими аргументами. Компилируется, работает — и даёт коллизии.
Внешняя библиотека без связывания. Развернуть контракт не получится: в байткоде остался незаполненный адрес.
Подключение пакета, которого нет на площадке. Проверять в первые минуты модуля, а не перед сдачей.
Пароль или его хеш в хранилище. Читается снаружи, перебирается за миллисекунды, да ещё и попадает в сеть с транзакцией.
Слова главы
library— набор функций без изменяемого состояния.using A for T— функции библиотеки становятся «методами» типа.internal-библиотека — тело встраивается в байткод контракта; почти бесплатна.external-библиотека — отдельный контракт; вызов стоит перехода, требует связывания.- Связывание (linking) — подстановка адреса библиотеки в байткод при развёртывании.
import— подключение файла: из папки, из пакета или точечно по имени.keccak256— хеш: любой вход, ровно 32 байта на выходе, обратно хода нет.abi.encode— кодировка с длинами и смещениями;abi.encodePacked— байты подряд.- Коллизия — разные данные с одинаковым хешем; у упакованной кодировки возникает легко.
- Соль (salt) — случайные байты, добавленные к секрету перед хешированием.
Тренажёр: набрать вслепую
Цель: скорость ≥ 100 зн/мин, точность ≥ 88%
library FeeLib {
На чемпионате
Модуль В, 14 баллов. Из этой главы в отчёты идут три пункта, и все три — про эталонный контракт:
- Информационная безопасность: пароли в контракте. Формулировка с числами: хранится хеш в открытом хранилище, читается посторонним без транзакций, короткий пин восстанавливается перебором за десятки миллисекунд; предлагается вход по подписи кошелька.
- Логика: попытка положить
bytes32в словарь строк — тип не тот, компилятор откажет. - Оптимизация: повторяющиеся формулы вынести в
internal-библиотеку; экономия по коду, а не по газу — так и написать, не завышая эффект.
Модуль А, 37 баллов. Практически: подключение библиотек проверяется в первые минуты; расчёт долей — 70 % кредиторам, 30 % протоколу — выносится в одно место, потому что повторяется в каждом маркете; хеширование при этом не нужно нигде, кроме служебных ключей.
Что дальше
Осталась последняя глава блока — про приёмы, без которых контракт не проходит аудит: владелец и права, порядок действий при выплатах, защита от повторного входа и правило «тяни, а не толкай». После неё — экзамен по всему блоку про объекты и связи.
Углубиться
- Библиотеки, работающие с хранилищем вызывающего — как
using forприменяют к структурам и почему при этом нуженstorage. - Спецификация упакованной кодировки — формальное описание того, почему возникают коллизии.
- Хеш адреса будущего контракта — как заранее вычислить адрес, по которому развернётся контракт.
- Подпись вместо пароля — как контракт проверяет, что сообщение подписал владелец адреса.
- Готовые библиотеки — что полезного лежит в стандартных наборах: безопасные переводы, контроль доступа, защита от повторного входа.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Библиотеки — docs.soliditylang.org — ограничения,
internalпротивexternal, связывание. - using for — как функции становятся методами типов.
- Подключение файлов — все формы
import. - Упакованная кодировка — формальное описание и предупреждение о коллизиях.
- Глобальные функции: keccak256 — хеши, подписи, восстановление адреса.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.