zera-sdk 0.1.0

Rust SDK for ZERA transactions, validator APIs, and bridge workflows
Documentation
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256, Sha512};

use crate::crypto::constants::KeyType;
use crate::crypto::signature::{Ed25519KeyPair, Ed448KeyPair};
use crate::error::{Result, ZeraError};
use crate::wallet::constants::{EXTENDED_PRIVATE_VERSION, EXTENDED_PUBLIC_VERSION};

const SLIP0010_HARDENED_OFFSET: u32 = 0x8000_0000;
const SLIP0010_SEED_KEY: &[u8] = b"ZERA seed";

#[derive(Debug, Clone)]
pub struct Slip0010HdWallet {
    pub seed: Vec<u8>,
    pub derivation_path: String,
    pub key_type: KeyType,
    pub depth: u8,
    pub index: u32,
    pub chain_code: [u8; 32],
    pub private_key: [u8; 32],
    pub public_key: Vec<u8>,
}

impl Slip0010HdWallet {
    pub fn new(seed: &[u8], derivation_path: &str, key_type: KeyType) -> Result<Self> {
        let path_parts = parse_derivation_path(derivation_path)?;
        let depth = path_parts.len().saturating_sub(1) as u8;
        let index = *path_parts.last().unwrap_or(&0);

        let (private_key, chain_code) = derive_private_key(seed, &path_parts)?;
        let public_key = generate_public_key(&private_key, key_type)?;

        Ok(Self {
            seed: seed.to_vec(),
            derivation_path: derivation_path.to_string(),
            key_type,
            depth,
            index,
            chain_code,
            private_key,
            public_key,
        })
    }

    pub fn from_parent_node(
        parent_private_key: [u8; 32],
        parent_chain_code: [u8; 32],
        child_indices: &[u32],
        full_path: &str,
        seed: &[u8],
        key_type: KeyType,
    ) -> Result<Self> {
        let mut current_private = parent_private_key;
        let mut current_chain = parent_chain_code;

        for index in child_indices {
            let (next_private, next_chain) =
                derive_child_key(&current_private, &current_chain, *index)?;
            current_private = next_private;
            current_chain = next_chain;
        }

        let public_key = generate_public_key(&current_private, key_type)?;

        Ok(Self {
            seed: seed.to_vec(),
            derivation_path: full_path.to_string(),
            key_type,
            depth: full_path.split('/').count().saturating_sub(2) as u8,
            index: *child_indices.last().unwrap_or(&0),
            chain_code: current_chain,
            private_key: current_private,
            public_key,
        })
    }

    pub fn get_key_material(&self) -> ([u8; 32], [u8; 32]) {
        (self.private_key, self.chain_code)
    }

    pub fn get_address(&self) -> String {
        bs58::encode(&self.public_key).into_string()
    }

    pub fn get_extended_private_key(&self) -> String {
        let mut bytes = Vec::with_capacity(4 + 1 + 4 + 32 + 32);
        bytes.extend_from_slice(&EXTENDED_PRIVATE_VERSION.to_be_bytes());
        bytes.push(self.depth);
        bytes.extend_from_slice(&self.index.to_be_bytes());
        bytes.extend_from_slice(&self.chain_code);
        bytes.extend_from_slice(&self.private_key);
        bs58::encode(bytes).into_string()
    }

    pub fn get_extended_public_key(&self) -> String {
        let mut bytes = Vec::with_capacity(4 + 1 + 4 + 32 + 57);
        bytes.extend_from_slice(&EXTENDED_PUBLIC_VERSION.to_be_bytes());
        bytes.push(self.depth);
        bytes.extend_from_slice(&self.index.to_be_bytes());
        bytes.extend_from_slice(&self.chain_code);
        bytes.extend_from_slice(&self.public_key);
        bs58::encode(bytes).into_string()
    }

    pub fn get_private_key_base58(&self) -> String {
        bs58::encode(self.private_key).into_string()
    }

    pub fn get_fingerprint(&self) -> String {
        let hash = Sha256::digest(&self.public_key);
        hex::encode(&hash[..4])
    }

    pub fn secure_clear(&mut self) {
        self.seed.fill(0);
        self.private_key.fill(0);
        self.chain_code.fill(0);
    }
}

fn parse_derivation_path(path: &str) -> Result<Vec<u32>> {
    if !path.starts_with("m/") {
        return Err(ZeraError::Validation(
            "Invalid derivation path: must start with \"m/\"".into(),
        ));
    }

    let parts = path.split('/').skip(1);
    let mut out = Vec::new();

    for part in parts {
        if part.is_empty() {
            return Err(ZeraError::Validation(
                "Invalid derivation path segment".into(),
            ));
        }

        if let Some(stripped) = part.strip_suffix('\'') {
            let value: u32 = stripped.parse().map_err(|_| {
                ZeraError::Validation(format!("Invalid derivation path segment: {part}"))
            })?;
            out.push(value + SLIP0010_HARDENED_OFFSET);
        } else {
            let value: u32 = part.parse().map_err(|_| {
                ZeraError::Validation(format!("Invalid derivation path segment: {part}"))
            })?;
            out.push(value);
        }
    }

    Ok(out)
}

fn derive_private_key(seed: &[u8], path_indices: &[u32]) -> Result<([u8; 32], [u8; 32])> {
    let (mut current_private, mut current_chain) = derive_key_from_seed(seed)?;

    for index in path_indices {
        let (next_private, next_chain) =
            derive_child_key(&current_private, &current_chain, *index)?;
        current_private = next_private;
        current_chain = next_chain;
    }

    Ok((current_private, current_chain))
}

fn derive_key_from_seed(seed: &[u8]) -> Result<([u8; 32], [u8; 32])> {
    let mut mac = Hmac::<Sha512>::new_from_slice(SLIP0010_SEED_KEY)
        .map_err(|e| ZeraError::Crypto(format!("failed to init seed HMAC: {e}")))?;
    mac.update(seed);
    let out = mac.finalize().into_bytes();

    let mut private_key = [0_u8; 32];
    private_key.copy_from_slice(&out[..32]);

    let mut chain_code = [0_u8; 32];
    chain_code.copy_from_slice(&out[32..64]);

    Ok((private_key, chain_code))
}

fn derive_child_key(
    private_key: &[u8; 32],
    chain_code: &[u8; 32],
    index: u32,
) -> Result<([u8; 32], [u8; 32])> {
    let mut mac = Hmac::<Sha512>::new_from_slice(chain_code)
        .map_err(|e| ZeraError::Crypto(format!("failed to init child HMAC: {e}")))?;

    // JS parity: index is little-endian and concatenated before private key.
    mac.update(&index.to_le_bytes());
    mac.update(private_key);

    let out = mac.finalize().into_bytes();

    let mut child_private = [0_u8; 32];
    child_private.copy_from_slice(&out[..32]);

    let mut child_chain = [0_u8; 32];
    child_chain.copy_from_slice(&out[32..64]);

    Ok((child_private, child_chain))
}

fn generate_public_key(private_key: &[u8; 32], key_type: KeyType) -> Result<Vec<u8>> {
    match key_type {
        KeyType::Ed25519 => {
            let key = Ed25519KeyPair::from_private_key(private_key)?;
            Ok(key.public_key_bytes().to_vec())
        }
        KeyType::Ed448 => {
            let key = Ed448KeyPair::from_private_key(private_key)?;
            Ok(key.public_key_bytes().to_vec())
        }
    }
}