Libra源碼分析:分層確定性錢包的實現

博客鏈接:https://hello2mao.github.io/2019/12/29/libra-wallet/

1. 概述

libra-wallet是libra的錢包模塊,模塊位置:client/libra_wallet.

libra-wallet是一個分層確定性錢包。什麼是分層確定性錢包可以參考博客:數字貨幣確定性錢包

其助記詞參考的是比特幣的BIP39,而祕鑰派生以及分層錢包的設計則是參考的比特幣的BIP32。與比特幣的主要區別是:比特幣使用基於橢圓曲線加密的橢圓曲線數字簽名算法(ECDSA),特定的橢圓曲線稱爲secp256k1;而Libra使用的是基於Curve25519橢圓曲線的Ed25519愛德華曲線(Edwards Curve)數字簽名。所以Libra對CKD(Child key derivation)做了調整,實現了自己的KDF(Key Derivation Function)。

2. 錢包架構

目前libra-wallet作爲client一部分,提供賬戶創建、導入、導出的功能,如下圖所示:

libra-wallet一共有三個關鍵模塊:

  • WalletLibrary:libra錢包實例,對外提供錢包的所有功能,導入、導出、生成新賬戶等、
  • KeyFactory:實現祕鑰派生,基於HKDF。
  • Mnemonic:生成助記詞。

3. 錢包初始化

在client的ClientProxy初始化時,會進行錢包的初始化,如下:

impl ClientProxy {
    /// Construct a new TestClient.
    pub fn new(
        host: &str,
        ac_port: u16,
        faucet_account_file: &str,
        sync_on_wallet_recovery: bool,
        faucet_server: Option<String>,
        mnemonic_file: Option<String>,
        waypoint: Option<Waypoint>,
    ) -> Result<Self> {
        let mut client = GRPCClient::new(host, ac_port, waypoint)?;

        .....  

        Ok(ClientProxy {
            client,
            accounts,
            address_to_ref_id,
            faucet_server,
            faucet_account,
            wallet: Self::get_libra_wallet(mnemonic_file)?,   // 初始化錢包
            sync_on_wallet_recovery,
            temp_files: vec![],
        })
    }

    // 如果沒有指定mnemonic_file,初始化錢包
    // 如果指定了mnemonic_file,則從中恢復錢包
    fn get_libra_wallet(mnemonic_file: Option<String>) -> Result<WalletLibrary> {
        let wallet_recovery_file_path = if let Some(input_mnemonic_word) = mnemonic_file {
            Path::new(&input_mnemonic_word).to_path_buf()
        } else {
            let mut file_path = std::env::current_dir()?;
            file_path.push(CLIENT_WALLET_MNEMONIC_FILE);
            file_path
        };

        let wallet = if let Ok(recovered_wallet) = io_utils::recover(&wallet_recovery_file_path) {
            recovered_wallet
        } else {
            let new_wallet = WalletLibrary::new(); // WalletLibrary初始化
            new_wallet.write_recovery(&wallet_recovery_file_path)?;
            new_wallet
        };
        Ok(wallet)
    }    
}    

下面看下錢包的WalletLibrary的初始化:

impl WalletLibrary {
    /// Constructor that generates a Mnemonic from OS randomness and subsequently instantiates an
    /// empty WalletLibrary from that Mnemonic
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        let mut rng = EntropyRng::new();
        let data: [u8; 32] = rng.gen(); // 256位隨機熵
        let mnemonic = Mnemonic::mnemonic(&data).unwrap(); // 生成24個助記詞
        Self::new_from_mnemonic(mnemonic)
    }

    /// Constructor that instantiates a new WalletLibrary from Mnemonic
    pub fn new_from_mnemonic(mnemonic: Mnemonic) -> Self {
        let seed = Seed::new(&mnemonic, "LIBRA"); // 生成種子
        WalletLibrary {
            mnemonic,
            key_factory: KeyFactory::new(&seed).unwrap(), // 初始化KeyFactory
            addr_map: HashMap::new(),
            key_leaf: ChildNumber(0),
        }
    }
}   

主要做了三件事:

  • 先生成256位隨機熵,然後通過Mnemonic生成24個助記詞
  • 通過助記詞生成種子
  • 通過種子初始化KeyFactory

下面看下KeyFactory的初始化:

impl KeyFactory {
    const MNEMONIC_SALT_PREFIX: &'static [u8] = b"LIBRA WALLET: mnemonic salt prefix$";
    const MASTER_KEY_SALT: &'static [u8] = b"LIBRA WALLET: master key salt$";
    const INFO_PREFIX: &'static [u8] = b"LIBRA WALLET: derived key$";
    /// Instantiate a new KeyFactor from a Seed, where the [u8; 64] raw bytes of the Seed are used
    /// to derive both the Master
    pub fn new(seed: &Seed) -> Result<Self> {
        let hkdf_extract = Hkdf::<Sha3_256>::extract(Some(KeyFactory::MASTER_KEY_SALT), &seed.0)?;

        Ok(Self {
            master: Master::from(&hkdf_extract[..32]),
        })
    }
}   

在KeyFactory初始化時,通過Hkdf,生成了master private key。

Hkdf主要做的就是對種子做HMAC-SHA3-256,然後取左邊的256位作爲master private key、

整個過程如下所示:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WqPx8LyL-1577624634218)

4. 錢包導入、導出

錢包導入導出的功能比較簡單:

impl WalletLibrary {

    /// Function that writes the wallet Mnemonic to file
    /// NOTE: This is not secure, and in general the Mnemonic would need to be decrypted before it
    /// can be written to file; otherwise the encrypted Mnemonic should be written to file
    pub fn write_recovery(&self, output_file_path: &Path) -> Result<()> {
        io_utils::write_recovery(&self, &output_file_path)?;
        Ok(())
    }

    /// Recover wallet from input_file_path
    pub fn recover(input_file_path: &Path) -> Result<WalletLibrary> {
        let wallet = io_utils::recover(&input_file_path)?;
        Ok(wallet)
    }
}    

導入導出的實現在io_utils中:

/// Recover wallet from the path specified.
pub fn recover<P: AsRef<Path>>(path: &P) -> Result<WalletLibrary> {
    let input = File::open(path)?;
    let mut buffered = BufReader::new(input);

    let mut line = String::new();
    let _ = buffered.read_line(&mut line)?;
    let parts: Vec<&str> = line.split(DELIMITER).collect();
    ensure!(parts.len() == 2, format!("Invalid entry '{}'", line));

    let mnemonic = Mnemonic::from(&parts[0].to_string()[..])?;
    let mut wallet = WalletLibrary::new_from_mnemonic(mnemonic);
    wallet.generate_addresses(parts[1].trim().to_string().parse::<u64>()?)?;

    Ok(wallet)
}

/// Write wallet seed to file.
pub fn write_recovery<P: AsRef<Path>>(wallet: &WalletLibrary, path: &P) -> Result<()> {
    let mut output = File::create(path)?;
    writeln!(
        output,
        "{}{}{}",
        wallet.mnemonic().to_string(),
        DELIMITER,
        wallet.key_leaf()
    )?;

    Ok(())
}

可以看到,導出就是把助記詞和ChildNumber導出到文件,文件中類似:

mule grant merry attitude tape census upgrade lab lava news repair brisk pattern dream side lumber ice sock siege speak joke disease ensure gospel;4

分號後的4就是指有4個child。

而導入,則是從助記詞文件加載。

5. 祕鑰派生新賬戶

通過WalletLibrarynew_address()從master派生新賬戶:

    /// Function that generates a new key and adds it to the addr_map and subsequently returns the
    /// AccountAddress associated to the PrivateKey, along with it's ChildNumber
    pub fn new_address(&mut self) -> Result<(AccountAddress, ChildNumber)> {
        let child = self.key_factory.private_child(self.key_leaf)?; // 派生下一個child
        let address = child.get_address()?;
        let old_key_leaf = self.key_leaf;
        self.key_leaf.increment();
        if self.addr_map.insert(address, old_key_leaf).is_none() {
            Ok((address, old_key_leaf))
        } else {
            Err(WalletError::LibraWalletGeneric(
                "This address is already in your wallet".to_string(),
            ))
        }
    }

祕鑰派生新賬戶實現在KeyFactoryprivate_child()中:

    /// Derive a particular PrivateKey at a certain ChildNumber
    ///
    /// Note that the function below  adheres to [HKDF RFC 5869](https://tools.ietf.org/html/rfc5869).
    pub fn private_child(&self, child: ChildNumber) -> Result<ExtendedPrivKey> {
        // application info in the HKDF context is defined as Libra derived key$child_number.
        let mut le_n = [0u8; 8];
        LittleEndian::write_u64(&mut le_n, child.0);
        let mut info = KeyFactory::INFO_PREFIX.to_vec();
        info.extend_from_slice(&le_n);

        let hkdf_expand = Hkdf::<Sha3_256>::expand(&self.master(), Some(&info), 32)?;
        let sk = Ed25519PrivateKey::try_from(hkdf_expand.as_slice())
            .expect("Unable to convert into private key");

        Ok(ExtendedPrivKey::new(child, sk))
    }

可以看到Libra只實現了從matser的私鑰派生child,派生只需要master priv key 和 ChildNumber,而KDF的具體實現在crypto/crypto/src/hkdf.rs中,這是是Libra對HKDF(HMAC-based Extract-and-Expand Key Derivation Function)的實現,這邊不展開了。

6. 總結

綜上,Libra的分層確定性錢包如下所示:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-27ccQEW2-1577624532516)

可以看到相比比特幣,Libra實現的還是比較簡單的,CKD只實現了Private parent key → private child key,只有master和child兩層。

發佈了68 篇原創文章 · 獲贊 40 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章