深入理解solana-keygen

grind

Grind for vanity keypairs, grind 指的是生成固定前缀的地址

用法 solana-keygen grind --starts-with <prefix>

我想到应用场景是 0 转账攻击 https://x.com/33357xyz/status/1809847602583925114

由于钱包软件转账记录只会显示对方地址的前6和后6位,黑客利用用户复制上次转账地址的习惯,伪造一个首尾跟用户频繁转账的地址相同地址给用户转账,骗取用户复制到错误的地址

两种 keypair 生成方法

核心逻辑在 solana 源码 keygen/src/keygen.rsclap-utils/src/keypair.rs

pub fn keypair_from_seed_phrase(
    keypair_name: &str,
    skip_validation: bool,
    confirm_pubkey: bool,
    derivation_path: Option<DerivationPath>,
    legacy: bool,
) -> Result<Keypair, Box<dyn error::Error>> {
    let seed_phrase = prompt_password(format!("[{keypair_name}] seed phrase: "))?;
    let seed_phrase = seed_phrase.trim();
    let passphrase_prompt = format!(
        "[{keypair_name}] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue: ",
    );

    let keypair = if skip_validation {
        let passphrase = prompt_passphrase(&passphrase_prompt)?;
        if legacy {
            keypair_from_seed_phrase_and_passphrase(seed_phrase, &passphrase)?
        } else {
            let seed = generate_seed_from_seed_phrase_and_passphrase(seed_phrase, &passphrase);
            keypair_from_seed_and_derivation_path(&seed, derivation_path)?
        }
    }
    // ...
}

可以看到 solana 有两种 keypair 的生成方式

solflare

助记词: cereal uniform print regret once ocean oxygen okay exclude hollow force obtain

地址: EpCkVjyF571saE6wdckx1LSWkYsDQPS3qNNdbkGiCfNt

(为了教学演示泄露了助记词,浏览器删掉钱包插件应该就能清空掉这个已泄露的助记词了吧)

solana-keygen recover --outfile temp

Recovered pubkey "BQPEK47UNACymPru3mdDnPbRbphRzoL4JfbJotVVgfSi". Continue? (y/n):

可见恢复出的地址不对 solana-keygen newsolana-keygen recover 默认都用 legacy 方法进行 助记词->私钥 转换

solana cli 导入助记词 需要加上参数 'prompt://?key=0/0' 指明用 derive_path 方法处理助记词,这样就得到跟 solflare 钱包一样的地址了

w@w:~/sol_keys$ solana-keygen recover 'prompt://?key=0/0' --outfile temp
[recover] seed phrase:
[recover] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue:
Recovered pubkey `"EpCkVjyF571saE6wdckx1LSWkYsDQPS3qNNdbkGiCfNt"`. Continue? (y/n):

用 typescript 写个单元测试进行验证

import * as bip39 from 'bip39';
// const bip39 = require('bip39');
import { Keypair } from '@solana/web3.js';
import { derivePath } from 'ed25519-hd-key';
test('sol_mnemonic', () => {
    const mnemonic = "cereal uniform print regret once ocean oxygen okay exclude hollow force obtain";
    // https://solana.com/developers/cookbook/wallets/restore-from-mnemonic
    const derivationPath = `m/44'/501'/0'/0'`; // SOL奇葩的是没有UTXO找零地址change
    const seed = bip39.mnemonicToSeedSync(mnemonic);
    const derivedSeed = derivePath(derivationPath, seed.toString('hex')).key;
    const keypair = Keypair.fromSeed(derivedSeed);
    expect(keypair.publicKey.toBase58()).toBe("EpCkVjyF571saE6wdckx1LSWkYsDQPS3qNNdbkGiCfNt");
});

幻影钱包

跟 solflare 一样用的 助记词+derive_path 生成私钥,过程就不演示了

看来主流钱包都不用这个 legacy 方式的地址生成,开发者不需要掌握 legacy keypair 生成规则

Rust实现助记词解析

let mnemonic = bip39::Mnemonic::parse_in_normalized(bip39::Language::English, mnemonic).unwrap();
let seed = mnemonic.to_seed("");

let coin_type_sol = 501;
let account_idx = 0;
let addr_idx = 0;
// SOL比较奇葩derive_path没有找零地址 https://solana.com/developers/cookbook/wallets/restore-from-mnemonic
// let change = 0; // UTXO找零地址
let derivation_path = format!("m/44'/{coin_type_sol}'/{account_idx}'/{addr_idx}'");
let derivation_path: ed25519_dalek_bip32::DerivationPath =
    derivation_path.parse().unwrap();
let private_key = ed25519_dalek_bip32::ExtendedSigningKey::from_seed(&seed)
    .unwrap()
    .derive(&derivation_path)
    .unwrap();

let indexes = vec![
    44 | (1 << 31),
    coin_type_sol | (1 << 31),
    account_idx | (1 << 31),
    // change,
    addr_idx,
];
// let indexes2 = [
//     ChildIndex::hardened(44).unwrap(),
//     ChildIndex::hardened(coin_type_sol).unwrap(),
//     ChildIndex::hardened(0).unwrap(),
//     ChildIndex::normal(0).unwrap(),
// ];
// let private_key2 = ed25519_dalek_bip32::ExtendedSigningKey::from_seed(&seed)
//     .unwrap()
//     .derive(&indexes2)
//     .unwrap();
// assert_eq!(private_key.signing_key, private_key2.signing_key);
let private_key3 = slip10_ed25519::derive_ed25519_private_key(&seed, &indexes);
assert_eq!(private_key.signing_key.to_bytes(), private_key3);

slip10_ed25519和ed25519_dalek_bip32

两个库 slip10_ed25519 和 ed25519_dalek_bip32 都能将 seed 处理成 私钥

slip10_ed25519 是 sui 源码在用,由于 Rust 孤儿规则限制,sui 用 new type 设计模式包了下 ed25519 去实现 sui 源码定义的 fastcrypto 或者 signer 相关 trait

solana-keygen 导入导出助记词

legacy 版本(不常用了)

w@w:~/sol_keys$ solana-keygen new --no-bip39-passphrase --outfile no_derive_path
Generating a new keypair
Wrote new keypair to no_derive_path
=================================================================================
pubkey: 8sP2do52kmKYGiWiDRs2hRQRr8KJviS36u3nyzxs2Azs
=================================================================================
Save this seed phrase to recover your new keypair:
issue name secret genuine emerge appear please midnight have clown peanut catalog
=================================================================================
w@w:~/sol_keys$ solana-keygen recover --outfile no_derive_path.recover
[recover] seed phrase:
[recover] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue:
Recovered pubkey `"8sP2do52kmKYGiWiDRs2hRQRr8KJviS36u3nyzxs2Azs"`. Continue? (y/n):

bip44 版本

w@w:~/sol_keys$ solana-keygen new --no-bip39-passphrase --derivation-path --outfile has_derive_path
Generating a new keypair
Wrote new keypair to has_derive_path
=======================================================================
pubkey: FzeVW4z1pwdJbAqDxZdgQUA3BqMa7JFXFzJ9HVWmRgWH
=======================================================================
Save this seed phrase to recover your new keypair:
chef any album file avocado struggle during hair bird detail leave drip
=======================================================================

w@w:~/sol_keys$ solana-keygen pubkey has_derive_path
FzeVW4z1pwdJbAqDxZdgQUA3BqMa7JFXFzJ9HVWmRgWH
w@w:~/sol_keys$ solana-keygen verify FzeVW4z1pwdJbAqDxZdgQUA3BqMa7JFXFzJ9HVWmRgWH has_derive_path
Verification for public key: FzeVW4z1pwdJbAqDxZdgQUA3BqMa7JFXFzJ9HVWmRgWH: Success

w@w:~/sol_keys$ solana-keygen recover 'prompt://?key=0/0' --outfile has_derive_path.recover
[recover] seed phrase:
[recover] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue:
Recovered pubkey `"FzeVW4z1pwdJbAqDxZdgQUA3BqMa7JFXFzJ9HVWmRgWH"`. Continue? (y/n):

keygen new 生成出来的密钥,通过 solana config set --keypair /path/to/id.json 即可切换账户

sol/sui cli 相似命令

sol sui
导入账户助记词 solana-keygen recover 'prompt://?key=0/0' keytool import '$mnemonic' ed25519
获取账户地址 address client active-address
获取账户余额 balance client balance
切换账户 config set --keypair $path client switch --address $alias
获取rpc地址 config get client envs
全部评论(0)