В этой статье рассматриваются секреты создания приватных ключей для Биткоина, а также способы получения адреса кошелька. Приводятся практические примеры на языке bash. Статья является результатом анализа информации исходного кода нескольких проектов на GitHub, а также ресурсов зарубежных авторов.
Создание приватного ключа.
Приватный ключ содержит 256 бит (32 байта) значений, начинающихся с 0х1 до 0xFFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF BAAE DCE6 AF48 A03B BFD2 5E8C D036 4140. В результате всего может существовать 2^256 или 1.16 x 10^77 приватных ключей.
Для создания приватного ключа используется хэш SHA256. Хэш может быть сгенерирован какой-либо программой либо получен на основе заложенной человеком определенная комбинация символов, которую знает только он. Это делается с целью того, чтобы человек всегда смог восстановить свой приватный ключ из своей памяти. Такие кошельки принято называть Brain Wallets. Т.е. в них заложена какая-то смысловая информация. Но всегда есть риск забыть ее и потерять доступ к своему кошельку. После преобразования смысловой строки в хэш SHA256, на выходе всегда получается срока имеющая 256 битное значение. Учитывая то, что каждые 2 символа представляют собой 8 бит или 1 байт, то в итоговое значение содержит 64 символа.
Ниже показан пример, как исходная строка преобразуется в хэш SHA256 используя bash.
$ echo "this is a group of words .... generate a private key" | openssl sha256 # out: a966eb6058f8ec9f47074a2faadd3dab42e2c60ed05bc34d39d6c0e1d32b8bdf
Создание публичного ключа.
Публичный ключ генерируется из приватного ключа используя эллиптическую кривую secp256k1 по следующей формуле K = k * G, где K — это публичный ключ, k — приватный ключ, а G это константа называемая точкой генератора. Этот алгоритм не имеет обратного преобразования, т.е. нельзя по формуле восстановить приватный ключ имея публичный.
Ниже представлен пример, как приватный ключ «a966eb6058f8ec9f47074a2faadd3dab42e2c60ed05bc34d39d6c0e1d32b8bdf» полученный на предыдущем этапе используется для генерации публичного ключа.
На выходе в примере мы получаем открытый ключ «043cba1f4d12d1ce0bced725373769…….dfe47e5dce2e08601d6f11f5a4», который включает префикс 0х04, а также X и Y координаты на элиптической кривой secp256k1 (для наглядности ниже в примере они отмечены).
$ openssl ec -inform DER -text -noout -in <(cat <(echo -n "302e0201010420") <(echo -n "a966eb6058f8ec9f47074a2faadd3dab42e2c60ed05bc34d39d6c0e1d32b8bdf") <(echo -n "a00706052b8104000a") | xxd -r -p) 2>/dev/null | tail -6 | head -5 | sed 's/[ :]//g' | tr -d '\n' && echo # out: # PREFIX: 04 # X: 3cba1f4d12d1ce0bced725373769b2262c6daa97be6a0588cfec8ce1a5f0bd09 # Y: 2f56b5492adbfc570b15644c74cc8a4874ed20dfe47e5dce2e08601d6f11f5a4
Получение сжатого публичного ключа.
Большинство кошельков реализуют сжатый публичный ключ для экономии пространства блокчейна. Для конвертации не сжатого публичного ключа в сжатый, вы можете не учитывать Х, потому что его можно расчитать имея значение Y, решив уравнение элиптической кривой Y² = X³ + 7. Префикс 0x02 добавляется для положительных значений Y, а 0x03 для отрицательных. Если последняя двоичная цифра координаты равна 0, то число положительное, а если 1 — отрицательное. В итоге получаем сжатую версию публичного ключа:
02 3cba1f4d12d1ce0bced725373769b2262c6daa97be6a0588cfec8ce1a5f0bd09
Префикс 0x02 добавлен потому, что Y координата содержала в конце значение 0xa4, что говорит о ее положительном значении.
Генерация bitcoin-адреса.
Биткоин использует 2 типа адресов, один из них называется P2SH-P2WPKH (pay-to-script hash & pay-to-witness-public-key-hash) который сейчас исползуется по умолчанию во всех кошельках.
Второй называется P2PKH (Pay to Public Key Hash) и был предшественником первого. Скрипты дают больше функциональности, поэтому они более популярны сейчас.
Получаем хеш публичного адреса.
Сжатую версию публичного ключа, полученную на предыдущем шаге, нужно хэшировать в sha256, а после этого в ripemd160. Это гарантирует, что в случае непредвиденной взаимосвязи между элиптической кривой и sha256, другая не связанная хэш функция значительно усложнит возможность расшифровки хеша.
$ echo 023cba1f4d12d1ce0bced725373769b2262c6daa97be6a0588cfec8ce1a5f0bd09 | xxd -r -p | openssl sha256 # out: (stdin)= 8eb001a42122826648e66005a549fc4b4511a7ad3fc378221aa1c73c5efe77ef $ echo 8eb001a42122826648e66005a549fc4b4511a7ad3fc378221aa1c73c5efe77ef | xxd -r -p | openssl ripemd160 # out: (stdin)= 3a38d44d6a0c8d0bb84e0232cc632b7e48c72e0e
Обратите внимание, т.к. на входе у нас строка, то команда xxd -r -p будет конвертировать hex строку в бинарную а затем выведет ее в ascii, которую openssl ожидает на входе.
Кодирование в base58.
В итоге мы получили хеш публичного ключа, который можно преобразовать в более компактный вид при помощи base58check. Алгоритм base58check позволяет получать компактный вид хеша за счет большего использования букв алфавита и избегая похоших символов, например, таких как О и 0. Контрольная сумма используется для того, чтобы проверить правильность преобразования адреса.
Формат адреса P2PKH.
Формат P2PKH адреса Биткоина начинаются со значения байта версии 0х00 и заканчиваются 4 байтовой контрольной суммой. Сначала мы добавляем байт с номером версии к нашему хэшу открытого ключа и вычисляем его. А затем добавляем контрольную сумму, после чего кодируем результат в base58.
$ echo 003a38d44d6a0c8d0bb84e0232cc632b7e48c72e0e | xxd -p -r | base58 -c && echo # out: 16JrGhLx5bcBSA34kew9V6Mufa4aXhFe9X
Примечание: опция -c означает, что была применена чексумма. Чексумма расчитывается как SHA256(SHA256(prefix+data)) после чего беруется только первые 4 байта и добавляются в конец хеша ключа.
В результате значение адреса P2PKH для Bitcoin примет вид:
19P1LctLQmH6tuHCRkv8QznNBGBvFCyKxi
Формат адреса P2SH-P2WPKH.
Похожие шаги используются для генерации P2SH-P2WPKH адреса за исключением того, что хэш выполняется в сценарии транзакции, вместо открытого ключа.
У Биткоина есть язык сценариев. В основном это позволяет устанавливать требования к сигнатуре для отправки биткоинов, задержки времени перед отправкой и т.д. Обычно используемый сценарий является транзакцией с несколькими сигналами: OP_0 0x14 <PubKey Hash>, где PubKey Hash это RIPEMD160 от SHA256 от открытого ключа (как определялось для P2WPKH) и 0х14 байтов в PubKey Hash. Таким образом, чтобы превратить этот скрипт в адрес, вы просто применяете BASE58CHECK к RIPEMD160 от SHA256 от OP_0 0x14 <PubKey Hash>, за исключением того, что вы добавляете 0х05 к хэш-скрипту вместо 0х00 для обозначения типа адреса P2SH.
$ echo 00143a38d44d6a0c8d0bb84e0232cc632b7e48c72e0e | xxd -r -p | openssl sha256 # out: (stdin)= 1ae968057eaef06c3e13439695edd7a54982fc99f36c3aa41d8cc41340f30195 $ echo 1ae968057eaef06c3e13439695edd7a54982fc99f36c3aa41d8cc41340f30195 | xxd -r -p | openssl ripemd160 # out: (stdin)= 1d521dcf4983772b3c1e6ef937103ebdfaa1ad77 $ echo 051d521dcf4983772b3c1e6ef937103ebdfaa1ad77 | xxd -p -r | base58 -c && echo # out: 34N3tf5m5rdNhW5zpTXNEJucHviFEa8KEq