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;
8use super::ed25519::extract_ed25519_public_key_from_der;
9
10#[derive(Debug, thiserror::Error)]
11pub enum AddressDerivationError {
12    #[error("Parse Error: {0}")]
13    ParseError(String),
14}
15
16/// Derive EVM address from the DER payload.
17pub fn derive_ethereum_address_from_der(der: &[u8]) -> Result<[u8; 20], AddressDerivationError> {
18    let pub_key = extract_public_key_from_der(der)
19        .map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
20
21    let hash = alloy::primitives::keccak256(pub_key);
22
23    // Take the last 20 bytes of the hash
24    let address_bytes = &hash[hash.len() - 20..];
25
26    let mut array = [0u8; 20];
27    array.copy_from_slice(address_bytes);
28
29    Ok(array)
30}
31
32/// Derive EVM address from the PEM string.
33pub fn derive_ethereum_address_from_pem(pem_str: &str) -> Result<[u8; 20], AddressDerivationError> {
34    let pkey =
35        pem::parse(pem_str).map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
36    let der = pkey.contents();
37    derive_ethereum_address_from_der(der)
38}
39
40/// Derive Solana address from a PEM-encoded public key.
41pub fn derive_solana_address_from_pem(pem_str: &str) -> Result<String, AddressDerivationError> {
42    let pkey =
43        pem::parse(pem_str).map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
44    let content = pkey.contents();
45    derive_solana_address_from_der(content)
46}
47
48/// Derive Stellar address from a PEM-encoded public key.
49/// Stellar uses Ed25519 keys and addresses are encoded with StrKey format (G prefix for accounts).
50pub fn derive_stellar_address_from_pem(pem_str: &str) -> Result<String, AddressDerivationError> {
51    let pkey =
52        pem::parse(pem_str).map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
53    let content = pkey.contents();
54    derive_stellar_address_from_der(content)
55}
56
57/// Derive Solana address from DER/SPKI encoded Ed25519 public key.
58/// Solana addresses are base58-encoded Ed25519 public keys.
59///
60/// Accepts:
61/// - 44 bytes: Full SPKI format (12-byte header + 32-byte key)
62/// - 32 bytes: Raw public key
63pub fn derive_solana_address_from_der(der: &[u8]) -> Result<String, AddressDerivationError> {
64    let pubkey = extract_ed25519_public_key_from_der(der)
65        .map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
66    Ok(bs58::encode(pubkey).into_string())
67}
68
69/// Derive Stellar address from DER/SPKI encoded Ed25519 public key.
70/// Stellar addresses use StrKey encoding (G prefix for public accounts).
71///
72/// Accepts:
73/// - 44 bytes: Full SPKI format (12-byte header + 32-byte key)
74/// - 32 bytes: Raw public key
75pub fn derive_stellar_address_from_der(der: &[u8]) -> Result<String, AddressDerivationError> {
76    let pubkey = extract_ed25519_public_key_from_der(der)
77        .map_err(|e| AddressDerivationError::ParseError(e.to_string()))?;
78
79    use stellar_strkey::ed25519::PublicKey;
80    Ok(PublicKey(pubkey).to_string())
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    const VALID_SECP256K1_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEjJaJh5wfZwvj8b3bQ4GYikqDTLXWUjMh\nkFs9lGj2N9B17zo37p4PSy99rDio0QHLadpso0rtTJDSISRW9MdOqA==\n-----END PUBLIC KEY-----\n"; // noboost
88
89    const VALID_ED25519_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAnUV+ReQWxMZ3Z2pC/5aOPPjcc8jzOo0ZgSl7+j4AMLo=\n-----END PUBLIC KEY-----\n";
90
91    #[test]
92    fn test_derive_ethereum_address_from_pem_with_invalid_data() {
93        let invalid_pem = "not-a-valid-pem";
94        let result = derive_ethereum_address_from_pem(invalid_pem);
95        assert!(result.is_err());
96
97        // Verify it returns the expected error type
98        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
99    }
100
101    #[test]
102    fn test_derive_ethereum_address_from_pem_with_valid_secp256k1() {
103        let result = derive_ethereum_address_from_pem(VALID_SECP256K1_PEM);
104        assert!(result.is_ok());
105
106        let address = result.unwrap();
107        assert_eq!(address.len(), 20); // Ethereum addresses are 20 bytes
108
109        assert_eq!(
110            format!("0x{}", hex::encode(address)),
111            "0xeeb8861f51b3f3f2204d64bbf7a7eb25e1b4d6cd"
112        );
113    }
114
115    #[test]
116    fn test_derive_ethereum_address_from_der_with_invalid_data() {
117        let invalid_der = &[1, 2, 3];
118        let result = derive_ethereum_address_from_der(invalid_der);
119        assert!(result.is_err());
120
121        // Verify it returns the expected error type
122        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
123    }
124
125    #[test]
126    fn test_derive_ethereum_address_from_der_with_valid_secp256k1() {
127        let pem = pem::parse(VALID_SECP256K1_PEM).unwrap();
128        let der = pem.contents();
129        let result = derive_ethereum_address_from_der(der);
130
131        assert!(result.is_ok());
132
133        let address = result.unwrap();
134        assert_eq!(address.len(), 20); // Ethereum addresses are 20 bytes
135
136        assert_eq!(
137            format!("0x{}", hex::encode(address)),
138            "0xeeb8861f51b3f3f2204d64bbf7a7eb25e1b4d6cd"
139        );
140    }
141
142    #[test]
143    fn test_derive_solana_address_from_pem_with_invalid_data() {
144        let invalid_pem = "not-a-valid-pem";
145        let result = derive_solana_address_from_pem(invalid_pem);
146        assert!(result.is_err());
147
148        // Verify it returns the expected error type
149        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
150    }
151
152    #[test]
153    fn test_derive_solana_address_from_pem_with_valid_ed25519() {
154        let result = derive_solana_address_from_pem(VALID_ED25519_PEM);
155        assert!(result.is_ok());
156
157        let address = result.unwrap();
158        // Solana addresses are base58 encoded, should be around 32-44 characters
159        assert!(!address.is_empty());
160        assert!(address.len() >= 32 && address.len() <= 44);
161
162        assert_eq!(address, "BavUBpkD77FABnevMkBVqV8BDHv7gX8sSoYYJY9WU9L5");
163    }
164
165    #[test]
166    fn test_derive_solana_address_from_pem_with_invalid_key_length() {
167        // Create a PEM with invalid ed25519 key length
168        let invalid_ed25519_der = vec![0u8; 10]; // Too short
169        let invalid_pem = pem::Pem::new("PUBLIC KEY", invalid_ed25519_der);
170        let invalid_pem_str = pem::encode(&invalid_pem);
171
172        let result = derive_solana_address_from_pem(&invalid_pem_str);
173        assert!(result.is_err());
174
175        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
176    }
177
178    #[test]
179    fn test_derive_stellar_address_from_pem_with_valid_ed25519() {
180        let result = derive_stellar_address_from_pem(VALID_ED25519_PEM);
181        assert!(result.is_ok());
182
183        let address = result.unwrap();
184        // Stellar addresses start with 'G' for accounts
185        assert!(address.starts_with('G'));
186        // Stellar addresses are base32 encoded and 56 characters long
187        assert_eq!(address.len(), 56);
188    }
189
190    #[test]
191    fn test_derive_stellar_address_from_pem_with_invalid_data() {
192        let invalid_pem = "not-a-valid-pem";
193        let result = derive_stellar_address_from_pem(invalid_pem);
194        assert!(result.is_err());
195
196        // Verify it returns the expected error type
197        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
198    }
199
200    #[test]
201    fn test_derive_stellar_address_from_pem_with_invalid_key_length() {
202        // Create a PEM with invalid ed25519 key length
203        let invalid_ed25519_der = vec![0u8; 10]; // Too short
204        let invalid_pem = pem::Pem::new("PUBLIC KEY", invalid_ed25519_der);
205        let invalid_pem_str = pem::encode(&invalid_pem);
206
207        let result = derive_stellar_address_from_pem(&invalid_pem_str);
208        assert!(result.is_err());
209
210        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
211    }
212
213    // Tests for DER-based derivation functions
214    fn create_ed25519_spki_der(public_key: &[u8; 32]) -> Vec<u8> {
215        let mut der = vec![
216            0x30, 0x2a, // SEQUENCE, 42 bytes
217            0x30, 0x05, // SEQUENCE, 5 bytes
218            0x06, 0x03, 0x2b, 0x65, 0x70, // OID 1.3.101.112 (Ed25519)
219            0x03, 0x21, // BIT STRING, 33 bytes
220            0x00, // zero unused bits
221        ];
222        der.extend_from_slice(public_key);
223        der
224    }
225
226    // Test public key matching VALID_ED25519_PEM
227    const TEST_ED25519_PUBLIC_KEY: [u8; 32] = [
228        0x9d, 0x45, 0x7e, 0x45, 0xe4, 0x16, 0xc4, 0xc6, 0x77, 0x67, 0x6a, 0x42, 0xff, 0x96, 0x8e,
229        0x3c, 0xf8, 0xdc, 0x73, 0xc8, 0xf3, 0x3a, 0x8d, 0x19, 0x81, 0x29, 0x7b, 0xfa, 0x3e, 0x00,
230        0x30, 0xba,
231    ];
232
233    #[test]
234    fn test_derive_solana_address_from_der_with_spki_format() {
235        let spki_der = create_ed25519_spki_der(&TEST_ED25519_PUBLIC_KEY);
236        let result = derive_solana_address_from_der(&spki_der);
237        assert!(result.is_ok());
238
239        let address = result.unwrap();
240        assert_eq!(address, "BavUBpkD77FABnevMkBVqV8BDHv7gX8sSoYYJY9WU9L5");
241    }
242
243    #[test]
244    fn test_derive_solana_address_from_der_with_raw_key() {
245        let result = derive_solana_address_from_der(&TEST_ED25519_PUBLIC_KEY);
246        assert!(result.is_ok());
247
248        let address = result.unwrap();
249        assert_eq!(address, "BavUBpkD77FABnevMkBVqV8BDHv7gX8sSoYYJY9WU9L5");
250    }
251
252    #[test]
253    fn test_derive_solana_address_from_der_with_invalid_length() {
254        let invalid_der = vec![0u8; 10];
255        let result = derive_solana_address_from_der(&invalid_der);
256        assert!(result.is_err());
257        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
258    }
259
260    #[test]
261    fn test_derive_stellar_address_from_der_with_spki_format() {
262        let spki_der = create_ed25519_spki_der(&TEST_ED25519_PUBLIC_KEY);
263        let result = derive_stellar_address_from_der(&spki_der);
264        assert!(result.is_ok());
265
266        let address = result.unwrap();
267        assert!(address.starts_with('G'));
268        assert_eq!(address.len(), 56);
269    }
270
271    #[test]
272    fn test_derive_stellar_address_from_der_with_raw_key() {
273        let result = derive_stellar_address_from_der(&TEST_ED25519_PUBLIC_KEY);
274        assert!(result.is_ok());
275
276        let address = result.unwrap();
277        assert!(address.starts_with('G'));
278        assert_eq!(address.len(), 56);
279    }
280
281    #[test]
282    fn test_derive_stellar_address_from_der_with_invalid_length() {
283        let invalid_der = vec![0u8; 50];
284        let result = derive_stellar_address_from_der(&invalid_der);
285        assert!(result.is_err());
286        assert!(matches!(result, Err(AddressDerivationError::ParseError(_))));
287    }
288
289    #[test]
290    fn test_der_and_pem_produce_same_addresses() {
291        // Verify that DER and PEM functions produce the same results
292        let pem = pem::parse(VALID_ED25519_PEM).unwrap();
293        let der = pem.contents();
294
295        let solana_from_pem = derive_solana_address_from_pem(VALID_ED25519_PEM).unwrap();
296        let solana_from_der = derive_solana_address_from_der(der).unwrap();
297        assert_eq!(solana_from_pem, solana_from_der);
298
299        let stellar_from_pem = derive_stellar_address_from_pem(VALID_ED25519_PEM).unwrap();
300        let stellar_from_der = derive_stellar_address_from_der(der).unwrap();
301        assert_eq!(stellar_from_pem, stellar_from_der);
302    }
303}