Перейти к содержанию

Foundry: проверка байтов, кодирования и хеша

В предыдущем уроке реестр сохранял переданный bytes32. Мы часто получали его из строки edition-one, но настоящее приложение связывает запись с учебными материалами. Теперь выясним, какие именно байты должны участвовать в вычислении отпечатка.

Подготовим маленький манифест и две разные операции кодирования. Это позволит избежать ситуации, когда контрактные проверки проходят, а браузер вычисляет другой хеш того же по смыслу документа. Код остаётся неисполненным; конкретные выходные хеши и результат сборки не заявляются.

Отпечаток относится к байтам

Фраза «тот же документ» неоднозначна. Два JSON-файла могут содержать одинаковые поля, но отличаться пробелами, порядком ключей или последним переводом строки. При побайтовом хешировании это разные входы. Для воспроизводимого выпуска нужно сначала определить форму файла, затем вычислять отпечаток именно от её байтов.

В нашей учебной фикстуре используем строку {"courseId":17,"edition":1} без пробелов и завершающего перевода строки. Символы здесь входят в ASCII и совпадают с соответствующими однобайтовыми значениями UTF-8. Позднее русский заголовок потребует внимательнее учитывать многобайтовое представление и нормализацию текста.

Контракт не знает, существует ли такой манифест. Метод publish получает уже вычисленный набор из 32 байтов и проверяет лишь ненулевое значение. Проверка правильного формирования файла относится к отдельному компоненту, поэтому её нельзя приписывать сценарию простого сохранения digest.

Публикуем хеш конкретной фикстуры

Добавим следующую функцию в CourseCatalogTest. Общий веб-проект выбирает SHA-256 точных байтов манифеста; в Solidity для него доступна функция sha256. Она согласуется с выбором алгоритма в браузерном курсе. Вычисление остаётся внутри будущего сценария, поэтому мы не вставляем в статью придуманный результат в шестнадцатеричном виде.

function testManifestBytesArePublished() public {
    string memory manifest = '{"courseId":17,"edition":1}';
    bytes memory fileBytes = bytes(manifest);
    bytes32 digest = sha256(fileBytes);

    vm.prank(publisher);
    catalog.publish(17, digest);
    assertCourse(17, digest, true);
}

Такой сценарий проверяет хранение digest, но не является независимым доказательством правильности SHA-256. Ожидание и переданный аргумент здесь вычислены одной операцией. Это допустимо для договора реестра, если мы ясно называем границу. Для проверки реализации хеширования нужны известные внешние векторы и отдельная цель.

Алгоритм Ethereum Keccak-256 следует отличать от стандартизованного SHA3-256, несмотря на родство названий. Если сервер или браузер выбирает библиотечную функцию только по похожему имени, одинаковые входные байты могут дать несовместимый результат. В межкомпонентном договоре записываем точный алгоритм, а не расплывчатое «sha3».

Хеши keccak256("edition-one") из ранних сценариев были различимыми учебными входами для метода хранения. Они не объявлялись настоящими манифестами библиотеки. Сейчас вводится отдельное соглашение приложения: SHA-256 сохранённого файла. Реестр не различает алгоритмы по внешнему виду bytes32, поэтому совместимость обеспечивают публикующий инструмент и проверяющий читатель, использующие одно правило.

ABI-кодирование решает другую задачу

В Solidity выражение abi.encode(manifest) формирует стандартное ABI-представление динамической строки. Оно включает сведения о размещении и длине, а не только содержимое файла. Поэтому хеш этого результата относится к кодированному аргументу и не совпадает по смыслу с хешем сырых JSON-байтов.

function testFileBytesAreDifferentFromAbiArgument() public {
    string memory manifest = '{"courseId":17,"edition":1}';
    bytes memory fileBytes = bytes(manifest);
    bytes memory encodedArgument = abi.encode(manifest);

    assertTrue(encodedArgument.length > fileBytes.length);
    assertTrue(keccak256(encodedArgument) != keccak256(fileBytes));
}

Это конкретный сравнительный пример двух входов. Он не доказывает, что разные байты математически никогда не имеют одинаковый хеш. Мы используем ожидаемое различие для выбранной фикстуры и объясняем причину несовместимости форматов. Не стоит превращать такую функцию в универсальное доказательство отсутствия коллизий.

abi.encodePacked удаляет часть разделителей и метаданных. С одной строкой его байты соответствуют содержимому строки, но объединение нескольких динамических значений может быть неоднозначным. Например, пары строк ("ab", "c") и ("a", "bc") дают одинаковую конкатенацию. Это проблема входного формата ещё до криптографического хеша.

Меняем документ одной новой строкой

Рассмотрим две фикстуры: исходный JSON и те же символы с \n в конце. Вторая часто появляется при сохранении файла редактором. Если публикующий инструмент вычисляет хеш до сохранения, а читатель проверяет скачанный файл, такая незаметная разница может сломать проверку выпуска.

Правильный порядок работы для байтового договора: сформировать окончательный файл, получить его точные байты, вычислить digest и сохранить эти же байты в архиве. Нельзя после вычисления «красиво переформатировать» JSON, продолжая использовать прежний отпечаток. Семантическое сходство не сохраняет побайтовую идентичность.

Канонизация JSON является отдельным соглашением со своими правилами чисел, строк и порядка ключей. Простое обещание «отсортируем свойства» не определяет весь формат. В курсе IPFS обсудим воспроизводимый выпуск подробнее; здесь достаточно маленькой явно записанной фикстуры без притязания на общий стандарт.

Длина текста и длина байтов

Добавление заголовка "Введение" показывает ещё одну границу. Человек считает буквы, тогда как хеширование получает байты UTF-8. Нельзя заранее выделять буфер по количеству видимых символов и считать, что в нём окажется весь файл. Для браузера используем байты ответа или корректный кодировщик текста, а не самодельное преобразование кодов букв.

Даже одинаково выглядящие символы могут иметь разное Unicode-представление. Если формат выпуска требует нормализации, её выполняют до сохранения и вычисления хеша, по одному явно выбранному правилу. Если договор побайтовый без нормализации, проверяется сохранённая форма. Эти решения относятся к формату манифеста и не появляются автоматически благодаря полю bytes32.

CID не является готовым bytes32

IPFS CID описывает адрес содержимого вместе с дополнительными сведениями о кодеке и хеше. Он не обязан быть 32-байтовым digest нашего алгоритма. Нельзя обрезать строку CID до подходящей длины и передать результат в реестр как будто это проверенный отпечаток учебника.

В нашей системе можно хранить CID во внешнем манифесте и публиковать SHA-256 от точных байтов этого манифеста. Тогда у потребителя появляется ясная цепочка: запись реестра подтверждает байты манифеста, а ссылки манифеста указывают на набор материалов. Проверка доступности этих материалов и поведения gateway остаётся отдельной задачей.

Величина От чего получается Где используется
sha256(fileBytes) Точные байты выпуска Поле contentHash веб-проекта
keccak256(abi.encode(value)) ABI-представление значения Отдельный договор кодированных данных
CID IPFS-представление содержимого Адрес материалов в IPFS

Теперь термин «отпечаток» имеет определённый вход и алгоритм, а сценарий хранения не подменяет проверку файлов. В следующем уроке построим независимую модель ожидаемого состояния. Правила ABI и неоднозначность packed-кодирования описаны в Solidity 0.8.30. Оглавление курса.

Оглавление серии