Раздел 30 · Web3 на Rust

Кошелёк: ключи, адреса и мнемоники

senior~30 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Кошелёк: ключи, адреса и мнемоники

Мы создавали ключи прямо в коде из массива байтов. Для настоящего кошелька так нельзя: 32 случайных байта невозможно запомнить и легко потерять. Сделаем кошелёк по-человечески: мнемоника из обычных слов, из которой детерминированно восстанавливается целое дерево ключей.

Идея

Сначала развеем главное заблуждение: кошелёк не хранит монеты. Монеты живут в состоянии цепи (балансы по адресам). Кошелёк хранит ключи, которыми ты доказываешь право этими монетами распоряжаться. Потеряешь ключ, монеты останутся в цепи, но станут навсегда недоступны: никто, включая тебя, не подпишет с них перевод.

Адрес у нас это отпечаток открытого ключа: первые 20 байт его SHA-256. Так адрес короче ключа и не раскрывает сам ключ до первой траты. Ethereum делает похоже, но не так же: там адрес это последние 20 байт Keccak-256 открытого ключа. Идея общая, детали разные: другой хеш и другой срез байтов.

impl Address {
    /// Выводит адрес из открытого ключа: первые 20 байт хеша ключа.
    pub fn from_public_key(pk: &PublicKey) -> Address {
        let digest = Hash::of(&pk.0);
        let mut out = [0u8; 20];
        out.copy_from_slice(&digest.0[..20]);
        Address(out)
    }
}

Ключи и подпись

Подпись у нас Ed25519 (кривая ed25519 из урока про эллиптические кривые): детерминированная, быстрая, без ловушки повтора nonce, на которой горел ECDSA (урок про подписи). Bitcoin и Ethereum исторически берут secp256k1, его разберём в уроке про ключи и транзакции Bitcoin, а для своего узла Ed25519 строже и проще.

impl Keypair {
    /// Строит пару из 32-байтового зерна. Зерно может прийти от ОС или из
    /// HD-кошелька: одно зерно всегда даёт один и тот же ключ.
    pub fn from_seed(seed: &[u8; 32]) -> Keypair {
        Keypair { signing: SigningKey::from_bytes(seed) }
    }

    pub fn public_key(&self) -> PublicKey {
        PublicKey(self.signing.verifying_key().to_bytes())
    }

    pub fn address(&self) -> Address {
        Address::from_public_key(&self.public_key())
    }

    pub fn sign(&self, message: &[u8]) -> Signature {
        Signature(self.signing.sign(message).to_bytes())
    }
}

Закрытый ключ (signing) никогда не покидает кошелёк, наружу идут только открытый ключ и подписи. Это и есть вся асимметрия: одним ключом подписываю, другим любой проверяет.

Мнемоника BIP-39

Записать на бумагу 32 случайных байта в hex это рецепт катастрофы: одна описка, и кошелёк потерян. BIP-39 решает это: те же байты кодируются в список обычных слов, который человек спокойно перепишет. Последнее слово несёт контрольную сумму, поэтому описку чаще всего видно сразу.

Здесь в дело вступают три зависимости из Cargo.toml, который мы завели в первом уроке проекта: bip39 = { version = "2", features = ["rand"] } даёт мнемонику и словарь (фича rand нужна для генерации случайной фразы), а hmac = "0.12" и sha2 = "0.10" понадобятся ниже для вывода дочерних ключей.

use bip39::Mnemonic;

pub struct Wallet {
    mnemonic: Mnemonic,
    seed: [u8; 64],
}

impl Wallet {
    /// Заводит новый кошелёк со случайной мнемоникой из 12 слов.
    pub fn generate() -> Wallet {
        let mnemonic = Mnemonic::generate(12).expect("12 слов это валидная длина");
        Wallet::from_mnemonic(mnemonic)
    }

    /// Восстанавливает кошелёк из записанной фразы.
    pub fn from_phrase(phrase: &str) -> Result<Wallet, bip39::Error> {
        Ok(Wallet::from_mnemonic(Mnemonic::parse(phrase)?))
    }

    fn from_mnemonic(mnemonic: Mnemonic) -> Wallet {
        let seed = mnemonic.to_seed(""); // без дополнительного пароля
        Wallet { mnemonic, seed }
    }

    /// Мнемоника для записи на бумагу.
    pub fn phrase(&self) -> String {
        self.mnemonic.to_string()
    }
}

Из мнемоники получается 64-байтовое зерно (seed): детерминированно, одна фраза всегда даёт одно зерно.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

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