Пост

BIP44. HD-кошельки

BIP44. HD-кошельки

BIP 32 - это спецификация для создания иерархических детерминированных (Hierarchical Deterministic HD) кошельков. Детерминированные кошельки были разработаны для того, чтобы упростить получение множества ключей из одного исходного сидаНаиболее продвинутой формой детерминированного кошелька является иерархический детерминированный (HD) кошелек. HD кошельки содержат ключи, полученные в древовидной структуре, так что родительский ключ может вывести последовательность дочерних ключей, каждый из которых может вывести последовательность внучатых ключей и так далее.

Дерево ключей, сгенерированных из одного сида:

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

Второе преимущество HD-кошельков заключается в том, что пользователи могут создавать последовательность публичных ключей, не имея доступа к соответствующим приватным ключам. Это позволяет использовать HD-кошельки на незащищенном сервере или в качестве read-only кошелька, когда нет приватных ключей и никто не может с него потратить средства.

Создание HD-кошелька из сида

Большинство HD-кошельков следуют стандарту BIP32, который стал фактическим отраслевым стандартом для детерминированной генерации ключей.

Он используется в Bitcoin, Ethereum и других блокчейнах для создания практически бесконечного списка аккаунтов. Причем все эти аккаунты можно восстановить всего лишь с одной мнемонической фразой или сидом. Также рассмотрим еще один BIP - BIP44, который описывает то, как будут генерироваться дочерние ключи.

Чтобы не запутаться со всех этих BIP-ах подитожим:

  • BIP39 — “Mnemonic code for generating deterministic keys”
  • BIP32 — “Hierarchical Deterministic Wallets”
  • BIP44 — “Multi-Account Hierarchy for Deterministic Wallets”

Мы не будем обсуждать все детали BIP32, только компоненты, необходимые для понимания того, как он используется в кошельках. Главным важным аспектом являются древовидные иерархические отношения, которые могут иметь производные ключи. Также важно понимать идеи расширенных (extended) ключей и усиленных (hardened) ключей, которые объясняются дальше.

Для работы с HD-кошельками будем использовать библиотеку protonlabs/bitcoin:

1
composer require protonlabs/bitcoin

Сгенерируем мастер ключ из сида:

1
2
3
4
5
6
7
8
use BitWasp\Bitcoin\Key\Factory\HierarchicalKeyFactory;  
use BitWasp\Buffertools\Buffer;

$seed = 'd0b894aaed90f6a4c3956ca342c4b48208367fcaa72c4df79fac24a1e9c1425808002acd871c69e531f91ec9c6b69c8d20f765bb36bc30e58eb0dd34d5bf5d9f';  
  
$hdFactory = new HierarchicalKeyFactory();  
$masterKey = $hdFactory->fromEntropy(Buffer::hex($seed));
echo 'Master private key: ' . $masterKey->toExtendedPrivateKey() . PHP_EOL;

В результате выполнения получим:

1
Master private key: xprv9s21ZrQH143K33ERvZwrLdDAMpAMURoh1dNpeSBZ9WvXASr3Ri9eHR2Ryog9kvmgBqL3KJnBhhHyGSkQDNYDn2jDBPfy9nbJWXHhnei3KHD

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

Расширенные публичный и приватный ключи

В терминологии BIP32 ключи могут быть «расширенными» (extended). Эти расширенные «родительские» ключи могут использоваться для получения «дочерних» ключей, создавая таким образом иерархию ключей и адресов, описанную ранее. Родительский ключ не обязательно должен находиться наверху дерева. Его можно выбрать из любого места в иерархии. Расширение ключа включает в себя взятие самого ключа и добавление к нему специального чейн кода. Чейн код представляет собой 256-битную двоичную строку, которая добавляется к каждому ключу для создания дочерних ключей.

Если ключ является приватным ключом, он становится расширенным приватным ключом, отличающимся префиксом xprv:

1
xprv9s21ZrQH143K33ERvZwrLdDAMpAMURoh1dNpeSBZ9WvXASr3Ri9eHR2Ryog9kvmgBqL3KJnBhhHyGSkQDNYDn2jDBPfy9nbJWXHhnei3KHD

Расширенный публичный ключ отличается префиксом xpub:

1
xpub661MyMwAqRbcFXJu2bUrhm9tuqzqstXYNrJRSpbAhrTW3FBByFTtqDLuq7LCJtaqQcy1swGXaEhi8saaeVTuxxpzMg7fVz5uShWzmumuLDX

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

Таким образом, расширенный публичный ключ может использоваться для получения всех публичных ключей (и только публичных ключей) в этой ветви структуры HD-кошелька. Такой подход можно использовать для создания очень безопасных public-key only деплоев, когда сервер или приложение имеет копию расширенного публичного ключа, но не имеет никаких приватных ключей. Такой тип деплоя может создавать бесконечное количество публичных ключей и адресов Ethereum, но не может тратить средства, отправленные на эти адреса. Между тем, на другом, более безопасном сервере расширенный приватный ключ может сгенерировать все соответствующие приватные ключи для подписи транзакций и расходования средств.

Одним из распространенных применений этого метода является установка расширенного публичного ключа на веб-сервере, который обслуживает приложение электронной коммерции. Веб-сервер может использовать функцию выведения публичного ключа для создания нового адреса Ethereum для каждой транзакции (например, для корзины покупок клиента) и не будет иметь никаких приватных ключей, которые были бы уязвимы для кражи. Без HD-кошельков единственный способ сделать это — сгенерировать тысячи адресов Ethereum на отдельном защищенном сервере, а затем предварительно загрузить их на сервер электронной коммерции. Такой подход громоздок и требует постоянного обслуживания, чтобы гарантировать, что на сервере не закончатся ключи, поэтому предпочтение отдается использованию расширенных публичных ключей из HD-кошельков.

Например можно установить расширенный публичный ключ на веб-сервере для e-commerce приложения. Веб-сервер может использовать этот расширенный ключ для создания нового Ethereum адреса под каждую входящую транзакцию. При этом на сервере не будет приватного ключа, который можно было бы украсть. Без HD-кошельков пришлось бы генерировать тысячи адресов Ethereum на отдельном защищенном сервере, а затем предварительно загрузить их на сервер с приложением.

Другое распространенное применение HD-кошельков — холодное хранилище или аппаратные кошельки. В таком случае расширенный приватный ключ может храниться в аппаратном кошельке, в то время как расширенный публичный ключ может храниться онлайн. Пользователь может создавать адреса для получения средств, в то время как приватные ключи хранятся оффлайн. Чтобы потратить средства (подписать транзакцию) пользователь может использовать расширенный приватный ключ на аппаратном кошельке.

Усиленная (hardened) деривация дочернего ключа

Очень удобно иметь возможность вывести ветку публичных ключей из расширенного публичного ключа (xpub), очень полезна, но она сопряжена с потенциальным риском. Доступ к xpub не дает доступа к дочерним приватным ключам. Однако, поскольку xpub содержит чейн код (используемый для выведения дочерних публичных ключей из родительского публичного ключа), если дочерний приватный ключ известен, его можно использовать вместе с чейн кодом для получения всех других дочерних приватных ключей. Один украденный дочерний приватный ключ вместе с родительским чейн кодом раскрывает все приватные ключи всех дочерних ключей. Хуже того, дочерний приватный ключ вместе с родительским чейн кодом можно использовать для выведения родительского приватного ключа.

Чтобы убрать этот риск, HD-кошельки используют альтернативную функцию деривации, называемую “усиленной деривацией”, которая «разрывает» связь между родительским публичным ключом и дочерним чейн кодом. Укрепленная функция деривации использует родительский приватный ключ для получения дочернего чейн кода вместо родительского публичного ключа. Это создает «файервол» между родительским и дочерним ключом, с таким чейн кодом, который не может быть использован для компрометации родительского или родственного приватного ключа.

Простыми словами, если хотим использовать удобство xpub для вывода веток публичных ключей, не подвергая себя риску утечки чейн кода, то лучше вывести его из усиленного родителя, а не из обычного. Best practice тут в том, чтобы всегда делать ключи первого уровня усиленными (усиленная деривация от мастер ключа). Это позволит избежать компрометации мастер ключей.

Индексы для нормальной и усиленной деривации

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

Чтобы легко различать обычные и защищенные ключи этот индексный номер разделен на два диапазона. Индексные номера от 0 до 2³¹–1 (от 0x0 до 0x7FFFFFFF) используются только для обычного вывода. Индексные номера от 2³¹ до 2³²–1 (от 0x80000000 до 0xFFFFFFFF) используются только для защищенного вывода. Таким образом, если индексное число меньше 2³¹, то дочерний ключ обычный, а если индексное число равно или больше 2³¹, то дочерний ключ защищенный.

Для удобства чтения индексные номера для укрепленных ключей отображаются, начиная с нуля, но с символом #. Таким образом первый нормальный дочерний ключ, отображается как 0, тогда как первый укрепленный дочерний ключ (индекс 0x80000000) отображается как 0'. Затем, последовательно, второй укрепленный ключ будет иметь индекс 0x80000001 и будет отображаться как 1', и так далее. Когда вы видите индекс HD-кошелька i', это означает 2³¹ + i.

Идентификатор ключа HD-кошелька (path)

Ключи в HD-кошельках идентифицируются по путю деривации (derivation path). В нём каждый уровень дерева отделяется слэшем (/). Приватные ключи, полученные из закрытого мастер ключа начинаются с символа m. Публичные ключи, полученные из публичного мастер ключа, начинаются с M. Таким образом, первый дочерний закрытый ключ от закрытого мастер ключа будет иметь путь m/0. Первый публичный ключ - M/0. Второй дочерний ключ первого дочернего ключа будет m/0/1 и т.д.

“Происхождение” ключа читается в пути справа налево, пока не будет достигнут мастер ключ. Например, путь m/x/y/z описывает ключ, который является z-м дочерним к ключу m/x/y, который является y-м дочерним к ключу m/x, который является x-м к ключу m.

Примеры:

HD pathОписание
m/0Первый (0) дочерний приватный ключ от приватного мастер ключа (m)
m/0/0Первый дочерний приватный ключ от первого дочернего приватного клю
m/0'/0Первый обычный дочерний ключ от первого усиленного дочернего ключа (m/0)
m/1/0Первый дочерний приватный ключ от второго дочернего ключа (m)
M/23/17/0/0Первый дочерний ключ от первого дочернего ключа от 18-го дочернего
ключа от 24 дочернего ключа.

Навигация по древовидной структуре HD-кошелька

Структура дерева HD-кошелька очень гибкая. При этом она допускает неограниченную сложность: каждый родительский расширенный ключ может иметь 4 миллиарда потомков: 2 обычных и 2 усиленных. Каждый из этих потомков может иметь еще 4 миллиарда потомков и так далее. Дерево может быть бесконечно глубоким. При этом может стать довольно сложно ориентироваться в очень больших деревьях.

Два BIP-а предлагают стандарт для структуры деревьев HD-кошельков как способ управления этой потенциальной сложностью. BIP43 предлагает использовать первый укрепленный дочерний индекс в качестве специального идентификатора, который обозначает “назначение” древовидной структуры. Исходя из BIP43, кошелек должен использовать только одну ветвь первого уровня, при этом номер индекса определяет цель кошелька путем идентификации структура и неймспейса остальной части дерева. Например, если HD-кошелек использует только ветвь m/i'/, то он предназначен для определенной цели, которая идентифицируется индексом i.

BIP44 расширяет эту спецификацию и предлагает мультивалютную струткуру с несолькими аккаунтами. При этом часть пути “цель” (purpose) устанавливается в значение 44. Все HD-кошельки, которые следуют структуре BIP44, определяются тем, что они используют одну ветвь дерева m/44'/*.

BIP44 определяет структуру, состоящую из пяти заранее предопределенных уровней дерева:

1
m / purpose’ / coin_type’ / account’ / change / address_index

Первый уровень purpose&#x27 всегда установлен в 44'.

Второй уровень coin_type' определяет тип криптовалюты. Мы получаем мультивалютные HD-кошельки мультивалютными, где каждая валюта имеет своё поддерево на втором уровне.

Список всех коинов/сетей из стандарта можно найти здесь. Например, Ethereum Будет иметь поддерево m/44'/60', а Bitcoin — m/44'/0'.

Третий уровень account&#x27 позволяет разделять пользователям свои кошельки на отдельные логические субсчета. Например, HD-кошелек может содержать два “счета” Ethereum: m/44'/60'/0' и m/44'/60'/1'. Каждый счет является корнем своего собственного поддерева.

Изначально BIP44 был разработан для Bitcoin, и он содержит маленькую “фичу”, которая неактульна для Ethereum. По спецификации на четвертом уровне пути — “сдача” (change), HD-кошелек здесь имеет два поддерева: одно для адресов получения, другое — для адресов сдачи. В Ethereum используется только путь для “получения”, т.к. сдача актуальная только для UTXO-блокчейнов. Важно обратить внимание, что в то время как предыдущие уровни использовали защищенную (hardened) деривацию, этот уровень использует обычную деривацию. Это позволяет уровню аккаунтов экспортировать расширенные открытые ключи для использования в незащищенном окружении. Используемые адреса выводятся HD-кошельком как дочерние элементы четвертого уровня с использованием пятого уровня дерева address_index. Например, третий адрес получения для платежей Ethereum на основном аккаунте будет M/44'/60'/0'/0/2.

Примеры структуры HD-кошелька BIP-44:

HD путьОписание ключа
M/44'/60'/0'/0/2Третий открытый ключ “получения” для основного аккаунта Ethereum.
M/44'/0'/3'/1/1415-ый публичный ключ для сдачи от 4-го Bitcoin аккаунта.
m/44'/2'/0'/0/1Второй приватный ключ Litcoin аккаунта для подписи транзакций.

BIP44 в PHP

Имя мастер ключ из него можно легко вывести ключи нужного пути. Например, если нам нужен первый приватный ключ Ethereum (44'/60'/0'/0/0):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use BitWasp\Bitcoin\Key\Factory\HierarchicalKeyFactory;  
use BitWasp\Buffertools\Buffer;

$seed = 'd0b894aaed90f6a4c3956ca342c4b48208367fcaa72c4df79fac24a1e9c1425808002acd871c69e531f91ec9c6b69c8d20f765bb36bc30e58eb0dd34d5bf5d9f';  
  
$hdFactory = new HierarchicalKeyFactory();  
$masterKey = $hdFactory->fromEntropy(Buffer::hex($seed));

$firstChild = $masterKey->derivePath("44'/60'/0'/0/0");
echo sprintf(  
    "Private key: 0x%s\nPublic key: 0x%s\n",  
    $firstChild->getPublicKey()->getHex(),  
    $firstChild->getPrivateKey()->getHex(),  
) ;

Выведет:

1
2
Private key: 0x03fc032b4b74d1da9bf2a4e520718a4aa334b5ddb7dadda4ee210b7410c45752da
Public key: 0x03745df0d894294653b79aa88f4a7956cd557f9cb3179e93d9f6b30362f3dcf0

Чтобы получить адрес для этого ключа используем библиотеку из предыдущего поста:

1
composer req kornrunner/php-ethereum-address

Для получения адреса нам понадобится приватный ключ:

1
2
3
4
5
6
7
8
9
10
use kornrunner\Ethereum\Address;

// ...

$firstChild = $masterKey->derivePath("44'/60'/0'/0/0");

// ... 
$address = new Address($firstChild->getPrivateKey()->getHex());  
echo sprintf('Address: 0x%s\n', $address->get()) . PHP_EOL;
// Address: 0x2c1d7ad408ed47fea866f1c861da8fdfcd15a9d1

Полученные ключи и адрес можно проверить в онлайн генераторе.

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

1
2
$changeKey = $masterKey->derivePath("44'/60'/0'/0");  
$firstChild = $changeKey->deriveChild(0);

Популярные теги