soroban_rs/
signer.rs

1//! # Soroban Transaction Signing
2//!
3//! This module provides functionality for creating signers and signing Soroban transactions.
4//! It handles the cryptographic operations required to sign transactions using Ed25519 keys,
5//! following the Stellar transaction signing protocol.
6//!
7//! ## Example
8//!
9//! ```rust,no_run
10//! use soroban_rs::{Env, Signer};
11//! use ed25519_dalek::SigningKey;
12//! use stellar_xdr::curr::Transaction;
13//!
14//! async fn example(tx: Transaction, env: Env) {
15//!     // Create a signer from a signing key
16//!     let private_key_bytes: [u8; 32] = [
17//!         1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
18//!         26, 27, 28, 29, 30, 31, 32,
19//!     ];
20//!     let signing_key = SigningKey::from_bytes(&private_key_bytes);
21//!     let signer = Signer::new(signing_key);
22//!
23//!     // Get the associated Stellar account ID
24//!     let account_id = signer.account_id();
25//!
26//!     // Sign a transaction
27//!     let signature = signer.sign_transaction(&tx, &env.network_id()).unwrap();
28//! }
29//! ```
30use crate::error::SorobanHelperError;
31use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey};
32use sha2::{Digest, Sha256};
33use stellar_strkey::ed25519::PublicKey;
34use stellar_xdr::curr::{
35    AccountId, DecoratedSignature, Hash, Limits, PublicKey as XDRPublicKey, Signature,
36    SignatureHint, Transaction, TransactionSignaturePayload,
37    TransactionSignaturePayloadTaggedTransaction, WriteXdr,
38};
39
40impl From<&[u8; 32]> for Signer {
41    fn from(bytes: &[u8; 32]) -> Self {
42        Signer::new(SigningKey::from_bytes(bytes))
43    }
44}
45
46/// A transaction signer for Soroban operations.
47///
48/// The Signer manages an Ed25519 key pair and provides methods to sign Stellar transactions.
49/// It handles the conversion between various key formats used in the Stellar ecosystem
50/// and implements the Stellar transaction signing protocol.
51#[derive(Clone)]
52pub struct Signer {
53    /// The Ed25519 signing key (private key)
54    signing_key: SigningKey,
55    /// The corresponding public key in Stellar format
56    public_key: PublicKey,
57    /// The Stellar account ID derived from the public key
58    account_id: AccountId,
59}
60
61impl Signer {
62    /// Creates a new signer from an Ed25519 signing key.
63    ///
64    /// # Parameters
65    ///
66    /// * `signing_key` - The Ed25519 signing key (private key)
67    ///
68    /// # Returns
69    ///
70    /// A new Signer instance
71    pub fn new(signing_key: SigningKey) -> Self {
72        let public_key = PublicKey(*signing_key.verifying_key().as_bytes());
73        let account_id = AccountId(XDRPublicKey::PublicKeyTypeEd25519(public_key.0.into()));
74
75        Self {
76            signing_key,
77            public_key,
78            account_id,
79        }
80    }
81
82    /// Returns the public key associated with this signer.
83    ///
84    /// # Returns
85    ///
86    /// The Stellar public key
87    pub fn public_key(&self) -> PublicKey {
88        self.public_key
89    }
90
91    /// Returns the Stellar account ID associated with this signer.
92    ///
93    /// # Returns
94    ///
95    /// The Stellar account ID
96    pub fn account_id(&self) -> AccountId {
97        self.account_id.clone()
98    }
99
100    /// Signs a transaction with this signer's private key.
101    ///
102    /// # Parameters
103    ///
104    /// * `tx` - The transaction to sign
105    /// * `network_id` - The network ID hash
106    ///
107    /// # Returns
108    ///
109    /// A decorated signature that can be attached to the transaction
110    ///
111    /// # Errors
112    ///
113    /// Returns:
114    /// - `SorobanHelperError::XdrEncodingFailed` if the transaction payload cannot be encoded
115    /// - `SorobanHelperError::SigningFailed` if there is an error creating the signature
116    pub fn sign_transaction(
117        &self,
118        tx: &Transaction,
119        network_id: &Hash,
120    ) -> Result<DecoratedSignature, SorobanHelperError> {
121        let signature_payload = TransactionSignaturePayload {
122            network_id: network_id.clone(),
123            tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(tx.clone()),
124        };
125
126        let tx_hash: [u8; 32] = Sha256::digest(
127            signature_payload
128                .to_xdr(Limits::none())
129                .map_err(|e| SorobanHelperError::XdrEncodingFailed(e.to_string()))?,
130        )
131        .into();
132
133        let hint = SignatureHint(
134            self.signing_key.verifying_key().to_bytes()[28..]
135                .try_into()
136                .map_err(|_| {
137                    SorobanHelperError::SigningFailed("Failed to create signature hint".to_string())
138                })?,
139        );
140
141        let signature = Signature(
142            self.signing_key
143                .clone()
144                .sign(&tx_hash)
145                .to_bytes()
146                .to_vec()
147                .try_into()
148                .map_err(|_| {
149                    SorobanHelperError::SigningFailed(
150                        "Failed to convert signature to XDR".to_string(),
151                    )
152                })?,
153        );
154
155        Ok(DecoratedSignature { hint, signature })
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use stellar_xdr::curr::BytesM;
162
163    use crate::mock::mock_transaction;
164
165    use super::*;
166
167    #[test]
168    fn test_public_key() {
169        let signing_key = SigningKey::from_bytes(&[42; 32]);
170        let public_key = PublicKey(*signing_key.verifying_key().as_bytes());
171
172        let signer = Signer::new(signing_key);
173        assert_eq!(signer.public_key(), public_key);
174    }
175
176    #[test]
177    fn test_account_id() {
178        let signing_key = SigningKey::from_bytes(&[42; 32]);
179        let public_key = PublicKey(*signing_key.verifying_key().as_bytes());
180        let account_id = AccountId(XDRPublicKey::PublicKeyTypeEd25519(public_key.0.into()));
181
182        let signer = Signer::new(signing_key);
183        assert_eq!(signer.account_id(), account_id);
184    }
185
186    #[test]
187    fn test_sign_transaction() {
188        let signing_key = SigningKey::from_bytes(&[42; 32]);
189        let public_key = PublicKey(*signing_key.verifying_key().as_bytes());
190        let account_id = AccountId(XDRPublicKey::PublicKeyTypeEd25519(public_key.0.into()));
191
192        let signer = Signer::new(signing_key);
193
194        let transaction = mock_transaction(account_id, vec![]);
195        let network_id = Hash::from([42; 32]);
196
197        let decorated_signature = signer.sign_transaction(&transaction, &network_id).unwrap();
198
199        // hex encoded hint
200        let hint_vec = hex::decode("3d368d61").expect("Invalid hex");
201        let hint: [u8; 4] = hint_vec[..4]
202            .try_into()
203            .expect("slice with incorrect length");
204
205        // hex encoded signature
206        let signature_vec = hex::decode("c84612be60b83b3e13e18880b6f35c94bda449a53103367b78e211f0a7614dc0df02e45539a4879fc37fb908d7983efba2d7019c1ef5732f0c1331b808eec102").expect("Invalid hex");
207        let signature_bytes: BytesM<64> = signature_vec
208            .try_into()
209            .expect("slice with incorrect length");
210
211        assert_eq!(decorated_signature.hint, SignatureHint(hint));
212        assert_eq!(decorated_signature.signature, Signature(signature_bytes));
213    }
214}