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