1#[cfg(not(feature = "std"))]
8use alloc::{format, string::String, vec::Vec};
9
10use crate::{
11 fee_model::FeeModelParams,
12 types::{Hash, Timestamp},
13 Error, Result,
14};
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct ImmutableSignature {
20 pub algorithm: String,
22
23 pub public_key: Vec<u8>,
25
26 pub signature: Vec<u8>,
28
29 pub config_hash: Hash,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ImmutableConfig {
36 pub enabled: bool,
38
39 pub locked_at: Option<Timestamp>,
41
42 pub lock_signature: Option<ImmutableSignature>,
44
45 pub governance_key: Option<Vec<u8>>,
47
48 pub locked_config_hash: Option<Hash>,
50
51 pub grace_period_seconds: u64,
53}
54
55impl Default for ImmutableConfig {
56 fn default() -> Self {
57 Self {
58 enabled: false,
59 locked_at: None,
60 lock_signature: None,
61 governance_key: None,
62 locked_config_hash: None,
63 grace_period_seconds: 24 * 60 * 60, }
65 }
66}
67
68impl ImmutableConfig {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn enable(&mut self) {
76 self.enabled = true;
77 }
78
79 pub fn disable(&mut self) -> Result<()> {
81 if self.is_locked() {
82 return Err(Error::Other(
83 "Cannot disable immutable mode: system is locked".into(),
84 ));
85 }
86 self.enabled = false;
87 Ok(())
88 }
89
90 pub fn is_locked(&self) -> bool {
92 self.enabled && self.locked_at.is_some() && self.lock_signature.is_some()
93 }
94
95 pub fn is_in_grace_period(&self, current_time: Timestamp) -> bool {
97 if let Some(locked_at) = self.locked_at {
98 let grace_end = locked_at.value() + self.grace_period_seconds;
99 current_time.value() < grace_end
100 } else {
101 false
102 }
103 }
104
105 pub fn is_enforced(&self, current_time: Timestamp) -> bool {
107 self.is_locked() && !self.is_in_grace_period(current_time)
108 }
109
110 pub fn set_grace_period(&mut self, seconds: u64) -> Result<()> {
112 if self.is_locked() {
113 return Err(Error::Other(
114 "Cannot change grace period: system is locked".into(),
115 ));
116 }
117 self.grace_period_seconds = seconds;
118 Ok(())
119 }
120
121 pub fn set_governance_key(&mut self, key: Vec<u8>) -> Result<()> {
123 if self.is_locked() {
124 return Err(Error::Other(
125 "Cannot set governance key: system is locked".into(),
126 ));
127 }
128 self.governance_key = Some(key);
129 Ok(())
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct LockableConfig {
136 pub fee_params: FeeModelParams,
138
139 pub max_total_supply: u64,
141
142 pub min_account_balance: u64,
144
145 pub chain_id: u64,
147
148 pub protocol_version: u32,
150
151 pub allow_negative_balances: bool,
153}
154
155impl Default for LockableConfig {
156 fn default() -> Self {
157 Self {
158 fee_params: FeeModelParams::default(),
159 max_total_supply: u64::MAX,
160 min_account_balance: 0,
161 chain_id: 1,
162 protocol_version: 1,
163 allow_negative_balances: false,
164 }
165 }
166}
167
168impl LockableConfig {
169 pub fn hash(&self) -> Result<Hash> {
171 let bytes =
172 bincode::serialize(self).map_err(|e| Error::SerializationError(e.to_string()))?;
173 let hash = blake3::hash(&bytes);
174 Ok(Hash::from_bytes(*hash.as_bytes()))
175 }
176
177 pub fn validate(&self) -> Result<()> {
179 self.fee_params.validate()?;
180
181 if self.max_total_supply == 0 {
182 return Err(Error::Other(
183 "max_total_supply must be greater than 0".into(),
184 ));
185 }
186
187 if self.chain_id == 0 {
188 return Err(Error::Other("chain_id must be greater than 0".into()));
189 }
190
191 if self.protocol_version == 0 {
192 return Err(Error::Other(
193 "protocol_version must be greater than 0".into(),
194 ));
195 }
196
197 Ok(())
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ImmutableDeployment {
204 pub config: ImmutableConfig,
206
207 pub system_config: LockableConfig,
209}
210
211impl ImmutableDeployment {
212 pub fn new() -> Self {
214 Self {
215 config: ImmutableConfig::new(),
216 system_config: LockableConfig::default(),
217 }
218 }
219
220 pub fn with_config(system_config: LockableConfig) -> Result<Self> {
222 system_config.validate()?;
223 Ok(Self {
224 config: ImmutableConfig::new(),
225 system_config,
226 })
227 }
228
229 pub fn enable_immutable_mode(&mut self) -> Result<()> {
231 self.system_config.validate()?;
232 self.config.enable();
233 Ok(())
234 }
235
236 #[cfg(feature = "std")]
238 pub fn lock_system(
239 &mut self,
240 keypair: &qudag_crypto::MlDsaKeyPair,
241 current_time: Timestamp,
242 ) -> Result<()> {
243 if !self.config.enabled {
244 return Err(Error::Other("Immutable mode not enabled".into()));
245 }
246
247 if self.config.is_locked() {
248 return Err(Error::Other("System is already locked".into()));
249 }
250
251 self.system_config.validate()?;
253
254 let config_hash = self.system_config.hash()?;
256
257 let mut message = Vec::new();
259 message.extend_from_slice(config_hash.as_bytes());
260 message.extend_from_slice(¤t_time.value().to_le_bytes());
261
262 let signature = keypair
264 .sign(&message, &mut rand::thread_rng())
265 .map_err(|e| Error::Other(format!("Signing failed: {:?}", e)))?;
266
267 let public_key = keypair
268 .to_public_key()
269 .map_err(|e| Error::Other(format!("Public key extraction failed: {:?}", e)))?;
270
271 let immutable_sig = ImmutableSignature {
273 algorithm: "ML-DSA-87".to_string(),
274 public_key: public_key.as_bytes().to_vec(),
275 signature,
276 config_hash,
277 };
278
279 self.config.locked_at = Some(current_time);
281 self.config.lock_signature = Some(immutable_sig);
282 self.config.locked_config_hash = Some(config_hash);
283
284 Ok(())
285 }
286
287 #[cfg(feature = "std")]
289 pub fn verify_lock_signature(&self, current_time: Timestamp) -> Result<bool> {
290 let sig_data = self
291 .config
292 .lock_signature
293 .as_ref()
294 .ok_or_else(|| Error::Other("No lock signature present".into()))?;
295
296 let locked_at = self
297 .config
298 .locked_at
299 .ok_or_else(|| Error::Other("No lock timestamp present".into()))?;
300
301 let mut message = Vec::new();
303 message.extend_from_slice(sig_data.config_hash.as_bytes());
304 message.extend_from_slice(&locked_at.value().to_le_bytes());
305
306 let public_key = qudag_crypto::MlDsaPublicKey::from_bytes(&sig_data.public_key)
308 .map_err(|e| Error::Other(format!("Invalid public key: {:?}", e)))?;
309
310 match public_key.verify(&message, &sig_data.signature) {
312 Ok(()) => {
313 let current_hash = self.system_config.hash()?;
315 Ok(current_hash == sig_data.config_hash)
316 }
317 Err(_) => Ok(false),
318 }
319 }
320
321 pub fn can_modify_config(&self, current_time: Timestamp) -> bool {
323 !self.config.is_enforced(current_time)
324 }
325
326 pub fn update_fee_params(
328 &mut self,
329 params: FeeModelParams,
330 current_time: Timestamp,
331 ) -> Result<()> {
332 if !self.can_modify_config(current_time) {
333 return Err(Error::Other(
334 "Cannot modify configuration: system is immutably locked".into(),
335 ));
336 }
337
338 params.validate()?;
339 self.system_config.fee_params = params;
340 Ok(())
341 }
342
343 pub fn update_system_config(
345 &mut self,
346 config: LockableConfig,
347 current_time: Timestamp,
348 ) -> Result<()> {
349 if !self.can_modify_config(current_time) {
350 return Err(Error::Other(
351 "Cannot modify configuration: system is immutably locked".into(),
352 ));
353 }
354
355 config.validate()?;
356 self.system_config = config;
357 Ok(())
358 }
359
360 pub fn get_status(&self, current_time: Timestamp) -> ImmutableStatus {
362 ImmutableStatus {
363 enabled: self.config.enabled,
364 locked: self.config.is_locked(),
365 enforced: self.config.is_enforced(current_time),
366 in_grace_period: self.config.is_in_grace_period(current_time),
367 locked_at: self.config.locked_at,
368 grace_period_seconds: self.config.grace_period_seconds,
369 config_hash: self.config.locked_config_hash,
370 }
371 }
372
373 #[cfg(feature = "std")]
375 pub fn governance_override(
376 &mut self,
377 governance_keypair: &qudag_crypto::MlDsaKeyPair,
378 current_time: Timestamp,
379 ) -> Result<()> {
380 let governance_key = self
381 .config
382 .governance_key
383 .as_ref()
384 .ok_or_else(|| Error::Other("No governance key set".into()))?;
385
386 let public_key = governance_keypair
388 .to_public_key()
389 .map_err(|e| Error::Other(format!("Governance key extraction failed: {:?}", e)))?;
390
391 if public_key.as_bytes() != governance_key {
392 return Err(Error::Other("Invalid governance key".into()));
393 }
394
395 self.config.locked_at = None;
397 self.config.lock_signature = None;
398 self.config.locked_config_hash = None;
399
400 Ok(())
401 }
402}
403
404impl Default for ImmutableDeployment {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct ImmutableStatus {
413 pub enabled: bool,
415 pub locked: bool,
417 pub enforced: bool,
419 pub in_grace_period: bool,
421 pub locked_at: Option<Timestamp>,
423 pub grace_period_seconds: u64,
425 pub config_hash: Option<Hash>,
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_immutable_config_lifecycle() {
435 let mut config = ImmutableConfig::new();
436
437 assert!(!config.enabled);
439 assert!(!config.is_locked());
440
441 config.enable();
443 assert!(config.enabled);
444 assert!(!config.is_locked()); config.disable().unwrap();
448 assert!(!config.enabled);
449 }
450
451 #[test]
452 fn test_grace_period() {
453 let mut config = ImmutableConfig::new();
454 config.enable();
455
456 let lock_time = Timestamp::new(1000);
457 config.locked_at = Some(lock_time);
458 config.lock_signature = Some(ImmutableSignature {
459 algorithm: "ML-DSA-87".to_string(),
460 public_key: vec![1, 2, 3],
461 signature: vec![4, 5, 6],
462 config_hash: Hash::from_bytes([0u8; 32]),
463 });
464
465 let grace_time = Timestamp::new(1000 + 12 * 60 * 60); assert!(config.is_locked());
468 assert!(config.is_in_grace_period(grace_time));
469 assert!(!config.is_enforced(grace_time));
470
471 let post_grace = Timestamp::new(1000 + 25 * 60 * 60); assert!(config.is_locked());
474 assert!(!config.is_in_grace_period(post_grace));
475 assert!(config.is_enforced(post_grace));
476 }
477
478 #[test]
479 fn test_lockable_config_validation() {
480 let mut config = LockableConfig::default();
481 assert!(config.validate().is_ok());
482
483 config.fee_params.f_min = -0.1;
485 assert!(config.validate().is_err());
486
487 config = LockableConfig::default();
489 config.max_total_supply = 0;
490 assert!(config.validate().is_err());
491
492 config = LockableConfig::default();
493 config.chain_id = 0;
494 assert!(config.validate().is_err());
495 }
496
497 #[test]
498 fn test_lockable_config_hash() {
499 let config1 = LockableConfig::default();
500 let config2 = LockableConfig::default();
501
502 assert_eq!(config1.hash().unwrap(), config2.hash().unwrap());
504
505 let mut config3 = LockableConfig::default();
507 config3.chain_id = 42;
508 assert_ne!(config1.hash().unwrap(), config3.hash().unwrap());
509 }
510
511 #[test]
512 fn test_immutable_deployment_lifecycle() {
513 let mut deployment = ImmutableDeployment::new();
514 let current_time = Timestamp::new(1000);
515
516 assert!(deployment.can_modify_config(current_time));
518
519 deployment.enable_immutable_mode().unwrap();
521 assert!(deployment.can_modify_config(current_time)); let mut new_params = FeeModelParams::default();
525 new_params.f_min = 0.002;
526 deployment
527 .update_fee_params(new_params, current_time)
528 .unwrap();
529 assert_eq!(deployment.system_config.fee_params.f_min, 0.002);
530 }
531
532 #[test]
533 fn test_config_modification_restrictions() {
534 let mut deployment = ImmutableDeployment::new();
535 deployment.enable_immutable_mode().unwrap();
536
537 let current_time = Timestamp::new(1000);
538
539 deployment.config.locked_at = Some(current_time);
541 deployment.config.lock_signature = Some(ImmutableSignature {
542 algorithm: "ML-DSA-87".to_string(),
543 public_key: vec![1, 2, 3],
544 signature: vec![4, 5, 6],
545 config_hash: Hash::from_bytes([0u8; 32]),
546 });
547
548 let post_grace = Timestamp::new(current_time.value() + 25 * 60 * 60);
550 assert!(!deployment.can_modify_config(post_grace));
551
552 let new_params = FeeModelParams::default();
553 let result = deployment.update_fee_params(new_params, post_grace);
554 assert!(result.is_err());
555 assert!(result.unwrap_err().to_string().contains("immutably locked"));
556 }
557
558 #[test]
559 fn test_status_reporting() {
560 let mut deployment = ImmutableDeployment::new();
561 let current_time = Timestamp::new(1000);
562
563 let status = deployment.get_status(current_time);
565 assert!(!status.enabled);
566 assert!(!status.locked);
567 assert!(!status.enforced);
568
569 deployment.enable_immutable_mode().unwrap();
571 let status = deployment.get_status(current_time);
572 assert!(status.enabled);
573 assert!(!status.locked);
574 assert!(!status.enforced);
575 }
576}