Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/configuration-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Configuration Management Architecture

VeriNode Core uses a schema-first configuration subsystem that validates every
candidate configuration before activation. `ConfigManager` owns the active
`SystemConfig`, applies monotonic version checks, rejects invalid service and
operational settings, and records a compact change event for monitoring.

## Hot-reload flow

1. Load a candidate configuration from the operator source of truth.
2. Run `validate_reload(current, candidate)`.
3. Reject changes that fail schema validation, version monotonicity, or attempt
to mutate a service whose `hot_reload` flag is disabled.
4. Activate the candidate atomically and emit a `ConfigChangeEvent`.
5. Let deployment automation progress blue-green/canary rollout according to
the validated `DeploymentConfig`.

## Operational bounds

- Critical-path P99 target must be between 1 and 100 ms.
- Availability target must be 99.99% or higher.
- Metrics and alerting must remain enabled for every accepted configuration.
- Security review is represented by the `security_review_required` schema flag
and must not be disabled.
22 changes: 22 additions & 0 deletions docs/runbooks/configuration-hot-reload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Configuration Hot-Reload Runbook

## Pre-checks

- Confirm the candidate version is greater than the active version.
- Confirm security review approval is attached to the change request.
- Confirm metrics, alerting, and dashboards are healthy.

## Rollout

1. Apply the candidate to a green environment.
2. Run schema validation and hot-reload validation.
3. Start canary at the configured percentage.
4. Watch P99 latency and error budget alerts for at least one dashboard refresh
interval.
5. Promote green to blue only when canary analysis stays within budget.

## Rollback

If validation fails or alerts fire, keep the active configuration unchanged,
stop the canary, and open an incident with the rejected config version and
validation error.
222 changes: 222 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
//! Configuration management with schema validation and deterministic hot reload.
//!
//! The module is intentionally runtime-agnostic: node processes can wire
//! `ConfigManager::reload` to a file watcher, governance event stream, or
//! blue-green/canary deployment controller while tests and contracts can drive
//! reloads deterministically. Validation is pure and bounded by the number of
//! fields in `SystemConfig`, keeping critical-path checks small and predictable.

use alloc::string::String;
use alloc::vec::Vec;

pub const MAX_SERVICE_NAME_LEN: usize = 64;
pub const MAX_SERVICES: usize = 64;
pub const MAX_CONFIG_VERSION_JUMP: u64 = 1_000;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServiceConfig {
pub name: String,
pub enabled: bool,
pub critical_path_timeout_ms: u64,
pub max_inflight_requests: u32,
pub hot_reload: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MonitoringConfig {
pub metrics_enabled: bool,
pub alerting_enabled: bool,
pub dashboard_refresh_seconds: u64,
pub p99_latency_alert_ms: u64,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeploymentConfig {
pub blue_green_enabled: bool,
pub canary_percent: u8,
pub canary_error_budget_bps: u32,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SystemConfig {
pub version: u64,
pub availability_target_bps: u32,
pub critical_path_p99_ms: u64,
pub security_review_required: bool,
pub services: Vec<ServiceConfig>,
pub monitoring: MonitoringConfig,
pub deployment: DeploymentConfig,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ConfigError {
EmptyServiceName,
ServiceNameTooLong,
DuplicateServiceName,
TooManyServices,
InvalidAvailabilityTarget,
CriticalPathTargetTooHigh,
ServiceTimeoutExceedsTarget,
ZeroInflightLimit,
MonitoringDisabled,
InvalidDashboardRefresh,
InvalidCanaryPercent,
InvalidCanaryErrorBudget,
SecurityReviewMissing,
NonMonotonicVersion,
VersionJumpTooLarge,
HotReloadDisabled(String),
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigChangeEvent {
pub previous_version: u64,
pub current_version: u64,
pub changed_services: Vec<String>,
pub canary_percent: u8,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigManager {
active: SystemConfig,
history: Vec<ConfigChangeEvent>,
}

impl ConfigManager {
pub fn new(config: SystemConfig) -> Result<Self, ConfigError> {
validate_config(&config)?;
Ok(Self {
active: config,
history: Vec::new(),
})
}

pub fn active(&self) -> &SystemConfig {
&self.active
}

pub fn history(&self) -> &[ConfigChangeEvent] {
&self.history
}

pub fn reload(&mut self, candidate: SystemConfig) -> Result<ConfigChangeEvent, ConfigError> {
validate_reload(&self.active, &candidate)?;
let event = ConfigChangeEvent {
previous_version: self.active.version,
current_version: candidate.version,
changed_services: changed_services(&self.active.services, &candidate.services),
canary_percent: candidate.deployment.canary_percent,
};
self.active = candidate;
self.history.push(event.clone());
Ok(event)
}
}

pub fn validate_reload(
current: &SystemConfig,
candidate: &SystemConfig,
) -> Result<(), ConfigError> {
validate_config(candidate)?;
if candidate.version <= current.version {
return Err(ConfigError::NonMonotonicVersion);
}
if candidate.version - current.version > MAX_CONFIG_VERSION_JUMP {
return Err(ConfigError::VersionJumpTooLarge);
}
for service in &candidate.services {
if service_changed(&current.services, service) && !service.hot_reload {
return Err(ConfigError::HotReloadDisabled(service.name.clone()));
}
}
Ok(())
}

pub fn validate_config(config: &SystemConfig) -> Result<(), ConfigError> {
if config.services.len() > MAX_SERVICES {
return Err(ConfigError::TooManyServices);
}
if !(9_999..=10_000).contains(&config.availability_target_bps) {
return Err(ConfigError::InvalidAvailabilityTarget);
}
if config.critical_path_p99_ms == 0 || config.critical_path_p99_ms > 100 {
return Err(ConfigError::CriticalPathTargetTooHigh);
}
if !config.security_review_required {
return Err(ConfigError::SecurityReviewMissing);
}
if !config.monitoring.metrics_enabled || !config.monitoring.alerting_enabled {
return Err(ConfigError::MonitoringDisabled);
}
if config.monitoring.dashboard_refresh_seconds == 0
|| config.monitoring.dashboard_refresh_seconds > 300
{
return Err(ConfigError::InvalidDashboardRefresh);
}
if config.monitoring.p99_latency_alert_ms > config.critical_path_p99_ms {
return Err(ConfigError::CriticalPathTargetTooHigh);
}
if config.deployment.canary_percent > 100 {
return Err(ConfigError::InvalidCanaryPercent);
}
if config.deployment.canary_error_budget_bps > 10_000 {
return Err(ConfigError::InvalidCanaryErrorBudget);
}

let mut seen: Vec<&str> = Vec::new();
for service in &config.services {
if service.name.is_empty() {
return Err(ConfigError::EmptyServiceName);
}
if service.name.len() > MAX_SERVICE_NAME_LEN {
return Err(ConfigError::ServiceNameTooLong);
}
if seen.iter().any(|name| *name == service.name.as_str()) {
return Err(ConfigError::DuplicateServiceName);
}
seen.push(service.name.as_str());
if service.critical_path_timeout_ms > config.critical_path_p99_ms {
return Err(ConfigError::ServiceTimeoutExceedsTarget);
}
if service.max_inflight_requests == 0 {
return Err(ConfigError::ZeroInflightLimit);
}
}
Ok(())
}

fn service_changed(current: &[ServiceConfig], candidate: &ServiceConfig) -> bool {
current
.iter()
.find(|service| service.name == candidate.name)
.map_or(true, |service| service != candidate)
}

fn changed_services(current: &[ServiceConfig], candidate: &[ServiceConfig]) -> Vec<String> {
candidate
.iter()
.filter(|service| service_changed(current, service))
.map(|service| service.name.clone())
.collect()
}

impl Default for MonitoringConfig {
fn default() -> Self {
Self {
metrics_enabled: true,
alerting_enabled: true,
dashboard_refresh_seconds: 30,
p99_latency_alert_ms: 100,
}
}
}

impl Default for DeploymentConfig {
fn default() -> Self {
Self {
blue_green_enabled: true,
canary_percent: 5,
canary_error_budget_bps: 100,
}
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ pub mod pool_manager;
// span semantics for ingestion by observability pipelines.
pub mod tracing;

// System-wide configuration management with schema validation and hot reload.
pub mod config;

// --- ERROR CODES ---

#[contracterror]
Expand Down
106 changes: 106 additions & 0 deletions tests/config_management_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use sorosusu_contracts::config::{
ConfigError, ConfigManager, DeploymentConfig, MonitoringConfig, ServiceConfig, SystemConfig,
};

fn valid_config(version: u64) -> SystemConfig {
SystemConfig {
version,
availability_target_bps: 9_999,
critical_path_p99_ms: 100,
security_review_required: true,
services: vec![
ServiceConfig {
name: "mempool".to_string(),
enabled: true,
critical_path_timeout_ms: 50,
max_inflight_requests: 128,
hot_reload: true,
},
ServiceConfig {
name: "attestation".to_string(),
enabled: true,
critical_path_timeout_ms: 80,
max_inflight_requests: 256,
hot_reload: true,
},
],
monitoring: MonitoringConfig::default(),
deployment: DeploymentConfig::default(),
}
}

#[test]
fn accepts_valid_config_and_records_hot_reload_event() {
let mut manager = ConfigManager::new(valid_config(1)).expect("valid initial config");
let mut next = valid_config(2);
next.deployment.canary_percent = 10;
next.services[0].max_inflight_requests = 512;

let event = manager.reload(next).expect("hot reload succeeds");

assert_eq!(event.previous_version, 1);
assert_eq!(event.current_version, 2);
assert_eq!(event.changed_services, vec!["mempool".to_string()]);
assert_eq!(event.canary_percent, 10);
assert_eq!(manager.history(), &[event]);
assert_eq!(manager.active().version, 2);
}

#[test]
fn rejects_schema_violations_before_activation() {
let mut invalid = valid_config(1);
invalid.critical_path_p99_ms = 101;
assert_eq!(
ConfigManager::new(invalid),
Err(ConfigError::CriticalPathTargetTooHigh)
);

let mut invalid = valid_config(1);
invalid.monitoring.alerting_enabled = false;
assert_eq!(
ConfigManager::new(invalid),
Err(ConfigError::MonitoringDisabled)
);

let mut invalid = valid_config(1);
invalid.security_review_required = false;
assert_eq!(
ConfigManager::new(invalid),
Err(ConfigError::SecurityReviewMissing)
);
}

#[test]
fn rejects_non_monotonic_versions_and_non_reloadable_service_changes() {
let mut manager = ConfigManager::new(valid_config(7)).expect("valid initial config");
assert_eq!(
manager.reload(valid_config(7)),
Err(ConfigError::NonMonotonicVersion)
);

let mut blocked = valid_config(8);
blocked.services[1].hot_reload = false;
blocked.services[1].critical_path_timeout_ms = 70;
assert_eq!(
manager.reload(blocked),
Err(ConfigError::HotReloadDisabled("attestation".to_string()))
);
assert_eq!(manager.active().version, 7);
}

#[test]
fn rejects_duplicate_services_and_invalid_canary_settings() {
let mut duplicate = valid_config(1);
duplicate.services[1].name = "mempool".to_string();
assert_eq!(
ConfigManager::new(duplicate),
Err(ConfigError::DuplicateServiceName)
);

let mut invalid_canary = valid_config(1);
invalid_canary.deployment.canary_percent = 101;
assert_eq!(
ConfigManager::new(invalid_canary),
Err(ConfigError::InvalidCanaryPercent)
);
}
Loading