Частичный перевод моей статьи с Medium

Начнем с того, что эта статья как и большинство моих статей родилась в процессе разработки. В данном конкретном случае - разработки внешней крипто-библиотеки для генерации крипто-кошельков.

Да, определенно. Я считаю, что приставка "крипто" любому словосочетанию придает привкус гигантской яхты дрейфующей где-то в Майами.

В итоге, выбор упал на Rust потому что я уже работал с ним, и знаю, что crates.io изобилует всякими библиотеками для работы с разнообразными криптографическими функциями.

Bitcoin

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

Ключ-пара (Keypair) состоит из двух частей - приватный ключ и публичный, который выводится из приватного путем "Деривации"

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

Обычно приватный ключ - это просто случайное 256-битное число между 0 и n - 1, где n - константа (n = 1.1578 * 10⁷⁷). Как я уже сказал в Bitcoin публичный ключ выводится из приватного при помощи функции secp256k1 которая имеет 128-битную сложность. Это значит, что для того, чтобы провести обратную деривацию потребуется ~2¹²⁸ операций (тут могло бы быть сравнение с кол-вом атомов во вселенной). Короче, вся безопасность вашего приватного ключа основывается на том, насколько сложно получить его из публичного.

Обратно никак
Обратно никак

Больше технических подробностей про сложность и входные параметры secp256k1 любителям технических подробностей (стр. 4 и cтр. 9).

Вот пример того как сгенерировать пару ключей для Bitcoin используя крейт secp256k1. OsRng тут - это структура-генератор "Достаточно рандомных чисел" из крейта rand.

Окей, у нас есть пара ключей, So what? Чтобы мы могли принимать Bitcoin от кого-то, нам нужно конвертировать публичный ключ в Адрес.

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

Чтобы получить адрес из публичного ключа нам нужно прогнать его через double-hash функцию (SHA256 + RIPEMP160, иногда еще называют HASH160) и потом кодировать результат используя Base58Check.

Компутируем HASH160, должен быть 20 байтов в длину. В ширину не важно, вроде как

Интересный момент, Base58Check используется не случайно. Эта кодировка предоставляет дополнительные проверки на опечатки, а именно Байт версии и Чексумму.

Байт версии (Version byte) - индикатор сети (0x00 в mainnet например, список всех возможных префиксов)

Чексумма (Checksum) - первые 4 байта результата конкатенации байта версии и SHA256(SHA256(HASH160))

Картинки я сам рисовал
Картинки я сам рисовал

Инсертим байт версии в начало вектора с HASH160. Два раза через SHA256 и достаем первые 4 байта, достаточно просто.

Следущий шаг - конкатенировать HASH160 с чексуммой и прогнать это все через base58 используя крейт bs58.

Теперь у нас есть случайно сгенерированный приватный ключ, публичный ключ полученный из него и Bitcoin адрес, на который мы уже можем принимать бинкоины чинаа. Последний шаг - конвертировать приватный ключ в WIF (Wallet Import Format). Для этого нужно просто прогнать его через Base58Check с байтом версии 0x80.

Ура! Мы создали Bitcoin кошелек с WIF приватным ключом и Base58 адресом.


В оригинальной статье я дополнительно рассказываю про генерацию кошельков под Ethereum, Litecoin, Solana, Tron, Aptos и Sui и немного про Ethereum Keystore для безопасного хранения приватных ключей. Но тут я в заголовке пообещал FFI, поэтому придется про FFI.

FFI (Foreign Function Interface)

Дальнейшей целью было портирование данной библиотеки в Golang (grpc не предлагать). Поэтому я решил просто сбилдить динамическую библиотеку (.dylib) и использовать ее через CGO.

Для этого надо прописать в Cargo.toml, что мы билдим библиотеку как cdylib

Напишем небольшой библиотечный крейт с функцией generate_wallet, которая принимает enum Network с доступными сетями. Атрибутный макрос #[no_mangle] говорит компилятору не изменять имя функции во время компиляции (это называется Name mangling). Из функции мы возвращаем сырой указатель на нуль-терминированную строку. Атрибутный Макрос #[repr(C)] значит, что enum будет расположен в памяти как в C.

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

Теперь просто импортируем нашу библиотеку вместе с заголовочным файлом через преамбулу CGO. С.GoString мы используем, чтобы привести указатель char* к string. Go итерируется по памяти пока не встретит нуль терминатор \0.

  • -L - По умолчанию .dylib файл сохраняется в target/debug/

  • -l - По умолчанию соответствует имени из Cargo.toml

Ну вот и все, теперь билдим либу cargo build. И возможно если Go изначально не был настроен (как у меня) надо будет добавить несколько env переменных указывающих на архитектуру (список всех возможных архитектур и осей) CGO_ENABLED="1" GOOS="darwin" GOARCH="arm64" go build


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

Мой тг:

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


  1. lea
    17.07.2023 13:47
    +2

    Всеми признанный король криптовалют использует хеш-функцию secp256k1

    Это не хеш-функция...


    1. mpa4b
      17.07.2023 13:47

      Там и в оригинале написано secp256k1 elliptic curves hash algorithm...


      1. MatveySss Автор
        17.07.2023 13:47
        +2

        Спасибо за комментарий, я и правда перепутал алгоритм цифровой подписи с хеш-функцией в статье про криптографию :( Когда переводил, забыл исправить этот момент


  1. nikolz
    17.07.2023 13:47
    +3

    Биткойн использует алгоритм цифровой подписи с эллиптической кривой (ECDSA), основанный на шифровании с эллиптической кривой. Конкретная эллиптическая кривая называется secp256k1, т.е. кривая y² = x³ + 7

    https://russianblogs.com/article/22411554642/


    1. qalisander
      17.07.2023 13:47

      За что вам минус поставили в комментарий??


      1. nikolz
        17.07.2023 13:47

        очевидно, что человек не хочет признать свою ошибку, а написать открыто, что не так - кишка тонка, вот и стучит.


        1. MatveySss Автор
          17.07.2023 13:47

          Я не ставил вам минус, веткой выше я уже признал свою ошибку, спасибо.


          1. nikolz
            17.07.2023 13:47
            -5

            Я не про Вас, а про того, кто поставил. В интернете много г..на плавает.


  1. vilgeforce
    17.07.2023 13:47

    Насколько качественный генератор в rand::rngs::OsRng, к слову?


    1. MatveySss Автор
      17.07.2023 13:47
      +1

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


      1. vilgeforce
        17.07.2023 13:47

        Вроде хорошие генераторы


  1. Fenex
    17.07.2023 13:47

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


    1. MatveySss Автор
      17.07.2023 13:47

      На самом деле я не до конца понял этот момент. По сути C.GoString - обычная структура { ptr*, len } и должна менеджиться GC как и остальные строки в Go. Тем более, если посмотреть в документацию там указано, что например C.CString надо высвобождать вручную, но про C.GoString такого нет (https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C). Но наверное лучше делать так:

      // ..
      
      c_ptr := C.foo()
      go_str := C.GoString(c_ptr)
      defer C.free(unsafe.Pointer(c_ptr))
      
      // ..

      UPD: Так же в документации написано, что C.GoString делает копию оригинальной строки. Так что судя по всему высвобождать оригинальную - обязательно.

      A few special functions convert between Go and C types by making copies of the data. In pseudo-Go definitions.