openzeppelin_relayer/utils/
address_derivation.rs

1//! Derivation of blockchain addresses from cryptographic keys.
2//!
3//! This module provides utilities for deriving blockchain addresses from cryptographic
4//! public keys in various formats (DER, PEM). It supports multiple blockchain networks
5//! including Ethereum, Solana, and potentially others.
6
7use super::der::extract_public_key_from_der;
8
9#[derive(Debug, thiserror::Error)]
10pub enum AddressDerivationError {
11    #[error("Parse Error: {0}")]
12    ParseError(String),
13}
14
15/// Derive EVM address from the DER payload.
16pub fn derive_ethereum_address_from_der(der: &[u8]) -> Result<[u8; 20], AddressDerivationError> {
17    let pub_key = extract_public_key_from_der(der)
18        .map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
19
20    let hash = alloy::primitives::keccak256(pub_key);
21
22    // Take the last 20 bytes of the hash
23    let address_bytes = &hash[hash.len() - 20..];
24
25    let mut array = [0u8; 20];
26    array.copy_from_slice(address_bytes);
27
28    Ok(array)
29}
30
31/// Derive EVM address from the PEM string.
32pub fn derive_ethereum_address_from_pem(pem_str: &str) -> Result<[u8; 20], AddressDerivationError> {
33    let pkey =
34        pem::parse(pem_str).map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
35    let der = pkey.contents();
36    derive_ethereum_address_from_der(der)
37}
38
39/// Derive Solana address from a PEM-encoded public key.
40pub fn derive_solana_address_from_pem(pem_str: &str) -> Result<String, AddressDerivationError> {
41    let pkey =
42        pem::parse(pem_str).map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
43    let content = pkey.contents();
44
45    let mut array = [0u8; 32];
46
47    match content.len() {
48        32 => array.copy_from_slice(content),
49        44 => array.copy_from_slice(&content[12..]),
50        _ => {
51            return Err(AddressDerivationError::ParseError(format!(
52                "Unexpected ed25519 public key length: got {} bytes (expected 32 or 44).",
53                content.len()
54            )));
55        }
56    }
57
58    let solana_address = bs58::encode(array).into_string();
59    Ok(solana_address)
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    const VALID_SECP256K1_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEjJaJh5wfZwvj8b3bQ4GYikqDTLXWUjMh\nkFs9lGj2N9B17zo37p4PSy99rDio0QHLadpso0rtTJDSISRW9MdOqA==\n-----END PUBLIC KEY-----\n"; // noboost
67
68    const VALID_ED25519_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAnUV+ReQWxMZ3Z2pC/5aOPPjcc8jzOo0ZgSl7+j4AMLo=\n-----END PUBLIC KEY-----\n";
69
70    #[test]
71    fn test_derive_ethereum_address_from_pem_with_invalid_data() {
72        let invalid_pem = "not-a-valid-pem";
73        let result = derive_ethereum_address_from_pem(invalid_pem);
74        assert!(result.is_err());
75
76        // Verify it returns the expected error type
77        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
78    }
79
80    #[test]
81    fn test_derive_ethereum_address_from_pem_with_valid_secp256k1() {
82        let result = derive_ethereum_address_from_pem(VALID_SECP256K1_PEM);
83        assert!(result.is_ok());
84
85        let address = result.unwrap();
86        assert_eq!(address.len(), 20); // Ethereum addresses are 20 bytes
87
88        assert_eq!(
89            format!("0x{}", hex::encode(address)),
90            "0xeeb8861f51b3f3f2204d64bbf7a7eb25e1b4d6cd"
91        );
92    }
93
94    #[test]
95    fn test_derive_ethereum_address_from_der_with_invalid_data() {
96        let invalid_der = &[1, 2, 3];
97        let result = derive_ethereum_address_from_der(invalid_der);
98        assert!(result.is_err());
99
100        // Verify it returns the expected error type
101        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
102    }
103
104    #[test]
105    fn test_derive_ethereum_address_from_der_with_valid_secp256k1() {
106        let pem = pem::parse(VALID_SECP256K1_PEM).unwrap();
107        let der = pem.contents();
108        let result = derive_ethereum_address_from_der(der);
109
110        assert!(result.is_ok());
111
112        let address = result.unwrap();
113        assert_eq!(address.len(), 20); // Ethereum addresses are 20 bytes
114
115        assert_eq!(
116            format!("0x{}", hex::encode(address)),
117            "0xeeb8861f51b3f3f2204d64bbf7a7eb25e1b4d6cd"
118        );
119    }
120
121    #[test]
122    fn test_derive_solana_address_from_pem_with_invalid_data() {
123        let invalid_pem = "not-a-valid-pem";
124        let result = derive_solana_address_from_pem(invalid_pem);
125        assert!(result.is_err());
126
127        // Verify it returns the expected error type
128        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
129    }
130
131    #[test]
132    fn test_derive_solana_address_from_pem_with_valid_ed25519() {
133        let result = derive_solana_address_from_pem(VALID_ED25519_PEM);
134        assert!(result.is_ok());
135
136        let address = result.unwrap();
137        // Solana addresses are base58 encoded, should be around 32-44 characters
138        assert!(!address.is_empty());
139        assert!(address.len() >= 32 && address.len() <= 44);
140
141        assert_eq!(address, "BavUBpkD77FABnevMkBVqV8BDHv7gX8sSoYYJY9WU9L5");
142    }
143
144    #[test]
145    fn test_derive_solana_address_from_pem_with_invalid_key_length() {
146        // Create a PEM with invalid ed25519 key length
147        let invalid_ed25519_der = vec![0u8; 10]; // Too short
148        let invalid_pem = pem::Pem::new("PUBLIC KEY", invalid_ed25519_der);
149        let invalid_pem_str = pem::encode(&invalid_pem);
150
151        let result = derive_solana_address_from_pem(&invalid_pem_str);
152        assert!(result.is_err());
153
154        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
155    }
156}