Как подписывать транзакции ключом, который нигде не собирается целиком
Пароль можно сбросить. Перевод — оспорить. Доступ — отозвать. С приватным ключом всё жёстче: кто может создать валидную подпись — тот распоряжается активами. Эта статья — про переход от «ключ где-то лежит» к системе, где ключ не собирается ни на одной машине — но подписи всё равно появляются и проходят проверку в блокчейне.
В блокчейне ключ — это и есть распоряжение
В обычном бэкенде у вас есть тысяча способов всё откатить: отменить операцию, вернуть платёж, восстановить пароль, заблокировать аккаунт, позвонить в банк. Система прощает ошибки, потому что над ней стоит ещё одна система — люди, регуляторы, процедуры.
В блокчейне этого слоя нет. Сети безразлично, кто именно подписал транзакцию: владелец, бэкенд-сервис, скомпрометированный CI/CD-раннер или атакующий, который вытащил ключ из памяти на 30 миллисекунд. Подпись валидна — значит, средства ушли.
Поэтому главный вопрос custody-инфраструктуры звучит не «где безопасно хранить приватный ключ». Правильный вопрос радикальнее:
Можно ли построить систему, где приватный ключ никогда не существует целиком — но подписи всё равно создаются?
Это и есть практический смысл MPC в блокчейне. Прежде чем разбираться, как ключ можно «не собирать», договоримся о паре слов — а потом дойдём до самой красивой части, до карты, на которой и живёт ключ.
MPC — это не продукт, а способ договориться о вычислении
В индустрии часто говорят «MPC-кошелёк», «MPC-подпись», «MPC-custody». Это нормальный жаргон, но он слегка размазывает смысл. MPC — это общий класс протоколов, где несколько участников совместно что-то вычисляют, не раскрывая друг другу свои секретные входы. TSS — threshold signature scheme — конкретный случай: несколько нод совместно создают одну цифровую подпись.
MPC
Широкая идея: считаем функцию вместе, но приватные входы остаются у участников. Это может быть подпись, дешифрование, генерация ключа, приватная аналитика.
TSS
Узкий практический случай для кошельков: есть общий публичный ключ, у нод есть доли, а на выходе получается одна обычная подпись.
Дальше я буду использовать «MPC» в привычном custody-смысле, но технически мы почти всегда говорим именно про threshold signatures: распределённое право создать подпись без сборки приватного ключа. А чтобы понять, как такое право вообще можно «разрезать», начнём с того, что такое один обычный ключ.
Хотите понять математику threshold-подписи — идите по порядку: точки, дискретный логарифм, Schnorr, Shamir и partial signatures.
Пришли за production-архитектурой, а основы уже знаете — прыгайте к разделу «Почему ECDSA больнее» и дальше: DKG, signer-ноды, policy, HSM, abort-сценарии и threat model.
Нужен только главный вывод: MPC/TSS не «прячет ключ лучше» — оно убирает саму точку, где полный приватный ключ вообще существует.
ключ = власть · подпись Schnorr · почему Shamir мало · сборка подписи · ECDSA сложнее · в production · что MPC не решает
Как возможна подпись без собранного ключа
Сначала — только механика, без единого упоминания продакшена: как из обычной точки на кривой получается подпись и почему её можно разрезать на доли, не собирая ключ обратно.
Приватный ключ — это номер шага
Технически приватный ключ — это секретное число. Но для интуиции полезнее думать о нём как о
номере шага. Представьте карту. На ней
есть стартовая точка O и один-единственный стандартный шаг — G, генератор.
Вся арифметика ключей сводится к одной операции: «сделать ещё один шаг G». Не умножение
координат, не хеш — буквально шаг по карте.
// старт — нулевая точка O, и дальше просто шагаем 0G = O // начало 1G = O + G = G 2G = G + G 3G = 2G + G 4G = 3G + G ... xG = X // сделали x шагов — пришли в точку X
Тогда вся пара ключей складывается в одну строчку интуиции:
private key x → номер шага (сколько раз нажали «+G») public key X → точка, в которую мы пришли после x шагов G → стандартный шаг, известен всем
Если знать число x, можно почти мгновенно телепортироваться на x
шагов от O и оказаться в точке X = x·G — компьютер делает это за доли
секунды. А вот обратно — стоя в X, восстановить номер шага x —
практически невозможно. На этой асимметрии держится вся подпись.
И вот что делает обратный путь по-настоящему безнадёжным: эта «карта» не сохраняет
порядок. Сделали ещё один шаг ·G — и оказались не «рядом», а в совершенно
другом, заранее непредсказуемом месте поля. Соседние номера x и x+1 дают
точки, между которыми нет никакой видимой связи; всё облако точек выглядит так, будто их разбросали
наугад. По самой точке не понять, какой у неё номер и где лежит «соседняя» — нет той геометрической
подсказки, по которой можно было бы доехать от X обратно к x. Сначала
пощупаем саму карту.
Снизу — номер шага: обычная прямая 1, 2, 3, … идёте куда
хотите. Каждый номер проходит через одну и ту же операцию ·G — детерминированную
«карту», которая переводит число с прямой в адрес точки в публичном
поле наверху. Представьте гигантский словарь: ключ — номер шага, значение — адрес точки; поиск
«туда» мгновенный. Двигайте шаг: снизу вы идёте по порядку, а наверху точку швыряет по полю без
всякой логики. И главное — это туман войны: где лежит соседняя
точка, неизвестно, пока вы её прямо не посчитаете. На самой точке её номер не написан.
Шаг вперёд и шаг назад делаются одинаково легко — это движение по нижней прямой. «Развеять туман» — это режим бога: мы рисуем сразу все точки с их номерами. В реальности так нельзя — у сети нет этой карты, а для 2²⁵⁶ точек её просто негде хранить. Самый прямой способ узнать номер точки — искать перебором: идти от начала и считать, пока не упрёшься. Есть и алгоритмы поумнее (о них чуть ниже), но вывод они не меняют. Здесь точек пара сотен — мгновенно. На 2²⁵⁶ эта прогулка заняла бы дольше, чем существует Вселенная.
Телепорт вперёд и туман войны
Телепорт вперёд вы уже видели: знаешь x — мгновенно оказываешься в X = x·G.
Теперь представьте, что вы стоите в этой точке. Карта вокруг покрыта туманом войны. Вы
видите саму точку, можете шагнуть от неё дальше, можете шагнуть назад, можете прыгнуть на известное
число шагов. Но на точке не написано: «вы на шаге 847239…». Координаты не показывают номер шага.
X + G = (x + 1) · G // один шаг вперёд X - G = (x - 1) · G // один шаг назад X + 1000G = (x + 1000) · G // прыжок на известное число шагов
И вот здесь важный момент: вычитание не запрещено. На эллиптической кривой можно
легко вычитать точки: A - B — это просто A + (-B). Проблема не в том,
что мы не можем сделать один шаг назад. Проблема в том, что мы не знаем, сколько таких
шагов нужно сделать до начала.
Один шаг назад сделать легко.
Узнать номер шага — практически невозможно.
Логичная мысль: G известна, точка X известна. Почему бы просто не
отнимать G по разу, пока не вернёмся к началу? Или наоборот — идти от начала вперёд,
пока не совпадём с X, и запомнить номер шага?
Это абсолютно валидный способ. Он называется тупой перебор. И он работает — на маленькой карте выше вы только что так и делали кнопкой «развеять туман». Проблема ровно одна: размер пространства.
У настоящей кривой secp256k1 порядок группы около 2²⁵⁶. Это не «очень много».
Это астрономически бессмысленно много: даже триллионы проверок в секунду не превращают перебор
в практическую атаку.
Почему вперёд — быстро
Когда вы считаете публичный ключ, вы знаете x. А раз знаете — можно
разложить его в двоичном виде и прыгать большими степенями двойки, а не шагать по одному:
2G = G + G 4G = 2G + 2G 8G = 4G + 4G // 13 шагов? Нет — три удвоения и пара сложений: 13G = 8G + 4G + G
Для огромного x то же самое: компьютер раскладывает его на степени двойки и
собирает xG за сотни операций вместо 2²⁵⁶.
Почему назад — нет
Назад вам дают только две точки — G и X — и говорят «найди x».
Но вы не знаете, из каких степеней двойки был собран x. Вы можете двигать
точку, но не можете прочитать из неё скрытый номер шага. Остаётся перебор. А пространство — 2²⁵⁶.
Forward: known scalar x + known point G → fast scalar mult → X Backward: known point X + known point G → unknown scalar x → discrete log → no known fast algorithm
«Тупой перебор» — это худший случай, а не лучшая известная атака. Есть способы поумнее
(baby-step giant-step, ро-метод Полларда), которые срезают поиск примерно до √n ≈ 2¹²⁸ шагов.
Поэтому реальная стойкость secp256k1 — около 128 бит, а не 256. Хорошая новость в
том, что 2¹²⁸ — это всё ещё «никогда»: запас от всех известных атак остаётся астрономическим.
Выберите секрет x. Слева компьютер считает X = x·G трюком
«удвой-и-сложи». Справа — атакующий ищет x назад, шагая по одному. Один и тот же
результат, несопоставимая цена.
бит длиной 1
операций ≈ 1
худший случай
операций ≈ 1
Forward: ~256 удвоений. Backward тупым перебором: до 2²⁵⁶ ≈ 1.2·10⁷⁷ шагов; лучшие generic-атаки (Pollard rho) — порядка 2¹²⁸. И то и другое практически невозможно. Разница не «в тысячу раз» — она между «миллисекунда» и «никогда».
«Какая разница, как точки выглядят?»
Справедливое возражение: ведь это просто другая система отсчёта. Математически — да, вы правы.
Группа, порождённая G, изоморфна обычным числам по модулю n:
0, 1, 2, … n−1. Номер у точки существует. Проблема не в том, что его нет.
Проблема в том, что у нас нет дешёвого способа перевести точку в эту систему отсчёта.
У точки есть два описания. Первое — публичное: координаты (a, b), легко проверить.
Второе — секретное: X = 12384723…·G, номер шага. Переход
x → X быстрый. Обратный X → x — неизвестно как
делать быстро.
Обычная ось
0 — 1 — 2 — 3 — 4 — 5. Видишь точку «5» — мгновенно знаешь её номер. Координата и есть номер шага.
Эллиптическая группа
Координаты точки — не номер шага. Это просто пара огромных чисел в конечном поле. По ним не видно, это 4·G, 4 000 000·G или 2²⁰⁰·G.
Дело не в том, что «хаос магически защищает». Дело в отсутствии видимого порядка: координаты не кодируют номер. Был бы порядок — координата сама бы выдавала шаг, и криптография сломалась бы в ту же секунду.
А конечность поля — это «закольцованность»?
Да, но осторожно — это не вся причина. После n шагов последовательность
возвращается к началу (nG = O), и арифметика ключей идёт по модулю n:
(x + n)·G = x·G. Кольцо есть. Но на обычных часах тоже всё закольцовано —
и там номер шага виден сразу. Сложность не в цикле, а в том, что внутри гигантского цикла
точки «перемешаны»: их публичные координаты не выдают порядкового номера.
Складывать вклады участников легко. Поэтому Schnorr хорошо ложится на threshold-подпись.
Восстанавливать скрытый множитель — невозможно практически. Поэтому публичный ключ можно показывать всем.
Делить распределённый секрет — сложно. Поэтому threshold ECDSA больнее, чем threshold Schnorr.
Подпись Schnorr, разобранная по шагам
Возьмём Schnorr, а не ECDSA — она математически чище, и на ней видно всё. Подписант берёт
одноразовое случайное число (nonce) k и считает:
k = random nonce // одноразовый расходник R = k · G // «публичная» часть nonce e = H(R, X, message) // хеш-вызов: связывает всё вместе s = k + e · x // единственное место, где участвует ключ x signature = (R, s)
А проверяющий (вся сеть) считает левую и правую часть и сверяет:
s · G == R + e · X
Почему это вообще сходится — видно в три строчки, и это тот редкий случай, когда математика красива:
s · G = (k + e · x) · G = k · G + e · x · G = R + e · X ✓
Проверка никогда не трогает x напрямую — только X = x·G,
публичную точку. Сеть убеждается, что подпись мог сделать только владелец x, но
сам x остаётся секретом. Именно эта линейность s = k + e·x позволит
дальше «разрезать» подпись на доли.
Та же карта, что в Lab 01, но чисел теперь два: ключ x и одноразовый нонс
k. Снизу — приватный сектор: числа, которые знает
только подписант. Сверху — публичное поле: точки и числа,
которые видит вся сеть. Идите по шагам и смотрите, как из двух приватных чисел собирается подпись —
и как в самом конце две независимые формулы попадают в одну и ту же точку. Это попадание и
есть проверка.
Вся соль — в «развилке» на последнем шаге. Левая часть s·G идёт от приватного
s через ту же карту ·G. Правая R + e·X собирается только из
публичного — R, e, X. Они обязаны попасть в одну точку — и
попадают, но лишь если s и правда равно k + e·x с настоящими x и
k. Подделать s, не зная x — это угадать номер шага по точке.
А это, как мы уже видели в Lab 01, туман войны.
Никаких метафор — две настоящие машины, которые считают руками. Подкрутите секрет
x и нонс k, жмите «Подписать» и «Проверить» — слева увидите все 4
строки формул с числами, справа — что видит проверяющий и почему уравнение сходится, хотя
x он не знает. Под капотом здесь используется мультипликативная группа (mod 467)
для наглядности вычислений в браузере, но визуально мы сохраняем аддитивную нотацию эллиптических
кривых (s·G == R + e·X).
Главное наблюдение: подписывающий пишет s = k + e·x, прячущий x за
одноразовым k. Проверяющий считает s·G и R + e·X — и они
совпадают только если s было собрано из правильного x
(того, что в X). Без знания x. Без шанса его вычислить.
Shamir — это threshold. Но threshold восстановления
Итак, подпись делает тот, кто знает x. Но мы же не хотим, чтобы x целиком
лежал на одной машине. Первый напрашивающийся ответ — разрезать ключ на доли. И тут стоит вспомнить
Shamir, но аккуратно: он решает почти ту задачу, что нам нужна — почти, но не её.
Shamir Secret Sharing — уже пороговая криптография: любые
m долей из n могут восстановить секрет, а меньше m — не
узнают ничего полезного. Это отличный инструмент для бэкапов, escrow и редких процедур
восстановления.
Но у Shamir по умолчанию другой вопрос:
Shamir SSS: сколько долей нужно, чтобы восстановить секрет? TSS: сколько участников нужно, чтобы создать подпись, не восстанавливая секрет?
Если использовать Shamir напрямую для ежедневной подписи транзакций, путь получается таким:
// классический Shamir в signing path: 1. собрать достаточное число долей 2. восстановить приватный ключ x в памяти одной машины 3. подписать транзакцию 4. стереть x
Пусть ключ живёт в памяти 20 миллисекунд — этого достаточно. У нас снова есть single point of compromise. Точка, где весь ключ существует целиком.
Shamir даёт threshold-доступ к секрету.
TSS даёт threshold-действие от имени секрета.
Поворот, на котором держится всё дальнейшее: в threshold-подписи участники не восстанавливают приватный ключ. Они совместно вычисляют подпись так, будто ключ существует — хотя целиком он не появляется нигде и никогда.
Откуда берутся коэффициенты Лагранжа
Прежде чем собирать подпись из долей, разберём одну деталь — иначе дальше будет казаться, что
появляется какая-то новая магия. Магии нет. Доли в Shamir — это значения одного многочлена
в разных точках, а секрет — его значение в нуле, f(0). Чтобы по нескольким точкам узнать
f(0), есть стандартный школьный приём — интерполяция: сложить известные
значения, но каждое со своим множителем. Вот эти множители и называются коэффициентами
Лагранжа λᵢ. Это не «вес важности» доли и не отдельная криптография — это просто
арифметика восстановления многочлена, расписанная по шагам.
В Shamir секрет — это значение многочлена в нуле:
secret = f(0)
А доли — значения того же многочлена в других точках:
share₁ = f(1) share₂ = f(2) share₃ = f(3)
Поэтому сложить две доли «в лоб» и получить секрет не выйдет. Доля — это точка на
многочлене. Чтобы выбранные доли вернули нас в f(0), каждую долю надо взять с
правильным весом.
// для простой схемы 2-of-3, если выбраны доли f(1) и f(2): f(0) = 2 · f(1) - 1 · f(2) // то есть: λ₁ = 2 λ₂ = -1
Выбора тут нет: как только зафиксировано, какие доли участвуют (какие точки
x = 1, 2, 3…), коэффициенты λᵢ определяются однозначно — и зависят только
от номеров участников в кворуме, а не от секрета. Поэтому каждый signer считает свой
λᵢ сам, зная лишь, кто ещё в кворуме. И да — в сумме это та самая «элементарная
арифметика»: сложение и умножение по модулю, ничего сверх. Просто без правильных множителей сумма
долей дала бы не f(0), а случайное число.
Если мы реально посчитаем x = Σ λᵢ·shareᵢ в одном месте — мы восстановили секрет.
Это обычный Shamir reconstruction. В threshold-подписи мы используем те же веса иначе: каждый
signer применяет свой вес локально внутри своей части подписи, а сам x отдельно
нигде не появляется.
Как одна подпись рождается из долей
Теперь главный переход. Пусть приватный ключ x не хранится целиком, а представлен
долями между подписантами. Никто не знает x, но у каждого есть свой фрагмент права
подписи. В учебной версии это выглядит так:
signer A → share x₁
signer B → share x₂
signer C → share x₃
// никто не знает x. но вместе они выдают s, как будто знали.
Каждый берёт свою долю одноразового nonce, считает свою публичную commitment-точку Rᵢ, и они складываются в общий R:
Rᵢ = kᵢ · G // у каждого своя R = R₁ + R₂ + R₃ // агрегируем
Формулы выше — учебный скелет. Если нода B сначала увидит R_A, она может попытаться
подобрать свой nonce commitment так, чтобы повлиять на итоговый R и хеш-вызов
e = H(R, X, message) — это nonce manipulation (подстройка под чужой
nonce), не путать с rogue-key. Поэтому реальные threshold Schnorr-протоколы не начинают с простого
раскрытия Rᵢ: сначала участники фиксируют свои nonce commitments, и только
потом раскрывают значения.
В настоящем FROST это даже не просто H(Rᵢ): там hiding/binding nonce commitments и
binding factors. Для статьи достаточно идеи: сначала фиксируем commitments, потом раскрываем
значения — поэтому threshold-подпись это минимум два раунда, а не один HTTP-запрос
sign(tx).
Дальше каждый считает частичную подпись своей долей ключа. λᵢ — коэффициент Лагранжа: он задаёт, с каким весом конкретная доля участвует в выбранном кворуме.
sᵢ = kᵢ + λᵢ · e · xᵢ // partial signature s = s₁ + s₂ + s₃ // складываем signature = (R, s) // обычная Schnorr-подпись!
Важно сразу: sᵢ — это не отдельная подпись транзакции. Блокчейн не
может проверить s₁, s₂, s₃ по отдельности. Это protocol
messages, которые имеют смысл только внутри одной signing-сессии и только после агрегации в
(R, s).
И ещё момент, который легко упустить. Публично мы видим не приватное число, а саму точку —
а точки можно складывать, не зная чисел внутри них. Пусть секрет разрезан на части
x = x₁ + x₂: каждой части соответствует своя публичная точка, и сложение чисел ровно
соответствует сложению точек.
X₁ = x₁·G, X₂ = x₂·G X₁ + X₂ = x₁·G + x₂·G = (x₁ + x₂)·G = x·G = X
Сложение точек на кривой берёт два адреса и выдаёт третий, пользуясь только их координатами; знать,
сколько шагов ·G спрятано внутри каждой точки — а это и есть приватные x₁,
x₂ — для этого не нужно. Та же асимметрия из Lab 01, только другими словами:
видеть точку ≠ знать, сколько шагов от G до неё уметь сложить точки ≠ уметь восстановить приватные числа
Поэтому сеть и агрегатор свободно складывают публичные точки (тот же приём работает и для нонсов,
R = R₁ + R₂ + …), а приватные «номера шагов» так и остаются скрытыми. Именно это
свойство и делает возможными threshold/MPC-схемы: секрет можно разрезать между участниками, а общий
публичный ключ всё равно собрать — как сумму их публичных вкладов.
Агрегатор соберёт итоговую подпись — но не сможет из частичных подписей восстановить приватный ключ.
Для сети это выглядит как самая обычная подпись нужного типа. Если цепочка ждёт Schnorr — на выходе Schnorr. Если цепочка ждёт ECDSA — на выходе ECDSA, просто созданная threshold-протоколом. Bitcoin или Ethereum не обязаны знать, что внутри компании подпись сделали 3 ноды из 5. Это и есть главное отличие от on-chain multisig:
Multisig
Пороговая логика живёт на цепочке — в скрипте или smart-contract wallet. Сеть видит, что это не обычный single-sig spend: это дороже, заметнее и зависит от конкретной chain-модели.
MPC / TSS
Блокчейн видит одну обычную подпись от обычного публичного ключа. Порог, кворум и вся signing ceremony остаются вне цепочки.
Для Schnorr есть стандартизованный пороговый протокол FROST (RFC 9591): пороговое число участников собирает подпись за два раунда обмена. Дальше — мини-лаборатория в том же духе: не полная спецификация FROST, а интерактивная модель, которая показывает саму идею partial signatures.
Приватное право подписи представлено долями через многочлен, как в Shamir-подходе. Включайте подписантов и смотрите, как их частичные подписи складываются в одну. Любые 2 из 3 дают валидную подпись. Один — нет. И ключ нигде не собирается целиком. Арифметика настоящая, но параметры маленькие, чтобы всё было видно глазами.
В учебном интерфейсе значения долей показаны только для понимания механики. В реальном протоколе
агрегатор не видит приватные shareᵢ — он получает commitments, protocol
messages и partial signatures. Из этих данных приватный ключ не восстанавливается; агрегатор может
собрать только итоговую подпись. И он ещё обязан проверять каждую partial signature, иначе одна
плохая нода может сорвать всю церемонию.
Собирают не ключ. Собирают подпись.
Вот здесь обычно и ломается интуиция. После Shamir хочется думать через реконструкцию секрета:
если доли всё равно складываются с весами λᵢ, чтобы получить x, значит
ключ всё-таки где-то собирается?
Ответ: нет, если мы не считаем эту сумму отдельно. Реконструкция
Σ λᵢ·shareᵢ = x — это плохой путь для signing path: он действительно воссоздаёт
ключ в одной памяти. В настоящей threshold-подписи делают хитрее: эту сумму вшивают внутрь
уравнения подписи.
Не каждая доля «подписывает сама».
Каждая доля участвует в вычислении одной общей подписи.
Обычная Schnorr-подпись хочет получить:
s = k + e · x
Если ключ разделён по Shamir, то математически:
x = λ₁·share₁ + λ₂·share₂
Плохой путь — сначала посчитать эту сумму и получить x. Хороший путь — пусть каждый
подписант вычислит только свой слагаемый:
s₁ = k₁ + e · λ₁ · share₁ // считается локально на signer A s₂ = k₂ + e · λ₂ · share₂ // считается локально на signer B
А агрегатор просто складывает partial signatures, ничего не зная про доли:
s = s₁ + s₂ = (k₁ + e·λ₁·share₁) + (k₂ + e·λ₂·share₂) = (k₁ + k₂) + e · (λ₁·share₁ + λ₂·share₂) = k + e · x
Вот в чём вся соль. Та же лагранжева сумма, которая в Shamir могла бы восстановить ключ,
теперь спрятана внутри подписи. Она не возникает как отдельный объект в памяти. На руках у
агрегатора — только s₁, s₂ и их сумма s. Из них нельзя
вытащить x: каждый кусок подписи замаскирован своим одноразовым nonce kᵢ.
Строчка s = s₁ + s₂ + s₃ не значит, что мы складываем три готовые подписи.
Эти sᵢ жёстко привязаны к одной сессии — к общему R, общему
публичному ключу X и одному сообщению. Куски от разных сессий не «склеятся».
Поглядите на обе дорожки рядом — красную (реконструкция, ключ в памяти) и зелёную (подпись, ключа нет нигде).
Один и тот же кворум {A, B}, один и тот же λ. Но слева доли стекаются в одну точку и образуют ключ. Справа доли не покидают ноды — наружу идут лишь кусочки подписи, и ключ не появляется. Жмите «шаг» и следите, что именно «вытекает» из каждой ноды.
Ключевая разница: слева по проводам течёт share₁, share₂ — приватные доли, и в узле сборки лежит готовый x. Справа по проводам течёт только s₁, s₂ — кусочки подписи, замаскированные нонсами; в узле сборки лежит s, а x не появляется ни на одной ноде.
Один вопрос мы пока обошли: доли всё это время уже лежали на нодах. Откуда они там взялись —
так, чтобы полный x не собирался даже при генерации, — это отдельный шаг,
DKG (distributed key generation). К нему вернёмся в production-части.
Как threshold-подпись живёт в production custody
В учебной Schnorr-модели всё красиво: доли складываются, подпись рождается, ключ не собирается. В production добавляются ECDSA, signer-ноды, координатор, policy engine, DKG, HSM, audit trail и отказоустойчивость.
Деление над секретом, которого нет целиком
Schnorr мы взяли не потому, что весь мир уже живёт на Schnorr, а потому что на нём хорошо видно
идею. В реальной custody-инфраструктуре часто приходится подписывать там, где формат уже задан:
legacy Bitcoin-выходы и Ethereum EOA-транзакции — это ECDSA на secp256k1; Taproot
в Bitcoin уже использует Schnorr, но это не отменяет огромного хвоста ECDSA-активов и интеграций.
И вот тут начинается боль. У Schnorr основная формула почти линейная — доли естественно складываются. У ECDSA внутри подписи сидит обратный nonce:
// Schnorr — аккуратное сложение: s = k + e · x // ECDSA — инверсия и умножение над секретами: r = x_coord(k · G) s = k⁻¹ · (H(m) + r · x)
x_coord здесь — не приватный ключ x, а x-координата точки k·G.
Видите k⁻¹ и r·x? Нужно делать обращение и умножение над значениями,
которые распределены между участниками и не существуют целиком. Как перемножить два секрета, если ни одна нода не знает оба?
Ключевое слово здесь — MtA: Multiplicative-to-Additive share conversion. Это протокол, который позволяет превратить произведение секретных значений в сумму новых долей. Участники получают additive shares результата, но ни один из них не узнаёт чужой секрет и не видит произведение целиком.
Под капотом MtA опирается на гомоморфное шифрование (например, Paillier) или Oblivious Transfer (OT): нода A зашифровывает свою долю и отправляет ноде B, нода B умножает зашифрованное значение на свою долю «не вскрывая» его. Но — и это важно — результат дополнительно маскируется случайностью. На выходе стороны получают новые доли результата, которые можно использовать дальше внутри протокола, но ни одна сторона не получает произведение целиком.
Threshold Schnorr — это в основном линейность и аккуратные commitments.
Threshold ECDSA — это деление, умножение и доказательства над секретами, не раскрывая секреты.
Поэтому ECDSA-MPC обрастает криптографической сантехникой: pre-signing, zero-knowledge proofs,
commitment schemes, Paillier/OT-подходы, identifiable aborts. Например, семейство протоколов
GG18/GG20/CMP/CGGMP и open-source реализации вроде Taurus multi-party-sig решают именно эту
задачу: получить обычную ECDSA-подпись для secp256k1, не собирая приватный ключ.
Сложно — но это позволяет работать с уже существующими Bitcoin/Ethereum-адресами и инфраструктурой.
Из чего собрана живая MPC-система
Математика — Schnorr, ECDSA, пруфы, коммитменты — это 20% работы. Остальное — инженерия: оркестрация сессий, политики, бэкапы долей,
аудит, обработка отказов и доказуемость того, кто что сделал. Signing ceremony — это не один
вызов sign(tx), а короткий распределённый протокол с участниками, раундами и таймаутами.
Нажмите «Запустить церемонию» и проследите путь транзакции от запроса до блокчейна.
Кто за что отвечает
Transaction Builder — собирает raw-транзакцию: кому, сколько, chain id, nonce, gas, fee, memo. Policy Engine — решает, можно ли вообще подписывать: лимиты, whitelist, роли, approvals, risk score. Signing Coordinator — оркестрирует MPC-сессию: выбирает кворум, рассылает запрос, следит за раундами и таймаутами. Signer Node — хранит только свою долю, локально проверяет запрос и участвует в протоколе. Assembler — агрегатор: проверяет partial signatures и склеивает итоговую подпись; приватных долей он не видит. Broadcaster — отправляет уже подписанную транзакцию в сеть.
Маленькая, но частая путаница: signature nonce k — секретный
одноразовый материал внутри подписи (тот самый, который нельзя переиспользовать), а
transaction nonce — публичный счётчик транзакций аккаунта в сети, который кладёт
в raw-tx Transaction Builder. Это разные вещи.
Signer-нода не должна слепо подписывать всё, что прислал координатор. Каждая нода сама проверяет: chain id, актив, сумму, адрес получателя, хеш транзакции, решение политики, id сессии, свежесть запроса, кто аппрувнул и не подписывали ли это уже. Иначе координатор становится слишком сильным участником — и снова появляется единая точка отказа.
Ключ не создаётся «где-то»
Наивная модель уже лучше, чем хранить ключ целиком — но всё ещё с дырой:
1. создать приватный ключ на одной машине ← вот здесь ключ существовал целиком
2. поделить на доли
3. раздать
4. удалить оригинал
Правильно — Distributed Key Generation (DKG): ноды совместно порождают публичный ключ и свои приватные доли так, что полный приватный ключ никогда не появляется ни на одной машине. Даже в момент рождения.
В хорошей MPC-системе приватный ключ не «создаётся и прячется».
Он вообще никогда не материализуется как единый объект.
Как тогда вообще появляется общий публичный ключ X, если полный приватный ключ никто не знает? Здесь работает механизм Verifiable Secret Sharing (VSS). Каждый участник генерирует свой секретный полином, приватно раздаёт доли другим нодам и публикует commitments к коэффициентам (своим публичным вкладам X₁, X₂, …). По этим commitments остальные могут проверить, что полученные доли согласованы — не подсунул ли сосед «мусорную» долю, — не раскрывая сами доли. Общий публичный ключ участники собирают из публичных вкладов: X = X₁ + X₂ + X₃ + …. Блокчейн позже видит этот X как обычный публичный ключ — он не знает и не должен знать, что внутри компании ключ создан распределённо.
DKG — это не только первичная генерация. В зрелой системе нужны resharing и key refresh: можно менять состав signer-нод, обновлять доли, выводить скомпрометированную ноду из кворума — и при этом сохранять тот же публичный ключ, если бизнес-процесс этого требует.
Грабли, на которые вы наступите
Самый полезный раздел для инженера. Не «криптография красивая», а «вот где больно».
Прежде чем перечислять грабли — честно про границы. MPC/TSS убирает single point of compromise: утечку одного memory dump, компрометацию одной signer-ноды, одного бэкапа, одного CI/CD-раннера, доступ одного инженера. Этого уже очень много.
Но MPC не спасает, если атакующий контролирует порог m signer-нод, если
policy engine сам одобряет вредную транзакцию или если скомпрометирован весь approval path. MPC
распределяет право подписи — это один слой защиты, а не вся оборона.
Nonce reuse
Использовали один k дважды — и подпись может выдать приватный ключ или его доли.
nonce — это не «просто random number». Это одноразовый криптографический расходник.
Применил дважды — возможно, сжёг ключ.
ECDSA malleability / Low-S
У ECDSA есть зеркальная форма подписи: если (r, s) валидна, то (r, n − s)
тоже может быть валидна. Поэтому Bitcoin и многие secp256k1-интеграции требуют
Low-S normalization. MPC-агрегатор или signing-сессия должны гарантировать, что
итоговое s приведено к ожидаемой форме. Иначе транзакция может быть отклонена сетью,
зависнуть в мемпуле или сломать downstream-интеграцию.
Pre-signing risk
Threshold ECDSA часто выносит тяжёлую криптографию в pre-signing — заранее подготовленный material, который ускоряет online-подпись. Удобно, но это новый класс риска: pre-signature material должен быть строго одноразовым. Его нельзя переиспользовать, восстанавливать из старого бэкапа, рассинхронизировать между нодами или логировать. Ошибка здесь может быть катастрофой уровня nonce reuse.
Abort attack
Один участник может сорвать подпись на позднем этапе: A и B отправили свои сообщения, C посмотрел на состояние и отказался продолжать. В распределённых системах сложно понять, кто именно прислал невалидные данные. Поэтому в зрелых протоколах сообщения сопровождаются проверками и доказательствами корректности — часто zero-knowledge proofs (ZKP): нода доказывает остальным «я посчитал свою часть строго по протоколу», не раскрывая саму долю. Это помогает отличить плохую partial signature от сетевого сбоя или саботажа и, в ряде протоколов, доказуемо указать виновника abort-а.
Но без иллюзий: ZKP и Identifiable Abort уменьшают пространство и для саботажа, и для ложных обвинений — однако это не магическая кнопка «виновник всегда найден». Что именно удаётся доказать и насколько строго, зависит от конкретного протокола и его threat model.
Network partition
MPC — это не локальная функция sign(tx). Это распределённый протокол с раундами,
коммитментами, пруфами и таймаутами. Значит, прилетает весь зоопарк distributed-systems проблем:
timeouts · message ordering · duplicate messages · replay partial failure · version mismatch · clock drift · coordinator crash
Selective failure / злой координатор
Координатор — это не обязательно честный роутер. Он может не доставить сообщение от A к B, вызвать timeout, а потом записать в лог, что «нода A упала». Поэтому signer-ноды должны валидировать session transcript, хранить доказуемый audit trail и не считать координатора источником истины. Координатор может помогать с маршрутизацией, но не должен быть trusted party.
Domain separation
Нода должна подписывать не «какой-то хеш», а строго типизированный контекст: chain id, asset, адрес, сумму, session id, версию протокола, policy decision id. Иначе можно получить cross-chain или cross-protocol replay: подпись была задумана для одного смысла, а использована в другом.
Share backup
Потеря доли — это не «переустановим сервис». В зависимости от порога это может означать невозможность подписи навсегда. Думать заранее: encrypted backups, share recovery, resharing, key rotation, disaster recovery ceremony, контроль доступа к бэкапам.
Logging
Нельзя логировать секретные protocol messages как обычный debug. Разделяем потоки:
audit logs: кто аппрувнул, что подписали, когда security logs: сорванные сессии, подозрительные ретраи protocol logs: только безопасные метаданные secret material: никогда
Частый вопрос в custody-контексте. HSM защищает приватный ключ как объект внутри защищённого устройства — ключ есть, он просто заперт. MPC/TSS меняет саму модель: полного ключа нет ни в одном устройстве. Это не «или-или»: на практике подходы сочетаются — доли TSS вполне могут жить внутри HSM, TEE (доверенной среды исполнения вроде Intel SGX) или изолированных signer-сервисов.
MPC ≠ policy
MPC отвечает ровно на один вопрос — сколько нод нужно, чтобы создать подпись. И всё. На все остальные вопросы (кому уходят деньги? какая это сумма? кто аппрувнул? законен ли перевод?) он не отвечает — это работа policy. Сравните две колонки:
MPC решает
Сколько подписантов в кворуме. Где живёт криптографическая власть. Чтобы финальная подпись не находилась в одной точке.
MPC НЕ решает
Можно ли слать $500k на этот адрес? Кто аппрувнул? Это адрес клиента или атакующего? Нормальная ли это сумма? Прошёл ли AML/KYT?
MPC защищает private key path.
Policy engine защищает business decision path.
MPC не заменяет risk engine, approvals, лимиты, whitelist, мониторинг и human review. Он лишь делает так, чтобы финальная криптографическая власть не лежала в одной точке. Это два разных слоя защиты, и путать их — классическая ошибка.
MPC/TSS убирает single point of compromise и сильно меняет модель риска — но не отменяет policy, изоляцию инфраструктуры, мониторинг, аудит и управление кворумом.