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/// Derive Stellar address from a PEM-encoded public key.
63/// Stellar uses Ed25519 keys and addresses are encoded with StrKey format (G prefix for accounts).
64pub fn derive_stellar_address_from_pem(pem_str: &str) -> Result<String, AddressDerivationError> {
65    let pkey =
66        pem::parse(pem_str).map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
67    let content = pkey.contents();
68
69    let mut array = [0u8; 32];
70
71    match content.len() {
72        32 => array.copy_from_slice(content),
73        44 => array.copy_from_slice(&content[12..]),
74        _ => {
75            return Err(AddressDerivationError::ParseError(format!(
76                "Unexpected ed25519 public key length: got {} bytes (expected 32 or 44).",
77                content.len()
78            )));
79        }
80    }
81
82    // Use stellar_strkey to encode the public key
83    use stellar_strkey::ed25519::PublicKey;
84    let public_key = PublicKey(array);
85    Ok(public_key.to_string())
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    const VALID_SECP256K1_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEjJaJh5wfZwvj8b3bQ4GYikqDTLXWUjMh\nkFs9lGj2N9B17zo37p4PSy99rDio0QHLadpso0rtTJDSISRW9MdOqA==\n-----END PUBLIC KEY-----\n"; // noboost
93
94    const VALID_ED25519_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAnUV+ReQWxMZ3Z2pC/5aOPPjcc8jzOo0ZgSl7+j4AMLo=\n-----END PUBLIC KEY-----\n";
95
96    #[test]
97    fn test_derive_ethereum_address_from_pem_with_invalid_data() {
98        let invalid_pem = "not-a-valid-pem";
99        let result = derive_ethereum_address_from_pem(invalid_pem);
100        assert!(result.is_err());
101
102        // Verify it returns the expected error type
103        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
104    }
105
106    #[test]
107    fn test_derive_ethereum_address_from_pem_with_valid_secp256k1() {
108        let result = derive_ethereum_address_from_pem(VALID_SECP256K1_PEM);
109        assert!(result.is_ok());
110
111        let address = result.unwrap();
112        assert_eq!(address.len(), 20); // Ethereum addresses are 20 bytes
113
114        assert_eq!(
115            format!("0x{}", hex::encode(address)),
116            "0xeeb8861f51b3f3f2204d64bbf7a7eb25e1b4d6cd"
117        );
118    }
119
120    #[test]
121    fn test_derive_ethereum_address_from_der_with_invalid_data() {
122        let invalid_der = &[1, 2, 3];
123        let result = derive_ethereum_address_from_der(invalid_der);
124        assert!(result.is_err());
125
126        // Verify it returns the expected error type
127        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
128    }
129
130    #[test]
131    fn test_derive_ethereum_address_from_der_with_valid_secp256k1() {
132        let pem = pem::parse(VALID_SECP256K1_PEM).unwrap();
133        let der = pem.contents();
134        let result = derive_ethereum_address_from_der(der);
135
136        assert!(result.is_ok());
137
138        let address = result.unwrap();
139        assert_eq!(address.len(), 20); // Ethereum addresses are 20 bytes
140
141        assert_eq!(
142            format!("0x{}", hex::encode(address)),
143            "0xeeb8861f51b3f3f2204d64bbf7a7eb25e1b4d6cd"
144        );
145    }
146
147    #[test]
148    fn test_derive_solana_address_from_pem_with_invalid_data() {
149        let invalid_pem = "not-a-valid-pem";
150        let result = derive_solana_address_from_pem(invalid_pem);
151        assert!(result.is_err());
152
153        // Verify it returns the expected error type
154        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
155    }
156
157    #[test]
158    fn test_derive_solana_address_from_pem_with_valid_ed25519() {
159        let result = derive_solana_address_from_pem(VALID_ED25519_PEM);
160        assert!(result.is_ok());
161
162        let address = result.unwrap();
163        // Solana addresses are base58 encoded, should be around 32-44 characters
164        assert!(!address.is_empty());
165        assert!(address.len() >= 32 && address.len() <= 44);
166
167        assert_eq!(address, "BavUBpkD77FABnevMkBVqV8BDHv7gX8sSoYYJY9WU9L5");
168    }
169
170    #[test]
171    fn test_derive_solana_address_from_pem_with_invalid_key_length() {
172        // Create a PEM with invalid ed25519 key length
173        let invalid_ed25519_der = vec![0u8; 10]; // Too short
174        let invalid_pem = pem::Pem::new("PUBLIC KEY", invalid_ed25519_der);
175        let invalid_pem_str = pem::encode(&invalid_pem);
176
177        let result = derive_solana_address_from_pem(&invalid_pem_str);
178        assert!(result.is_err());
179
180        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
181    }
182
183    #[test]
184    fn test_derive_stellar_address_from_pem_with_valid_ed25519() {
185        let result = derive_stellar_address_from_pem(VALID_ED25519_PEM);
186        assert!(result.is_ok());
187
188        let address = result.unwrap();
189        // Stellar addresses start with 'G' for accounts
190        assert!(address.starts_with('G'));
191        // Stellar addresses are base32 encoded and 56 characters long
192        assert_eq!(address.len(), 56);
193    }
194
195    #[test]
196    fn test_derive_stellar_address_from_pem_with_invalid_data() {
197        let invalid_pem = "not-a-valid-pem";
198        let result = derive_stellar_address_from_pem(invalid_pem);
199        assert!(result.is_err());
200
201        // Verify it returns the expected error type
202        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
203    }
204
205    #[test]
206    fn test_derive_stellar_address_from_pem_with_invalid_key_length() {
207        // Create a PEM with invalid ed25519 key length
208        let invalid_ed25519_der = vec![0u8; 10]; // Too short
209        let invalid_pem = pem::Pem::new("PUBLIC KEY", invalid_ed25519_der);
210        let invalid_pem_str = pem::encode(&invalid_pem);
211
212        let result = derive_stellar_address_from_pem(&invalid_pem_str);
213        assert!(result.is_err());
214
215        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
216    }
217}