Rust and WASM did-method-plc tools and structures
at main 44 kB view raw
1//! Validation logic for operations and operation chains 2 3use crate::crypto::VerifyingKey; 4use crate::document::{PlcState, MAX_VERIFICATION_METHODS}; 5use crate::error::{PlcError, Result}; 6use crate::operations::Operation; 7use chrono::{DateTime, Duration, Utc}; 8use std::collections::HashMap; 9 10/// Recovery window duration (72 hours) 11const RECOVERY_WINDOW_HOURS: i64 = 72; 12 13/// Represents an operation with its server-assigned timestamp 14#[derive(Debug, Clone)] 15pub struct OperationWithTimestamp { 16 /// The operation itself 17 pub operation: Operation, 18 /// Server-assigned timestamp when the operation was received 19 pub timestamp: DateTime<Utc>, 20} 21 22/// Represents a fork point where multiple operations reference the same prev CID 23#[derive(Debug)] 24struct ForkPoint { 25 /// The prev CID that multiple operations reference 26 #[allow(dead_code)] 27 prev_cid: String, 28 /// Competing operations at this fork point, with their timestamps and signing key indices 29 operations: Vec<(Operation, DateTime<Utc>, usize)>, 30} 31 32impl ForkPoint { 33 /// Create a new fork point 34 fn new(prev_cid: String) -> Self { 35 Self { 36 prev_cid, 37 operations: Vec::new(), 38 } 39 } 40 41 /// Add an operation to this fork point 42 fn add_operation(&mut self, operation: Operation, timestamp: DateTime<Utc>, key_index: usize) { 43 self.operations.push((operation, timestamp, key_index)); 44 } 45 46 /// Resolve this fork point and return the canonical operation 47 /// 48 /// Resolution algorithm: 49 /// 1. Sort operations by timestamp (first-received wins by default) 50 /// 2. Check if any later-received operation with higher priority can invalidate within 72 hours 51 /// 3. Return the winning operation 52 fn resolve(&self) -> Result<Operation> { 53 if self.operations.is_empty() { 54 return Err(PlcError::ForkResolutionError( 55 "Fork point has no operations".to_string(), 56 )); 57 } 58 59 // If only one operation, it wins by default 60 if self.operations.len() == 1 { 61 return Ok(self.operations[0].0.clone()); 62 } 63 64 // Sort by timestamp - first-received wins by default 65 let mut sorted = self.operations.clone(); 66 sorted.sort_by_key(|(_, timestamp, _)| *timestamp); 67 68 // The first operation in chronological order is the default winner 69 let (mut canonical_op, mut canonical_ts, mut canonical_key_idx) = sorted[0].clone(); 70 71 // Check if any later-received operation can invalidate the current canonical 72 for (competing_op, competing_ts, competing_key_idx) in &sorted[1..] { 73 // Can only invalidate if the competing operation has higher priority (lower index) 74 if *competing_key_idx < canonical_key_idx { 75 // Check if within recovery window from the canonical operation 76 let time_diff = *competing_ts - canonical_ts; 77 if time_diff <= Duration::hours(RECOVERY_WINDOW_HOURS) 78 && time_diff >= Duration::zero() 79 { 80 // This higher-priority operation invalidates the canonical one 81 // Update the canonical to this operation 82 canonical_op = competing_op.clone(); 83 canonical_ts = *competing_ts; 84 canonical_key_idx = *competing_key_idx; 85 } 86 } 87 } 88 89 Ok(canonical_op) 90 } 91} 92 93/// Builder for constructing the canonical chain from a set of operations with timestamps 94struct CanonicalChainBuilder { 95 /// All operations with timestamps, in the order they were received 96 operations: Vec<OperationWithTimestamp>, 97} 98 99impl CanonicalChainBuilder { 100 /// Create a new builder 101 fn new(operations: Vec<OperationWithTimestamp>) -> Self { 102 Self { operations } 103 } 104 105 /// Build the canonical chain by detecting and resolving forks 106 /// 107 /// Algorithm: 108 /// 1. Build a graph of operations by CID 109 /// 2. Detect fork points (multiple operations with same prev CID) 110 /// 3. Resolve each fork using rotation key priority and recovery window 111 /// 4. Build the canonical chain from genesis to tip 112 fn build(&self) -> Result<Vec<Operation>> { 113 if self.operations.is_empty() { 114 return Err(PlcError::EmptyChain); 115 } 116 117 // Build a map of CID -> (operation, timestamp) for quick lookup 118 let mut operation_map: HashMap<String, (Operation, DateTime<Utc>)> = HashMap::new(); 119 for op_with_ts in &self.operations { 120 let cid = op_with_ts.operation.cid()?; 121 operation_map.insert(cid, (op_with_ts.operation.clone(), op_with_ts.timestamp)); 122 } 123 124 // Find the genesis operation (prev = None) 125 let genesis = self 126 .operations 127 .iter() 128 .find(|op| op.operation.is_genesis()) 129 .ok_or_else(|| PlcError::FirstOperationNotGenesis)?; 130 131 // Detect fork points - group operations by their prev CID 132 let mut prev_to_operations: HashMap<String, Vec<(Operation, DateTime<Utc>)>> = 133 HashMap::new(); 134 135 for op_with_ts in &self.operations { 136 if let Some(prev_cid) = op_with_ts.operation.prev() { 137 prev_to_operations 138 .entry(prev_cid.to_string()) 139 .or_default() 140 .push((op_with_ts.operation.clone(), op_with_ts.timestamp)); 141 } 142 } 143 144 // Build fork points for any prev CID with multiple operations 145 let mut fork_points: HashMap<String, ForkPoint> = HashMap::new(); 146 for (prev_cid, operations) in &prev_to_operations { 147 if operations.len() > 1 { 148 // This is a fork point - multiple operations reference the same prev 149 let mut fork_point = ForkPoint::new(prev_cid.clone()); 150 151 // For each operation, determine which rotation key signed it 152 // We need to look at the state at the prev operation 153 for (operation, timestamp) in operations { 154 // Get the state at the prev operation to find rotation keys 155 let rotation_keys = if let Some((prev_op, _)) = operation_map.get(prev_cid) { 156 // Build state up to prev operation to get its rotation keys 157 self.get_state_at_operation(prev_op)? 158 } else { 159 PlcState::new() 160 }; 161 162 // Find which rotation key signed this operation 163 let key_index = self.find_signing_key_index(operation, &rotation_keys)?; 164 fork_point.add_operation(operation.clone(), *timestamp, key_index); 165 } 166 167 fork_points.insert(prev_cid.clone(), fork_point); 168 } 169 } 170 171 // Build the canonical chain by starting from genesis and following the canonical path 172 let mut canonical_chain = vec![genesis.operation.clone()]; 173 let mut current_cid = genesis.operation.cid()?; 174 175 // Follow the chain until we reach the tip 176 loop { 177 // Check if there's a fork at this point 178 if let Some(fork_point) = fork_points.get(&current_cid) { 179 // Resolve the fork and get the canonical operation 180 let canonical_op = fork_point.resolve()?; 181 let next_cid = canonical_op.cid()?; 182 canonical_chain.push(canonical_op); 183 current_cid = next_cid; 184 } else if let Some(operations) = prev_to_operations.get(&current_cid) { 185 // No fork, just a single operation 186 if operations.len() == 1 { 187 let (operation, _) = &operations[0]; 188 let next_cid = operation.cid()?; 189 canonical_chain.push(operation.clone()); 190 current_cid = next_cid; 191 } else { 192 // This shouldn't happen - we should have detected this as a fork 193 return Err(PlcError::ForkResolutionError( 194 "Unexpected multiple operations without fork point".to_string(), 195 )); 196 } 197 } else { 198 // No more operations - we've reached the tip 199 break; 200 } 201 } 202 203 Ok(canonical_chain) 204 } 205 206 /// Find the index of the rotation key that signed this operation 207 fn find_signing_key_index( 208 &self, 209 operation: &Operation, 210 state: &PlcState, 211 ) -> Result<usize> { 212 if state.rotation_keys.is_empty() { 213 return Err(PlcError::InvalidRotationKeys( 214 "No rotation keys available for verification".to_string(), 215 )); 216 } 217 218 // Try each rotation key and return the index of the first one that verifies 219 for (index, key_did) in state.rotation_keys.iter().enumerate() { 220 let verifying_key = VerifyingKey::from_did_key(key_did)?; 221 if operation.verify(&[verifying_key]).is_ok() { 222 return Ok(index); 223 } 224 } 225 226 // No key verified the signature 227 Err(PlcError::SignatureVerificationFailed) 228 } 229 230 /// Get the state at a specific operation 231 /// 232 /// This reconstructs the state by applying all operations from genesis up to 233 /// and including the specified operation. 234 fn get_state_at_operation(&self, target_operation: &Operation) -> Result<PlcState> { 235 let mut state = PlcState::new(); 236 let target_cid = target_operation.cid()?; 237 238 // Build the chain up to the target operation 239 // We need to traverse from genesis to the target 240 for op_with_ts in &self.operations { 241 let op = &op_with_ts.operation; 242 243 // Apply this operation to the state 244 match op { 245 Operation::PlcOperation { 246 rotation_keys, 247 verification_methods, 248 also_known_as, 249 services, 250 .. 251 } => { 252 state.rotation_keys = rotation_keys.clone(); 253 state.verification_methods = verification_methods.clone(); 254 state.also_known_as = also_known_as.clone(); 255 state.services = services.clone(); 256 } 257 Operation::PlcTombstone { .. } => { 258 state = PlcState::new(); 259 } 260 Operation::LegacyCreate { .. } => { 261 return Err(PlcError::InvalidOperationType( 262 "Legacy create operations not fully supported".to_string(), 263 )); 264 } 265 } 266 267 // Check if this is the target operation 268 if op.cid()? == target_cid { 269 break; 270 } 271 } 272 273 Ok(state) 274 } 275} 276 277/// Operation chain validator 278pub struct OperationChainValidator; 279 280impl OperationChainValidator { 281 /// Validate a complete operation chain and return the final state 282 /// 283 /// # Errors 284 /// 285 /// Returns errors if: 286 /// - Chain is empty 287 /// - First operation is not genesis 288 /// - Any operation has invalid prev reference 289 /// - Any signature is invalid 290 /// - Any operation violates constraints 291 pub fn validate_chain(operations: &[Operation]) -> Result<PlcState> { 292 if operations.is_empty() { 293 return Err(PlcError::EmptyChain); 294 } 295 296 // First operation must be genesis 297 if !operations[0].is_genesis() { 298 return Err(PlcError::FirstOperationNotGenesis); 299 } 300 301 let mut current_state = PlcState::new(); 302 let mut prev_cid: Option<String> = None; 303 304 for (i, operation) in operations.iter().enumerate() { 305 // Verify prev field matches expected CID 306 if i == 0 { 307 // Genesis operation must have prev = None 308 if operation.prev().is_some() { 309 return Err(PlcError::InvalidPrev( 310 "Genesis operation must have prev = null".to_string(), 311 )); 312 } 313 } else { 314 // Non-genesis operations must reference previous CID 315 let expected_prev = prev_cid.as_ref().ok_or_else(|| { 316 PlcError::ChainValidationFailed("Missing previous CID".to_string()) 317 })?; 318 319 let actual_prev = operation.prev().ok_or_else(|| { 320 PlcError::InvalidPrev("Non-genesis operation must have prev field".to_string()) 321 })?; 322 323 if actual_prev != expected_prev { 324 return Err(PlcError::InvalidPrev(format!( 325 "Expected prev = {}, got {}", 326 expected_prev, actual_prev 327 ))); 328 } 329 } 330 331 // Verify signature using current rotation keys 332 if !current_state.rotation_keys.is_empty() { 333 let verifying_keys: Result<Vec<VerifyingKey>> = current_state 334 .rotation_keys 335 .iter() 336 .map(|k| VerifyingKey::from_did_key(k)) 337 .collect(); 338 339 let verifying_keys = verifying_keys?; 340 operation.verify(&verifying_keys)?; 341 } else if i > 0 { 342 // After genesis, we must have rotation keys 343 return Err(PlcError::InvalidRotationKeys( 344 "No rotation keys available for verification".to_string(), 345 )); 346 } 347 348 // Apply operation to state 349 match operation { 350 Operation::PlcOperation { 351 rotation_keys, 352 verification_methods, 353 also_known_as, 354 services, 355 .. 356 } => { 357 current_state.rotation_keys = rotation_keys.clone(); 358 current_state.verification_methods = verification_methods.clone(); 359 current_state.also_known_as = also_known_as.clone(); 360 current_state.services = services.clone(); 361 362 // Validate the state 363 current_state.validate()?; 364 } 365 Operation::PlcTombstone { .. } => { 366 // Tombstone marks the DID as deleted 367 // Clear all state 368 current_state = PlcState::new(); 369 } 370 Operation::LegacyCreate { .. } => { 371 // Legacy create format - convert to modern format 372 // This is for backwards compatibility 373 return Err(PlcError::InvalidOperationType( 374 "Legacy create operations not fully supported".to_string(), 375 )); 376 } 377 } 378 379 // Update prev CID for next iteration 380 prev_cid = Some(operation.cid()?); 381 } 382 383 Ok(current_state) 384 } 385 386 /// Validate a chain with fork resolution 387 /// 388 /// This handles the recovery mechanism where operations signed by higher-priority 389 /// rotation keys can invalidate later operations if submitted within 72 hours. 390 /// 391 /// # Arguments 392 /// 393 /// * `operations` - All operations in the audit log (may contain forks) 394 /// * `timestamps` - Server-assigned timestamps for each operation 395 /// 396 /// # Returns 397 /// 398 /// The final state after applying the canonical chain (after fork resolution) 399 /// 400 /// # Errors 401 /// 402 /// Returns errors if: 403 /// - Operations and timestamps arrays have different lengths 404 /// - Chain is empty 405 /// - First operation is not genesis 406 /// - Fork resolution fails 407 /// - Any signature is invalid 408 /// - Any operation violates constraints 409 pub fn validate_chain_with_forks( 410 operations: &[Operation], 411 timestamps: &[DateTime<Utc>], 412 ) -> Result<PlcState> { 413 if operations.len() != timestamps.len() { 414 return Err(PlcError::ChainValidationFailed( 415 "Operations and timestamps length mismatch".to_string(), 416 )); 417 } 418 419 if operations.is_empty() { 420 return Err(PlcError::EmptyChain); 421 } 422 423 // Build operations with timestamps 424 let operations_with_timestamps: Vec<OperationWithTimestamp> = operations 425 .iter() 426 .zip(timestamps.iter()) 427 .map(|(op, ts)| OperationWithTimestamp { 428 operation: op.clone(), 429 timestamp: *ts, 430 }) 431 .collect(); 432 433 // Use the canonical chain builder to resolve forks 434 let builder = CanonicalChainBuilder::new(operations_with_timestamps); 435 let canonical_chain = builder.build()?; 436 437 // Validate the canonical chain 438 Self::validate_chain(&canonical_chain) 439 } 440 441 /// Check if an operation is within the recovery window relative to another operation 442 /// 443 /// Returns true if the time difference is less than 72 hours 444 pub fn is_within_recovery_window( 445 fork_timestamp: DateTime<Utc>, 446 current_timestamp: DateTime<Utc>, 447 ) -> bool { 448 let diff = current_timestamp - fork_timestamp; 449 diff < Duration::hours(RECOVERY_WINDOW_HOURS) && diff >= Duration::zero() 450 } 451} 452 453/// Validate rotation keys 454/// 455/// # Errors 456/// 457/// Returns errors if: 458/// - Not 1-5 keys 459/// - Contains duplicates 460/// - Invalid did:key format 461/// - Unsupported key type 462pub fn validate_rotation_keys(keys: &[String]) -> Result<()> { 463 if keys.is_empty() { 464 return Err(PlcError::InvalidRotationKeys( 465 "At least one rotation key is required".to_string(), 466 )); 467 } 468 469 if keys.len() > 5 { 470 return Err(PlcError::TooManyEntries { 471 field: "rotation_keys".to_string(), 472 max: 5, 473 actual: keys.len(), 474 }); 475 } 476 477 // Check for duplicates 478 let mut seen = std::collections::HashSet::new(); 479 for key in keys { 480 if !seen.insert(key) { 481 return Err(PlcError::DuplicateEntry { 482 field: "rotation_keys".to_string(), 483 value: key.clone(), 484 }); 485 } 486 487 // Validate format 488 if !key.starts_with("did:key:") { 489 return Err(PlcError::InvalidRotationKeys(format!( 490 "Rotation key must be in did:key format: {}", 491 key 492 ))); 493 } 494 495 // Try to parse to ensure it's valid 496 VerifyingKey::from_did_key(key)?; 497 } 498 499 Ok(()) 500} 501 502/// Validate verification methods 503/// 504/// # Errors 505/// 506/// Returns errors if: 507/// - More than 10 methods 508/// - Invalid did:key format 509pub fn validate_verification_methods( 510 methods: &std::collections::HashMap<String, String>, 511) -> Result<()> { 512 if methods.len() > MAX_VERIFICATION_METHODS { 513 return Err(PlcError::TooManyEntries { 514 field: "verification_methods".to_string(), 515 max: MAX_VERIFICATION_METHODS, 516 actual: methods.len(), 517 }); 518 } 519 520 for (name, key) in methods { 521 if !key.starts_with("did:key:") { 522 return Err(PlcError::InvalidVerificationMethods(format!( 523 "Verification method '{}' must be in did:key format: {}", 524 name, key 525 ))); 526 } 527 528 // Try to parse to ensure it's valid 529 VerifyingKey::from_did_key(key)?; 530 } 531 532 Ok(()) 533} 534 535/// Validate also-known-as URIs 536/// 537/// # Errors 538/// 539/// Returns errors if any URI is invalid 540pub fn validate_also_known_as(uris: &[String]) -> Result<()> { 541 for uri in uris { 542 if uri.is_empty() { 543 return Err(PlcError::InvalidAlsoKnownAs( 544 "URI cannot be empty".to_string(), 545 )); 546 } 547 548 // Basic URI validation - should start with a scheme 549 if !uri.contains(':') { 550 return Err(PlcError::InvalidAlsoKnownAs(format!( 551 "URI must contain a scheme: {}", 552 uri 553 ))); 554 } 555 } 556 557 Ok(()) 558} 559 560/// Validate service endpoints 561/// 562/// # Errors 563/// 564/// Returns errors if any service is invalid 565pub fn validate_services( 566 services: &std::collections::HashMap<String, crate::document::ServiceEndpoint>, 567) -> Result<()> { 568 for (name, service) in services { 569 if name.is_empty() { 570 return Err(PlcError::InvalidService( 571 "Service name cannot be empty".to_string(), 572 )); 573 } 574 575 service.validate()?; 576 } 577 578 Ok(()) 579} 580 581#[cfg(test)] 582mod tests { 583 use super::*; 584 use crate::crypto::SigningKey; 585 use crate::document::ServiceEndpoint; 586 use std::collections::HashMap; 587 588 #[test] 589 fn test_validate_rotation_keys() { 590 let key1 = SigningKey::generate_p256(); 591 let key2 = SigningKey::generate_k256(); 592 593 let keys = vec![key1.to_did_key(), key2.to_did_key()]; 594 assert!(validate_rotation_keys(&keys).is_ok()); 595 596 // Empty keys 597 assert!(validate_rotation_keys(&[]).is_err()); 598 599 // Too many keys 600 let many_keys: Vec<String> = (0..6).map(|_| SigningKey::generate_p256().to_did_key()).collect(); 601 assert!(validate_rotation_keys(&many_keys).is_err()); 602 603 // Duplicate keys 604 let dup_key = key1.to_did_key(); 605 let dup_keys = vec![dup_key.clone(), dup_key]; 606 assert!(validate_rotation_keys(&dup_keys).is_err()); 607 } 608 609 #[test] 610 fn test_validate_verification_methods() { 611 let mut methods = HashMap::new(); 612 let key = SigningKey::generate_p256(); 613 methods.insert("atproto".to_string(), key.to_did_key()); 614 615 assert!(validate_verification_methods(&methods).is_ok()); 616 617 // Too many methods 618 let mut many_methods = HashMap::new(); 619 for i in 0..11 { 620 let key = SigningKey::generate_p256(); 621 many_methods.insert(format!("key{}", i), key.to_did_key()); 622 } 623 assert!(validate_verification_methods(&many_methods).is_err()); 624 } 625 626 #[test] 627 fn test_validate_also_known_as() { 628 let uris = vec![ 629 "at://alice.example.com".to_string(), 630 "https://example.com".to_string(), 631 ]; 632 assert!(validate_also_known_as(&uris).is_ok()); 633 634 // Empty URI 635 assert!(validate_also_known_as(&[String::new()]).is_err()); 636 637 // Invalid URI (no scheme) 638 assert!(validate_also_known_as(&["not-a-uri".to_string()]).is_err()); 639 } 640 641 #[test] 642 fn test_recovery_window() { 643 let base = Utc::now(); 644 let within = base + Duration::hours(24); 645 let outside = base + Duration::hours(100); 646 647 assert!(OperationChainValidator::is_within_recovery_window(base, within)); 648 assert!(!OperationChainValidator::is_within_recovery_window(base, outside)); 649 } 650 651 #[test] 652 fn test_validate_chain_genesis() { 653 let key = SigningKey::generate_p256(); 654 let did_key = key.to_did_key(); 655 656 let unsigned = Operation::new_genesis( 657 vec![did_key], 658 HashMap::new(), 659 vec![], 660 HashMap::new(), 661 ); 662 663 let signed = unsigned.sign(&key).unwrap(); 664 665 // Single genesis operation should validate 666 let state = OperationChainValidator::validate_chain(&[signed]).unwrap(); 667 assert_eq!(state.rotation_keys.len(), 1); 668 } 669 670 #[test] 671 fn test_validate_chain_empty() { 672 assert!(OperationChainValidator::validate_chain(&[]).is_err()); 673 } 674 675 // ======================================================================== 676 // Fork Resolution Tests 677 // ======================================================================== 678 679 /// Test simple fork resolution where higher priority key wins within recovery window 680 #[test] 681 fn test_fork_resolution_priority_within_window() { 682 // Create two rotation keys with different priorities 683 let primary_key = SigningKey::generate_p256(); // Index 0 - highest priority 684 let backup_key = SigningKey::generate_k256(); // Index 1 - lower priority 685 686 let rotation_keys = vec![primary_key.to_did_key(), backup_key.to_did_key()]; 687 688 // Create genesis operation 689 let genesis = Operation::new_genesis( 690 rotation_keys.clone(), 691 HashMap::new(), 692 vec![], 693 HashMap::new(), 694 ) 695 .sign(&primary_key) 696 .unwrap(); 697 698 let genesis_cid = genesis.cid().unwrap(); 699 let genesis_time = Utc::now(); 700 701 // Create two competing operations that both reference genesis 702 // Operation A: signed by backup key (lower priority) 703 let mut services_a = HashMap::new(); 704 services_a.insert( 705 "pds".to_string(), 706 ServiceEndpoint { 707 service_type: "AtprotoPersonalDataServer".to_string(), 708 endpoint: "https://pds-a.example.com".to_string(), 709 }, 710 ); 711 712 let op_a = Operation::new_update( 713 rotation_keys.clone(), 714 HashMap::new(), 715 vec![], 716 services_a, 717 genesis_cid.clone(), 718 ) 719 .sign(&backup_key) 720 .unwrap(); 721 722 let op_a_time = genesis_time + Duration::hours(1); 723 724 // Operation B: signed by primary key (higher priority), arrives 24 hours after A 725 let mut services_b = HashMap::new(); 726 services_b.insert( 727 "pds".to_string(), 728 ServiceEndpoint { 729 service_type: "AtprotoPersonalDataServer".to_string(), 730 endpoint: "https://pds-b.example.com".to_string(), 731 }, 732 ); 733 734 let op_b = Operation::new_update( 735 rotation_keys.clone(), 736 HashMap::new(), 737 vec![], 738 services_b, 739 genesis_cid, 740 ) 741 .sign(&primary_key) 742 .unwrap(); 743 744 let op_b_time = op_a_time + Duration::hours(24); // Within 72-hour window 745 746 // Build operations and timestamps arrays 747 let operations = vec![genesis.clone(), op_a.clone(), op_b.clone()]; 748 let timestamps = vec![genesis_time, op_a_time, op_b_time]; 749 750 // Validate with fork resolution 751 let result = OperationChainValidator::validate_chain_with_forks(&operations, &timestamps); 752 assert!(result.is_ok()); 753 754 let state = result.unwrap(); 755 756 // Operation B should win because it has higher priority (lower index) 757 // and was received within the 72-hour recovery window 758 assert_eq!( 759 state.services.get("pds").unwrap().endpoint, 760 "https://pds-b.example.com" 761 ); 762 } 763 764 /// Test fork resolution where lower priority operation wins after recovery window expires 765 #[test] 766 fn test_fork_resolution_priority_outside_window() { 767 let primary_key = SigningKey::generate_p256(); // Index 0 768 let backup_key = SigningKey::generate_k256(); // Index 1 769 770 let rotation_keys = vec![primary_key.to_did_key(), backup_key.to_did_key()]; 771 772 let genesis = Operation::new_genesis( 773 rotation_keys.clone(), 774 HashMap::new(), 775 vec![], 776 HashMap::new(), 777 ) 778 .sign(&primary_key) 779 .unwrap(); 780 781 let genesis_cid = genesis.cid().unwrap(); 782 let genesis_time = Utc::now(); 783 784 // Operation A: signed by backup key (lower priority) 785 let mut services_a = HashMap::new(); 786 services_a.insert( 787 "pds".to_string(), 788 ServiceEndpoint { 789 service_type: "AtprotoPersonalDataServer".to_string(), 790 endpoint: "https://pds-a.example.com".to_string(), 791 }, 792 ); 793 794 let op_a = Operation::new_update( 795 rotation_keys.clone(), 796 HashMap::new(), 797 vec![], 798 services_a, 799 genesis_cid.clone(), 800 ) 801 .sign(&backup_key) 802 .unwrap(); 803 804 let op_a_time = genesis_time + Duration::hours(1); 805 806 // Operation B: signed by primary key, arrives 100 hours after A (outside 72-hour window) 807 let mut services_b = HashMap::new(); 808 services_b.insert( 809 "pds".to_string(), 810 ServiceEndpoint { 811 service_type: "AtprotoPersonalDataServer".to_string(), 812 endpoint: "https://pds-b.example.com".to_string(), 813 }, 814 ); 815 816 let op_b = Operation::new_update( 817 rotation_keys.clone(), 818 HashMap::new(), 819 vec![], 820 services_b, 821 genesis_cid, 822 ) 823 .sign(&primary_key) 824 .unwrap(); 825 826 let op_b_time = op_a_time + Duration::hours(100); // Outside 72-hour window 827 828 let operations = vec![genesis.clone(), op_a.clone(), op_b.clone()]; 829 let timestamps = vec![genesis_time, op_a_time, op_b_time]; 830 831 let result = OperationChainValidator::validate_chain_with_forks(&operations, &timestamps); 832 assert!(result.is_ok()); 833 834 let state = result.unwrap(); 835 836 // Operation A should win because even though B has higher priority, 837 // it arrived outside the 72-hour recovery window 838 assert_eq!( 839 state.services.get("pds").unwrap().endpoint, 840 "https://pds-a.example.com" 841 ); 842 } 843 844 /// Test multiple forks at different points in the chain 845 #[test] 846 fn test_fork_resolution_multiple_forks() { 847 let key1 = SigningKey::generate_p256(); 848 let key2 = SigningKey::generate_k256(); 849 let key3 = SigningKey::generate_p256(); 850 851 let rotation_keys = vec![key1.to_did_key(), key2.to_did_key(), key3.to_did_key()]; 852 853 // Genesis 854 let genesis = Operation::new_genesis( 855 rotation_keys.clone(), 856 HashMap::new(), 857 vec![], 858 HashMap::new(), 859 ) 860 .sign(&key1) 861 .unwrap(); 862 863 let genesis_cid = genesis.cid().unwrap(); 864 let genesis_time = Utc::now(); 865 866 // First fork at genesis 867 let mut services_1a = HashMap::new(); 868 services_1a.insert( 869 "pds".to_string(), 870 ServiceEndpoint { 871 service_type: "AtprotoPersonalDataServer".to_string(), 872 endpoint: "https://fork1a.example.com".to_string(), 873 }, 874 ); 875 876 let op_1a = Operation::new_update( 877 rotation_keys.clone(), 878 HashMap::new(), 879 vec![], 880 services_1a, 881 genesis_cid.clone(), 882 ) 883 .sign(&key2) 884 .unwrap(); 885 886 let op_1a_time = genesis_time + Duration::hours(1); 887 888 // Second operation at same fork (higher priority, within window) 889 let mut services_1b = HashMap::new(); 890 services_1b.insert( 891 "pds".to_string(), 892 ServiceEndpoint { 893 service_type: "AtprotoPersonalDataServer".to_string(), 894 endpoint: "https://fork1b.example.com".to_string(), 895 }, 896 ); 897 898 let op_1b = Operation::new_update( 899 rotation_keys.clone(), 900 HashMap::new(), 901 vec![], 902 services_1b, 903 genesis_cid, 904 ) 905 .sign(&key1) 906 .unwrap(); 907 908 let op_1b_time = op_1a_time + Duration::hours(2); 909 910 // Next operation continues from op_1b (the winner) 911 let op_1b_cid = op_1b.cid().unwrap(); 912 913 let mut services_2 = HashMap::new(); 914 services_2.insert( 915 "pds".to_string(), 916 ServiceEndpoint { 917 service_type: "AtprotoPersonalDataServer".to_string(), 918 endpoint: "https://continuation.example.com".to_string(), 919 }, 920 ); 921 922 let op_2 = Operation::new_update( 923 rotation_keys.clone(), 924 HashMap::new(), 925 vec![], 926 services_2, 927 op_1b_cid, 928 ) 929 .sign(&key1) 930 .unwrap(); 931 932 let op_2_time = op_1b_time + Duration::hours(1); 933 934 let operations = vec![ 935 genesis.clone(), 936 op_1a.clone(), 937 op_1b.clone(), 938 op_2.clone(), 939 ]; 940 let timestamps = vec![genesis_time, op_1a_time, op_1b_time, op_2_time]; 941 942 let result = OperationChainValidator::validate_chain_with_forks(&operations, &timestamps); 943 assert!(result.is_ok()); 944 945 let state = result.unwrap(); 946 947 // The final state should be from op_2, which continues from op_1b 948 assert_eq!( 949 state.services.get("pds").unwrap().endpoint, 950 "https://continuation.example.com" 951 ); 952 } 953 954 /// Test fork with three competing operations 955 #[test] 956 fn test_fork_resolution_three_way_fork() { 957 let key1 = SigningKey::generate_p256(); // Highest priority 958 let key2 = SigningKey::generate_k256(); // Medium priority 959 let key3 = SigningKey::generate_p256(); // Lowest priority 960 961 let rotation_keys = vec![key1.to_did_key(), key2.to_did_key(), key3.to_did_key()]; 962 963 let genesis = Operation::new_genesis( 964 rotation_keys.clone(), 965 HashMap::new(), 966 vec![], 967 HashMap::new(), 968 ) 969 .sign(&key1) 970 .unwrap(); 971 972 let genesis_cid = genesis.cid().unwrap(); 973 let genesis_time = Utc::now(); 974 975 // Three competing operations 976 let mut services_a = HashMap::new(); 977 services_a.insert( 978 "pds".to_string(), 979 ServiceEndpoint { 980 service_type: "AtprotoPersonalDataServer".to_string(), 981 endpoint: "https://op-a.example.com".to_string(), 982 }, 983 ); 984 985 let op_a = Operation::new_update( 986 rotation_keys.clone(), 987 HashMap::new(), 988 vec![], 989 services_a, 990 genesis_cid.clone(), 991 ) 992 .sign(&key3) 993 .unwrap(); // Lowest priority, first to arrive 994 995 let op_a_time = genesis_time + Duration::hours(1); 996 997 let mut services_b = HashMap::new(); 998 services_b.insert( 999 "pds".to_string(), 1000 ServiceEndpoint { 1001 service_type: "AtprotoPersonalDataServer".to_string(), 1002 endpoint: "https://op-b.example.com".to_string(), 1003 }, 1004 ); 1005 1006 let op_b = Operation::new_update( 1007 rotation_keys.clone(), 1008 HashMap::new(), 1009 vec![], 1010 services_b, 1011 genesis_cid.clone(), 1012 ) 1013 .sign(&key2) 1014 .unwrap(); // Medium priority 1015 1016 let op_b_time = op_a_time + Duration::hours(2); 1017 1018 let mut services_c = HashMap::new(); 1019 services_c.insert( 1020 "pds".to_string(), 1021 ServiceEndpoint { 1022 service_type: "AtprotoPersonalDataServer".to_string(), 1023 endpoint: "https://op-c.example.com".to_string(), 1024 }, 1025 ); 1026 1027 let op_c = Operation::new_update( 1028 rotation_keys.clone(), 1029 HashMap::new(), 1030 vec![], 1031 services_c, 1032 genesis_cid, 1033 ) 1034 .sign(&key1) 1035 .unwrap(); // Highest priority, within window 1036 1037 let op_c_time = op_a_time + Duration::hours(10); 1038 1039 let operations = vec![ 1040 genesis.clone(), 1041 op_a.clone(), 1042 op_b.clone(), 1043 op_c.clone(), 1044 ]; 1045 let timestamps = vec![genesis_time, op_a_time, op_b_time, op_c_time]; 1046 1047 let result = OperationChainValidator::validate_chain_with_forks(&operations, &timestamps); 1048 assert!(result.is_ok()); 1049 1050 let state = result.unwrap(); 1051 1052 // Operation C should win (highest priority, within window) 1053 assert_eq!( 1054 state.services.get("pds").unwrap().endpoint, 1055 "https://op-c.example.com" 1056 ); 1057 } 1058 1059 /// Test no fork - linear chain should work as before 1060 #[test] 1061 fn test_fork_resolution_no_fork() { 1062 let key = SigningKey::generate_p256(); 1063 let rotation_keys = vec![key.to_did_key()]; 1064 1065 let genesis = Operation::new_genesis( 1066 rotation_keys.clone(), 1067 HashMap::new(), 1068 vec![], 1069 HashMap::new(), 1070 ) 1071 .sign(&key) 1072 .unwrap(); 1073 1074 let genesis_cid = genesis.cid().unwrap(); 1075 let genesis_time = Utc::now(); 1076 1077 let mut services = HashMap::new(); 1078 services.insert( 1079 "pds".to_string(), 1080 ServiceEndpoint { 1081 service_type: "AtprotoPersonalDataServer".to_string(), 1082 endpoint: "https://pds.example.com".to_string(), 1083 }, 1084 ); 1085 1086 let op1 = Operation::new_update( 1087 rotation_keys.clone(), 1088 HashMap::new(), 1089 vec![], 1090 services.clone(), 1091 genesis_cid, 1092 ) 1093 .sign(&key) 1094 .unwrap(); 1095 1096 let op1_time = genesis_time + Duration::hours(1); 1097 1098 let operations = vec![genesis.clone(), op1.clone()]; 1099 let timestamps = vec![genesis_time, op1_time]; 1100 1101 let result = OperationChainValidator::validate_chain_with_forks(&operations, &timestamps); 1102 assert!(result.is_ok()); 1103 1104 let state = result.unwrap(); 1105 assert_eq!( 1106 state.services.get("pds").unwrap().endpoint, 1107 "https://pds.example.com" 1108 ); 1109 } 1110 1111 /// Test fork resolution with rotation key changes 1112 #[test] 1113 fn test_fork_resolution_with_key_rotation() { 1114 let key1 = SigningKey::generate_p256(); 1115 let key2 = SigningKey::generate_k256(); 1116 let key3 = SigningKey::generate_p256(); 1117 1118 // Initial rotation keys 1119 let rotation_keys_v1 = vec![key1.to_did_key(), key2.to_did_key()]; 1120 1121 let genesis = Operation::new_genesis( 1122 rotation_keys_v1.clone(), 1123 HashMap::new(), 1124 vec![], 1125 HashMap::new(), 1126 ) 1127 .sign(&key1) 1128 .unwrap(); 1129 1130 let genesis_cid = genesis.cid().unwrap(); 1131 let genesis_time = Utc::now(); 1132 1133 // Update rotation keys in first operation 1134 let rotation_keys_v2 = vec![key1.to_did_key(), key3.to_did_key()]; 1135 1136 let op1 = Operation::new_update( 1137 rotation_keys_v2.clone(), 1138 HashMap::new(), 1139 vec![], 1140 HashMap::new(), 1141 genesis_cid, 1142 ) 1143 .sign(&key1) 1144 .unwrap(); 1145 1146 let op1_cid = op1.cid().unwrap(); 1147 let op1_time = genesis_time + Duration::hours(1); 1148 1149 // Create a fork at op1 - both operations use the new rotation keys 1150 let mut services_a = HashMap::new(); 1151 services_a.insert( 1152 "pds".to_string(), 1153 ServiceEndpoint { 1154 service_type: "AtprotoPersonalDataServer".to_string(), 1155 endpoint: "https://op-a.example.com".to_string(), 1156 }, 1157 ); 1158 1159 let op_a = Operation::new_update( 1160 rotation_keys_v2.clone(), 1161 HashMap::new(), 1162 vec![], 1163 services_a, 1164 op1_cid.clone(), 1165 ) 1166 .sign(&key3) 1167 .unwrap(); // Index 1 in new keys 1168 1169 let op_a_time = op1_time + Duration::hours(1); 1170 1171 let mut services_b = HashMap::new(); 1172 services_b.insert( 1173 "pds".to_string(), 1174 ServiceEndpoint { 1175 service_type: "AtprotoPersonalDataServer".to_string(), 1176 endpoint: "https://op-b.example.com".to_string(), 1177 }, 1178 ); 1179 1180 let op_b = Operation::new_update( 1181 rotation_keys_v2.clone(), 1182 HashMap::new(), 1183 vec![], 1184 services_b, 1185 op1_cid, 1186 ) 1187 .sign(&key1) 1188 .unwrap(); // Index 0 in new keys (higher priority) 1189 1190 let op_b_time = op_a_time + Duration::hours(2); 1191 1192 let operations = vec![ 1193 genesis.clone(), 1194 op1.clone(), 1195 op_a.clone(), 1196 op_b.clone(), 1197 ]; 1198 let timestamps = vec![genesis_time, op1_time, op_a_time, op_b_time]; 1199 1200 let result = OperationChainValidator::validate_chain_with_forks(&operations, &timestamps); 1201 assert!(result.is_ok()); 1202 1203 let state = result.unwrap(); 1204 1205 // Operation B should win (signed by key1 which is index 0 in the new rotation keys) 1206 assert_eq!( 1207 state.services.get("pds").unwrap().endpoint, 1208 "https://op-b.example.com" 1209 ); 1210 } 1211 1212 /// Test that operations with mismatched timestamps and operations fail 1213 #[test] 1214 fn test_fork_resolution_mismatched_lengths() { 1215 let key = SigningKey::generate_p256(); 1216 let rotation_keys = vec![key.to_did_key()]; 1217 1218 let genesis = Operation::new_genesis( 1219 rotation_keys, 1220 HashMap::new(), 1221 vec![], 1222 HashMap::new(), 1223 ) 1224 .sign(&key) 1225 .unwrap(); 1226 1227 let operations = vec![genesis]; 1228 let timestamps = vec![Utc::now(), Utc::now()]; // Different length 1229 1230 let result = OperationChainValidator::validate_chain_with_forks(&operations, &timestamps); 1231 assert!(result.is_err()); 1232 } 1233 1234 /// Test recovery window boundary (exactly 72 hours) 1235 #[test] 1236 fn test_fork_resolution_recovery_window_boundary() { 1237 let primary_key = SigningKey::generate_p256(); 1238 let backup_key = SigningKey::generate_k256(); 1239 1240 let rotation_keys = vec![primary_key.to_did_key(), backup_key.to_did_key()]; 1241 1242 let genesis = Operation::new_genesis( 1243 rotation_keys.clone(), 1244 HashMap::new(), 1245 vec![], 1246 HashMap::new(), 1247 ) 1248 .sign(&primary_key) 1249 .unwrap(); 1250 1251 let genesis_cid = genesis.cid().unwrap(); 1252 let genesis_time = Utc::now(); 1253 1254 // Operation A: signed by backup key 1255 let mut services_a = HashMap::new(); 1256 services_a.insert( 1257 "pds".to_string(), 1258 ServiceEndpoint { 1259 service_type: "AtprotoPersonalDataServer".to_string(), 1260 endpoint: "https://op-a.example.com".to_string(), 1261 }, 1262 ); 1263 1264 let op_a = Operation::new_update( 1265 rotation_keys.clone(), 1266 HashMap::new(), 1267 vec![], 1268 services_a, 1269 genesis_cid.clone(), 1270 ) 1271 .sign(&backup_key) 1272 .unwrap(); 1273 1274 let op_a_time = genesis_time + Duration::hours(1); 1275 1276 // Operation B: exactly at 72-hour boundary (should still be within window) 1277 let mut services_b = HashMap::new(); 1278 services_b.insert( 1279 "pds".to_string(), 1280 ServiceEndpoint { 1281 service_type: "AtprotoPersonalDataServer".to_string(), 1282 endpoint: "https://op-b.example.com".to_string(), 1283 }, 1284 ); 1285 1286 let op_b = Operation::new_update( 1287 rotation_keys.clone(), 1288 HashMap::new(), 1289 vec![], 1290 services_b, 1291 genesis_cid, 1292 ) 1293 .sign(&primary_key) 1294 .unwrap(); 1295 1296 // Exactly 72 hours after op_a 1297 let op_b_time = op_a_time + Duration::hours(72); 1298 1299 let operations = vec![genesis.clone(), op_a.clone(), op_b.clone()]; 1300 let timestamps = vec![genesis_time, op_a_time, op_b_time]; 1301 1302 let result = OperationChainValidator::validate_chain_with_forks(&operations, &timestamps); 1303 assert!(result.is_ok()); 1304 1305 let state = result.unwrap(); 1306 1307 // At exactly 72 hours, the higher priority operation should still win 1308 assert_eq!( 1309 state.services.get("pds").unwrap().endpoint, 1310 "https://op-b.example.com" 1311 ); 1312 } 1313}