Перейти к содержимому
ecdsa.com
← Все статьи

secp256k1: как устроена кривая Bitcoin и Ethereum

y² = x³ + 7 по модулю 256-битного простого числа. Мы проверяем каждую константу из SEC 2 несколькими строками на Python, показываем приём, на котором построена libsecp256k1, и выясняем, какие библиотеки принимают подпись с high-S; все примеры запущены 20 октября 2026 года.

Коротко. secp256k1 — эллиптическая кривая y² = x³ + 7 над простым полем порядка p = 2²⁵⁶ − 2³² − 977, определённая в SEC 2 в 2000 году. Её группа содержит простое число точек (кофактор 1) и обеспечивает стойкость около 128 бит. Коэффициенты подобраны ради скорости, а не получены из seed: a = 0 упрощает формулы, а у кривой есть эффективно вычислимый эндоморфизм, который ускоряет умножение точки на скаляр. Bitcoin использует её с первого коммита в 2009 году, затем к нему присоединился Ethereum, а сегодня на ней работают Nostr и большинство EVM-сетей. Её нет ни в WebCrypto, ни в TLS 1.3, ни в большей части защищённого оборудования; вне блокчейн-протоколов выбирайте P-256 или Ed25519. Поверх обычного ECDSA её экосистемы вводят собственные правила: только подписи в форме low-S в политике ретрансляции Bitcoin и в транзакциях Ethereum, восстановление открытого ключа вместо передачи ключей, а в качестве хеша — Keccak-256 или двойной SHA-256.

Параметры

В SEC 2 v2.0 (§ 2.4.1) кривая задана шестью числами (SEC 2):

ПараметрЗначениеСмысл
p2²⁵⁶ − 2³² − 977 (0xffff…fffefffffc2f)Простой модуль поля; благодаря его виду приведение по модулю p обходится дёшево
a, b0, 7Кривая y² = x³ + 7
Gx = 0x79be667e…16f81798, y = 0x483ada77…fb10d4b8Базовая точка
n0xffff…fffebaaedce6af48a03bbfd25e8cd0364141Порядок G, простое число
h1Кофактор: каждая точка, кроме бесконечно удалённой, порождает всю группу

Здесь ничего не нужно принимать на веру, и ничто не получено из seed неизвестного происхождения. Скрипт ниже проверяет константы на обычных целых числах Python:

curve.py (фрагмент): проверка параметров SEC 2, Python 3.14.8
G = (Gx, Gy)
print("p == 2^256 - 2^32 - 977:", hex(p) == "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f")
print("G is on y^2 = x^3 + 7:  ", (Gy * Gy - Gx**3 - b) % p == 0)
print("n is a probable prime:  ", probable_prime(n))
print("n * G is infinity:      ", mul(n, G) is None)
t = p + 1 - n
print("trace t = p + 1 - n:    ", t)
print("Hasse bound |t| <= 2*sqrt(p):", t * t <= 4 * p)
print("n < p, p - n has", (p - n).bit_length(), "bits: the group has n points, cofactor 1")
Результат (python:3.14.8-slim-trixie)
p == 2^256 - 2^32 - 977: True
G is on y^2 = x^3 + 7:   True
n is a probable prime:   True
n * G is infinity:       True
trace t = p + 1 - n:     432420386565659656852420866390673177327
Hasse bound |t| <= 2*sqrt(p): True
n < p, p - n has 129 bits: the group has n points, cofactor 1

add и mul — учебные реализации сложения точек в аффинных координатах и умножения методом удвоения и сложения (double-and-add): для проверки констант они годятся, а для работы с секретами нет, потому что не выполняются за постоянное время. След t совпадает со значением, опубликованным на сайте SafeCurves. Поскольку n простое и совпадает с числом точек, малых подгрупп нет: проблем с кофактором, которые усложняют протоколы на Curve25519, здесь не возникает.

Почему эти числа делают кривую быстрой

Три свойства этих чисел выбраны намеренно:

  • Простой модуль равен 2²⁵⁶ минус небольшое число, поэтому приведение 512-битного произведения по модулю p сводится к нескольким умножениям на 2³² + 977 вместо деления общего вида.
  • a = 0 убирает одно умножение из каждого удвоения точки.
  • Эндоморфизм. Поскольку p ≡ 1 (mod 3), существует кубический корень из единицы β по модулю p, и отображение (x, y) → (βx, y) переводит каждую точку в другую точку той же кривой. Это отображение равносильно умножению на кубический корень из единицы λ по модулю n. Тогда скаляр k можно разбить на две половины длиной около 128 бит, k = k₁ + k₂λ, и вычислить kP как k₁P + k₂(λP), где λP обходится в одно умножение в поле. Это метод GLV (Gallant, Lambert, Vanstone, 2001).

Тот же скрипт находит β и λ и проверяет это тождество:

curve.py (фрагмент): эндоморфизм
print("p % 3 == 1 (cube roots of unity exist mod p):", p % 3 == 1)
beta = pow(2, (p - 1) // 3, p)
lam = pow(3, (n - 1) // 3, n)
# Pick the lambda that matches this beta (there are two non-trivial cube roots of unity mod n).
if mul(lam, G) != (beta * Gx % p, Gy):
    lam = lam * lam % n
print("beta^3 == 1 mod p, lambda^3 == 1 mod n:", pow(beta, 3, p) == 1, pow(lam, 3, n) == 1)
print("lambda * G == (beta * Gx, Gy):        ", mul(lam, G) == (beta * Gx % p, Gy))
print("beta   =", hex(beta))
print("lambda =", hex(lam))
Результат
p % 3 == 1 (cube roots of unity exist mod p): True
beta^3 == 1 mod p, lambda^3 == 1 mod n: True True
lambda * G == (beta * Gx, Gy):         True
beta   = 0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee
lambda = 0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72

Именно эти константы использует libsecp256k1. Хэл Финни описал этот приём применительно к Bitcoin в ветке форума bitcointalk от февраля 2011 года и рассчитывал ускорить проверку подписей на 20–25 %; его код стал отправной точкой для libsecp256k1, которую написал Pieter Wuille. Bitcoin Core включил оптимизацию GLV по умолчанию вместе с обновлением libsecp256k1 в PR #20147 (Bitcoin Core 0.21, 2021), и там же замерили, что начальная синхронизация с включённой проверкой подписей ускорилась примерно на 16 %. Насколько всё это важно, зависит от библиотеки: OpenSSL не содержит специального кода для secp256k1 и на P-256 работает намного быстрее (цифры приведены в сравнении P-256 или secp256k1).

Почему её используют Bitcoin и Ethereum

Первый коммит Bitcoin (август 2009 года) создаёт ключи вызовом EC_KEY_new_by_curve_name(NID_secp256k1) из OpenSSL. Сатоси Накамото так и не объяснил этот выбор. Обычно называют причины, которые относятся к свойствам кривой, а не к словам того, кто её выбрал: в Bitcoin wiki отмечается, что она «построена особым неслучайным способом, который позволяет выполнять вычисления особенно эффективно» и что «в отличие от популярных кривых NIST, константы secp256k1 выбраны предсказуемым образом» (Bitcoin wiki). Ethereum сохранил кривую Bitcoin, но заменил хеш-функцию: транзакции и сообщения хешируются с помощью Keccak-256, и с её же помощью из ключа выводится адрес.

Органы стандартизации подтянулись позже. RFC 8812 (2020) зарегистрировал ES256K для JOSE и COSE, а в NIST SP 800-186 (2023), § H.2, сказано, что кривую «разрешено использовать в приложениях, связанных с блокчейном» (в число кривых, рекомендованных для общего применения, она не входит).

Насколько она стойкая

Лучшая из известных атак на задачу дискретного логарифмирования в этой группе — ро-метод Полларда: по данным SafeCurves, ему нужно около 0,886·√n ≈ 2¹²⁷·⁸ сложений. Это та же оценка, что и для P-256, и та самая привычная «стойкость 128 бит». (Эндоморфизм порядка 3 позволяет на этой кривой ещё немного ускорить ро-метод, примерно в √3 раза, но картину это не меняет.) SafeCurves не признаёт secp256k1 «безопасной», но по причинам, которые касаются реализаций, а не дискретного логарифма:

Критерий SafeCurvessecp256k1Что это значит для вас
ро-метод, перенос, жёсткость (rho, transfer, rigidity)пройденДискретный логарифм вычислить трудно, а происхождение констант объяснено
дискриминант комплексного умножения (CM discriminant)не пройден (D = −3)Именно малый дискриминант даёт эндоморфизм; никакой атаки из этого не следует
стойкость кручения (twist security)пройден, 2¹⁰⁹·⁵Ниже, чем у самой кривой; важно только для кода, который пропускает проверку точек
лестница, полные формулы (ladder, complete formulas)не пройденКод с постоянным временем выполнения и без исключительных случаев приходится писать аккуратнее, чем для Curve25519
неотличимость (indistinguishability)не пройденТочки не выглядят как случайные строки; это важно для протоколов, устойчивых к цензуре, а не для подписей

Практический вывод тот же, что и для P-256: используйте библиотеку, написанную для этой кривой (libsecp256k1, @noble/curves, k256 для Rust, привязку из go-ethereum), проверяйте открытые ключи на входе и никогда не пишите собственное умножение на скаляр для секретных значений. Если ключ приходит в виде точки, перед использованием обязательно проверьте, что она лежит на кривой y² = x³ + 7; без этой проверки возможна классическая атака с недопустимой кривой, и именно здесь играет роль стойкость кручения. И, как на любой кривой ECDSA, повторный или смещённый nonce раскрывает ключ (повторное использование nonce в ECDSA).

Ключи и адреса

Закрытый ключ — число d из диапазона [1, n − 1], открытый ключ — точка Q = dG. Одну и ту же Q записывают несколькими способами, и путаница между ними — частая причина ошибок вида «не тот адрес». Пример для d = 1, у которого открытый ключ — сама точка G (этот ключ известен всем, поэтому никогда его не используйте):

keys.py: один ключ, несколько кодировок (pycryptodome 3.23.0 для Keccak и RIPEMD-160)
compressed   (33 bytes): 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
uncompressed (65 bytes): 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
x-only, BIP-340 (32 bytes): 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Ethereum address, keccak256(x || y)[12:]: 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
Bitcoin P2PKH from the compressed key: 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
Bitcoin P2PKH from the uncompressed key: 1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm
  • Сжатая форма (SEC 1, § 2.3.3): 02 или 03 для чётной или нечётной координаты y, затем x. Bitcoin по умолчанию использует сжатые ключи уже больше десятилетия. Для распаковки нужен квадратный корень по модулю p, а для этого p (p ≡ 3 mod 4) он вычисляется одним возведением в степень: y = (x³ + 7)^((p+1)/4).
  • Несжатая форма: 04 ‖ x ‖ y. В этой форме тот же ключ даёт другой адрес Bitcoin, поэтому старые кошельки, которые использовали несжатые ключи, показывают для того же секрета другие адреса.
  • x-only (32 байта): Taproot и Nostr используют подписи Schnorr по BIP-340, ключи которых опускают координату y и подразумевают, что она чётная.
  • Адрес Ethereum: последние 20 байт хеша Keccak-256 от 64-байтного x ‖ y, без префикса 04. Keccak-256 — не SHA3-256: у них разное дополнение, и hashlib.sha3_256 даёт неверные адреса.

Low-S: одна подпись, две действительные формы

У подписи ECDSA (r, s) есть двойник (r, n − s), который проходит проверку точно так же, потому что при замене точки nonce на противоположную значение r не меняется. Для P-256 в TLS это никого не волнует. Для Bitcoin это было важно: двойник давал той же транзакции другой идентификатор, и сервисы, которые отслеживали платежи по идентификатору, можно было обмануть. Bitcoin Core 0.11.1 (2015) обязал узел «требовать каноническую кодировку „low-s“ для подписей ECDSA при ретрансляции или майнинге» (примечания к выпуску), и это политика ретрансляции, а не правило консенсуса. Ethereum пошёл дальше в EIP-2 (Homestead, 2016): «Все подписи транзакций, у которых значение s больше secp256k1n/2, теперь считаются недействительными», при этом «прекомпилированный контракт восстановления ECDSA остаётся без изменений и по-прежнему будет принимать большие значения s».

Универсальные библиотеки этих правил не соблюдают, а блокчейн-библиотеки соблюдают. Мы подписали 1 000 случайных сообщений с помощью OpenSSL из Node.js, а затем проверили одну подпись с high-S и её двойника с low-S в трёх библиотеках:

lows.mjs (фрагмент): node 24.22.0, @noble/curves 2.4.0, ethers 6.17.0
const { msg, sig, s } = sample;
const low = Buffer.concat([sig.subarray(0, 32), Buffer.from((n - s).toString(16).padStart(64, "0"), "hex")]);
const opensslOk = (x) => verify("sha256", msg, { key: publicKey, dsaEncoding: "ieee-p1363" }, x);
console.log(`node:crypto verify  high-S: ${opensslOk(sig)}  low-S twin: ${opensslOk(low)}`);
console.log(`noble default       high-S: ${secp256k1.verify(sig, msg, pub)}  low-S twin: ${secp256k1.verify(low, msg, pub)}`);
console.log(`noble lowS: false   high-S: ${secp256k1.verify(sig, msg, pub, { lowS: false })}`);
Результат (node:24.22.0-bookworm-slim)
OpenSSL 3.5.8: 494 of 1000 signatures have s > n/2
node:crypto verify  high-S: true  low-S twin: true
noble default       high-S: false  low-S twin: true
noble lowS: false   high-S: true
ethers 6.17.0 v=27 high-S: non-canonical s; use ._s

Примерно половина подписей OpenSSL имеет форму high-S; OpenSSL принимает обе формы, @noble/curves на secp256k1 отклоняет high-S, если явно не настроить её иначе, а ethers отказывается даже создавать из такой подписи объект подписи. Система, которая подписывает универсальной библиотекой или в HSM, а подписи отправляет в блокчейн, обязана их нормализовать: если s > n/2, замените s на n − s и инвертируйте бит восстановления. Google Cloud KMS указывает в документации, что ключи secp256k1 в нём выдают только подписи в форме low-S; документация AWS KMS по ECC_SECG_P256K1 такого не обещает, поэтому нормализуйте его подписи сами. Кодировки DER и raw, в которых передаются эти подписи, разобраны в статье Форматы подписей ECDSA.

Восстановление открытого ключа

По подписи, хешу сообщения и одному или двум дополнительным битам можно вычислить открытый ключ подписанта, поэтому транзакции Ethereum вообще не содержат открытого ключа, а подписанные сообщения Bitcoin проверяют по адресу. Эти дополнительные биты — идентификатор восстановления (v, yParity или байт заголовка в подписи сообщения Bitcoin). Как работает восстановление, что при нём может пойти не так и как ведёт себя ecrecover, рассказано в статье Восстановление открытого ключа ECDSA.

Где она поддерживается, а где нет

Платформаsecp256k1
WebCrypto (браузеры, Deno, crypto.subtle в Node)Нет; используйте @noble/curves или WASM-сборку libsecp256k1
OpenSSL и crypto в Node.jsДа, с универсальным (более медленным) кодом
BoringSSLНет: в нём встроены только P-224, P-256, P-384 и P-521
Java (SunEC)Удалена в JDK 16: Curve not supported: secp256k1; используйте Bouncy Castle
Apple CryptoKit, Secure Enclave, Android Keystore, TPM 2.0, карты PIVНет
Облачные KMSAWS KMS ECC_SECG_P256K1; Google Cloud KMS EC_SIGN_SECP256K1_SHA256 (только с уровнем защиты HSM); Azure Key Vault P-256K с ES256K
TLS 1.3, X.509 в Web PKI, WebAuthnНет

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

Частые ошибки

  • Хеширование с SHA3-256 вместо Keccak-256 для Ethereum или с одинарным SHA-256 вместо двойного SHA-256, который Bitcoin использует в сообщениях и транзакциях. Подпись проверяется уже для другого сообщения, а восстановление выдаёт другой адрес, с виду вполне корректный.
  • Отправка подписей с high-S, полученных от OpenSSL, HSM или облачного KMS, в Ethereum или в сеть Bitcoin. Сначала нормализуйте их.
  • Смешение сжатых и несжатых ключей при выводе адресов Bitcoin.
  • Длина подписи в формате raw. 64-байтная подпись r ‖ s и 65-байтная r ‖ s ‖ v — разные форматы, и библиотеки прямо сообщают об этом, как, например, ethers с ошибкой invalid raw signature length.
  • Расчёт на поддержку платформы. Код, который работал на Java 11, перестаёт работать на Java 17, а в WebCrypto этой кривой не было никогда.
  • Использование secp256k1 там, где её ничто не требует. Для токенов, TLS, удостоверений устройств или ключей доступа она не даёт никаких преимуществ, а аппаратная поддержка теряется.

Источники