Skip to main content

chains_sdk/solana/
mod.rs

1//! Solana Ed25519 signer (pure Ed25519).
2//!
3//! Uses `ed25519-dalek` for signing. No pre-hashing — the blockchain
4//! hashes transactions before feeding them to the signer.
5
6pub mod programs;
7pub mod transaction;
8
9use crate::error::SignerError;
10use crate::traits;
11use ed25519_dalek::Signer as DalekSigner;
12use ed25519_dalek::Verifier as DalekVerifier;
13use sha2::{Digest, Sha512};
14use zeroize::Zeroizing;
15
16/// A Solana Ed25519 signature (64 bytes).
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[must_use]
20pub struct SolanaSignature {
21    /// The 64-byte Ed25519 signature.
22    #[cfg_attr(feature = "serde", serde(with = "crate::hex_bytes"))]
23    pub bytes: [u8; 64],
24}
25
26impl SolanaSignature {
27    /// Export the 64-byte signature.
28    pub fn to_bytes(&self) -> [u8; 64] {
29        self.bytes
30    }
31
32    /// Import from 64 bytes.
33    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
34        if bytes.len() != 64 {
35            return Err(SignerError::InvalidSignature(format!(
36                "expected 64 bytes, got {}",
37                bytes.len()
38            )));
39        }
40        let mut out = [0u8; 64];
41        out.copy_from_slice(bytes);
42        Ok(Self { bytes: out })
43    }
44}
45
46impl core::fmt::Display for SolanaSignature {
47    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
48        for byte in &self.bytes {
49            write!(f, "{byte:02x}")?;
50        }
51        Ok(())
52    }
53}
54
55/// Solana Ed25519 signer.
56pub struct SolanaSigner {
57    pub(crate) signing_key: ed25519_dalek::SigningKey,
58}
59
60impl SolanaSigner {
61    /// Return the Solana address as a Base58-encoded string.
62    ///
63    /// Solana addresses are simply the Base58 encoding of the 32-byte Ed25519 public key.
64    pub fn address(&self) -> String {
65        bs58::encode(self.signing_key.verifying_key().as_bytes()).into_string()
66    }
67
68    /// Return the 32-byte public key as a fixed-size array.
69    #[must_use]
70    pub fn public_key_bytes_32(&self) -> [u8; 32] {
71        *self.signing_key.verifying_key().as_bytes()
72    }
73}
74
75/// Validate a Solana address string.
76///
77/// Solana addresses are Base58-encoded 32-byte Ed25519 public keys.
78pub fn validate_address(address: &str) -> bool {
79    match bs58::decode(address).into_vec() {
80        Ok(bytes) => bytes.len() == 32,
81        Err(_) => false,
82    }
83}
84
85impl Drop for SolanaSigner {
86    fn drop(&mut self) {
87        // ed25519_dalek::SigningKey handles its own zeroization
88    }
89}
90
91impl traits::Signer for SolanaSigner {
92    type Signature = SolanaSignature;
93    type Error = SignerError;
94
95    fn sign(&self, message: &[u8]) -> Result<SolanaSignature, SignerError> {
96        let sig = DalekSigner::sign(&self.signing_key, message);
97        Ok(SolanaSignature {
98            bytes: sig.to_bytes(),
99        })
100    }
101
102    /// **Note:** Ed25519 hashes internally per RFC 8032. This method is identical to
103    /// `sign()` — the `digest` parameter is treated as a raw message, not a
104    /// pre-computed hash. For consistency with the `Signer` trait, this is provided as-is.
105    fn sign_prehashed(&self, digest: &[u8]) -> Result<SolanaSignature, SignerError> {
106        // Ed25519 has no internal pre-hashing in Solana context.
107        // sign_prehashed is equivalent to sign (the caller provides the raw payload).
108        self.sign(digest)
109    }
110
111    fn public_key_bytes(&self) -> Vec<u8> {
112        self.signing_key.verifying_key().as_bytes().to_vec()
113    }
114
115    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
116        // Ed25519 has no uncompressed form
117        self.public_key_bytes()
118    }
119}
120
121impl traits::KeyPair for SolanaSigner {
122    fn generate() -> Result<Self, SignerError> {
123        let mut key_bytes = zeroize::Zeroizing::new([0u8; 32]);
124        crate::security::secure_random(&mut *key_bytes)?;
125        let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_bytes);
126        Ok(Self { signing_key })
127    }
128
129    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
130        if private_key.len() != 32 {
131            return Err(SignerError::InvalidPrivateKey(format!(
132                "expected 32 bytes (Ed25519 seed), got {}",
133                private_key.len()
134            )));
135        }
136        let mut bytes = [0u8; 32];
137        bytes.copy_from_slice(private_key);
138        let signing_key = ed25519_dalek::SigningKey::from_bytes(&bytes);
139        Ok(Self { signing_key })
140    }
141
142    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
143        Zeroizing::new(self.signing_key.to_bytes().to_vec())
144    }
145
146    /// Import from Solana's 64-byte keypair format (seed ∥ pubkey).
147    /// Validates that the derived public key matches bytes[32..64]
148    /// using constant-time comparison to prevent timing side-channels.
149    fn from_keypair_bytes(keypair: &[u8]) -> Result<Self, SignerError> {
150        use subtle::ConstantTimeEq;
151        if keypair.len() != 64 {
152            return Err(SignerError::InvalidPrivateKey(format!(
153                "expected 64-byte keypair, got {}",
154                keypair.len()
155            )));
156        }
157        let signer = Self::from_bytes(&keypair[..32])?;
158        let derived_pk = signer.signing_key.verifying_key().as_bytes().to_vec();
159        if derived_pk.ct_eq(&keypair[32..]).into() {
160            Ok(signer)
161        } else {
162            Err(SignerError::InvalidPrivateKey(
163                "pubkey in keypair does not match derived pubkey".into(),
164            ))
165        }
166    }
167
168    /// Export as Solana's 64-byte keypair (seed ∥ pubkey).
169    fn keypair_bytes(&self) -> Zeroizing<Vec<u8>> {
170        let mut kp = Vec::with_capacity(64);
171        kp.extend_from_slice(&self.signing_key.to_bytes());
172        kp.extend_from_slice(self.signing_key.verifying_key().as_bytes());
173        Zeroizing::new(kp)
174    }
175}
176
177impl SolanaSigner {
178    /// Export the clamped Ed25519 scalar (first 32 bytes of SHA-512(seed), clamped).
179    ///
180    /// ⚠️ **Advanced use only** — for MPC, threshold signing, or key derivation.
181    /// The scalar is the actual private scalar used in Ed25519 signing.
182    pub fn scalar_bytes(&self) -> Zeroizing<Vec<u8>> {
183        let expanded = Sha512::digest(self.signing_key.to_bytes());
184        let mut scalar = [0u8; 32];
185        scalar.copy_from_slice(&expanded[..32]);
186        // Apply Ed25519 clamping
187        scalar[0] &= 248;
188        scalar[31] &= 127;
189        scalar[31] |= 64;
190        Zeroizing::new(scalar.to_vec())
191    }
192}
193
194/// Solana Ed25519 verifier.
195pub struct SolanaVerifier {
196    verifying_key: ed25519_dalek::VerifyingKey,
197}
198
199impl SolanaVerifier {
200    /// Create from 32-byte Ed25519 public key.
201    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
202        if bytes.len() != 32 {
203            return Err(SignerError::InvalidPublicKey(format!(
204                "expected 32 bytes, got {}",
205                bytes.len()
206            )));
207        }
208        let mut pk_bytes = [0u8; 32];
209        pk_bytes.copy_from_slice(bytes);
210        let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes)
211            .map_err(|e| SignerError::InvalidPublicKey(e.to_string()))?;
212        Ok(Self { verifying_key })
213    }
214}
215
216impl traits::Verifier for SolanaVerifier {
217    type Signature = SolanaSignature;
218    type Error = SignerError;
219
220    fn verify(&self, message: &[u8], signature: &SolanaSignature) -> Result<bool, SignerError> {
221        let sig = ed25519_dalek::Signature::from_bytes(&signature.bytes);
222        match DalekVerifier::verify(&self.verifying_key, message, &sig) {
223            Ok(()) => Ok(true),
224            Err(_) => Ok(false),
225        }
226    }
227
228    fn verify_prehashed(
229        &self,
230        digest: &[u8],
231        signature: &SolanaSignature,
232    ) -> Result<bool, SignerError> {
233        self.verify(digest, signature)
234    }
235}
236
237#[cfg(test)]
238#[allow(clippy::unwrap_used, clippy::expect_used)]
239mod tests {
240    use super::*;
241    use crate::traits::{KeyPair, Signer, Verifier};
242
243    #[test]
244    fn test_generate_keypair() {
245        let signer = SolanaSigner::generate().unwrap();
246        assert_eq!(signer.public_key_bytes().len(), 32);
247    }
248
249    #[test]
250    fn test_from_bytes_roundtrip() {
251        let signer = SolanaSigner::generate().unwrap();
252        let restored = SolanaSigner::from_bytes(&signer.private_key_bytes()).unwrap();
253        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
254    }
255
256    #[test]
257    fn test_sign_verify_roundtrip() {
258        let signer = SolanaSigner::generate().unwrap();
259        let sig = signer.sign(b"hello solana").unwrap();
260        let verifier = SolanaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
261        assert!(verifier.verify(b"hello solana", &sig).unwrap());
262    }
263
264    #[test]
265    fn test_64_byte_signature() {
266        let signer = SolanaSigner::generate().unwrap();
267        let sig = signer.sign(b"test").unwrap();
268        assert_eq!(sig.bytes.len(), 64);
269    }
270
271    // RFC 8032 §7.1 Test Vector 1 — Empty message
272    #[test]
273    fn test_rfc8032_vector1_empty() {
274        let sk = hex::decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
275            .unwrap();
276        let expected_sig = hex::decode(
277            "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"
278        ).unwrap();
279
280        let signer = SolanaSigner::from_bytes(&sk).unwrap();
281        let sig = signer.sign(b"").unwrap();
282        assert_eq!(sig.bytes.to_vec(), expected_sig);
283
284        // Verify the signature we produced is valid
285        let verifier = SolanaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
286        assert!(verifier.verify(b"", &sig).unwrap());
287    }
288
289    // RFC 8032 §7.1 Test Vector 2 — Single byte 0x72
290    #[test]
291    fn test_rfc8032_vector2_single_byte() {
292        let sk = hex::decode("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb")
293            .unwrap();
294        let expected_pk =
295            hex::decode("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c")
296                .unwrap();
297        let msg = hex::decode("72").unwrap();
298        let expected_sig = hex::decode(
299            "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"
300        ).unwrap();
301
302        let signer = SolanaSigner::from_bytes(&sk).unwrap();
303        assert_eq!(signer.public_key_bytes(), expected_pk);
304
305        let sig = signer.sign(&msg).unwrap();
306        assert_eq!(sig.bytes.to_vec(), expected_sig);
307    }
308
309    // RFC 8032 §7.1 Test Vector 3 — Two bytes 0xaf82
310    #[test]
311    fn test_rfc8032_vector3_two_bytes() {
312        let sk = hex::decode("c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7")
313            .unwrap();
314        let expected_pk =
315            hex::decode("fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025")
316                .unwrap();
317        let msg = hex::decode("af82").unwrap();
318        let expected_sig = hex::decode(
319            "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"
320        ).unwrap();
321
322        let signer = SolanaSigner::from_bytes(&sk).unwrap();
323        assert_eq!(signer.public_key_bytes(), expected_pk);
324
325        let sig = signer.sign(&msg).unwrap();
326        assert_eq!(sig.bytes.to_vec(), expected_sig);
327    }
328
329    #[test]
330    fn test_invalid_key_rejected() {
331        assert!(SolanaSigner::from_bytes(&[1u8; 31]).is_err());
332        assert!(SolanaSigner::from_bytes(&[1u8; 33]).is_err());
333    }
334
335    #[test]
336    fn test_tampered_sig_fails() {
337        let signer = SolanaSigner::generate().unwrap();
338        let sig = signer.sign(b"tamper").unwrap();
339        let verifier = SolanaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
340        let mut tampered = sig.clone();
341        tampered.bytes[0] ^= 0xff;
342        assert!(!verifier.verify(b"tamper", &tampered).unwrap());
343    }
344
345    #[test]
346    fn test_wrong_pubkey_fails() {
347        let s1 = SolanaSigner::generate().unwrap();
348        let s2 = SolanaSigner::generate().unwrap();
349        let sig = s1.sign(b"wrong").unwrap();
350        let verifier = SolanaVerifier::from_public_key_bytes(&s2.public_key_bytes()).unwrap();
351        assert!(!verifier.verify(b"wrong", &sig).unwrap());
352    }
353
354    #[test]
355    fn test_sign_prehashed_roundtrip() {
356        let signer = SolanaSigner::generate().unwrap();
357        let msg = b"prehash solana";
358        let sig = signer.sign_prehashed(msg).unwrap();
359        let verifier = SolanaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
360        assert!(verifier.verify(msg, &sig).unwrap());
361    }
362
363    #[test]
364    fn test_zeroize_on_drop() {
365        let signer = SolanaSigner::generate().unwrap();
366        let _: Zeroizing<Vec<u8>> = signer.private_key_bytes();
367        drop(signer);
368    }
369}