Перейти к основному содержимому
Блокчейн12ГЛАВА 12Библиотеки, import и хеши: keccak256 и abi.encodelibrarykeccak256 → 32 байтаобщий коди отпечаток

Библиотеки, import и хеши: keccak256 и abi.encode

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

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

В эталонном контракте задания пароли пользователей хранятся так:

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
ТРИ СПОСОБА НАПИСАТЬ ОДНУ ФОРМУЛУформула прямо в теле функции27 282 газа637 байтinternal-библиотека27 345 газа666 байтexternal-библиотека31 403 газа765 + 569 байтinternal встраивается в байткод — разница с формулой 63 газа; external живёт отдельным контрактоми стоит +4 121 газа на каждый вызов, потому что это переход в другой контракт
internal-библиотека почти бесплатна — её тело встраивается в контракт; external живёт отдельно и стоит перехода

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") → 0x616263("a", "bc") → 0x616263хеши равны: trueбайты склеены подряд, границ нетabi.encode("ab", "c") → 192 байта("a", "bc") → 192 байтахеши равны: falseдлины и смещения различают аргументыупакованная кодировка склеивает динамические аргументы так, что границу между ними уже не восстановитьправило: хешируете больше одного аргумента — берите abi.encodeтот же хеш считается вне сети, на обычном ноутбуке — это чистая математика
Упакованная кодировка склеивает аргументы подряд — граница между ними теряется, и разные пары дают один хеш

abi.encodePacked склеивает байты подряд, без длин и выравнивания. Для двух строк «ab» + «c» и «a» + «bc» получаются одинаковые три байта — и, соответственно, одинаковый хеш. Это коллизия: два разных набора данных неотличимы. Если на таком хеше построена проверка прав или уникальности, её можно обойти.

abi.encode пишет каждое поле в 32-байтовые слоты вместе с длинами. Занимает больше места, зато границы между аргументами сохраняются.

Правило: хешируете больше одного аргумента — берите abi.encode. Упакованная форма уместна для одного значения или когда типы фиксированной длины.

Последняя строка вывода — важная сама по себе: тот же хеш считается вне сети, обычной программой на ноутбуке. Хеширование — чистая математика, никакой связи с блокчейном у него нет.

Отвечено вопросов: 0 из 2
keccak256(abi.encodePacked("ab","c")) и keccak256(abi.encodePacked("a","bc")). Что вернётся?
Чем internal-библиотека отличается от external по цене?
Отвечено верно: 0 из 2

Пароль в блокчейне: разбор за пятьдесят миллисекунд​

Теперь к строке из эталонного контракта. Третья модель повторяет её один в один: контракт хранит 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…, не деплоер)
ХЕШ ПИНА В ХРАНИЛИЩЕ — НЕ ЗАЩИТАконтрактbytes32 private pinHashgetter-а нетслот 0посторонний читает хранилище0x4c681d1f…23be5cfбез единой транзакцииперебор 000–999пин 742 за 55 мс743 попыткивсе 1000 вариантов перебираются за 70 мс; шесть цифр — примерно 70 секундпосле этого чужой аккаунт открывает замок: opened = trueслово private прячет данные от других контрактов, а не от людей: хранилище читается снаружи целиком
Приватное поле читается снаружи, а трёхзначный пин перебирается за сотую долю секунды — без единой транзакции

Три проблемы, каждая из которых сама по себе достаточна.

Слово private не прячет данные. Оно закрывает доступ другим контрактам и убирает автоматическую функцию-читалку. Но хранилище контракта публично: любой читает нужный слот напрямую, не отправляя ни одной транзакции.

Короткий вход перебирается мгновенно. Тысяча вариантов — 70 миллисекунд. Миллион — около семидесяти секунд. Односторонность хеша ничего не даёт, если вариантов мало: перебирают не хеш, а вход.

Пароль вообще попадает в сеть. Даже если бы хеш был неуязвим, сама транзакция с паролем видна всем: аргументы вызова хранятся в блоке открытым текстом.

Что делают вместо этого:

  • соль — случайные байты, подмешанные к паролю: одинаковые пароли дают разные хеши, а заранее посчитанные таблицы перестают работать;
  • длинный непредсказуемый секрет — перебор становится нереальным;
  • а по-хорошему — вход подписью кошелька, без паролей вообще: пользователь доказывает, что владеет адресом, и никакой секрет в сеть не уходит.

Для отчёта по информационной безопасности это готовый пункт с числами: «пароли хранятся как хеш в открытом хранилище; трёхзначный пин восстанавливается перебором за 70 мс без транзакций; предлагается вход по подписи».

Отвечено вопросов: 0 из 2
Поле с хешем пароля объявлено private. Кто может его прочитать?
Почему хеш не спасает трёхзначный пин?
Отвечено верно: 0 из 2

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

Изменяемая переменная в библиотеке. Компилятор запрещает: у библиотеки нет своего хранилища.

Вызов метода без 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 % протоколу — выносится в одно место, потому что повторяется в каждом маркете; хеширование при этом не нужно нигде, кроме служебных ключей.

Отвечено вопросов: 0 из 2
Как сформулировать пункт отчёта про пароли в контракте?
Что первым делом проверить в модуле А, если решение опирается на подключённую библиотеку?
Отвечено верно: 0 из 2

Что дальше​

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

Отвечено вопросов: 0 из 1
Зачем нужна соль при хешировании секрета?
Отвечено верно: 0 из 1

Углубиться​

  • Библиотеки, работающие с хранилищем вызывающего — как using for применяют к структурам и почему при этом нужен storage.
  • Спецификация упакованной кодировки — формальное описание того, почему возникают коллизии.
  • Хеш адреса будущего контракта — как заранее вычислить адрес, по которому развернётся контракт.
  • Подпись вместо пароля — как контракт проверяет, что сообщение подписал владелец адреса.
  • Готовые библиотеки — что полезного лежит в стандартных наборах: безопасные переводы, контроль доступа, защита от повторного входа.

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

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

Видео по теме

ВидеоSolidity и смарт-контракты Ethereum, урок #12 | Интерфейсы, библиотеки, тестыIlya Krukowski
ВидеоSolidity и смарт-контракты Ethereum, урок #8 | Древо Меркла, хэши, encode, циклыIlya Krukowski
ВидеоSolidity | Урок 4. Функция хеширования, приведение типов и событияАртем Кукуруза
ВидеоEthereum #1: Хэш функции, keccak256, применение в EthereumDmitry Strukovsky
ВидеоSolidity Encoding Explained: abi.encode vs abi.encodePacked vs bytes vs decodeCodeByAnkita

Комментарии

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