Функции хеширования в Solidity с использованием Keccak256

Алгоритм keccak256 (семейство SHA-3) вычисляет хэш входных для выходных фиксированной длины. Входными данными могут быть строка или число переменной длины, но результатом всегда будет фиксированный тип данных bytes32. Это односторонняя криптографическая хеш-функция, которую нельзя декодировать в обратном порядке. Он состоит из 64 символов (букв и цифр), которые могут быть представлены в виде шестнадцатеричных чисел.

Дайджест или результат хеширования не предназначен для расшифровки с помощью ключа, как в алгоритмах шифрования. (например, шифрование с открытым ключом). Лучший способ восстановить хеш-код — проверить его как результат хеш-функции. Альтернативным, но не самым быстрым способом решения хэш-кода для восстановления исходного сообщения может быть методология грубой силы. Это форма защиты от атак.

Основной принцип хеширования

Предоставил ввод строки, например «Hello World», и передал ее в хеш-функцию с помощью keccak256. Результат будет:

Hello World -> keccak256 -> 
592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba

Строка «Hello World» — это не то же самое, что «hello world». Если мы хешируем «hello world», мы получим совершенно другой результат.

hello world -> keccak256 ->
47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad

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

Возьмем, к примеру, две строки:

"Быстрая, коричневая лиса, перепрыгнула через ленивого пса"

"Привет"

Теперь давайте хэшируем каждую строку с помощью keccak256 и смотрим на результат (Примечание: кавычки не включены во входные данные).

The quick brown fox jumped over the lazy dog -> keccak256 ->
a82db2ff0b312da9d856a75ba260e9955f0fe467307cd7793521624d11921365
Hello -> keccak256 ->
06b3dfaec148fb1bb2b066f10ec285e7c9bf402ab32aa78a5d38e34566810cd2

Теперь давайте сравним результаты для каждой строки.

a82db2ff0b312da9d856a75ba260e9955f0fe467307cd7793521624d11921365
06b3dfaec148fb1bb2b066f10ec285e7c9bf402ab32aa78a5d38e34566810cd2

Как мы видим, обе они имеют фиксированную длину (64 символа), несмотря на то, что одна строка длиннее другой.

Теперь давайте посмотрим, как хеш-функции применяются с использованием keccak256 в Solidity смарт-контрактах.

Кодирование входных данных

В Solidity (язык программирования, используемый в Ethereum) хэш-функция должна сначала закодировать входные данные. В основном это необходимо для кодирования вызовов контракта к EVM (виртуальная машина Ethereum) на уровне байт-кода. Это позволяет разработчикам взаимодействовать со смарт-контрактом, раскрывая свои функции и методы.

Некоторые скажут, что вам не нужно кодировать данные, а просто напрямую хэшировать необработанные данные (исходные данные). Вы можете сделать это, но это не лучшая практика, особенно если вы хотите защитить данные (в первую очередь цель хеширования).

Ключевое слово abi.encodePacked используется с инструкциями для кодирования ввода данных. Используется при следующих состояниях:

  • Типы короче 32 байт объединяются напрямую, без заполнения или расширения знака.
  • Динамические типы кодируются на месте и без длины
  • Элементы массива дополняются, но все еще кодируются на месте
abi.encodePacked( <data input> )

Это означает, что динамические типы кодируются на месте без длины, в то время как статические типы не будут дополняться, если они короче 32 байт.

Например:

abi.encodePacked("AAAA")
0x41414141

Теперь мы можем применить нашу хеш-функцию, используя ключевое слово keccak256, следующим образом:

keccak256(abi.encodePacked( <data input> ))

Вот пример:

function hash(string memory _string) public pure returns(bytes32) {
     return keccak256(abi.encodePacked(_string));
}

Давайте назначим значение _string как «Хешировать эту строку». Результат, который мы получаем:

0x5f82559096154cc5c8b38479da101b29c56995ade10aa89e4940b68ef1002567

Обратите внимание на префикс 0x перед хеш-дайджестом. Таким образом, длина всей строки составляет 66 символов. Префикс 0x добавляется для обозначения шестнадцатеричной строки.

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

Предотвращение столкновений

Существует также более сложный метод кодирования, который называется abi.encode. Функция abi.encodePacked должна была быть более простой и компактной для кодирования данных. Функция abi.encode может быть полезна, когда речь идет о предотвращении коллизий в хеш-функциях.

Коллизия может произойти, когда два разных входа производят один и тот же результат. Это может показаться невозможным, но это может произойти самым неожиданным образом. Возьмем, к примеру, следующие входные данные:

(AAA, BBB) -> AAABBB         
(AA, ABBB) -> AAABBB

Предполагается, что они должны отличаться друг от друга, но при объединении в одну строку они на самом деле будут давать один и тот же результат.

При использовании abi.encode кодирование строки приводит к следующему:

abi.encode("AAAA")
0x0000000000000000000000000000000000000000000000000000000000000020
0x0000000000000000000000000000000000000000000000000000000000000004
0x4141414100000000000000000000000000000000000000000000000000000000

Это 96 байт или 3 слова в длину. Обратите внимание, что вы добавили нули, а в abi.encodePacked — нет.

Теперь давайте применим функцию keccak256 и посмотрим, почему могут возникать коллизии. Сначала используйте abi.encodePacked. Мы будем использовать две строки в качестве переменных (без состояния) _string1 и _string2.

function collisionExample(string memory _string1, string memory _string2)
public pure returns (bytes32) {
     return keccak256(abi.encodePacked(_string1, _string2));
}

Давайте рассмотрим простой пример (Пример 1):

_string1 = ААА

_string2 = BBB

Результат, когда мы объединяем строки и применяем хеш-функцию:

0xf6568e65381c5fc6a447ddf0dcb848248282b798b2121d9944a87599b7921358

Теперь давайте изменим значения на следующие (Пример 2):

_string1 = АА

_string2 = ABBB

Вот результат:

0xf6568e65381c5fc6a447ddf0dcb848248282b798b2121d9944a87599b7921358

Мы получаем точно такой же результат, как и в предыдущем примере. Причина в том, что при объединении строк получается одна и та же строка:

AAABBB

Независимо от порядка символов. Вы можете установить для _string1 значение A, а для _string2 значение AABBB, и результат все равно будет таким же. Это коллизия, и в продакшн-системах это будет проблемой.

Теперь воспользуемся той же функцией, но на этот раз с abi.encode.

function collisionExample2(string memory _string1, string memory _string2)
public pure returns (bytes32) {
     return keccak256(abi.encode(_string1, _string2));
}

В первом примере (Пример 1) результат:

0xd6da8b03238087e6936e7b951b58424ff2783accb08af423b2b9a71cb02bd18b

Это сильно отличается от использования abi.encodePacked. Теперь момент истины (Пример 2), получится ли такой же хэш?

0x54bc5894818c61a0ab427d51905bc936ae11a8111a8b373e53c8a34720957abb

Мы получаем совершенно другой результат, который предотвращает столкновение. Если существует вероятность того, что входные данные приведут к выходным данным, которые могут вызвать коллизию, рекомендуется использовать abi.encode вместо abi.encodePacked. Разработчики могут использовать и другие методы (например, добавление случайного значения к объединенной строке), но это один из наиболее распространенных.

Синопсис

Хеширование — важный аспект криптографической безопасности цифровых кошельков и транзакций в блокчейне. Это может помочь создать детерминированное уникальное значение из хеш-ввода, применяемое к схемам Commit-Reveal и для компактных криптографических подписей.

Использование keccak256 — это лишь один из примеров многих хеш-функций. Протокол Ethereum использует keccak256 в своей сети с механизмом консенсуса под названием Ethash. Он играет важную роль в создании блоков и их защите в блокчейне.