В последнее время я работал над Освоением Биткойна, реализуя как можно больше примеров из книги на Эликсире.

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

Давайте рассмотрим процесс шаг за шагом и создадим собственный модуль Elixir для генерации закрытых ключей и публичных адресов.

Что такое закрытые ключи и общедоступные адреса?

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

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

Давайте воспользуемся Elixir, чтобы сгенерировать криптографически безопасный закрытый ключ, а затем сгенерируем его наиболее простой соответствующий публичный адрес, чтобы мы могли получить немного биткойнов!

Извлечение закрытого ключа из воздуха

Как я упоминал ранее, закрытый ключ Биткойн — это просто случайное двухсотпятидесятишестибитное число. Другими словами, закрытый ключ может быть любым числом от 0 до 2^256.

Однако не все случайные числа создаются одинаково. Нам нужно быть уверенными, что мы генерируем наше случайное число из криптографически безопасного источника энтропии. К счастью, Elixir предоставляет функцию Erlang :crypto.strong_rand_bytes/1, которая позволяет нам легко генерировать список действительно случайных байтов.

Давайте используем :crypto.strong_rand_bytes/1 в качестве основы для нашего генератора закрытых ключей. Мы начнем с создания нового модуля PrivateKey и функции generate/0, не принимающей аргументов:

defmodule PrivateKey do
  def generate
end

Внутри нашей функции generate/0 мы будем запрашивать 32 случайных байтов (или 256 битов) из :crypto.strong_rand_bytes/1:

def generate do
  :crypto.strong_rand_bytes(32)
end

Это дает нам случайный набор 32 байтов, который, если рассматривать его как целое число без знака, находится в диапазоне от 0 до 2^256 - 1.

К сожалению, мы не совсем закончили.

Проверка нашего закрытого ключа

Чтобы гарантировать, что наш закрытый ключ трудно угадать, Группа стандартов эффективной криптографии рекомендует выбирать закрытый ключ между числом 1 и числом чуть меньше 1.158e77:

Отрывок из рекомендаций SECG.

Мы можем довольно легко добавить эту проверку правильности, добавив предоставленную SECG верхнюю границу в качестве атрибута в наш модуль PrivateKey:

@n :binary.decode_unsigned(<<
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
  0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B,
  0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41
>>)

Далее мы добавим в наш модуль функцию valid?/1, которая возвращает true, если предоставленный секретный ключ попадает в этот диапазон, и false, если он не входит:

defp valid?(key) when key > 1 and key < @n, do: true
defp valid?(_), do: false

Прежде чем мы передадим наш закрытый ключ в нашу функцию valid?/1, нам нужно преобразовать его из тридцатидвухбайтового двоичного файла в целое число без знака. Давайте добавим третью головку функции valid?/1, которая делает именно это:

defp valid?(key) when is_binary(key) do
  key
  |> :binary.decode_unsigned
  |> valid?
end

Мы завершим нашу проверку, передав сгенерированный закрытый ключ в нашу новую функцию valid?/1. Если ключ действителен, мы вернем его. В противном случае мы сгенерируем новый закрытый ключ и повторим попытку:

def generate do
  private_key = :crypto.strong_rand_bytes(32)
  case valid?(private_key) do
    true -> private_key
    false -> generate
  end
end

Теперь мы можем вызвать PrivateKey.generate, чтобы сгенерировать новый приватный биткойн-ключ!

От закрытого ключа к открытому ключу…

Самый простой процесс превращения закрытого ключа Биткойн в общий публичный адрес состоит из трех основных шагов. Первый шаг — преобразовать наш закрытый ключ в открытый ключ с помощью криптографии на эллиптических кривых.

Начнем с добавления новой функции to_public_key/1 в наш модуль PrivateKey:

def to_public_key(private_key)

В нашей функции to_public_key/1 мы будем использовать функцию Erlang :crypto.generate_key, чтобы подписать наш private_key с помощью эллиптической кривой. Мы специально будем использовать кривую :secp256k1:

:crypto.generate_key(:ecdh, :crypto.ec_curve(:secp256k1), private_key)

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

Функция :crypto.generate_key возвращает кортеж из двух элементов. Первый элемент в этом кортеже — наш открытый ключ Биткойн. Мы вытащим его с помощью функции Elixir elem/1:

:crypto.generate_key(:ecdh, :crypto.ec_curve(:secp256k1), private_key)
|> elem(0)

Возвращаемое значение представляет собой 65-байтовый двоичный файл, представляющий наш открытый ключ!

… Открытый ключ к открытому хэшу …

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

Давайте создадим новую функцию to_public_hash/1, которая принимает наше private_key в качестве аргумента:

def to_public_hash(private_key)

Мы начнем процесс хэширования, превратив наш private_key в открытый ключ с вызовом to_public_key:

private_key
|> to_public_key

Затем мы передаем наш открытый ключ через две функции хэширования: SHA-256, а затем RIPEMD-160:

private_key
|> to_public_key
|> hash(:sha256)
|> hash(:ripemd160)

Биткойн использует алгоритм хеширования RIPEMD-160, потому что он производит короткий хэш. Промежуточное хеширование SHA-256 используется для предотвращения ненадежности из-за неожиданных взаимодействий между нашим алгоритмом подписи на эллиптической кривой и алгоритмом RIPEMD.

В этом примере hash/1 — это вспомогательная функция, обертывающая :crypto.hash в Erlang.

defp hash(data, algorithm), do: :crypto.hash(algorithm, data)

Переключение аргументов на :crypto.hash таким образом позволяет нам легко передать наши данные через помощника hash/1.

… И публичный хэш в публичный адрес

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

Давайте добавим функцию to_public_address/2 в наш модуль PrivateKey:

def to_public_address(private_key, version \\ <<0x00>>)

Функция to_public_address/2 принимает в качестве аргументов байты private_key и version. По умолчанию version имеет значение <<0x00>>, что указывает на то, что этот адрес будет использоваться в действующей сети Биткойн.

Чтобы создать биткойн-адрес, мы начинаем с преобразования нашего private_key в общедоступный хеш с вызовом to_public_hash/1:

private_key
|> to_public_hash

Все, что осталось сделать, это закодировать Base58Check полученный хэш с предоставленным version байтом:

private_key
|> to_public_hash
|> Base58Check.encode(version)

После закладки основы последние части головоломки легко встают на свои места.

Использование нашего творения

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

В прямом смысле!

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

private_key = PrivateKey.generate

Это дает нам 32-байтовый двоичный файл. Если бы мы захотели, мы могли бы Base58Check закодировать это с помощью testnet version byte of 0xEF. Это известно как Формат импорта кошелька или WIF нашего закрытого ключа Биткойн:

Base58Check.encode(private_key, <<0xEF>>)

Как следует из названия, преобразование нашего закрытого ключа в WIF позволяет нам легко импортировать его в большинство программ для биткойн-кошельков:

Импорт нашего тестового закрытого ключа.

Затем давайте преобразуем наш закрытый ключ в публичный адрес тестовой сети, используя version байт 0x6F:

PrivateKey.to_public_address(private_key, <<0x6F>>)

Теперь, когда у нас есть публичный адрес, давайте найдем сборник тестовой сети и отправим несколько tBTC на наш только что сгенерированный адрес! После инициирования транзакции с помощью нашего крана мы должны увидеть, как наш биткойн поступает на наш адрес либо в обозревателе блокчейна, либо в программном обеспечении нашего кошелька.

Наш tBTC прибыл.

Победа!

Последние мысли

Elixir, благодаря своему наследию Erlang, поставляется с множеством инструментов, которые делают этот вид хеширования, подписи и байтового пюре прогулкой в ​​​​парке.

Я рекомендую вам проверить наш модуль PrivateKey на Github, чтобы лучше понять простоту кода, который мы написали сегодня. В общем, я очень доволен результатом.

Если вам понравилась эта статья, я настоятельно рекомендую вам ознакомиться с книгой Mastering Bitcoin. Если вам действительно понравилась эта статья, не стесняйтесь отправить несколько биткойнов на этот адрес, который я сгенерировал с помощью нашего нового модуля PrivateKey:

1HKz4XU7ENT46ztEzsT83jRezyiDjvnBV8

Следите за новостями, связанными с биткойнами, пока я работаю над Освоением биткойнов!

Первоначально опубликовано на сайте www.petecorey.com 22 января 2018 г.