Skip to main content

aelf_crypto/
lib.rs

1//! Crypto, wallet, signing and address helpers for the AElf Rust SDK.
2//!
3//! The APIs in this crate align with the reference AElf SDKs and power the
4//! facade exported by `aelf-sdk`.
5
6#![forbid(unsafe_code)]
7
8use aelf_proto::aelf::{Address, Hash, Transaction};
9use coins_bip32::{
10    path::DerivationPath,
11    prelude::{RecoveryId, Signature, SigningKey, VerifyingKey},
12    xkeys::XPriv,
13};
14use coins_bip39::{English, Mnemonic, MnemonicError};
15use prost::Message;
16use rand::rng;
17use sha2::{Digest, Sha256};
18use std::fmt;
19use std::str::FromStr;
20use thiserror::Error;
21use zeroize::{Zeroize, Zeroizing};
22
23/// Default BIP44 derivation path used by the reference AElf SDKs.
24pub const DEFAULT_BIP44_PATH: &str = "m/44'/1616'/0'/0/0";
25
26/// Errors returned by wallet, address and signing helpers.
27#[derive(Debug, Error)]
28pub enum CryptoError {
29    #[error("invalid hex string: {0}")]
30    Hex(#[from] hex::FromHexError),
31    #[error("invalid mnemonic: {0}")]
32    Mnemonic(#[from] MnemonicError),
33    #[error("invalid derivation path: {0}")]
34    DerivationPath(#[from] coins_bip32::Bip32Error),
35    #[error("invalid private key")]
36    InvalidPrivateKey,
37    #[error("invalid address")]
38    InvalidAddress,
39    #[error("invalid signature")]
40    InvalidSignature,
41    #[error("protobuf encode error: {0}")]
42    ProtobufEncode(#[from] prost::EncodeError),
43    #[error("protobuf decode error: {0}")]
44    ProtobufDecode(#[from] prost::DecodeError),
45}
46
47/// Wallet created from a mnemonic or raw private key.
48///
49/// Sensitive fields are zeroized on drop and redacted from `Debug` output.
50#[derive(Clone, PartialEq, Eq)]
51pub struct Wallet {
52    mnemonic: String,
53    bip44_path: String,
54    private_key: String,
55    public_key: String,
56    address: String,
57}
58
59impl Wallet {
60    /// Creates a new wallet using the default AElf BIP44 path.
61    pub fn create() -> Result<Self, CryptoError> {
62        Self::create_with_path(DEFAULT_BIP44_PATH)
63    }
64
65    /// Creates a new wallet using a custom derivation path.
66    pub fn create_with_path(path: &str) -> Result<Self, CryptoError> {
67        let mut rng = rng();
68        let mnemonic = Mnemonic::<English>::from_rng_with_count(&mut rng, 12)?;
69        Self::from_mnemonic_with_path(&mnemonic.to_phrase(), path)
70    }
71
72    /// Restores a wallet from a mnemonic using the default AElf BIP44 path.
73    pub fn from_mnemonic(mnemonic: &str) -> Result<Self, CryptoError> {
74        Self::from_mnemonic_with_path(mnemonic, DEFAULT_BIP44_PATH)
75    }
76
77    /// Restores a wallet from a mnemonic and custom derivation path.
78    pub fn from_mnemonic_with_path(mnemonic: &str, path: &str) -> Result<Self, CryptoError> {
79        let mnemonic = Mnemonic::<English>::new_from_phrase(mnemonic)?;
80        let path = DerivationPath::from_str(path)?;
81        let derived: XPriv = mnemonic.derive_key(path.clone(), None)?;
82        let signing_key: SigningKey = <XPriv as AsRef<SigningKey>>::as_ref(&derived).clone();
83
84        Ok(Self::from_signing_key(
85            signing_key,
86            mnemonic.to_phrase(),
87            path.derivation_string(),
88        ))
89    }
90
91    /// Restores a wallet from a raw secp256k1 private key hex string.
92    pub fn from_private_key(private_key_hex: &str) -> Result<Self, CryptoError> {
93        let bytes = Zeroizing::new(normalize_private_key_hex(private_key_hex)?);
94        let signing_key =
95            SigningKey::from_bytes((&*bytes).into()).map_err(|_| CryptoError::InvalidPrivateKey)?;
96
97        Ok(Self::from_signing_key(
98            signing_key,
99            String::new(),
100            DEFAULT_BIP44_PATH.to_owned(),
101        ))
102    }
103
104    /// Returns the mnemonic phrase, if the wallet was derived from one.
105    pub fn mnemonic(&self) -> &str {
106        &self.mnemonic
107    }
108
109    /// Returns the derivation path used to create the wallet.
110    pub fn bip44_path(&self) -> &str {
111        &self.bip44_path
112    }
113
114    /// Returns the private key as a lowercase hex string.
115    pub fn private_key(&self) -> &str {
116        &self.private_key
117    }
118
119    /// Returns the uncompressed public key as a hex string.
120    pub fn public_key(&self) -> &str {
121        &self.public_key
122    }
123
124    /// Returns the base58 AElf address.
125    pub fn address(&self) -> &str {
126        &self.address
127    }
128
129    /// Reconstructs the signing key from the stored private key.
130    pub fn signing_key(&self) -> Result<SigningKey, CryptoError> {
131        let bytes = Zeroizing::new(normalize_private_key_hex(&self.private_key)?);
132        SigningKey::from_bytes((&*bytes).into()).map_err(|_| CryptoError::InvalidPrivateKey)
133    }
134
135    /// Signs arbitrary payload bytes with AElf-compatible recoverable secp256k1.
136    pub fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, CryptoError> {
137        sign_payload(&self.signing_key()?, payload)
138    }
139
140    fn from_signing_key(signing_key: SigningKey, mnemonic: String, bip44_path: String) -> Self {
141        let private_key = hex::encode(signing_key.to_bytes());
142        let public_key_bytes = signing_key.verifying_key().to_encoded_point(false);
143        let public_key = hex::encode(public_key_bytes.as_bytes());
144        let address = address_from_public_key(public_key_bytes.as_bytes());
145
146        Self {
147            mnemonic,
148            bip44_path,
149            private_key,
150            public_key,
151            address,
152        }
153    }
154}
155
156impl fmt::Debug for Wallet {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.debug_struct("Wallet")
159            .field("mnemonic", &"<redacted>")
160            .field("bip44_path", &self.bip44_path)
161            .field("private_key", &"<redacted>")
162            .field("public_key", &self.public_key)
163            .field("address", &self.address)
164            .finish()
165    }
166}
167
168impl Drop for Wallet {
169    fn drop(&mut self) {
170        self.mnemonic.zeroize();
171        self.private_key.zeroize();
172    }
173}
174
175/// Hashes bytes with SHA-256.
176pub fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
177    Sha256::digest(bytes).into()
178}
179
180/// Converts an uncompressed public key to an AElf base58 address.
181pub fn address_from_public_key(public_key: &[u8]) -> String {
182    let first = sha256_bytes(public_key);
183    let second = sha256_bytes(&first);
184    base58check_encode(&second)
185}
186
187/// Converts a base58 or formatted AElf address into protobuf `Address`.
188pub fn address_to_pb(address: &str) -> Result<Address, CryptoError> {
189    Ok(Address {
190        value: decode_address(address)?,
191    })
192}
193
194/// Converts protobuf `Address` to an AElf base58 address.
195pub fn pb_to_address(address: &Address) -> String {
196    base58check_encode(&address.value)
197}
198
199/// Wraps raw bytes in protobuf `Hash`.
200pub fn hash_to_pb(bytes: impl AsRef<[u8]>) -> Hash {
201    Hash {
202        value: bytes.as_ref().to_vec(),
203    }
204}
205
206/// Extracts the raw base58 address from either a plain address or a formatted
207/// address such as `ELF_<address>_AELF`.
208pub fn parse_aelf_address(value: &str) -> &str {
209    let mut parts = value.split('_');
210    match (parts.next(), parts.next(), parts.next(), parts.next()) {
211        (Some(_prefix), Some(address), Some(_chain_id), None) if !address.is_empty() => address,
212        _ => value,
213    }
214}
215
216/// Decodes a base58 or formatted AElf address into its raw bytes.
217pub fn decode_address(address: &str) -> Result<Vec<u8>, CryptoError> {
218    base58check_decode(parse_aelf_address(address))
219}
220
221/// Encodes raw bytes as AElf base58check.
222pub fn base58check_encode(payload: &[u8]) -> String {
223    let checksum = sha256_bytes(&sha256_bytes(payload));
224    let mut bytes = payload.to_vec();
225    bytes.extend_from_slice(&checksum[..4]);
226    bs58::encode(bytes).into_string()
227}
228
229/// Decodes an AElf base58check string into raw bytes.
230pub fn base58check_decode(value: &str) -> Result<Vec<u8>, CryptoError> {
231    let bytes = bs58::decode(value)
232        .into_vec()
233        .map_err(|_| CryptoError::InvalidAddress)?;
234    if bytes.len() < 4 {
235        return Err(CryptoError::InvalidAddress);
236    }
237
238    let (payload, checksum) = bytes.split_at(bytes.len() - 4);
239    let expected = sha256_bytes(&sha256_bytes(payload));
240    if checksum != &expected[..4] {
241        return Err(CryptoError::InvalidAddress);
242    }
243
244    Ok(payload.to_vec())
245}
246
247/// Converts a numeric chain id into the base58 string used by node APIs.
248pub fn chain_id_to_base58(chain_id: i32) -> String {
249    bs58::encode(chain_id.to_le_bytes()).into_string()
250}
251
252/// Converts a base58 chain id string returned by node APIs to its numeric form.
253pub fn base58_to_chain_id(chain_id: &str) -> Result<i32, CryptoError> {
254    let bytes = bs58::decode(chain_id)
255        .into_vec()
256        .map_err(|_| CryptoError::InvalidAddress)?;
257    let bytes: [u8; 4] = bytes
258        .as_slice()
259        .try_into()
260        .map_err(|_| CryptoError::InvalidAddress)?;
261    Ok(i32::from_le_bytes(bytes))
262}
263
264/// Calculates the protobuf transaction hash used by AElf.
265pub fn transaction_hash(transaction: &Transaction) -> [u8; 32] {
266    sha256_bytes(&transaction.encode_to_vec())
267}
268
269/// Signs a protobuf transaction with an existing wallet.
270pub fn sign_transaction(
271    wallet: &Wallet,
272    transaction: &Transaction,
273) -> Result<Vec<u8>, CryptoError> {
274    sign_payload(&wallet.signing_key()?, &transaction.encode_to_vec())
275}
276
277/// Signs a protobuf transaction with a raw private key.
278pub fn sign_transaction_with_private_key(
279    private_key_hex: &str,
280    transaction: &Transaction,
281) -> Result<Vec<u8>, CryptoError> {
282    let wallet = Wallet::from_private_key(private_key_hex)?;
283    sign_transaction(&wallet, transaction)
284}
285
286/// Verifies a recoverable secp256k1 signature against a payload.
287pub fn verify_signature(
288    public_key: &[u8],
289    payload: &[u8],
290    signature_bytes: &[u8],
291) -> Result<bool, CryptoError> {
292    if signature_bytes.len() != 65 {
293        return Err(CryptoError::InvalidSignature);
294    }
295
296    let verifying_key =
297        VerifyingKey::from_sec1_bytes(public_key).map_err(|_| CryptoError::InvalidSignature)?;
298    let recovery_id =
299        RecoveryId::from_byte(signature_bytes[64]).ok_or(CryptoError::InvalidSignature)?;
300    let signature =
301        Signature::from_slice(&signature_bytes[..64]).map_err(|_| CryptoError::InvalidSignature)?;
302
303    let digest = Sha256::new_with_prefix(payload);
304    let recovered = VerifyingKey::recover_from_digest(digest, &signature, recovery_id)
305        .map_err(|_| CryptoError::InvalidSignature)?;
306
307    Ok(recovered == verifying_key)
308}
309
310fn sign_payload(signing_key: &SigningKey, payload: &[u8]) -> Result<Vec<u8>, CryptoError> {
311    let digest = Sha256::new_with_prefix(payload);
312    let (signature, recovery_id) = signing_key
313        .sign_digest_recoverable(digest)
314        .map_err(|_| CryptoError::InvalidSignature)?;
315
316    let mut bytes = Vec::with_capacity(65);
317    bytes.extend_from_slice(&signature.to_bytes());
318    bytes.push(recovery_id.to_byte());
319    Ok(bytes)
320}
321
322fn normalize_private_key_hex(private_key_hex: &str) -> Result<[u8; 32], CryptoError> {
323    let trimmed = private_key_hex
324        .strip_prefix("0x")
325        .unwrap_or(private_key_hex);
326    let normalized = if trimmed.len() >= 64 {
327        trimmed.to_owned()
328    } else {
329        format!("{trimmed:0>64}")
330    };
331
332    let bytes = hex::decode(normalized)?;
333    bytes
334        .as_slice()
335        .try_into()
336        .map_err(|_| CryptoError::InvalidPrivateKey)
337}
338
339#[cfg(test)]
340mod tests {
341    use super::{
342        base58_to_chain_id, chain_id_to_base58, decode_address, parse_aelf_address,
343        sign_transaction, verify_signature, Wallet,
344    };
345    use aelf_proto::aelf::Transaction;
346    use prost::Message;
347
348    const JS_ADDRESS_MNEMONIC: &str =
349        "history segment pizza all time regret robust animal loud gasp razor gadget";
350    const JS_EXPECTED_ADDRESS: &str = "CTqD1M6Kt2v2jS8QLR6tcTq7vv9dHsKibUr6BEaN3BZ94i92m";
351    const JS_SIGN_PRIVATE_KEY: &str =
352        "03bd0cea9730bcfc8045248fd7f4841ea19315995c44801a3dfede0ca872f808";
353    const JS_SIGN_EXPECTED: &str =
354        "276aa36fcab0ac3d4071a4bfb868f636d1a9639916afe4ec329529014f923a372b688b4eb59d6587481bc15e4a1684e1d92b7598967767713d1504dcea83dadb01";
355    const TEST_MNEMONIC: &str =
356        "orange learn result add snack curtain double state expose bless also clarify";
357    const TEST_PRIVATE_KEY: &str =
358        "cc2895b46707a34eefd3c61bd4a8487266e0398a93309a9910a2b88e587b6582";
359
360    #[test]
361    fn derives_known_private_key_from_mnemonic() {
362        let wallet = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
363        assert_eq!(wallet.private_key(), TEST_PRIVATE_KEY);
364    }
365
366    #[test]
367    fn derives_same_address_from_private_key_and_mnemonic() {
368        let from_mnemonic = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
369        let from_private_key = Wallet::from_private_key(TEST_PRIVATE_KEY).expect("wallet");
370        assert_eq!(from_mnemonic.address(), from_private_key.address());
371    }
372
373    #[test]
374    fn chain_id_roundtrip() {
375        let encoded = chain_id_to_base58(9_992_731);
376        let decoded = base58_to_chain_id(&encoded).expect("chain id");
377        assert_eq!(decoded, 9_992_731);
378    }
379
380    #[test]
381    fn transaction_signature_is_recoverable() {
382        let wallet = Wallet::from_private_key(TEST_PRIVATE_KEY).expect("wallet");
383        let transaction = Transaction {
384            from: None,
385            to: None,
386            ref_block_number: 1,
387            ref_block_prefix: vec![1, 2, 3, 4],
388            method_name: "Test".to_owned(),
389            params: vec![1, 2, 3],
390            signature: Vec::new(),
391        };
392
393        let signature = sign_transaction(&wallet, &transaction).expect("signature");
394        let public_key = hex::decode(wallet.public_key()).expect("public key");
395
396        assert!(
397            verify_signature(&public_key, &transaction.encode_to_vec(), &signature)
398                .expect("verify")
399        );
400    }
401
402    #[test]
403    fn derives_known_address_from_js_mnemonic_fixture() {
404        let wallet = Wallet::from_mnemonic(JS_ADDRESS_MNEMONIC).expect("wallet");
405        assert_eq!(wallet.address(), JS_EXPECTED_ADDRESS);
406    }
407
408    #[test]
409    fn signs_payload_compatible_with_js_fixture() {
410        let wallet = Wallet::from_private_key(JS_SIGN_PRIVATE_KEY).expect("wallet");
411        let signature = wallet.sign(b"hello world").expect("signature");
412        assert_eq!(hex::encode(signature), JS_SIGN_EXPECTED);
413    }
414
415    #[test]
416    fn parses_formatted_aelf_address() {
417        let formatted = format!("ELF_{JS_EXPECTED_ADDRESS}_AELF");
418        assert_eq!(parse_aelf_address(&formatted), JS_EXPECTED_ADDRESS);
419        assert_eq!(
420            decode_address(&formatted).expect("formatted address"),
421            decode_address(JS_EXPECTED_ADDRESS).expect("raw address")
422        );
423    }
424
425    #[test]
426    fn debug_redacts_wallet_secrets() {
427        let wallet = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
428        let debug = format!("{wallet:?}");
429        assert!(!debug.contains(TEST_MNEMONIC));
430        assert!(!debug.contains(TEST_PRIVATE_KEY));
431        assert!(debug.contains(wallet.address()));
432    }
433}