1use crate::{
2 error::KoraError,
3 signer::config::{SelectionStrategy, SignerConfig, SignerPoolConfig},
4};
5use rand::Rng;
6use solana_keychain::{Signer, SolanaSigner};
7use solana_sdk::pubkey::Pubkey;
8use std::{
9 str::FromStr,
10 sync::{
11 atomic::{AtomicU64, AtomicUsize, Ordering},
12 Arc,
13 },
14};
15
16const DEFAULT_WEIGHT: u32 = 1;
17
18pub(crate) struct SignerWithMetadata {
20 name: String,
22 signer: Arc<Signer>,
24 weight: u32,
26 last_used: AtomicU64,
28}
29
30impl Clone for SignerWithMetadata {
31 fn clone(&self) -> Self {
32 Self {
33 name: self.name.clone(),
34 signer: self.signer.clone(),
35 weight: self.weight,
36 last_used: AtomicU64::new(self.last_used.load(Ordering::Relaxed)),
37 }
38 }
39}
40
41impl SignerWithMetadata {
42 pub(crate) fn new(name: String, signer: Arc<Signer>, weight: u32) -> Self {
44 Self { name, signer, weight, last_used: AtomicU64::new(0) }
45 }
46
47 fn update_last_used(&self) {
49 let now = std::time::SystemTime::now()
50 .duration_since(std::time::UNIX_EPOCH)
51 .unwrap_or_default()
52 .as_secs();
53 self.last_used.store(now, Ordering::Relaxed);
54 }
55}
56
57pub struct SignerPool {
59 signers: Vec<SignerWithMetadata>,
61 strategy: SelectionStrategy,
63 current_index: AtomicUsize,
65 total_weight: u32,
67}
68
69#[derive(Debug, Clone)]
71pub struct SignerInfo {
72 pub public_key: String,
73 pub name: String,
74 pub weight: u32,
75 pub last_used: u64, }
77
78impl SignerPool {
79 #[cfg(test)]
80 pub(crate) fn new(signers: Vec<SignerWithMetadata>) -> Self {
81 let total_weight: u32 = signers.iter().map(|s| s.weight).sum();
82
83 Self {
84 signers,
85 strategy: SelectionStrategy::RoundRobin,
86 current_index: AtomicUsize::new(0),
87 total_weight,
88 }
89 }
90
91 pub async fn from_config(config: SignerPoolConfig) -> Result<Self, KoraError> {
93 if config.signers.is_empty() {
94 return Err(KoraError::ValidationError("Cannot create empty signer pool".to_string()));
95 }
96
97 let mut signers = Vec::new();
98
99 for signer_config in config.signers {
100 log::info!("Initializing signer: {}", signer_config.name);
101
102 let signer = SignerConfig::build_signer_from_config(&signer_config).await?;
103 let weight = signer_config.weight.unwrap_or(DEFAULT_WEIGHT);
104
105 signers.push(SignerWithMetadata::new(
106 signer_config.name.clone(),
107 Arc::new(signer),
108 weight,
109 ));
110
111 log::info!(
112 "Successfully initialized signer: {} (weight: {})",
113 signer_config.name,
114 weight
115 );
116 }
117
118 let total_weight: u32 = signers.iter().map(|s| s.weight).sum();
119
120 if matches!(config.signer_pool.strategy, SelectionStrategy::Weighted) && total_weight == 0 {
121 return Err(KoraError::InternalServerError(
122 "All signers have zero weight while using weighted selection strategy".to_string(),
123 ));
124 }
125
126 log::info!(
127 "Created signer pool with {} signers using {:?} strategy",
128 signers.len(),
129 config.signer_pool.strategy
130 );
131
132 Ok(Self {
133 signers,
134 strategy: config.signer_pool.strategy,
135 current_index: AtomicUsize::new(0),
136 total_weight,
137 })
138 }
139
140 pub fn get_next_signer(&self) -> Result<Arc<Signer>, KoraError> {
142 if self.signers.is_empty() {
143 return Err(KoraError::InternalServerError("Signer pool is empty".to_string()));
144 }
145
146 let signer_meta = match self.strategy {
147 SelectionStrategy::RoundRobin => self.round_robin_select(),
148 SelectionStrategy::Random => self.random_select(),
149 SelectionStrategy::Weighted => self.weighted_select(),
150 }?;
151
152 signer_meta.update_last_used();
153 Ok(Arc::clone(&signer_meta.signer))
154 }
155
156 fn round_robin_select(&self) -> Result<&SignerWithMetadata, KoraError> {
158 let index = self.current_index.fetch_add(1, Ordering::AcqRel);
159 let signer_index = index % self.signers.len();
160 Ok(&self.signers[signer_index])
161 }
162
163 fn random_select(&self) -> Result<&SignerWithMetadata, KoraError> {
165 let mut rng = rand::rng();
166 let index = rng.random_range(0..self.signers.len());
167 Ok(&self.signers[index])
168 }
169
170 fn weighted_select(&self) -> Result<&SignerWithMetadata, KoraError> {
172 let mut rng = rand::rng();
173 let mut target = rng.random_range(0..self.total_weight);
174
175 for signer in &self.signers {
176 if target < signer.weight {
177 return Ok(signer);
178 }
179 target -= signer.weight;
180 }
181
182 Ok(&self.signers[0])
184 }
185
186 pub fn get_signers_info(&self) -> Vec<SignerInfo> {
188 self.signers
189 .iter()
190 .map(|s| SignerInfo {
191 public_key: s.signer.pubkey().to_string(),
192 name: s.name.clone(),
193 weight: s.weight,
194 last_used: s.last_used.load(Ordering::Relaxed),
195 })
196 .collect()
197 }
198
199 pub fn len(&self) -> usize {
201 self.signers.len()
202 }
203
204 pub fn is_empty(&self) -> bool {
206 self.signers.is_empty()
207 }
208
209 pub fn strategy(&self) -> &SelectionStrategy {
211 &self.strategy
212 }
213
214 pub fn get_signer_by_pubkey(&self, pubkey: &str) -> Result<Arc<Signer>, KoraError> {
216 let target_pubkey = Pubkey::from_str(pubkey).map_err(|_| {
218 KoraError::ValidationError(format!("Invalid signer signer key pubkey: {pubkey}"))
219 })?;
220
221 let signer_meta =
223 self.signers.iter().find(|s| s.signer.pubkey() == target_pubkey).ok_or_else(|| {
224 KoraError::ValidationError(format!("Signer with pubkey {pubkey} not found in pool"))
225 })?;
226
227 signer_meta.update_last_used();
228 Ok(Arc::clone(&signer_meta.signer))
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use solana_sdk::signature::Keypair;
235
236 use super::*;
237 use std::collections::HashMap;
238
239 fn create_test_pool() -> SignerPool {
240 let keypair1 = Keypair::new();
242 let keypair2 = Keypair::new();
243
244 let external_signer1 =
245 solana_keychain::Signer::from_memory(&keypair1.to_base58_string()).unwrap();
246 let external_signer2 =
247 solana_keychain::Signer::from_memory(&keypair2.to_base58_string()).unwrap();
248
249 SignerPool {
250 signers: vec![
251 SignerWithMetadata::new("signer_1".to_string(), Arc::new(external_signer1), 1),
252 SignerWithMetadata::new("signer_2".to_string(), Arc::new(external_signer2), 2),
253 ],
254 strategy: SelectionStrategy::RoundRobin,
255 current_index: AtomicUsize::new(0),
256 total_weight: 3,
257 }
258 }
259
260 #[test]
261 fn test_round_robin_selection() {
262 let pool = create_test_pool();
263
264 let mut selections = HashMap::new();
266 for _ in 0..100 {
267 let signer = pool.get_next_signer().unwrap();
268 *selections.entry(signer.pubkey().to_string()).or_insert(0) += 1;
269 }
270
271 assert_eq!(selections.len(), 2);
273 assert!(selections.values().all(|&count| count == 50));
275 }
276
277 #[test]
278 fn test_weighted_selection() {
279 let mut pool = create_test_pool();
280 pool.strategy = SelectionStrategy::Weighted;
281
282 let signer1_pubkey = pool.signers[0].signer.pubkey().to_string();
284 let signer2_pubkey = pool.signers[1].signer.pubkey().to_string();
285
286 let mut selections = HashMap::new();
288 for _ in 0..300 {
289 let signer = pool.get_next_signer().unwrap();
290 *selections.entry(signer.pubkey().to_string()).or_insert(0) += 1;
291 }
292
293 let signer1_count = selections.get(&signer1_pubkey).unwrap_or(&0);
296 let signer2_count = selections.get(&signer2_pubkey).unwrap_or(&0);
297
298 assert!(*signer2_count > *signer1_count);
300 assert!(*signer2_count > 150); assert!(*signer1_count > 50); }
303
304 #[test]
305 fn test_empty_pool() {
306 let pool = SignerPool {
307 signers: vec![],
308 strategy: SelectionStrategy::RoundRobin,
309 current_index: AtomicUsize::new(0),
310 total_weight: 0,
311 };
312
313 assert!(pool.get_next_signer().is_err());
314 assert!(pool.is_empty());
315 assert_eq!(pool.len(), 0);
316 }
317}