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