Skip to content

Commit eef7515

Browse files
amilzgithub-actions
andauthored
chore:(PRO-259) Add additional Validation Checks (#209)
* chore/Warn when 'All' is used for allowed_spl_paid_tokens Adds a warning if 'All' is set for allowed_spl_paid_tokens in the config, alerting users to potential volatility and security risks. Updates tests to verify the warning is triggered. * Update coverage badge [skip ci] * wip: Add signers config validation to CLI and core Introduces optional signers configuration validation to CLI commands and RPC server startup. Adds `SignerValidator` for detailed validation and warnings, updates `ConfigValidator` to support signers config, and enforces non-zero weights for weighted strategy. Improves error and warning reporting for signer configuration issues. * Update coverage badge [skip ci] * refactor * Update coverage badge [skip ci] * chore: Increase test setup timeout to 90 seconds Extended the timeout for test setup from 30,000ms to 90,000ms to allow more time for airdrops and token initialization in integration tests. * chore: add strategy display impl --------- Co-authored-by: github-actions <github-actions@github.com>
1 parent 8458428 commit eef7515

File tree

7 files changed

+293
-23
lines changed

7 files changed

+293
-23
lines changed

.github/badges/coverage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"schemaVersion": 1, "label": "coverage", "message": "86.2%", "color": "green"}
1+
{"schemaVersion": 1, "label": "coverage", "message": "86.3%", "color": "green"}

crates/cli/src/main.rs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,17 @@ enum Commands {
4343
#[derive(Subcommand)]
4444
enum ConfigCommands {
4545
/// Validate configuration file (fast, no RPC calls)
46-
Validate,
46+
Validate {
47+
/// Path to signers configuration file (optional)
48+
#[arg(long)]
49+
signers_config: Option<std::path::PathBuf>,
50+
},
4751
/// Validate configuration file with RPC validation (slower but more thorough)
48-
ValidateWithRpc,
52+
ValidateWithRpc {
53+
/// Path to signers configuration file (optional)
54+
#[arg(long)]
55+
signers_config: Option<std::path::PathBuf>,
56+
},
4957
}
5058

5159
#[derive(Subcommand)]
@@ -116,22 +124,47 @@ async fn main() -> Result<(), KoraError> {
116124
match cli.command {
117125
Some(Commands::Config { config_command }) => {
118126
match config_command {
119-
ConfigCommands::Validate => {
120-
let _ = ConfigValidator::validate_with_result(rpc_client.as_ref(), true).await;
127+
ConfigCommands::Validate { signers_config } => {
128+
let _ = ConfigValidator::validate_with_result_and_signers(
129+
rpc_client.as_ref(),
130+
true,
131+
signers_config.as_ref(),
132+
)
133+
.await;
121134
}
122-
ConfigCommands::ValidateWithRpc => {
123-
let _ = ConfigValidator::validate_with_result(rpc_client.as_ref(), false).await;
135+
ConfigCommands::ValidateWithRpc { signers_config } => {
136+
let _ = ConfigValidator::validate_with_result_and_signers(
137+
rpc_client.as_ref(),
138+
false,
139+
signers_config.as_ref(),
140+
)
141+
.await;
124142
}
125143
}
126144
std::process::exit(0);
127145
}
128146
Some(Commands::Rpc { rpc_command }) => {
129147
match rpc_command {
130148
RpcCommands::Start { rpc_args } => {
131-
// Validate config before starting server
132-
if let Err(e) = ConfigValidator::validate(rpc_client.as_ref()).await {
133-
print_error(&format!("Config validation failed: {e}"));
134-
std::process::exit(1);
149+
// Validate config and signers before starting server
150+
match ConfigValidator::validate_with_result_and_signers(
151+
rpc_client.as_ref(),
152+
true,
153+
rpc_args.signers_config.as_ref(),
154+
)
155+
.await
156+
{
157+
Err(errors) => {
158+
for e in errors {
159+
print_error(&format!("Validation error: {e}"));
160+
}
161+
std::process::exit(1);
162+
}
163+
Ok(warnings) => {
164+
for w in warnings {
165+
println!("Warning: {w}");
166+
}
167+
}
135168
}
136169

137170
setup_logging(&rpc_args.logging_format);

crates/lib/src/signer/config.rs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
},
1111
};
1212
use serde::{Deserialize, Serialize};
13-
use std::{fs, path::Path};
13+
use std::{fmt, fs, path::Path};
1414

1515
/// Configuration for a pool of signers
1616
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -38,6 +38,17 @@ pub enum SelectionStrategy {
3838
Weighted,
3939
}
4040

41+
impl fmt::Display for SelectionStrategy {
42+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43+
let s = match self {
44+
SelectionStrategy::RoundRobin => "round_robin",
45+
SelectionStrategy::Random => "random",
46+
SelectionStrategy::Weighted => "weighted",
47+
};
48+
write!(f, "{s}")
49+
}
50+
}
51+
4152
fn default_strategy() -> SelectionStrategy {
4253
SelectionStrategy::RoundRobin
4354
}
@@ -99,18 +110,31 @@ impl SignerPoolConfig {
99110

100111
/// Validate the signer pool configuration
101112
pub fn validate_signer_config(&self) -> Result<(), KoraError> {
102-
if self.signers.is_empty() {
103-
return Err(KoraError::ValidationError(
104-
"At least one signer must be configured".to_string(),
105-
));
106-
}
113+
// Validate that at least one signer is configured
114+
self.validate_signer_not_empty()?;
107115

108116
// Validate each signer configuration
109117
for (index, signer) in self.signers.iter().enumerate() {
110118
signer.validate_individual_signer_config(index)?;
111119
}
112120

113-
// Check for duplicate names
121+
self.validate_signer_names()?;
122+
123+
self.validate_strategy_weights()?;
124+
125+
Ok(())
126+
}
127+
128+
pub fn validate_signer_not_empty(&self) -> Result<(), KoraError> {
129+
if self.signers.is_empty() {
130+
return Err(KoraError::ValidationError(
131+
"At least one signer must be configured".to_string(),
132+
));
133+
}
134+
Ok(())
135+
}
136+
137+
pub fn validate_signer_names(&self) -> Result<(), KoraError> {
114138
let mut names = std::collections::HashSet::new();
115139
for signer in &self.signers {
116140
if !names.insert(&signer.name) {
@@ -120,7 +144,22 @@ impl SignerPoolConfig {
120144
)));
121145
}
122146
}
147+
Ok(())
148+
}
123149

150+
pub fn validate_strategy_weights(&self) -> Result<(), KoraError> {
151+
if matches!(self.signer_pool.strategy, SelectionStrategy::Weighted) {
152+
for signer in &self.signers {
153+
if let Some(weight) = signer.weight {
154+
if weight == 0 {
155+
return Err(KoraError::ValidationError(format!(
156+
"Signer '{}' has weight of 0 in weighted strategy",
157+
signer.name
158+
)));
159+
}
160+
}
161+
}
162+
}
124163
Ok(())
125164
}
126165
}
@@ -145,7 +184,7 @@ impl SignerConfig {
145184
}
146185

147186
/// Validate an individual signer configuration
148-
fn validate_individual_signer_config(&self, index: usize) -> Result<(), KoraError> {
187+
pub fn validate_individual_signer_config(&self, index: usize) -> Result<(), KoraError> {
149188
if self.name.is_empty() {
150189
return Err(KoraError::ValidationError(format!(
151190
"Signer at index {index} must have a non-empty name"
@@ -230,6 +269,7 @@ weight = 2
230269
};
231270

232271
assert!(config.validate_signer_config().is_ok());
272+
assert!(config.validate_strategy_weights().is_ok());
233273
}
234274

235275
#[test]

crates/lib/src/validator/config_validator.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
use std::str::FromStr;
1+
use std::{path::Path, str::FromStr};
22

33
use crate::{
44
admin::token_util::find_missing_atas,
5-
config::Token2022Config,
5+
config::{SplTokenConfig, Token2022Config},
66
fee::price::PriceModel,
77
oracle::PriceSource,
8+
signer::SignerPoolConfig,
89
state::get_config,
910
token::{spl_token_2022_util, token::TokenUtil},
10-
validator::account_validator::{validate_account, AccountType},
11+
validator::{
12+
account_validator::{validate_account, AccountType},
13+
signer_validator::SignerValidator,
14+
},
1115
KoraError,
1216
};
1317
use solana_client::nonblocking::rpc_client::RpcClient;
@@ -41,6 +45,14 @@ impl ConfigValidator {
4145
pub async fn validate_with_result(
4246
rpc_client: &RpcClient,
4347
skip_rpc_validation: bool,
48+
) -> Result<Vec<String>, Vec<String>> {
49+
Self::validate_with_result_and_signers(rpc_client, skip_rpc_validation, None::<&Path>).await
50+
}
51+
52+
pub async fn validate_with_result_and_signers<P: AsRef<Path>>(
53+
rpc_client: &RpcClient,
54+
skip_rpc_validation: bool,
55+
signers_config_path: Option<P>,
4456
) -> Result<Vec<String>, Vec<String>> {
4557
let mut errors = Vec::new();
4658
let mut warnings = Vec::new();
@@ -119,6 +131,15 @@ impl ConfigValidator {
119131
errors.push(format!("Invalid spl paid token address: {e}"));
120132
}
121133

134+
// Warn if using "All" for allowed_spl_paid_tokens
135+
if matches!(config.validation.allowed_spl_paid_tokens, SplTokenConfig::All) {
136+
warnings.push(
137+
"⚠️ Using 'All' for allowed_spl_paid_tokens - this accepts ANY SPL token for payment. \
138+
Consider using an explicit allowlist to reduce volatility risk and protect against \
139+
potentially malicious or worthless tokens being used for fees.".to_string()
140+
);
141+
}
142+
122143
// Validate disallowed accounts
123144
if let Err(e) = TokenUtil::check_valid_tokens(&config.validation.disallowed_accounts) {
124145
errors.push(format!("Invalid disallowed account address: {e}"));
@@ -236,6 +257,23 @@ impl ConfigValidator {
236257
}
237258
}
238259

260+
// Validate signers configuration if provided
261+
if let Some(path) = signers_config_path {
262+
match SignerPoolConfig::load_config(path.as_ref()) {
263+
Ok(signer_config) => {
264+
let (signer_warnings, signer_errors) =
265+
SignerValidator::validate_with_result(&signer_config);
266+
warnings.extend(signer_warnings);
267+
errors.extend(signer_errors);
268+
}
269+
Err(e) => {
270+
errors.push(format!("Failed to load signers config: {e}"));
271+
}
272+
}
273+
} else {
274+
println!("ℹ️ Signers configuration not validated. Include --signers-config path/to/signers.toml to validate signers");
275+
}
276+
239277
// Output results
240278
println!("=== Configuration Validation ===");
241279
if errors.is_empty() {
@@ -684,6 +722,11 @@ mod tests {
684722

685723
let result = ConfigValidator::validate_with_result(&rpc_client, true).await;
686724
assert!(result.is_ok());
725+
726+
// Check that it warns about using "All" for allowed_spl_paid_tokens
727+
let warnings = result.unwrap();
728+
assert!(warnings.iter().any(|w| w.contains("Using 'All' for allowed_spl_paid_tokens")));
729+
assert!(warnings.iter().any(|w| w.contains("volatility risk")));
687730
}
688731

689732
#[tokio::test]

crates/lib/src/validator/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod account_validator;
22
pub mod config_validator;
3+
pub mod signer_validator;
34
pub mod transaction_validator;

0 commit comments

Comments
 (0)