У вас не возникало желания понять, почему майнеры тратят уйму электричества на перебор хешей, вместо того чтобы вычислить нужное значение алгебраически? Ведь все элементарные операции обратимы – в какой момент из обратимых операций появляется функция, для которой уже нельзя написать обратную? Где именно сокрыта магия?

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

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

Представим, что у нас есть некоторое неизвестное число x. Мы передаём его функции f и получаем конкретное число y:

y = f(x)

Теперь поставим обратную задачу. Мы знаем результат y и знаем, как устроена функция f (её исходный код открыт). Что мешает нам просто выполнить все операции в обратном порядке и вычислить x?

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

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

Ещё со школы мы знаем, как решать такие уравнения.

Начнём со сложения

x + 5 = 7

Чтобы найти неизвестное x, достаточно перенести пятёрку в правую часть:

x = 7 - 5 = 2

Аналогично с умножением

x \cdot 5 = 15

Делим обе части на пять и получаем:

x = \frac{15}{5} = 3

Остаток от деления

Ещё потребуется остаток от деления, или операция по модулю, которая в программировании обычно обозначается символом %.

Например, 17 % 5 = 2. Мы делим 17 на 5, получаем 3 и остаток 2. Именно этот остаток и является результатом операции.

На первый взгляд, здесь уже возникает проблема. Ведь по одному остатку невозможно восстановить исходное число. Но вспомним наше условие: нам не требуется восстановить именно исходное число – подойдёт любое, которое даст тот же результат.

Попробуем обратить операцию взятия остатка:

x \operatorname{mod} 5 = 2

Каким может быть x? Например, 2, 7, 12, 17, 22 и так далее. Все эти числа при делении на 5 дают остаток 2.

Иначе говоря, у нас есть даже готовая формула:

x = 2 + 5 \cdot k

где k – любое целое число.

А если нам подходит любое решение, можно просто взять k = 0 и получить x = 2.

То есть операция взятия остатка от деления действительно необратима в строгом математическом смысле: мы не можем однозначно восстановить исходное число. Но найти хотя бы одно подходящее значение совершенно несложно.

Итак, со сложением и умножением всё предельно просто: если к неизвестному числу что-то прибавили, мы это вычитаем. Если умножили на ненулевое число – делим. Даже остаток от деления сам по себе не представляет проблемы, если нам подходит любой исходный аргумент.

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

Я выбрал максимально простую программу, которая уже обладает магическим эффектом – после некоторого шага уже не позволяет применить привычные алгебраические вычисления. Вся программа будет состоять из повторяющейся инструкции:

export function hash(m: number): number {
  let x = m;
  x = (x * x + m) % P; // шаг 1
  x = (x * x + m) % P; // шаг 2
  x = (x * x + m) % P; // шаг 3
  // ...
  x = (x * x + m) % P; // шаг N
  return x;
}

Вроде бы всё просто – умножение и сложение. Но в какой-то момент найти обратное уже невозможно, причём момент наступает достаточно быстро.

Так в какой же момент эта простая схема перестаёт работать и возникает магия?

Давайте по порядку…

Шаг 1

Тут всё легко. К нашему значению x мы будем применять одну и ту же формулу последовательно несколько раз. Для первого шага – только один раз. Вот эта формула:

y = (x^2 + x) \bmod P

Или запись в TypeScript:

export function hash(m: number): number {
  return (m * m + m) % P;
}

Интересно, что сама по себе формула довольно простая – это то самое квадратное уравнение из школы. И несмотря на остаток от деления, если P – простое число, обратную задачу по-прежнему можно решить достаточно быстро. Мы можем найти подходящее значение без особых проблем!

При условии, что P – нечётное простое число, обратную задачу можно записать следующим образом.

Исходная функция:

h = (m^2 + m) \operatorname{mod} P

Это квадратное уравнение, которое без модуля имело бы вид:

m^2 + m - h = 0

По школьной формуле находим дискриминант и корни:

D = 1 + 4hm = {-1 \pm \sqrt{D} \over 2}

Теперь учитываем модуль P.

Дискриминант вычисляем по модулю:

D = (1 + 4h) \operatorname{mod} P

Минус единицу заменяем на P − 1:

-1 \equiv P - 1 \pmod P

Деление на 2 заменяем умножением на (P + 1) / 2, поскольку:

2 \cdot \frac{P+1}{2} = P+1 \equiv 1 \pmod P

Остаётся найти квадратный корень по модулю, то есть такое число s, для которого:

s^2 \bmod P = D

И получаем итоговую формулу:

m = \left((P - 1 + s) \cdot \frac{P+1}{2}\right) \bmod P

Здесь s – один из квадратных корней дискриминанта по модулю P. Если такого корня нет, решений тоже нет. Если есть, формула даёт подходящее значение m.

Таким образом, даже после добавления остатка от деления квадратное уравнение по модулю простого числа всё ещё можно эффективно решить.

Реализация этого примера – шаг 1 (step1.ts).

Шаг 2

Теперь наша функция принимает вид:

export function hash(m: number): number {
  let x = m;
  x = (x * x + m) % P; // шаг 1
  x = (x * x + m) % P; // шаг 2
  return x;
}

Можно подставить вместо x во втором шаге выражение из первого шага и записать иначе:

export function hash(m: number): number {
  let x = (((m * m + m) % P) * ((m * m + m) % P) + m) % P;
  return x;
}

Если раскрыть скобки:

export function hash(m: number): number {
  let x = (m * m * m * m + 2 * m * m * m + m * m + m) % P;
  return x;
}

Видно, что уже на втором шаге у нас возникло уравнение 4-й степени. Каждый новый шаг, при видимой простоте записи в коде, увеличивает степень уравнения. И это проблема, возможно, вы помните ещё со школы:

Теорема Абеля-Руффини утверждает:

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

Но тут есть нюанс. У нас-то уравнения по модулю, а не обычные, и для них таки алгоритмы существуют. Но и они не настолько оптимальны, как хотелось бы.

Реализация этого примера – шаг 2 (step2.ts).

Дальнейшие шаги

На третьем шаге у нас получится уравнение уже 8-й степени, на четвёртом – 16-й, на пятом – 32-й и так далее.

То есть каждый новый шаг удваивает степень уравнения. И уже после 20 шагов мы получаем степень больше миллиона.

В репозитории есть примеры для 17 шагов (step17.ts) и 25 шагов (step25.ts), а также реализация самого алгоритма обращения (algebra.ts).

Конкретная оценка для алгоритма Кантора-Цассенхауса:

Шаг

Время

Память и площадь SSD

20

2–3 минуты

1 ГБ, меньше одного SSD

24

Около часа

20 ГБ, меньше одного SSD

30

4 дня

1 ТБ, один SSD

40

20 лет

1 ПБ, весь пол небольшой комнаты (7 м²) покрыт SSD в один слой

64

100 миллионов лет (0,7% возраста Вселенной)

10 миллиардов ТБ, 70 км² покрыты SSD (небольшой город)

128

700 квадриллионов возрастов Вселенной

100 октиллионов ТБ, покрыть всю поверхность Земли SSD 1,4 триллиона раз

Выводы

Внимательный читатель заметит, что строгого математического доказательства невозможности решить данное уравнение за приемлемое время у меня нет (как нет такого доказательства и для SHA-256 – всё держится на вере, по сути).

Но уже понятно, что если быстрое решение и существует, то математика там достаточно сложная. Для большинства хватит посмотреть на существующее медленное решение, чтобы понять: халявы там нет, даже продвинутая математика пока не работает. Быстрое решение, если и существует, найти не так просто.

Комментарии (2)


  1. trublast
    25.10.2026 05:44

    Подойдет любое число, x для которой функция выдачи тот же результат y

    Сразу придумалась необратимая функция y=x*0

    Жаль, коллизий у нее очень много

    Спасибо за статью


  1. realbtr
    25.10.2026 05:44

    Обратимость работает если комбинаторно результат единственный. Например, 5=7-2=8-3=9-4 и так далее. Зная только результат исходные аргументы связаны, но не определены. То же самое и с умножением.

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

    Классически чтобы получить некоторую определенность, нужно часть причины спрятать, убрать с глаз долой. В квантовой парадигме мы для этого создаем пару. Пару чего? Ага, вот тут квантовый объект крайне инвариантен. Пару частиц и пару реальностей - вообще одно и то же. Потому что это комплиментарные конструкции. Чтобы забыть что-то не нужное, нужно создать это не нужное.

    Классическая математика следует принципу данности. Элементы множества даны. У них нет происхождения.

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

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

    Но неугомонный принцип данности все равно хватается за структуру структуры - она то дана?

    А это тоже комбинаторика комбинаторик.

    Так что кроличья нора ввглядит бесконечно глубокой, если считать что какой-то изначальная данность быть должна.

    И особенно интересно, если нам даны именно множества чисел и операций. Очень твердая опора. Пока не думаешь, как именно они нам даны.

    Гротендик пытался и бросил. Я то куда лезу :)